diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 4a42639c84..979543f7be 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,19 +2,19 @@ ### dev-v2 (not yet released) ### -* Optimize seeking in FMP4 by enabling seeking to the nearest sync sample within - a fragment. This benefits standalone FMP4 playbacks, DASH and SmoothStreaming. -* Moved initial bitrate estimate from `AdaptiveTrackSelection` to - `DefaultBandwidthMeter`. -* Updated default max buffer length in `DefaultLoadControl`. -* Added `AnalyticsListener` interface which can be registered in - `SimpleExoPlayer` to receive detailed meta data for each ExoPlayer event. -* UI components: - * Add support for listening to `AspectRatioFrameLayout`'s aspect ratio update - ([#3736](https://github.com/google/ExoPlayer/issues/3736)). - * Add PlayerNotificationManager. -* Downloading: Add `DownloadService`, `DownloadManager` and - related classes ([#2643](https://github.com/google/ExoPlayer/issues/2643)). +* Coming soon... + +### 2.8.0 ### + +* Downloading: + * Add `DownloadService`, `DownloadManager` and related classes + ([#2643](https://github.com/google/ExoPlayer/issues/2643)). Information on + using these components to download progressive formats can be found + [here](https://medium.com/google-exoplayer/downloading-streams-6d259eec7f95). + To see how to download DASH, HLS and SmoothStreaming media, take a look at + the app. + * Updated main demo app to support downloading DASH, HLS, SmoothStreaming and + progressive media. * MediaSources: * Allow reusing media sources after they have been released and also in parallel to allow adding them multiple times to a concatenation. @@ -31,9 +31,23 @@ * Support live stream clipping with `ClippingMediaSource`. * Allow setting tags for all media sources in their factories. The tag of the current window can be retrieved with `ExoPlayer.getCurrentTag`. -* IMA: Allow setting the ad media load timeout - ([#3691](https://github.com/google/ExoPlayer/issues/3691)). +* UI components: + * Add support for displaying error messages and a buffering spinner in + `PlayerView`. + * Add support for listening to `AspectRatioFrameLayout`'s aspect ratio update + ([#3736](https://github.com/google/ExoPlayer/issues/3736)). + * Add `PlayerNotificationManager` for displaying notifications reflecting the + player state. + * Add `TrackSelectionView` for selecting tracks with `DefaultTrackSelector`. + * Add `TrackNameProvider` for converting track `Format`s to textual + descriptions, and `DefaultTrackNameProvider` as a default implementation. +* Track selection: + * Reworked `MappingTrackSelector` and `DefaultTrackSelector`. + * `DefaultTrackSelector.Parameters` now implements `Parcelable`. + * Added UI components for track selection (see above). * Audio: + * Support extracting data from AMR container formats, including both narrow + and wide band ([#2527](https://github.com/google/ExoPlayer/issues/2527)). * FLAC: * Sniff FLAC files correctly if they have ID3 headers ([#4055](https://github.com/google/ExoPlayer/issues/4055)). @@ -48,14 +62,17 @@ * Fix an issue where playback of TrueHD streams would get stuck after seeking due to not finding a syncframe ((#3845)[https://github.com/google/ExoPlayer/issues/3845]). + * Fix an issue with eac3-joc playback where a codec would fail to configure + ((#4165)[https://github.com/google/ExoPlayer/issues/4165]). * Handle non-empty end-of-stream buffers, to fix gapless playback of streams with encoder padding when the decoder returns a non-empty final buffer. * Allow trimming more than one sample when applying an elst audio edit via gapless playback info. + * Allow overriding skipping/scaling with custom `AudioProcessor`s + ((#3142)[https://github.com/google/ExoPlayer/issues/3142]). * Caching: - * Add release method to Cache interface. - * Prevent multiple instances of SimpleCache in the same folder. - Previous instance must be released. + * Add release method to the `Cache` interface, and prevent multiple instances + of `SimpleCache` using the same folder at the same time. * Cache redirect URLs ([#2360](https://github.com/google/ExoPlayer/issues/2360)). * DRM: @@ -65,17 +82,41 @@ ([#4022][https://github.com/google/ExoPlayer/issues/4022]). * Fix handling of 307/308 redirects when making license requests ([#4108](https://github.com/google/ExoPlayer/issues/4108)). -* HLS: Fix playlist loading error propagation when the current selection does - not include all of the playlist's variants. +* HLS: + * Fix playlist loading error propagation when the current selection does + not include all of the playlist's variants. + * Fix SAMPLE-AES-CENC and SAMPLE-AES-CTR EXT-X-KEY methods + ([#4145](https://github.com/google/ExoPlayer/issues/4145)). + * Preeptively declare an ID3 track in chunkless preparation + ([#4016](https://github.com/google/ExoPlayer/issues/4016)). + * Add support for multiple #EXT-X-MAP tags in a media playlist + ([#4164](https://github.com/google/ExoPlayer/issues/4182)). + * Fix seeking in live streams + ([#4187](https://github.com/google/ExoPlayer/issues/4187)). +* IMA: + * Allow setting the ad media load timeout + ([#3691](https://github.com/google/ExoPlayer/issues/3691)). + * Expose ad load errors via `MediaSourceEventListener` on `AdsMediaSource`, + and allow setting an ad event listener on `ImaAdsLoader`. Deprecate the + `AdsMediaSource.EventListener`. +* Add `AnalyticsListener` interface which can be registered in + `SimpleExoPlayer` to receive detailed metadata for each ExoPlayer event. +* Optimize seeking in FMP4 by enabling seeking to the nearest sync sample within + a fragment. This benefits standalone FMP4 playbacks, DASH and SmoothStreaming. +* Updated default max buffer length in `DefaultLoadControl`. * Fix ClearKey decryption error if the key contains a forward slash ([#4075](https://github.com/google/ExoPlayer/issues/4075)). * Fix crash when switching surface on Huawei P9 Lite ([#4084](https://github.com/google/ExoPlayer/issues/4084)), and Philips QM163E ([#4104](https://github.com/google/ExoPlayer/issues/4104)). * Support ZLIB compressed PGS subtitles. +* Added `getPlaybackError` to `Player` interface. +* Moved initial bitrate estimate from `AdaptiveTrackSelection` to + `DefaultBandwidthMeter`. * Removed default renderer time offset of 60000000 from internal player. The actual renderer timestamp offset can be obtained by listening to `BaseRenderer.onStreamChanged`. +* Added dependencies on checkerframework annotations for static code analysis. ### 2.7.3 ### @@ -240,6 +281,7 @@ ([#3792](https://github.com/google/ExoPlayer/issues/3792). * Support 14-bit mode and little endianness in DTS PES packets ([#3340](https://github.com/google/ExoPlayer/issues/3340)). +* Demo app: Add ability to download not DRM protected content. ### 2.6.1 ### diff --git a/extensions/mediasession/src/main/res/values-ms-rMY/strings.xml b/checker-framework-lint.xml similarity index 62% rename from extensions/mediasession/src/main/res/values-ms-rMY/strings.xml rename to checker-framework-lint.xml index 829542b668..1d45f9de05 100644 --- a/extensions/mediasession/src/main/res/values-ms-rMY/strings.xml +++ b/checker-framework-lint.xml @@ -1,6 +1,4 @@ - - - - "Ulang semua" - "Tiada ulangan" - "Ulangan" - + + + + + diff --git a/constants.gradle b/constants.gradle index ea0c2436c0..dcadcceb4f 100644 --- a/constants.gradle +++ b/constants.gradle @@ -13,8 +13,8 @@ // limitations under the License. project.ext { // ExoPlayer version and version code. - releaseVersion = '2.7.3' - releaseVersionCode = 2703 + releaseVersion = '2.8.0' + releaseVersionCode = 2800 // Important: ExoPlayer specifies a minSdkVersion of 14 because various // components provided by the library may be of use on older devices. // However, please note that the core media playback functionality provided @@ -31,6 +31,8 @@ project.ext { junitVersion = '4.12' truthVersion = '0.39' robolectricVersion = '3.7.1' + autoValueVersion = '1.6' + checkerframeworkVersion = '2.5.0' modulePrefix = ':' if (gradle.ext.has('exoplayerModulePrefix')) { modulePrefix += gradle.ext.exoplayerModulePrefix diff --git a/demos/main/src/main/AndroidManifest.xml b/demos/main/src/main/AndroidManifest.xml index cde95300ab..3bedefc60e 100644 --- a/demos/main/src/main/AndroidManifest.xml +++ b/demos/main/src/main/AndroidManifest.xml @@ -19,6 +19,8 @@ + + @@ -73,6 +75,18 @@ + + + + + + + + + diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java index f54a1a95e0..b5c127d2e3 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java @@ -16,6 +16,13 @@ package com.google.android.exoplayer2.demo; import android.app.Application; +import com.google.android.exoplayer2.offline.DownloadAction.Deserializer; +import com.google.android.exoplayer2.offline.DownloadManager; +import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; +import com.google.android.exoplayer2.offline.ProgressiveDownloadAction; +import com.google.android.exoplayer2.source.dash.offline.DashDownloadAction; +import com.google.android.exoplayer2.source.hls.offline.HlsDownloadAction; +import com.google.android.exoplayer2.source.smoothstreaming.offline.SsDownloadAction; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; @@ -35,10 +42,24 @@ import java.io.File; */ public class DemoApplication extends Application { - private static final String DOWNLOAD_CACHE_FOLDER = "downloads"; + private static final String DOWNLOAD_ACTION_FILE = "actions"; + private static final String DOWNLOAD_TRACKER_ACTION_FILE = "tracked_actions"; + private static final String DOWNLOAD_CONTENT_DIRECTORY = "downloads"; + private static final int MAX_SIMULTANEOUS_DOWNLOADS = 2; + private static final Deserializer[] DOWNLOAD_DESERIALIZERS = + new Deserializer[] { + DashDownloadAction.DESERIALIZER, + HlsDownloadAction.DESERIALIZER, + SsDownloadAction.DESERIALIZER, + ProgressiveDownloadAction.DESERIALIZER + }; protected String userAgent; + + private File downloadDirectory; private Cache downloadCache; + private DownloadManager downloadManager; + private DownloadTracker downloadTracker; @Override public void onCreate() { @@ -50,7 +71,7 @@ public class DemoApplication extends Application { public DataSource.Factory buildDataSourceFactory(TransferListener listener) { DefaultDataSourceFactory upstreamFactory = new DefaultDataSourceFactory(this, listener, buildHttpDataSourceFactory(listener)); - return createReadOnlyCacheDataSource(upstreamFactory, getDownloadCache()); + return buildReadOnlyCacheDataSource(upstreamFactory, getDownloadCache()); } /** Returns a {@link HttpDataSource.Factory}. */ @@ -59,31 +80,69 @@ public class DemoApplication extends Application { return new DefaultHttpDataSourceFactory(userAgent, listener); } - /** Returns the download {@link Cache}. */ - public Cache getDownloadCache() { - if (downloadCache == null) { - File dir = getExternalFilesDir(null); - if (dir == null) { - dir = getFilesDir(); - } - File downloadCacheFolder = new File(dir, DOWNLOAD_CACHE_FOLDER); - downloadCache = new SimpleCache(downloadCacheFolder, new NoOpCacheEvictor()); - } - return downloadCache; - } - + /** Returns whether extension renderers should be used. */ public boolean useExtensionRenderers() { return "withExtensions".equals(BuildConfig.FLAVOR); } - private static CacheDataSourceFactory createReadOnlyCacheDataSource( + public DownloadManager getDownloadManager() { + initDownloadManager(); + return downloadManager; + } + + public DownloadTracker getDownloadTracker() { + initDownloadManager(); + return downloadTracker; + } + + private synchronized void initDownloadManager() { + if (downloadManager == null) { + DownloaderConstructorHelper downloaderConstructorHelper = + new DownloaderConstructorHelper( + getDownloadCache(), buildHttpDataSourceFactory(/* listener= */ null)); + downloadManager = + new DownloadManager( + downloaderConstructorHelper, + MAX_SIMULTANEOUS_DOWNLOADS, + DownloadManager.DEFAULT_MIN_RETRY_COUNT, + new File(getDownloadDirectory(), DOWNLOAD_ACTION_FILE), + DOWNLOAD_DESERIALIZERS); + downloadTracker = + new DownloadTracker( + /* context= */ this, + buildDataSourceFactory(/* listener= */ null), + new File(getDownloadDirectory(), DOWNLOAD_TRACKER_ACTION_FILE), + DOWNLOAD_DESERIALIZERS); + downloadManager.addListener(downloadTracker); + } + } + + private synchronized Cache getDownloadCache() { + if (downloadCache == null) { + File downloadContentDirectory = new File(getDownloadDirectory(), DOWNLOAD_CONTENT_DIRECTORY); + downloadCache = new SimpleCache(downloadContentDirectory, new NoOpCacheEvictor()); + } + return downloadCache; + } + + private File getDownloadDirectory() { + if (downloadDirectory == null) { + downloadDirectory = getExternalFilesDir(null); + if (downloadDirectory == null) { + downloadDirectory = getFilesDir(); + } + } + return downloadDirectory; + } + + private static CacheDataSourceFactory buildReadOnlyCacheDataSource( DefaultDataSourceFactory upstreamFactory, Cache cache) { return new CacheDataSourceFactory( cache, upstreamFactory, new FileDataSourceFactory(), - /*cacheWriteDataSinkFactory=*/ null, + /* cacheWriteDataSinkFactory= */ null, CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR, - /*eventListener=*/ null); + /* eventListener= */ null); } } diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoDownloadService.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoDownloadService.java new file mode 100644 index 0000000000..7d1ab16ce4 --- /dev/null +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoDownloadService.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.demo; + +import android.app.Notification; +import com.google.android.exoplayer2.offline.DownloadManager; +import com.google.android.exoplayer2.offline.DownloadManager.TaskState; +import com.google.android.exoplayer2.offline.DownloadService; +import com.google.android.exoplayer2.scheduler.PlatformScheduler; +import com.google.android.exoplayer2.ui.DownloadNotificationUtil; +import com.google.android.exoplayer2.util.NotificationUtil; +import com.google.android.exoplayer2.util.Util; + +/** A service for downloading media. */ +public class DemoDownloadService extends DownloadService { + + private static final String CHANNEL_ID = "download_channel"; + private static final int JOB_ID = 1; + private static final int FOREGROUND_NOTIFICATION_ID = 1; + + public DemoDownloadService() { + super( + FOREGROUND_NOTIFICATION_ID, + DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL, + CHANNEL_ID, + R.string.exo_download_notification_channel_name); + } + + @Override + protected DownloadManager getDownloadManager() { + return ((DemoApplication) getApplication()).getDownloadManager(); + } + + @Override + protected PlatformScheduler getScheduler() { + return Util.SDK_INT >= 21 ? new PlatformScheduler(this, JOB_ID) : null; + } + + @Override + protected Notification getForegroundNotification(TaskState[] taskStates) { + return DownloadNotificationUtil.buildProgressNotification( + /* context= */ this, + R.drawable.exo_controls_play, + CHANNEL_ID, + /* contentIntent= */ null, + /* message= */ null, + taskStates); + } + + @Override + protected void onTaskStateChanged(TaskState taskState) { + if (taskState.action.isRemoveAction) { + return; + } + Notification notification = null; + if (taskState.state == TaskState.STATE_COMPLETED) { + notification = + DownloadNotificationUtil.buildDownloadCompletedNotification( + /* context= */ this, + R.drawable.exo_controls_play, + CHANNEL_ID, + /* contentIntent= */ null, + Util.fromUtf8Bytes(taskState.action.data)); + } else if (taskState.state == TaskState.STATE_FAILED) { + notification = + DownloadNotificationUtil.buildDownloadFailedNotification( + /* context= */ this, + R.drawable.exo_controls_play, + CHANNEL_ID, + /* contentIntent= */ null, + Util.fromUtf8Bytes(taskState.action.data)); + } + int notificationId = FOREGROUND_NOTIFICATION_ID + 1 + taskState.taskId; + NotificationUtil.setNotification(this, notificationId, notification); + } +} diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoUtil.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoUtil.java deleted file mode 100644 index 2692bc4531..0000000000 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoUtil.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.demo; - -import android.text.TextUtils; -import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.util.MimeTypes; -import java.util.Locale; - -/** - * Utility methods for demo application. - */ -/* package */ final class DemoUtil { - - /** - * Builds a track name for display. - * - * @param format {@link Format} of the track. - * @return a generated name specific to the track. - */ - public static String buildTrackName(Format format) { - String trackName; - if (MimeTypes.isVideo(format.sampleMimeType)) { - trackName = joinWithSeparator(joinWithSeparator(joinWithSeparator( - buildResolutionString(format), buildBitrateString(format)), buildTrackIdString(format)), - buildSampleMimeTypeString(format)); - } else if (MimeTypes.isAudio(format.sampleMimeType)) { - trackName = joinWithSeparator(joinWithSeparator(joinWithSeparator(joinWithSeparator( - buildLanguageString(format), buildAudioPropertyString(format)), - buildBitrateString(format)), buildTrackIdString(format)), - buildSampleMimeTypeString(format)); - } else { - trackName = joinWithSeparator(joinWithSeparator(joinWithSeparator(buildLanguageString(format), - buildBitrateString(format)), buildTrackIdString(format)), - buildSampleMimeTypeString(format)); - } - return trackName.length() == 0 ? "unknown" : trackName; - } - - private static String buildResolutionString(Format format) { - return format.width == Format.NO_VALUE || format.height == Format.NO_VALUE - ? "" : format.width + "x" + format.height; - } - - private static String buildAudioPropertyString(Format format) { - return format.channelCount == Format.NO_VALUE || format.sampleRate == Format.NO_VALUE - ? "" : format.channelCount + "ch, " + format.sampleRate + "Hz"; - } - - private static String buildLanguageString(Format format) { - return TextUtils.isEmpty(format.language) || "und".equals(format.language) ? "" - : format.language; - } - - private static String buildBitrateString(Format format) { - return format.bitrate == Format.NO_VALUE ? "" - : String.format(Locale.US, "%.2fMbit", format.bitrate / 1000000f); - } - - private static String joinWithSeparator(String first, String second) { - return first.length() == 0 ? second : (second.length() == 0 ? first : first + ", " + second); - } - - private static String buildTrackIdString(Format format) { - return format.id == null ? "" : ("id:" + format.id); - } - - private static String buildSampleMimeTypeString(Format format) { - return format.sampleMimeType == null ? "" : format.sampleMimeType; - } - - private DemoUtil() {} -} diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java new file mode 100644 index 0000000000..b4bce01c7a --- /dev/null +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java @@ -0,0 +1,303 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.demo; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.net.Uri; +import android.os.Handler; +import android.os.HandlerThread; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.ArrayAdapter; +import android.widget.ListView; +import android.widget.Toast; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.offline.ActionFile; +import com.google.android.exoplayer2.offline.DownloadAction; +import com.google.android.exoplayer2.offline.DownloadHelper; +import com.google.android.exoplayer2.offline.DownloadManager; +import com.google.android.exoplayer2.offline.DownloadManager.TaskState; +import com.google.android.exoplayer2.offline.DownloadService; +import com.google.android.exoplayer2.offline.ProgressiveDownloadHelper; +import com.google.android.exoplayer2.offline.SegmentDownloadAction; +import com.google.android.exoplayer2.offline.TrackKey; +import com.google.android.exoplayer2.source.TrackGroup; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.source.dash.offline.DashDownloadHelper; +import com.google.android.exoplayer2.source.hls.offline.HlsDownloadHelper; +import com.google.android.exoplayer2.source.smoothstreaming.offline.SsDownloadHelper; +import com.google.android.exoplayer2.ui.DefaultTrackNameProvider; +import com.google.android.exoplayer2.ui.TrackNameProvider; +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.util.Util; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.concurrent.CopyOnWriteArraySet; + +/** + * Tracks media that has been downloaded. + * + *

Tracked downloads are persisted using an {@link ActionFile}, however in a real application + * it's expected that state will be stored directly in the application's media database, so that it + * can be queried efficiently together with other information about the media. + */ +public class DownloadTracker implements DownloadManager.Listener { + + /** Listens for changes in the tracked downloads. */ + public interface Listener { + + /** Called when the tracked downloads changed. */ + void onDownloadsChanged(); + } + + private static final String TAG = "DownloadTracker"; + + private final Context context; + private final DataSource.Factory dataSourceFactory; + private final TrackNameProvider trackNameProvider; + private final CopyOnWriteArraySet listeners; + private final HashMap trackedDownloadStates; + private final ActionFile actionFile; + private final Handler actionFileWriteHandler; + + public DownloadTracker( + Context context, + DataSource.Factory dataSourceFactory, + File actionFile, + DownloadAction.Deserializer[] deserializers) { + this.context = context.getApplicationContext(); + this.dataSourceFactory = dataSourceFactory; + this.actionFile = new ActionFile(actionFile); + trackNameProvider = new DefaultTrackNameProvider(context.getResources()); + listeners = new CopyOnWriteArraySet<>(); + trackedDownloadStates = new HashMap<>(); + HandlerThread actionFileWriteThread = new HandlerThread("DownloadTracker"); + actionFileWriteThread.start(); + actionFileWriteHandler = new Handler(actionFileWriteThread.getLooper()); + loadTrackedActions(deserializers); + } + + public void addListener(Listener listener) { + listeners.add(listener); + } + + public void removeListener(Listener listener) { + listeners.remove(listener); + } + + public boolean isDownloaded(Uri uri) { + return trackedDownloadStates.containsKey(uri); + } + + @SuppressWarnings("unchecked") + public List getOfflineStreamKeys(Uri uri) { + if (!trackedDownloadStates.containsKey(uri)) { + return Collections.emptyList(); + } + DownloadAction action = trackedDownloadStates.get(uri); + if (action instanceof SegmentDownloadAction) { + return ((SegmentDownloadAction) action).keys; + } + return Collections.emptyList(); + } + + public void toggleDownload(Activity activity, String name, Uri uri, String extension) { + if (isDownloaded(uri)) { + DownloadAction removeAction = + getDownloadHelper(uri, extension).getRemoveAction(Util.getUtf8Bytes(name)); + startServiceWithAction(removeAction); + } else { + StartDownloadDialogHelper helper = + new StartDownloadDialogHelper(activity, getDownloadHelper(uri, extension), name); + helper.prepare(); + } + } + + // DownloadManager.Listener + + @Override + public void onInitialized(DownloadManager downloadManager) { + // Do nothing. + } + + @Override + public void onTaskStateChanged(DownloadManager downloadManager, TaskState taskState) { + DownloadAction action = taskState.action; + Uri uri = action.uri; + if ((action.isRemoveAction && taskState.state == TaskState.STATE_COMPLETED) + || (!action.isRemoveAction && taskState.state == TaskState.STATE_FAILED)) { + // A download has been removed, or has failed. Stop tracking it. + if (trackedDownloadStates.remove(uri) != null) { + handleTrackedDownloadStatesChanged(); + } + } + } + + @Override + public void onIdle(DownloadManager downloadManager) { + // Do nothing. + } + + // Internal methods + + private void loadTrackedActions(DownloadAction.Deserializer[] deserializers) { + try { + DownloadAction[] allActions = actionFile.load(deserializers); + for (DownloadAction action : allActions) { + trackedDownloadStates.put(action.uri, action); + } + } catch (IOException e) { + Log.e(TAG, "Failed to load tracked actions", e); + } + } + + private void handleTrackedDownloadStatesChanged() { + for (Listener listener : listeners) { + listener.onDownloadsChanged(); + } + final DownloadAction[] actions = trackedDownloadStates.values().toArray(new DownloadAction[0]); + actionFileWriteHandler.post( + new Runnable() { + @Override + public void run() { + try { + actionFile.store(actions); + } catch (IOException e) { + Log.e(TAG, "Failed to store tracked actions", e); + } + } + }); + } + + private void startDownload(DownloadAction action) { + if (trackedDownloadStates.containsKey(action.uri)) { + // This content is already being downloaded. Do nothing. + return; + } + trackedDownloadStates.put(action.uri, action); + handleTrackedDownloadStatesChanged(); + startServiceWithAction(action); + } + + private void startServiceWithAction(DownloadAction action) { + DownloadService.startWithAction(context, DemoDownloadService.class, action, false); + } + + private DownloadHelper getDownloadHelper(Uri uri, String extension) { + int type = Util.inferContentType(uri, extension); + switch (type) { + case C.TYPE_DASH: + return new DashDownloadHelper(uri, dataSourceFactory); + case C.TYPE_SS: + return new SsDownloadHelper(uri, dataSourceFactory); + case C.TYPE_HLS: + return new HlsDownloadHelper(uri, dataSourceFactory); + case C.TYPE_OTHER: + return new ProgressiveDownloadHelper(uri); + default: + throw new IllegalStateException("Unsupported type: " + type); + } + } + + private final class StartDownloadDialogHelper + implements DownloadHelper.Callback, DialogInterface.OnClickListener { + + private final DownloadHelper downloadHelper; + private final String name; + + private final AlertDialog.Builder builder; + private final View dialogView; + private final List trackKeys; + private final ArrayAdapter trackTitles; + private final ListView representationList; + + public StartDownloadDialogHelper( + Activity activity, DownloadHelper downloadHelper, String name) { + this.downloadHelper = downloadHelper; + this.name = name; + builder = + new AlertDialog.Builder(activity) + .setTitle(R.string.exo_download_description) + .setPositiveButton(android.R.string.ok, this) + .setNegativeButton(android.R.string.cancel, null); + + // Inflate with the builder's context to ensure the correct style is used. + LayoutInflater dialogInflater = LayoutInflater.from(builder.getContext()); + dialogView = dialogInflater.inflate(R.layout.start_download_dialog, null); + + trackKeys = new ArrayList<>(); + trackTitles = + new ArrayAdapter<>( + builder.getContext(), android.R.layout.simple_list_item_multiple_choice); + representationList = dialogView.findViewById(R.id.representation_list); + representationList.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE); + representationList.setAdapter(trackTitles); + } + + public void prepare() { + downloadHelper.prepare(this); + } + + @Override + public void onPrepared(DownloadHelper helper) { + for (int i = 0; i < downloadHelper.getPeriodCount(); i++) { + TrackGroupArray trackGroups = downloadHelper.getTrackGroups(i); + for (int j = 0; j < trackGroups.length; j++) { + TrackGroup trackGroup = trackGroups.get(j); + for (int k = 0; k < trackGroup.length; k++) { + trackKeys.add(new TrackKey(i, j, k)); + trackTitles.add(trackNameProvider.getTrackName(trackGroup.getFormat(k))); + } + } + if (!trackKeys.isEmpty()) { + builder.setView(dialogView); + } + builder.create().show(); + } + } + + @Override + public void onPrepareError(DownloadHelper helper, IOException e) { + Toast.makeText( + context.getApplicationContext(), R.string.download_start_error, Toast.LENGTH_LONG) + .show(); + } + + @Override + public void onClick(DialogInterface dialog, int which) { + ArrayList selectedTrackKeys = new ArrayList<>(); + for (int i = 0; i < representationList.getChildCount(); i++) { + if (representationList.isItemChecked(i)) { + selectedTrackKeys.add(trackKeys.get(i)); + } + } + if (!selectedTrackKeys.isEmpty() || trackKeys.isEmpty()) { + // We have selected keys, or we're dealing with single stream content. + DownloadAction downloadAction = + downloadHelper.getDownloadAction(Util.getUtf8Bytes(name), selectedTrackKeys); + startDownload(downloadAction); + } + } + } +} diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index 4765278fcb..091e483155 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -16,14 +16,14 @@ package com.google.android.exoplayer2.demo; import android.app.Activity; +import android.app.AlertDialog; import android.content.Intent; import android.content.pm.PackageManager; import android.net.Uri; import android.os.Bundle; -import android.os.Handler; -import android.os.Parcelable; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.util.Pair; import android.view.KeyEvent; import android.view.View; import android.view.View.OnClickListener; @@ -48,6 +48,7 @@ import com.google.android.exoplayer2.drm.HttpMediaDrmCallback; import com.google.android.exoplayer2.drm.UnsupportedDrmException; import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.DecoderInitializationException; import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException; +import com.google.android.exoplayer2.offline.FilteringManifestParser; import com.google.android.exoplayer2.source.BehindLiveWindowException; import com.google.android.exoplayer2.source.ConcatenatingMediaSource; import com.google.android.exoplayer2.source.ExtractorMediaSource; @@ -57,14 +58,15 @@ import com.google.android.exoplayer2.source.ads.AdsLoader; import com.google.android.exoplayer2.source.ads.AdsMediaSource; import com.google.android.exoplayer2.source.dash.DashMediaSource; import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource; -import com.google.android.exoplayer2.source.dash.manifest.FilteringDashManifestParser; +import com.google.android.exoplayer2.source.dash.manifest.DashManifestParser; import com.google.android.exoplayer2.source.dash.manifest.RepresentationKey; import com.google.android.exoplayer2.source.hls.HlsMediaSource; -import com.google.android.exoplayer2.source.hls.playlist.FilteringHlsPlaylistParser; +import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistParser; +import com.google.android.exoplayer2.source.hls.playlist.RenditionKey; import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource; import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; -import com.google.android.exoplayer2.source.smoothstreaming.manifest.FilteringSsManifestParser; -import com.google.android.exoplayer2.source.smoothstreaming.manifest.TrackKey; +import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifestParser; +import com.google.android.exoplayer2.source.smoothstreaming.manifest.StreamKey; import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo; @@ -74,11 +76,12 @@ import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.ui.DebugTextViewHelper; import com.google.android.exoplayer2.ui.PlayerControlView; import com.google.android.exoplayer2.ui.PlayerView; +import com.google.android.exoplayer2.ui.TrackSelectionView; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer2.upstream.HttpDataSource; +import com.google.android.exoplayer2.util.ErrorMessageProvider; import com.google.android.exoplayer2.util.EventLogger; -import com.google.android.exoplayer2.util.ParcelableArray; import com.google.android.exoplayer2.util.Util; import java.lang.reflect.Constructor; import java.net.CookieHandler; @@ -99,13 +102,11 @@ public class PlayerActivity extends Activity public static final String ACTION_VIEW = "com.google.android.exoplayer.demo.action.VIEW"; public static final String EXTENSION_EXTRA = "extension"; - public static final String MANIFEST_FILTER_EXTRA = "manifest_filter"; public static final String ACTION_VIEW_LIST = "com.google.android.exoplayer.demo.action.VIEW_LIST"; public static final String URI_LIST_EXTRA = "uri_list"; public static final String EXTENSION_LIST_EXTRA = "extension_list"; - public static final String MANIFEST_FILTER_LIST_EXTRA = "manifest_filter_list"; public static final String AD_TAG_URI_EXTRA = "ad_tag_uri"; @@ -116,6 +117,12 @@ public class PlayerActivity extends Activity // For backwards compatibility only. private static final String DRM_SCHEME_UUID_EXTRA = "drm_scheme_uuid"; + // Saved instance state keys. + private static final String KEY_TRACK_SELECTOR_PARAMETERS = "track_selector_parameters"; + private static final String KEY_WINDOW = "window"; + private static final String KEY_POSITION = "position"; + private static final String KEY_AUTO_PLAY = "auto_play"; + private static final DefaultBandwidthMeter BANDWIDTH_METER = new DefaultBandwidthMeter(); private static final CookieManager DEFAULT_COOKIE_MANAGER; static { @@ -123,7 +130,6 @@ public class PlayerActivity extends Activity DEFAULT_COOKIE_MANAGER.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER); } - private Handler mainHandler; private PlayerView playerView; private LinearLayout debugRootView; private TextView debugTextView; @@ -132,14 +138,13 @@ public class PlayerActivity extends Activity private SimpleExoPlayer player; private MediaSource mediaSource; private DefaultTrackSelector trackSelector; - private TrackSelectionHelper trackSelectionHelper; + private DefaultTrackSelector.Parameters trackSelectorParameters; private DebugTextViewHelper debugViewHelper; - private boolean inErrorState; private TrackGroupArray lastSeenTrackGroupArray; - private boolean shouldAutoPlay; - private int resumeWindow; - private long resumePosition; + private boolean startAutoPlay; + private int startWindow; + private long startPosition; // Fields used only for ad playback. The ads loader is loaded via reflection. @@ -152,10 +157,7 @@ public class PlayerActivity extends Activity @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - shouldAutoPlay = true; - clearResumePosition(); mediaDataSourceFactory = buildDataSourceFactory(true); - mainHandler = new Handler(); if (CookieHandler.getDefault() != DEFAULT_COOKIE_MANAGER) { CookieHandler.setDefault(DEFAULT_COOKIE_MANAGER); } @@ -168,14 +170,24 @@ public class PlayerActivity extends Activity playerView = findViewById(R.id.player_view); playerView.setControllerVisibilityListener(this); + playerView.setErrorMessageProvider(new PlayerErrorMessageProvider()); playerView.requestFocus(); + + if (savedInstanceState != null) { + trackSelectorParameters = savedInstanceState.getParcelable(KEY_TRACK_SELECTOR_PARAMETERS); + startAutoPlay = savedInstanceState.getBoolean(KEY_AUTO_PLAY); + startWindow = savedInstanceState.getInt(KEY_WINDOW); + startPosition = savedInstanceState.getLong(KEY_POSITION); + } else { + trackSelectorParameters = new DefaultTrackSelector.ParametersBuilder().build(); + clearStartPosition(); + } } @Override public void onNewIntent(Intent intent) { releasePlayer(); - shouldAutoPlay = true; - clearResumePosition(); + clearStartPosition(); setIntent(intent); } @@ -220,17 +232,27 @@ public class PlayerActivity extends Activity @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { - if (grantResults.length > 0) { - if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { - initializePlayer(); - } else { - showToast(R.string.storage_permission_denied); - finish(); - } - } else { + if (grantResults.length == 0) { // Empty results are triggered if a permission is requested while another request was already // pending and can be safely ignored in this case. + return; } + if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { + initializePlayer(); + } else { + showToast(R.string.storage_permission_denied); + finish(); + } + } + + @Override + public void onSaveInstanceState(Bundle outState) { + updateTrackSelectorParameters(); + updateStartPosition(); + outState.putParcelable(KEY_TRACK_SELECTOR_PARAMETERS, trackSelectorParameters); + outState.putBoolean(KEY_AUTO_PLAY, startAutoPlay); + outState.putInt(KEY_WINDOW, startWindow); + outState.putLong(KEY_POSITION, startPosition); } // Activity input @@ -248,8 +270,19 @@ public class PlayerActivity extends Activity if (view.getParent() == debugRootView) { MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo(); if (mappedTrackInfo != null) { - trackSelectionHelper.showSelectionDialog( - this, ((Button) view).getText(), mappedTrackInfo, (int) view.getTag()); + CharSequence title = ((Button) view).getText(); + int rendererIndex = (int) view.getTag(); + int rendererType = mappedTrackInfo.getRendererType(rendererIndex); + boolean allowAdaptiveSelections = + rendererType == C.TRACK_TYPE_VIDEO + || (rendererType == C.TRACK_TYPE_AUDIO + && mappedTrackInfo.getTypeSupport(C.TRACK_TYPE_VIDEO) + == MappedTrackInfo.RENDERER_SUPPORT_NO_TRACKS); + Pair dialogPair = + TrackSelectionView.getDialog(this, title, trackSelector, rendererIndex); + dialogPair.second.setShowDisableOption(true); + dialogPair.second.setAllowAdaptiveSelections(allowAdaptiveSelections); + dialogPair.first.show(); } } } @@ -276,11 +309,9 @@ public class PlayerActivity extends Activity String action = intent.getAction(); Uri[] uris; String[] extensions; - Parcelable[] manifestFilters; if (ACTION_VIEW.equals(action)) { uris = new Uri[] {intent.getData()}; extensions = new String[] {intent.getStringExtra(EXTENSION_EXTRA)}; - manifestFilters = new Parcelable[] {intent.getParcelableExtra(MANIFEST_FILTER_EXTRA)}; } else if (ACTION_VIEW_LIST.equals(action)) { String[] uriStrings = intent.getStringArrayExtra(URI_LIST_EXTRA); uris = new Uri[uriStrings.length]; @@ -291,10 +322,6 @@ public class PlayerActivity extends Activity if (extensions == null) { extensions = new String[uriStrings.length]; } - manifestFilters = intent.getParcelableArrayExtra(MANIFEST_FILTER_LIST_EXTRA); - if (manifestFilters == null) { - manifestFilters = new Parcelable[uriStrings.length]; - } } else { showToast(getString(R.string.unexpected_intent_action, action)); finish(); @@ -361,23 +388,14 @@ public class PlayerActivity extends Activity new DefaultRenderersFactory(this, extensionRendererMode); trackSelector = new DefaultTrackSelector(trackSelectionFactory); - trackSelectionHelper = new TrackSelectionHelper(trackSelector, trackSelectionFactory); + trackSelector.setParameters(trackSelectorParameters); lastSeenTrackGroupArray = null; player = ExoPlayerFactory.newSimpleInstance(renderersFactory, trackSelector, drmSessionManager); player.addListener(new PlayerEventListener()); - - EventLogger eventLogger = new EventLogger(trackSelector); - player.addListener(eventLogger); - player.addMetadataOutput(eventLogger); - player.addAudioDebugListener(eventLogger); - player.addVideoDebugListener(eventLogger); - if (drmSessionManager != null) { - drmSessionManager.addListener(mainHandler, eventLogger); - } - - player.setPlayWhenReady(shouldAutoPlay); + player.setPlayWhenReady(startAutoPlay); + player.addAnalyticsListener(new EventLogger(trackSelector)); playerView.setPlayer(player); playerView.setPlaybackPreparer(this); debugViewHelper = new DebugTextViewHelper(player, debugTextView); @@ -385,9 +403,7 @@ public class PlayerActivity extends Activity MediaSource[] mediaSources = new MediaSource[uris.length]; for (int i = 0; i < uris.length; i++) { - ParcelableArray manifestFilter = (ParcelableArray) manifestFilters[i]; - List filter = manifestFilter != null ? manifestFilter.asList() : null; - mediaSources[i] = buildMediaSource(uris[i], extensions[i], filter); + mediaSources[i] = buildMediaSource(uris[i], extensions[i]); } mediaSource = mediaSources.length == 1 ? mediaSources[0] : new ConcatenatingMediaSource(mediaSources); @@ -398,8 +414,7 @@ public class PlayerActivity extends Activity releaseAdsLoader(); loadedAdTagUri = adTagUri; } - MediaSource adsMediaSource = - createAdsMediaSource(mediaSource, Uri.parse(adTagUriString), eventLogger); + MediaSource adsMediaSource = createAdsMediaSource(mediaSource, Uri.parse(adTagUriString)); if (adsMediaSource != null) { mediaSource = adsMediaSource; } else { @@ -408,24 +423,21 @@ public class PlayerActivity extends Activity } else { releaseAdsLoader(); } - mediaSource.addEventListener(mainHandler, eventLogger); } - boolean haveResumePosition = resumeWindow != C.INDEX_UNSET; - if (haveResumePosition) { - player.seekTo(resumeWindow, resumePosition); + boolean haveStartPosition = startWindow != C.INDEX_UNSET; + if (haveStartPosition) { + player.seekTo(startWindow, startPosition); } - player.prepare(mediaSource, !haveResumePosition, false); - inErrorState = false; + player.prepare(mediaSource, !haveStartPosition, false); updateButtonVisibilities(); } private MediaSource buildMediaSource(Uri uri) { - return buildMediaSource(uri, null, null); + return buildMediaSource(uri, null); } @SuppressWarnings("unchecked") - private MediaSource buildMediaSource( - Uri uri, @Nullable String overrideExtension, @Nullable List manifestFilter) { + private MediaSource buildMediaSource(Uri uri, @Nullable String overrideExtension) { @ContentType int type = Util.inferContentType(uri, overrideExtension); switch (type) { case C.TYPE_DASH: @@ -433,17 +445,22 @@ public class PlayerActivity extends Activity new DefaultDashChunkSource.Factory(mediaDataSourceFactory), buildDataSourceFactory(false)) .setManifestParser( - new FilteringDashManifestParser((List) manifestFilter)) + new FilteringManifestParser<>( + new DashManifestParser(), (List) getOfflineStreamKeys(uri))) .createMediaSource(uri); case C.TYPE_SS: return new SsMediaSource.Factory( new DefaultSsChunkSource.Factory(mediaDataSourceFactory), buildDataSourceFactory(false)) - .setManifestParser(new FilteringSsManifestParser((List) manifestFilter)) + .setManifestParser( + new FilteringManifestParser<>( + new SsManifestParser(), (List) getOfflineStreamKeys(uri))) .createMediaSource(uri); case C.TYPE_HLS: return new HlsMediaSource.Factory(mediaDataSourceFactory) - .setPlaylistParser(new FilteringHlsPlaylistParser((List) manifestFilter)) + .setPlaylistParser( + new FilteringManifestParser<>( + new HlsPlaylistParser(), (List) getOfflineStreamKeys(uri))) .createMediaSource(uri); case C.TYPE_OTHER: return new ExtractorMediaSource.Factory(mediaDataSourceFactory).createMediaSource(uri); @@ -453,45 +470,58 @@ public class PlayerActivity extends Activity } } + private List getOfflineStreamKeys(Uri uri) { + return ((DemoApplication) getApplication()).getDownloadTracker().getOfflineStreamKeys(uri); + } + private DefaultDrmSessionManager buildDrmSessionManagerV18( UUID uuid, String licenseUrl, String[] keyRequestPropertiesArray, boolean multiSession) throws UnsupportedDrmException { - HttpMediaDrmCallback drmCallback = new HttpMediaDrmCallback(licenseUrl, - buildHttpDataSourceFactory(false)); + HttpDataSource.Factory licenseDataSourceFactory = + ((DemoApplication) getApplication()).buildHttpDataSourceFactory(/* listener= */ null); + HttpMediaDrmCallback drmCallback = + new HttpMediaDrmCallback(licenseUrl, licenseDataSourceFactory); if (keyRequestPropertiesArray != null) { for (int i = 0; i < keyRequestPropertiesArray.length - 1; i += 2) { drmCallback.setKeyRequestProperty(keyRequestPropertiesArray[i], keyRequestPropertiesArray[i + 1]); } } - DefaultDrmSessionManager drmSessionManager = - new DefaultDrmSessionManager<>( - uuid, FrameworkMediaDrm.newInstance(uuid), drmCallback, null, multiSession); - return drmSessionManager; + return new DefaultDrmSessionManager<>( + uuid, FrameworkMediaDrm.newInstance(uuid), drmCallback, null, multiSession); } private void releasePlayer() { if (player != null) { + updateTrackSelectorParameters(); + updateStartPosition(); debugViewHelper.stop(); debugViewHelper = null; - shouldAutoPlay = player.getPlayWhenReady(); - updateResumePosition(); player.release(); player = null; mediaSource = null; trackSelector = null; - trackSelectionHelper = null; } } - private void updateResumePosition() { - resumeWindow = player.getCurrentWindowIndex(); - resumePosition = Math.max(0, player.getContentPosition()); + private void updateTrackSelectorParameters() { + if (trackSelector != null) { + trackSelectorParameters = trackSelector.getParameters(); + } } - private void clearResumePosition() { - resumeWindow = C.INDEX_UNSET; - resumePosition = C.TIME_UNSET; + private void updateStartPosition() { + if (player != null) { + startAutoPlay = player.getPlayWhenReady(); + startWindow = player.getCurrentWindowIndex(); + startPosition = Math.max(0, player.getContentPosition()); + } + } + + private void clearStartPosition() { + startAutoPlay = true; + startWindow = C.INDEX_UNSET; + startPosition = C.TIME_UNSET; } /** @@ -506,21 +536,8 @@ public class PlayerActivity extends Activity .buildDataSourceFactory(useBandwidthMeter ? BANDWIDTH_METER : null); } - /** - * Returns a new HttpDataSource factory. - * - * @param useBandwidthMeter Whether to set {@link #BANDWIDTH_METER} as a listener to the new - * DataSource factory. - * @return A new HttpDataSource factory. - */ - private HttpDataSource.Factory buildHttpDataSourceFactory(boolean useBandwidthMeter) { - return ((DemoApplication) getApplication()) - .buildHttpDataSourceFactory(useBandwidthMeter ? BANDWIDTH_METER : null); - } - /** Returns an ads media source, reusing the ads loader if one exists. */ - private @Nullable MediaSource createAdsMediaSource( - MediaSource mediaSource, Uri adTagUri, EventLogger eventLogger) { + private @Nullable MediaSource createAdsMediaSource(MediaSource mediaSource, Uri adTagUri) { // Load the extension source using reflection so the demo app doesn't have to depend on it. // The ads loader is reused for multiple playbacks, so that ad playback can resume. try { @@ -550,8 +567,7 @@ public class PlayerActivity extends Activity return new int[] {C.TYPE_DASH, C.TYPE_SS, C.TYPE_HLS, C.TYPE_OTHER}; } }; - return new AdsMediaSource( - mediaSource, adMediaSourceFactory, adsLoader, adUiViewGroup, mainHandler, eventLogger); + return new AdsMediaSource(mediaSource, adMediaSourceFactory, adsLoader, adUiViewGroup); } catch (ClassNotFoundException e) { // IMA extension not loaded. return null; @@ -582,20 +598,20 @@ public class PlayerActivity extends Activity return; } - for (int i = 0; i < mappedTrackInfo.length; i++) { + for (int i = 0; i < mappedTrackInfo.getRendererCount(); i++) { TrackGroupArray trackGroups = mappedTrackInfo.getTrackGroups(i); if (trackGroups.length != 0) { Button button = new Button(this); int label; switch (player.getRendererType(i)) { case C.TRACK_TYPE_AUDIO: - label = R.string.audio; + label = R.string.exo_track_selection_title_audio; break; case C.TRACK_TYPE_VIDEO: - label = R.string.video; + label = R.string.exo_track_selection_title_video; break; case C.TRACK_TYPE_TEXT: - label = R.string.text; + label = R.string.exo_track_selection_title_text; break; default: continue; @@ -646,48 +662,20 @@ public class PlayerActivity extends Activity @Override public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { - if (inErrorState) { - // This will only occur if the user has performed a seek whilst in the error state. Update - // the resume position so that if the user then retries, playback will resume from the - // position to which they seeked. - updateResumePosition(); + if (player.getPlaybackError() != null) { + // The user has performed a seek whilst in the error state. Update the resume position so + // that if the user then retries, playback resumes from the position to which they seeked. + updateStartPosition(); } } @Override public void onPlayerError(ExoPlaybackException e) { - String errorString = null; - if (e.type == ExoPlaybackException.TYPE_RENDERER) { - Exception cause = e.getRendererException(); - if (cause instanceof DecoderInitializationException) { - // Special case for decoder initialization failures. - DecoderInitializationException decoderInitializationException = - (DecoderInitializationException) cause; - if (decoderInitializationException.decoderName == null) { - if (decoderInitializationException.getCause() instanceof DecoderQueryException) { - errorString = getString(R.string.error_querying_decoders); - } else if (decoderInitializationException.secureDecoderRequired) { - errorString = getString(R.string.error_no_secure_decoder, - decoderInitializationException.mimeType); - } else { - errorString = getString(R.string.error_no_decoder, - decoderInitializationException.mimeType); - } - } else { - errorString = getString(R.string.error_instantiating_decoder, - decoderInitializationException.decoderName); - } - } - } - if (errorString != null) { - showToast(errorString); - } - inErrorState = true; if (isBehindLiveWindow(e)) { - clearResumePosition(); + clearStartPosition(); initializePlayer(); } else { - updateResumePosition(); + updateStartPosition(); updateButtonVisibilities(); showControls(); } @@ -700,11 +688,11 @@ public class PlayerActivity extends Activity if (trackGroups != lastSeenTrackGroupArray) { MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo(); if (mappedTrackInfo != null) { - if (mappedTrackInfo.getTrackTypeRendererSupport(C.TRACK_TYPE_VIDEO) + if (mappedTrackInfo.getTypeSupport(C.TRACK_TYPE_VIDEO) == MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) { showToast(R.string.error_unsupported_video); } - if (mappedTrackInfo.getTrackTypeRendererSupport(C.TRACK_TYPE_AUDIO) + if (mappedTrackInfo.getTypeSupport(C.TRACK_TYPE_AUDIO) == MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) { showToast(R.string.error_unsupported_audio); } @@ -712,7 +700,40 @@ public class PlayerActivity extends Activity lastSeenTrackGroupArray = trackGroups; } } + } + private class PlayerErrorMessageProvider implements ErrorMessageProvider { + + @Override + public Pair getErrorMessage(ExoPlaybackException e) { + String errorString = getString(R.string.error_generic); + if (e.type == ExoPlaybackException.TYPE_RENDERER) { + Exception cause = e.getRendererException(); + if (cause instanceof DecoderInitializationException) { + // Special case for decoder initialization failures. + DecoderInitializationException decoderInitializationException = + (DecoderInitializationException) cause; + if (decoderInitializationException.decoderName == null) { + if (decoderInitializationException.getCause() instanceof DecoderQueryException) { + errorString = getString(R.string.error_querying_decoders); + } else if (decoderInitializationException.secureDecoderRequired) { + errorString = + getString( + R.string.error_no_secure_decoder, decoderInitializationException.mimeType); + } else { + errorString = + getString(R.string.error_no_decoder, decoderInitializationException.mimeType); + } + } else { + errorString = + getString( + R.string.error_instantiating_decoder, + decoderInitializationException.decoderName); + } + } + } + return Pair.create(0, errorString); + } } } diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java index 5febb949f1..5524f98257 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java @@ -24,15 +24,17 @@ import android.os.AsyncTask; import android.os.Bundle; import android.util.JsonReader; import android.util.Log; -import android.view.LayoutInflater; import android.view.View; +import android.view.View.OnClickListener; import android.view.ViewGroup; import android.widget.BaseExpandableListAdapter; import android.widget.ExpandableListView; import android.widget.ExpandableListView.OnChildClickListener; +import android.widget.ImageButton; import android.widget.TextView; import android.widget.Toast; import com.google.android.exoplayer2.ParserException; +import com.google.android.exoplayer2.offline.DownloadService; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSourceInputStream; import com.google.android.exoplayer2.upstream.DataSpec; @@ -44,19 +46,27 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; -/** - * An activity for selecting from a list of samples. - */ -public class SampleChooserActivity extends Activity { +/** An activity for selecting from a list of media samples. */ +public class SampleChooserActivity extends Activity + implements DownloadTracker.Listener, OnChildClickListener { private static final String TAG = "SampleChooserActivity"; + private DownloadTracker downloadTracker; + private SampleAdapter sampleAdapter; + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.sample_chooser_activity); + sampleAdapter = new SampleAdapter(); + ExpandableListView sampleListView = findViewById(R.id.sample_list); + sampleListView.setAdapter(sampleAdapter); + sampleListView.setOnChildClickListener(this); + Intent intent = getIntent(); String dataUri = intent.getDataString(); String[] uris; @@ -79,8 +89,32 @@ public class SampleChooserActivity extends Activity { uriList.toArray(uris); Arrays.sort(uris); } + + downloadTracker = ((DemoApplication) getApplication()).getDownloadTracker(); SampleListLoader loaderTask = new SampleListLoader(); loaderTask.execute(uris); + + // Ping the download service in case it's not running (but should be). + startService( + new Intent(this, DemoDownloadService.class).setAction(DownloadService.ACTION_INIT)); + } + + @Override + public void onStart() { + super.onStart(); + downloadTracker.addListener(this); + sampleAdapter.notifyDataSetChanged(); + } + + @Override + public void onStop() { + downloadTracker.removeListener(this); + super.onStop(); + } + + @Override + public void onDownloadsChanged() { + sampleAdapter.notifyDataSetChanged(); } private void onSampleGroups(final List groups, boolean sawError) { @@ -88,20 +122,44 @@ public class SampleChooserActivity extends Activity { Toast.makeText(getApplicationContext(), R.string.sample_list_load_error, Toast.LENGTH_LONG) .show(); } - ExpandableListView sampleList = findViewById(R.id.sample_list); - sampleList.setAdapter(new SampleAdapter(this, groups)); - sampleList.setOnChildClickListener(new OnChildClickListener() { - @Override - public boolean onChildClick(ExpandableListView parent, View view, int groupPosition, - int childPosition, long id) { - onSampleSelected(groups.get(groupPosition).samples.get(childPosition)); - return true; - } - }); + sampleAdapter.setSampleGroups(groups); } - private void onSampleSelected(Sample sample) { + @Override + public boolean onChildClick( + ExpandableListView parent, View view, int groupPosition, int childPosition, long id) { + Sample sample = (Sample) view.getTag(); startActivity(sample.buildIntent(this)); + return true; + } + + private void onSampleDownloadButtonClicked(Sample sample) { + int downloadUnsupportedStringId = getDownloadUnsupportedStringId(sample); + if (downloadUnsupportedStringId != 0) { + Toast.makeText(getApplicationContext(), downloadUnsupportedStringId, Toast.LENGTH_LONG) + .show(); + } else { + UriSample uriSample = (UriSample) sample; + downloadTracker.toggleDownload(this, sample.name, uriSample.uri, uriSample.extension); + } + } + + private int getDownloadUnsupportedStringId(Sample sample) { + if (sample instanceof PlaylistSample) { + return R.string.download_playlist_unsupported; + } + UriSample uriSample = (UriSample) sample; + if (uriSample.drmInfo != null) { + return R.string.download_drm_unsupported; + } + if (uriSample.adTagUri != null) { + return R.string.download_ads_unsupported; + } + String scheme = uriSample.uri.getScheme(); + if (!("http".equals(scheme) || "https".equals(scheme))) { + return R.string.download_scheme_unsupported; + } + return 0; } private final class SampleListLoader extends AsyncTask> { @@ -175,7 +233,7 @@ public class SampleChooserActivity extends Activity { private Sample readEntry(JsonReader reader, boolean insidePlaylist) throws IOException { String sampleName = null; - String uri = null; + Uri uri = null; String extension = null; String drmScheme = null; String drmLicenseUrl = null; @@ -194,7 +252,7 @@ public class SampleChooserActivity extends Activity { sampleName = reader.nextString(); break; case "uri": - uri = reader.nextString(); + uri = Uri.parse(reader.nextString()); break; case "extension": extension = reader.nextString(); @@ -278,14 +336,17 @@ public class SampleChooserActivity extends Activity { } - private static final class SampleAdapter extends BaseExpandableListAdapter { + private final class SampleAdapter extends BaseExpandableListAdapter implements OnClickListener { - private final Context context; - private final List sampleGroups; + private List sampleGroups; - public SampleAdapter(Context context, List sampleGroups) { - this.context = context; + public SampleAdapter() { + sampleGroups = Collections.emptyList(); + } + + public void setSampleGroups(List sampleGroups) { this.sampleGroups = sampleGroups; + notifyDataSetChanged(); } @Override @@ -303,10 +364,12 @@ public class SampleChooserActivity extends Activity { View convertView, ViewGroup parent) { View view = convertView; if (view == null) { - view = LayoutInflater.from(context).inflate(android.R.layout.simple_list_item_1, parent, - false); + view = getLayoutInflater().inflate(R.layout.sample_list_item, parent, false); + View downloadButton = view.findViewById(R.id.download_button); + downloadButton.setOnClickListener(this); + downloadButton.setFocusable(false); } - ((TextView) view).setText(getChild(groupPosition, childPosition).name); + initializeChildView(view, getChild(groupPosition, childPosition)); return view; } @@ -330,8 +393,9 @@ public class SampleChooserActivity extends Activity { ViewGroup parent) { View view = convertView; if (view == null) { - view = LayoutInflater.from(context).inflate(android.R.layout.simple_expandable_list_item_1, - parent, false); + view = + getLayoutInflater() + .inflate(android.R.layout.simple_expandable_list_item_1, parent, false); } ((TextView) view).setText(getGroup(groupPosition).title); return view; @@ -352,6 +416,25 @@ public class SampleChooserActivity extends Activity { return true; } + @Override + public void onClick(View view) { + onSampleDownloadButtonClicked((Sample) view.getTag()); + } + + private void initializeChildView(View view, Sample sample) { + view.setTag(sample); + TextView sampleTitle = view.findViewById(R.id.sample_title); + sampleTitle.setText(sample.name); + + boolean canDownload = getDownloadUnsupportedStringId(sample) == 0; + boolean isDownloaded = canDownload && downloadTracker.isDownloaded(((UriSample) sample).uri); + ImageButton downloadButton = view.findViewById(R.id.download_button); + downloadButton.setTag(sample); + downloadButton.setColorFilter( + canDownload ? (isDownloaded ? 0xFF42A5F5 : 0xFFBDBDBD) : 0xFFEEEEEE); + downloadButton.setImageResource( + isDownloaded ? R.drawable.ic_download_done : R.drawable.ic_download); + } } private static final class SampleGroup { @@ -420,7 +503,7 @@ public class SampleChooserActivity extends Activity { private static final class UriSample extends Sample { - public final String uri; + public final Uri uri; public final String extension; public final String adTagUri; @@ -429,7 +512,7 @@ public class SampleChooserActivity extends Activity { boolean preferExtensionDecoders, String abrAlgorithm, DrmInfo drmInfo, - String uri, + Uri uri, String extension, String adTagUri) { super(name, preferExtensionDecoders, abrAlgorithm, drmInfo); @@ -441,7 +524,7 @@ public class SampleChooserActivity extends Activity { @Override public Intent buildIntent(Context context) { return super.buildIntent(context) - .setData(Uri.parse(uri)) + .setData(uri) .putExtra(PlayerActivity.EXTENSION_EXTRA, extension) .putExtra(PlayerActivity.AD_TAG_URI_EXTRA, adTagUri) .setAction(PlayerActivity.ACTION_VIEW); @@ -468,7 +551,7 @@ public class SampleChooserActivity extends Activity { String[] uris = new String[children.length]; String[] extensions = new String[children.length]; for (int i = 0; i < children.length; i++) { - uris[i] = children[i].uri; + uris[i] = children[i].uri.toString(); extensions[i] = children[i].extension; } return super.buildIntent(context) diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionHelper.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionHelper.java deleted file mode 100644 index 89b008dbfe..0000000000 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionHelper.java +++ /dev/null @@ -1,255 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.demo; - -import android.annotation.SuppressLint; -import android.app.Activity; -import android.app.AlertDialog; -import android.content.Context; -import android.content.DialogInterface; -import android.content.res.TypedArray; -import android.util.Pair; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.CheckedTextView; -import com.google.android.exoplayer2.RendererCapabilities; -import com.google.android.exoplayer2.source.TrackGroup; -import com.google.android.exoplayer2.source.TrackGroupArray; -import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; -import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.SelectionOverride; -import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo; -import com.google.android.exoplayer2.trackselection.TrackSelection; -import java.util.Arrays; - -/** - * Helper class for displaying track selection dialogs. - */ -/* package */ final class TrackSelectionHelper implements View.OnClickListener, - DialogInterface.OnClickListener { - - private final DefaultTrackSelector selector; - private final TrackSelection.Factory trackSelectionFactory; - - private MappedTrackInfo trackInfo; - private int rendererIndex; - private TrackGroupArray trackGroups; - private boolean[] trackGroupsAdaptive; - private boolean isDisabled; - private SelectionOverride override; - - private CheckedTextView disableView; - private CheckedTextView defaultView; - private CheckedTextView[][] trackViews; - - /** - * @param selector The track selector. - * @param trackSelectionFactory A factory for overriding {@link TrackSelection}s. - */ - public TrackSelectionHelper( - DefaultTrackSelector selector, TrackSelection.Factory trackSelectionFactory) { - this.selector = selector; - this.trackSelectionFactory = trackSelectionFactory; - } - - /** - * Shows the selection dialog for a given renderer. - * - * @param activity The parent activity. - * @param title The dialog's title. - * @param trackInfo The current track information. - * @param rendererIndex The index of the renderer. - */ - public void showSelectionDialog(Activity activity, CharSequence title, MappedTrackInfo trackInfo, - int rendererIndex) { - this.trackInfo = trackInfo; - this.rendererIndex = rendererIndex; - - trackGroups = trackInfo.getTrackGroups(rendererIndex); - trackGroupsAdaptive = new boolean[trackGroups.length]; - for (int i = 0; i < trackGroups.length; i++) { - trackGroupsAdaptive[i] = - trackInfo.getAdaptiveSupport(rendererIndex, i, false) - != RendererCapabilities.ADAPTIVE_NOT_SUPPORTED - && trackGroups.get(i).length > 1; - } - isDisabled = selector.getRendererDisabled(rendererIndex); - override = selector.getSelectionOverride(rendererIndex, trackGroups); - - AlertDialog.Builder builder = new AlertDialog.Builder(activity); - builder.setTitle(title) - .setView(buildView(builder.getContext())) - .setPositiveButton(android.R.string.ok, this) - .setNegativeButton(android.R.string.cancel, null) - .create() - .show(); - } - - @SuppressLint("InflateParams") - private View buildView(Context context) { - LayoutInflater inflater = LayoutInflater.from(context); - View view = inflater.inflate(R.layout.track_selection_dialog, null); - ViewGroup root = view.findViewById(R.id.root); - - TypedArray attributeArray = context.getTheme().obtainStyledAttributes( - new int[] {android.R.attr.selectableItemBackground}); - int selectableItemBackgroundResourceId = attributeArray.getResourceId(0, 0); - attributeArray.recycle(); - - // View for disabling the renderer. - disableView = (CheckedTextView) inflater.inflate( - android.R.layout.simple_list_item_single_choice, root, false); - disableView.setBackgroundResource(selectableItemBackgroundResourceId); - disableView.setText(R.string.selection_disabled); - disableView.setFocusable(true); - disableView.setOnClickListener(this); - root.addView(disableView); - - // View for clearing the override to allow the selector to use its default selection logic. - defaultView = (CheckedTextView) inflater.inflate( - android.R.layout.simple_list_item_single_choice, root, false); - defaultView.setBackgroundResource(selectableItemBackgroundResourceId); - defaultView.setText(R.string.selection_default); - defaultView.setFocusable(true); - defaultView.setOnClickListener(this); - root.addView(inflater.inflate(R.layout.list_divider, root, false)); - root.addView(defaultView); - - // Per-track views. - trackViews = new CheckedTextView[trackGroups.length][]; - for (int groupIndex = 0; groupIndex < trackGroups.length; groupIndex++) { - TrackGroup group = trackGroups.get(groupIndex); - boolean groupIsAdaptive = trackGroupsAdaptive[groupIndex]; - trackViews[groupIndex] = new CheckedTextView[group.length]; - for (int trackIndex = 0; trackIndex < group.length; trackIndex++) { - if (trackIndex == 0) { - root.addView(inflater.inflate(R.layout.list_divider, root, false)); - } - int trackViewLayoutId = groupIsAdaptive ? android.R.layout.simple_list_item_multiple_choice - : android.R.layout.simple_list_item_single_choice; - CheckedTextView trackView = (CheckedTextView) inflater.inflate( - trackViewLayoutId, root, false); - trackView.setBackgroundResource(selectableItemBackgroundResourceId); - trackView.setText(DemoUtil.buildTrackName(group.getFormat(trackIndex))); - if (trackInfo.getTrackFormatSupport(rendererIndex, groupIndex, trackIndex) - == RendererCapabilities.FORMAT_HANDLED) { - trackView.setFocusable(true); - trackView.setTag(Pair.create(groupIndex, trackIndex)); - trackView.setOnClickListener(this); - } else { - trackView.setFocusable(false); - trackView.setEnabled(false); - } - trackViews[groupIndex][trackIndex] = trackView; - root.addView(trackView); - } - } - - updateViews(); - return view; - } - - private void updateViews() { - disableView.setChecked(isDisabled); - defaultView.setChecked(!isDisabled && override == null); - for (int i = 0; i < trackViews.length; i++) { - for (int j = 0; j < trackViews[i].length; j++) { - trackViews[i][j].setChecked(override != null && override.groupIndex == i - && override.containsTrack(j)); - } - } - } - - // DialogInterface.OnClickListener - - @Override - public void onClick(DialogInterface dialog, int which) { - selector.setRendererDisabled(rendererIndex, isDisabled); - if (override != null) { - selector.setSelectionOverride(rendererIndex, trackGroups, override); - } else { - selector.clearSelectionOverrides(rendererIndex); - } - } - - // View.OnClickListener - - @Override - public void onClick(View view) { - if (view == disableView) { - isDisabled = true; - override = null; - } else if (view == defaultView) { - isDisabled = false; - override = null; - } else { - isDisabled = false; - @SuppressWarnings("unchecked") - Pair tag = (Pair) view.getTag(); - int groupIndex = tag.first; - int trackIndex = tag.second; - if (!trackGroupsAdaptive[groupIndex] || override == null - || override.groupIndex != groupIndex) { - setOverride(groupIndex, trackIndex); - } else { - // The group being modified is adaptive and we already have a non-null override. - boolean isEnabled = ((CheckedTextView) view).isChecked(); - int overrideLength = override.length; - if (isEnabled) { - // Remove the track from the override. - if (overrideLength == 1) { - // The last track is being removed, so the override becomes empty. - override = null; - isDisabled = true; - } else { - setOverride(groupIndex, getTracksRemoving(override, trackIndex)); - } - } else { - // Add the track to the override. - setOverride(groupIndex, getTracksAdding(override, trackIndex)); - } - } - } - // Update the views with the new state. - updateViews(); - } - - private void setOverride(int group, int... tracks) { - override = new SelectionOverride(trackSelectionFactory, group, tracks); - } - - // Track array manipulation. - - private static int[] getTracksAdding(SelectionOverride override, int addedTrack) { - int[] tracks = override.tracks; - tracks = Arrays.copyOf(tracks, tracks.length + 1); - tracks[tracks.length - 1] = addedTrack; - return tracks; - } - - private static int[] getTracksRemoving(SelectionOverride override, int removedTrack) { - int[] tracks = new int[override.length - 1]; - int trackCount = 0; - for (int i = 0; i < tracks.length + 1; i++) { - int track = override.tracks[i]; - if (track != removedTrack) { - tracks[trackCount++] = track; - } - } - return tracks; - } - -} diff --git a/demos/main/src/main/res/drawable-hdpi/ic_download.png b/demos/main/src/main/res/drawable-hdpi/ic_download.png new file mode 100644 index 0000000000..fa3ebbb310 Binary files /dev/null and b/demos/main/src/main/res/drawable-hdpi/ic_download.png differ diff --git a/demos/main/src/main/res/drawable-hdpi/ic_download_done.png b/demos/main/src/main/res/drawable-hdpi/ic_download_done.png new file mode 100644 index 0000000000..fa0ec9dd68 Binary files /dev/null and b/demos/main/src/main/res/drawable-hdpi/ic_download_done.png differ diff --git a/demos/main/src/main/res/drawable-mdpi/ic_download.png b/demos/main/src/main/res/drawable-mdpi/ic_download.png new file mode 100644 index 0000000000..c8a2039c58 Binary files /dev/null and b/demos/main/src/main/res/drawable-mdpi/ic_download.png differ diff --git a/demos/main/src/main/res/drawable-mdpi/ic_download_done.png b/demos/main/src/main/res/drawable-mdpi/ic_download_done.png new file mode 100644 index 0000000000..08073a2a6d Binary files /dev/null and b/demos/main/src/main/res/drawable-mdpi/ic_download_done.png differ diff --git a/demos/main/src/main/res/drawable-xhdpi/ic_download.png b/demos/main/src/main/res/drawable-xhdpi/ic_download.png new file mode 100644 index 0000000000..671e0b3ece Binary files /dev/null and b/demos/main/src/main/res/drawable-xhdpi/ic_download.png differ diff --git a/demos/main/src/main/res/drawable-xhdpi/ic_download_done.png b/demos/main/src/main/res/drawable-xhdpi/ic_download_done.png new file mode 100644 index 0000000000..2339c0bf16 Binary files /dev/null and b/demos/main/src/main/res/drawable-xhdpi/ic_download_done.png differ diff --git a/demos/main/src/main/res/drawable-xxhdpi/ic_download.png b/demos/main/src/main/res/drawable-xxhdpi/ic_download.png new file mode 100644 index 0000000000..f02715177a Binary files /dev/null and b/demos/main/src/main/res/drawable-xxhdpi/ic_download.png differ diff --git a/demos/main/src/main/res/drawable-xxhdpi/ic_download_done.png b/demos/main/src/main/res/drawable-xxhdpi/ic_download_done.png new file mode 100644 index 0000000000..b631a00088 Binary files /dev/null and b/demos/main/src/main/res/drawable-xxhdpi/ic_download_done.png differ diff --git a/demos/main/src/main/res/drawable-xxxhdpi/ic_download.png b/demos/main/src/main/res/drawable-xxxhdpi/ic_download.png new file mode 100644 index 0000000000..6602791545 Binary files /dev/null and b/demos/main/src/main/res/drawable-xxxhdpi/ic_download.png differ diff --git a/demos/main/src/main/res/drawable-xxxhdpi/ic_download_done.png b/demos/main/src/main/res/drawable-xxxhdpi/ic_download_done.png new file mode 100644 index 0000000000..52fe8f6990 Binary files /dev/null and b/demos/main/src/main/res/drawable-xxxhdpi/ic_download_done.png differ diff --git a/demos/main/src/main/res/layout/sample_list_item.xml b/demos/main/src/main/res/layout/sample_list_item.xml new file mode 100644 index 0000000000..cdb0058688 --- /dev/null +++ b/demos/main/src/main/res/layout/sample_list_item.xml @@ -0,0 +1,38 @@ + + + + + + + + + diff --git a/extensions/mediasession/src/main/res/values-gl-rES/strings.xml b/demos/main/src/main/res/layout/start_download_dialog.xml similarity index 62% rename from extensions/mediasession/src/main/res/values-gl-rES/strings.xml rename to demos/main/src/main/res/layout/start_download_dialog.xml index 6b65b3e843..acb9af5d97 100644 --- a/extensions/mediasession/src/main/res/values-gl-rES/strings.xml +++ b/demos/main/src/main/res/layout/start_download_dialog.xml @@ -1,6 +1,5 @@ - - - - "Repetir todo" - "Non repetir" - "Repetir un" - + diff --git a/demos/main/src/main/res/values/strings.xml b/demos/main/src/main/res/values/strings.xml index 42ef358a1a..eb260e6ffc 100644 --- a/demos/main/src/main/res/values/strings.xml +++ b/demos/main/src/main/res/values/strings.xml @@ -17,18 +17,10 @@ ExoPlayer - Video - - Audio - - Text - - Disabled - - Default - Unexpected intent action: %1$s + Playback failed + Unrecognized ABR algorithm Protected content not supported on API levels below 18 @@ -55,4 +47,14 @@ Playing sample without ads, as the IMA extension was not loaded + Failed to start download + + This demo app does not support downloading playlists + + This demo app does not support downloading protected content + + This demo app only supports downloading http streams + + IMA does not support offline ads + diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java index 7e0753ba10..84724cbb47 100644 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java @@ -19,6 +19,7 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.Log; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; @@ -307,6 +308,11 @@ public final class CastPlayer implements Player { return playbackState; } + @Override + public ExoPlaybackException getPlaybackError() { + return null; + } + @Override public void setPlayWhenReady(boolean playWhenReady) { if (remoteMediaClient == null) { diff --git a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java index 29bc874cd8..db980aa72b 100644 --- a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java +++ b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java @@ -280,6 +280,7 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou new SocketTimeoutException(), dataSpec, getStatus(currentUrlRequest)); } } catch (InterruptedException e) { + Thread.currentThread().interrupt(); throw new OpenException(new InterruptedIOException(e), dataSpec, Status.INVALID); } @@ -352,17 +353,18 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou if (!operation.block(readTimeoutMs)) { throw new SocketTimeoutException(); } - } catch (InterruptedException | SocketTimeoutException e) { - // If we're timing out or getting interrupted, the operation is still ongoing. - // So we'll need to replace readBuffer to avoid the possibility of it being written to by - // this operation during a subsequent request. + } catch (InterruptedException e) { + // The operation is ongoing so replace readBuffer to avoid it being written to by this + // operation during a subsequent request. readBuffer = null; + Thread.currentThread().interrupt(); throw new HttpDataSourceException( - e instanceof InterruptedException - ? new InterruptedIOException((InterruptedException) e) - : (SocketTimeoutException) e, - currentDataSpec, - HttpDataSourceException.TYPE_READ); + new InterruptedIOException(e), currentDataSpec, HttpDataSourceException.TYPE_READ); + } catch (SocketTimeoutException e) { + // The operation is ongoing so replace readBuffer to avoid it being written to by this + // operation during a subsequent request. + readBuffer = null; + throw new HttpDataSourceException(e, currentDataSpec, HttpDataSourceException.TYPE_READ); } if (exception != null) { diff --git a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetEngineWrapper.java b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetEngineWrapper.java index efe30d6525..db1394c1d6 100644 --- a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetEngineWrapper.java +++ b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetEngineWrapper.java @@ -21,6 +21,7 @@ import android.util.Log; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.reflect.Field; +import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; @@ -86,7 +87,7 @@ public final class CronetEngineWrapper { public CronetEngineWrapper(Context context, boolean preferGMSCoreCronet) { CronetEngine cronetEngine = null; @CronetEngineSource int cronetEngineSource = SOURCE_UNAVAILABLE; - List cronetProviders = CronetProvider.getAllProviders(context); + List cronetProviders = new ArrayList<>(CronetProvider.getAllProviders(context)); // Remove disabled and fallback Cronet providers from list for (int i = cronetProviders.size() - 1; i >= 0; i--) { if (!cronetProviders.get(i).isEnabled() diff --git a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java index 3e23659bf8..d7687e42ac 100644 --- a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java +++ b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java @@ -74,7 +74,12 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer { */ public FfmpegAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener, AudioSink audioSink, boolean enableFloatOutput) { - super(eventHandler, eventListener, null, false, audioSink); + super( + eventHandler, + eventListener, + /* drmSessionManager= */ null, + /* playClearSamplesWithoutKeys= */ false, + audioSink); this.enableFloatOutput = enableFloatOutput; } diff --git a/extensions/flac/build.gradle b/extensions/flac/build.gradle index 9bf8d39435..609953130b 100644 --- a/extensions/flac/build.gradle +++ b/extensions/flac/build.gradle @@ -34,6 +34,7 @@ dependencies { implementation 'com.android.support:support-annotations:' + supportLibraryVersion implementation project(modulePrefix + 'library-core') androidTestImplementation project(modulePrefix + 'testutils') + testImplementation project(modulePrefix + 'testutils-robolectric') } ext { diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java index 729a406315..34a6e6820d 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java @@ -79,7 +79,7 @@ public final class FlacExtractor implements Extractor { private static final byte[] FLAC_SIGNATURE = {'f', 'L', 'a', 'C', 0, 0, 0, 0x22}; private final Id3Peeker id3Peeker; - private final @Flags int flags; + private final boolean isId3MetadataDisabled; private FlacDecoderJni decoderJni; @@ -90,7 +90,6 @@ public final class FlacExtractor implements Extractor { private ByteBuffer outputByteBuffer; private Metadata id3Metadata; - private long id3SectionSize; private boolean metadataParsed; @@ -105,8 +104,8 @@ public final class FlacExtractor implements Extractor { * @param flags Flags that control the extractor's behavior. */ public FlacExtractor(int flags) { - this.flags = flags; id3Peeker = new Id3Peeker(); + isId3MetadataDisabled = (flags & FLAG_DISABLE_ID3_METADATA) != 0; } @Override @@ -125,24 +124,16 @@ public final class FlacExtractor implements Extractor { public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { if (input.getPosition() == 0) { id3Metadata = peekId3Data(input); - id3SectionSize = input.getPeekPosition(); } - boolean isFlacFormat = peekFlacSignature(input); - if (isFlacFormat) { - // If this is FLAC format, we should skip the whole ID3 section. - skipFullyId3Section(input); - } - return isFlacFormat; + return peekFlacSignature(input); } @Override public int read(final ExtractorInput input, PositionHolder seekPosition) throws IOException, InterruptedException { - if (input.getPosition() == 0) { + if (input.getPosition() == 0 && !isId3MetadataDisabled && id3Metadata == null) { id3Metadata = peekId3Data(input); - id3SectionSize = input.getPeekPosition(); } - skipFullyId3Section(input); decoderJni.setData(input); @@ -155,7 +146,7 @@ public final class FlacExtractor implements Extractor { } } catch (IOException e) { decoderJni.reset(0); - input.setRetryPosition(id3SectionSize, e); + input.setRetryPosition(0, e); throw e; // never executes } metadataParsed = true; @@ -163,7 +154,7 @@ public final class FlacExtractor implements Extractor { boolean isSeekable = decoderJni.getSeekPosition(0) != -1; extractorOutput.seekMap( isSeekable - ? new FlacSeekMap(streamInfo.durationUs(), decoderJni, id3SectionSize) + ? new FlacSeekMap(streamInfo.durationUs(), decoderJni) : new SeekMap.Unseekable(streamInfo.durationUs(), 0)); Format mediaFormat = Format.createAudioSampleFormat( @@ -181,7 +172,7 @@ public final class FlacExtractor implements Extractor { /* drmInitData= */ null, /* selectionFlags= */ 0, /* language= */ null, - (flags & FLAG_DISABLE_ID3_METADATA) != 0 ? null : id3Metadata); + isId3MetadataDisabled ? null : id3Metadata); trackOutput.format(mediaFormat); outputBuffer = new ParsableByteArray(streamInfo.maxDecodedFrameSize()); @@ -196,7 +187,7 @@ public final class FlacExtractor implements Extractor { } catch (IOException e) { if (lastDecodePosition >= 0) { decoderJni.reset(lastDecodePosition); - input.setRetryPosition(id3SectionSize + lastDecodePosition, e); + input.setRetryPosition(lastDecodePosition, e); } throw e; } @@ -212,12 +203,11 @@ public final class FlacExtractor implements Extractor { @Override public void seek(long position, long timeUs) { - if (position <= id3SectionSize) { + if (position == 0) { metadataParsed = false; } - long flacStreamPosition = Math.max(0, position - id3SectionSize); if (decoderJni != null) { - decoderJni.reset(flacStreamPosition); + decoderJni.reset(position); } } @@ -238,9 +228,8 @@ public final class FlacExtractor implements Extractor { @Nullable private Metadata peekId3Data(ExtractorInput input) throws IOException, InterruptedException { input.resetPeekPosition(); - boolean disableId3Frames = (flags & FLAG_DISABLE_ID3_METADATA) != 0; Id3Decoder.FramePredicate id3FramePredicate = - disableId3Frames ? Id3Decoder.NO_FRAMES_PREDICATE : null; + isId3MetadataDisabled ? Id3Decoder.NO_FRAMES_PREDICATE : null; return id3Peeker.peekId3Data(input, id3FramePredicate); } @@ -255,22 +244,14 @@ public final class FlacExtractor implements Extractor { return Arrays.equals(header, FLAC_SIGNATURE); } - /** Skips input until we have passed the whole Id3 section. */ - private void skipFullyId3Section(ExtractorInput input) throws IOException, InterruptedException { - int bytesToSkip = Math.max(0, (int) (id3SectionSize - input.getPosition())); - input.skipFully(bytesToSkip); - } - private static final class FlacSeekMap implements SeekMap { private final long durationUs; private final FlacDecoderJni decoderJni; - private final long id3SectionSize; - public FlacSeekMap(long durationUs, FlacDecoderJni decoderJni, long id3SectionSize) { + public FlacSeekMap(long durationUs, FlacDecoderJni decoderJni) { this.durationUs = durationUs; this.decoderJni = decoderJni; - this.id3SectionSize = id3SectionSize; } @Override @@ -281,8 +262,7 @@ public final class FlacExtractor implements Extractor { @Override public SeekPoints getSeekPoints(long timeUs) { // TODO: Access the seek table via JNI to return two seek points when appropriate. - return new SeekPoints( - new SeekPoint(timeUs, id3SectionSize + decoderJni.getSeekPosition(timeUs))); + return new SeekPoints(new SeekPoint(timeUs, decoderJni.getSeekPosition(timeUs))); } @Override diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index 60017a27e4..d3dbaaec96 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -52,6 +52,8 @@ import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.ads.AdPlaybackState; import com.google.android.exoplayer2.source.ads.AdPlaybackState.AdState; import com.google.android.exoplayer2.source.ads.AdsLoader; +import com.google.android.exoplayer2.source.ads.AdsMediaSource.AdLoadException; +import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; @@ -80,6 +82,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A private final Context context; private @Nullable ImaSdkSettings imaSdkSettings; + private @Nullable AdEventListener adEventListener; private int vastLoadTimeoutMs; private int mediaLoadTimeoutMs; @@ -108,6 +111,18 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A return this; } + /** + * Sets a listener for ad events that will be passed to {@link + * AdsManager#addAdEventListener(AdEventListener)}. + * + * @param adEventListener The ad event listener. + * @return This builder, for convenience. + */ + public Builder setAdEventListener(AdEventListener adEventListener) { + this.adEventListener = Assertions.checkNotNull(adEventListener); + return this; + } + /** * Sets the VAST load timeout, in milliseconds. * @@ -144,7 +159,13 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A */ public ImaAdsLoader buildForAdTag(Uri adTagUri) { return new ImaAdsLoader( - context, adTagUri, imaSdkSettings, null, vastLoadTimeoutMs, mediaLoadTimeoutMs); + context, + adTagUri, + imaSdkSettings, + null, + vastLoadTimeoutMs, + mediaLoadTimeoutMs, + adEventListener); } /** @@ -156,7 +177,13 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A */ public ImaAdsLoader buildForAdsResponse(String adsResponse) { return new ImaAdsLoader( - context, null, imaSdkSettings, adsResponse, vastLoadTimeoutMs, mediaLoadTimeoutMs); + context, + null, + imaSdkSettings, + adsResponse, + vastLoadTimeoutMs, + mediaLoadTimeoutMs, + adEventListener); } } @@ -214,6 +241,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A private final @Nullable String adsResponse; private final int vastLoadTimeoutMs; private final int mediaLoadTimeoutMs; + private final @Nullable AdEventListener adEventListener; private final Timeline.Period period; private final List adCallbacks; private final ImaSdkFactory imaSdkFactory; @@ -229,7 +257,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A private VideoProgressUpdate lastAdProgress; private AdsManager adsManager; - private AdErrorEvent pendingAdErrorEvent; + private AdLoadException pendingAdLoadError; private Timeline timeline; private long contentDurationMs; private int podIndexOffset; @@ -308,7 +336,8 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A /* imaSdkSettings= */ null, /* adsResponse= */ null, /* vastLoadTimeoutMs= */ TIMEOUT_UNSET, - /* mediaLoadTimeoutMs= */ TIMEOUT_UNSET); + /* mediaLoadTimeoutMs= */ TIMEOUT_UNSET, + /* adEventListener= */ null); } /** @@ -330,7 +359,8 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A imaSdkSettings, /* adsResponse= */ null, /* vastLoadTimeoutMs= */ TIMEOUT_UNSET, - /* mediaLoadTimeoutMs= */ TIMEOUT_UNSET); + /* mediaLoadTimeoutMs= */ TIMEOUT_UNSET, + /* adEventListener= */ null); } private ImaAdsLoader( @@ -339,12 +369,14 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A @Nullable ImaSdkSettings imaSdkSettings, @Nullable String adsResponse, int vastLoadTimeoutMs, - int mediaLoadTimeoutMs) { + int mediaLoadTimeoutMs, + @Nullable AdEventListener adEventListener) { Assertions.checkArgument(adTagUri != null || adsResponse != null); this.adTagUri = adTagUri; this.adsResponse = adsResponse; this.vastLoadTimeoutMs = vastLoadTimeoutMs; this.mediaLoadTimeoutMs = mediaLoadTimeoutMs; + this.adEventListener = adEventListener; period = new Timeline.Period(); adCallbacks = new ArrayList<>(1); imaSdkFactory = ImaSdkFactory.getInstance(); @@ -500,6 +532,9 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A this.adsManager = adsManager; adsManager.addAdErrorListener(this); adsManager.addAdEventListener(this); + if (adEventListener != null) { + adsManager.addAdEventListener(adEventListener); + } if (player != null) { // If a player is attached already, start playback immediately. try { @@ -544,13 +579,13 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A updateAdPlaybackState(); } else if (isAdGroupLoadError(error)) { try { - handleAdGroupLoadError(); + handleAdGroupLoadError(error); } catch (Exception e) { maybeNotifyInternalError("onAdError", e); } } - if (pendingAdErrorEvent == null) { - pendingAdErrorEvent = adErrorEvent; + if (pendingAdLoadError == null) { + pendingAdLoadError = AdLoadException.createForAllAds(error); } maybeNotifyPendingAdLoadError(); } @@ -937,9 +972,10 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A break; case LOG: Map adData = adEvent.getAdData(); - Log.i(TAG, "Log AdEvent: " + adData); + String message = "AdEvent: " + adData; + Log.i(TAG, message); if ("adLoadError".equals(adData.get("type"))) { - handleAdGroupLoadError(); + handleAdGroupLoadError(new IOException(message)); } break; case ALL_ADS_COMPLETED: @@ -1011,7 +1047,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A } } - private void handleAdGroupLoadError() { + private void handleAdGroupLoadError(Exception error) { int adGroupIndex = this.adGroupIndex == C.INDEX_UNSET ? expectedAdGroupIndex : this.adGroupIndex; if (adGroupIndex == C.INDEX_UNSET) { @@ -1033,6 +1069,9 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A } } updateAdPlaybackState(); + if (pendingAdLoadError == null) { + pendingAdLoadError = AdLoadException.createForAdGroup(error, adGroupIndex); + } } private void handleAdPrepareError(int adGroupIndex, int adIndexInAdGroup, Exception exception) { @@ -1111,21 +1150,15 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A } private void maybeNotifyPendingAdLoadError() { - if (pendingAdErrorEvent != null) { - if (eventListener != null) { - eventListener.onAdLoadError( - new IOException("Ad error: " + pendingAdErrorEvent, pendingAdErrorEvent.getError())); - } - pendingAdErrorEvent = null; + if (pendingAdLoadError != null && eventListener != null) { + eventListener.onAdLoadError(pendingAdLoadError, new DataSpec(adTagUri)); + pendingAdLoadError = null; } } private void maybeNotifyInternalError(String name, Exception cause) { String message = "Internal error in " + name; Log.e(TAG, message, cause); - if (eventListener != null) { - eventListener.onInternalAdLoadError(new RuntimeException(message, cause)); - } // We can't recover from an unexpected error in general, so skip all remaining ads. if (adPlaybackState == null) { adPlaybackState = new AdPlaybackState(); @@ -1135,6 +1168,11 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A } } updateAdPlaybackState(); + if (eventListener != null) { + eventListener.onAdLoadError( + AdLoadException.createForUnexpected(new RuntimeException(message, cause)), + new DataSpec(adTagUri)); + } } private static long[] getAdGroupTimesUs(List cuePoints) { diff --git a/extensions/jobdispatcher/src/main/java/com/google/android/exoplayer2/ext/jobdispatcher/JobDispatcherScheduler.java b/extensions/jobdispatcher/src/main/java/com/google/android/exoplayer2/ext/jobdispatcher/JobDispatcherScheduler.java index 9232efaaa9..c6701da964 100644 --- a/extensions/jobdispatcher/src/main/java/com/google/android/exoplayer2/ext/jobdispatcher/JobDispatcherScheduler.java +++ b/extensions/jobdispatcher/src/main/java/com/google/android/exoplayer2/ext/jobdispatcher/JobDispatcherScheduler.java @@ -15,8 +15,6 @@ */ package com.google.android.exoplayer2.ext.jobdispatcher; -import android.app.Notification; -import android.app.Service; import android.content.Context; import android.content.Intent; import android.os.Bundle; @@ -34,13 +32,8 @@ import com.google.android.exoplayer2.scheduler.Scheduler; import com.google.android.exoplayer2.util.Util; /** - * A {@link Scheduler} which uses {@link com.firebase.jobdispatcher.FirebaseJobDispatcher} to - * schedule a {@link Service} to be started when its requirements are met. The started service must - * call {@link Service#startForeground(int, Notification)} to make itself a foreground service upon - * being started, as documented by {@link Service#startForegroundService(Intent)}. - * - *

To use {@link JobDispatcherScheduler} application needs to have RECEIVE_BOOT_COMPLETED - * permission and you need to define JobDispatcherSchedulerService in your manifest: + * A {@link Scheduler} that uses {@link FirebaseJobDispatcher}. To use this scheduler, you must add + * {@link JobDispatcherSchedulerService} to your manifest: * *

{@literal
  * 
@@ -54,18 +47,6 @@ import com.google.android.exoplayer2.util.Util;
  * 
  * }
* - * The service to be scheduled must be defined in the manifest with an intent-filter: - * - *
{@literal
- * 
- *  
- *    
- *    
- *  
- * 
- * }
- * *

This Scheduler uses Google Play services but does not do any availability checks. Any uses * should be guarded with a call to {@code * GoogleApiAvailability#isGooglePlayServicesAvailable(android.content.Context)} @@ -76,44 +57,37 @@ import com.google.android.exoplayer2.util.Util; public final class JobDispatcherScheduler implements Scheduler { private static final String TAG = "JobDispatcherScheduler"; - private static final String SERVICE_ACTION = "SERVICE_ACTION"; - private static final String SERVICE_PACKAGE = "SERVICE_PACKAGE"; - private static final String REQUIREMENTS = "REQUIREMENTS"; + private static final String KEY_SERVICE_ACTION = "service_action"; + private static final String KEY_SERVICE_PACKAGE = "service_package"; + private static final String KEY_REQUIREMENTS = "requirements"; private final String jobTag; - private final Job job; private final FirebaseJobDispatcher jobDispatcher; /** - * @param context Used to create a {@link FirebaseJobDispatcher} service. - * @param requirements The requirements to execute the job. - * @param jobTag Unique tag for the job. Using the same tag as a previous job can cause that job - * to be replaced or canceled. - * @param serviceAction The action which the service will be started with. - * @param servicePackage The package of the service which contains the logic of the job. + * @param context A context. + * @param jobTag A tag for jobs scheduled by this instance. If the same tag was used by a previous + * instance, anything scheduled by the previous instance will be canceled by this instance if + * {@link #schedule(Requirements, String, String)} or {@link #cancel()} are called. */ - public JobDispatcherScheduler( - Context context, - Requirements requirements, - String jobTag, - String serviceAction, - String servicePackage) { - this.jobDispatcher = new FirebaseJobDispatcher(new GooglePlayDriver(context)); + public JobDispatcherScheduler(Context context, String jobTag) { + this.jobDispatcher = + new FirebaseJobDispatcher(new GooglePlayDriver(context.getApplicationContext())); this.jobTag = jobTag; - this.job = buildJob(jobDispatcher, requirements, jobTag, serviceAction, servicePackage); } @Override - public boolean schedule() { + public boolean schedule(Requirements requirements, String serviceAction, String servicePackage) { + Job job = buildJob(jobDispatcher, requirements, jobTag, serviceAction, servicePackage); int result = jobDispatcher.schedule(job); - logd("Scheduling JobDispatcher job: " + jobTag + " result: " + result); + logd("Scheduling job: " + jobTag + " result: " + result); return result == FirebaseJobDispatcher.SCHEDULE_RESULT_SUCCESS; } @Override public boolean cancel() { int result = jobDispatcher.cancel(jobTag); - logd("Canceling JobDispatcher job: " + jobTag + " result: " + result); + logd("Canceling job: " + jobTag + " result: " + result); return result == FirebaseJobDispatcher.CANCEL_RESULT_SUCCESS; } @@ -151,13 +125,12 @@ public final class JobDispatcherScheduler implements Scheduler { } builder.setLifetime(Lifetime.FOREVER).setReplaceCurrent(true); - // Extras, work duration. Bundle extras = new Bundle(); - extras.putString(SERVICE_ACTION, serviceAction); - extras.putString(SERVICE_PACKAGE, servicePackage); - extras.putInt(REQUIREMENTS, requirements.getRequirementsData()); - + extras.putString(KEY_SERVICE_ACTION, serviceAction); + extras.putString(KEY_SERVICE_PACKAGE, servicePackage); + extras.putInt(KEY_REQUIREMENTS, requirements.getRequirementsData()); builder.setExtras(extras); + return builder.build(); } @@ -167,26 +140,22 @@ public final class JobDispatcherScheduler implements Scheduler { } } - /** A {@link JobService} to start a service if the requirements are met. */ + /** A {@link JobService} that starts the target service if the requirements are met. */ public static final class JobDispatcherSchedulerService extends JobService { @Override public boolean onStartJob(JobParameters params) { logd("JobDispatcherSchedulerService is started"); Bundle extras = params.getExtras(); - Requirements requirements = new Requirements(extras.getInt(REQUIREMENTS)); + Requirements requirements = new Requirements(extras.getInt(KEY_REQUIREMENTS)); if (requirements.checkRequirements(this)) { - logd("requirements are met"); - String serviceAction = extras.getString(SERVICE_ACTION); - String servicePackage = extras.getString(SERVICE_PACKAGE); + logd("Requirements are met"); + String serviceAction = extras.getString(KEY_SERVICE_ACTION); + String servicePackage = extras.getString(KEY_SERVICE_PACKAGE); Intent intent = new Intent(serviceAction).setPackage(servicePackage); - logd("starting service action: " + serviceAction + " package: " + servicePackage); - if (Util.SDK_INT >= 26) { - startForegroundService(intent); - } else { - startService(intent); - } + logd("Starting service action: " + serviceAction + " package: " + servicePackage); + Util.startForegroundService(this, intent); } else { - logd("requirements are not met"); + logd("Requirements are not met"); jobFinished(params, /* needsReschedule */ true); } return false; diff --git a/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java b/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java index e513084974..03f53c263f 100644 --- a/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java +++ b/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java @@ -53,7 +53,7 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter { private @Nullable PlaybackPreparer playbackPreparer; private ControlDispatcher controlDispatcher; - private ErrorMessageProvider errorMessageProvider; + private @Nullable ErrorMessageProvider errorMessageProvider; private SurfaceHolderGlueHost surfaceHolderGlueHost; private boolean hasSurface; private boolean lastNotifiedPreparedState; @@ -110,7 +110,7 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter { * @param errorMessageProvider The {@link ErrorMessageProvider}. */ public void setErrorMessageProvider( - ErrorMessageProvider errorMessageProvider) { + @Nullable ErrorMessageProvider errorMessageProvider) { this.errorMessageProvider = errorMessageProvider; } diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java index 26e0d0e006..83fb16236d 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java @@ -334,12 +334,11 @@ public final class MediaSessionConnector { private Player player; private CustomActionProvider[] customActionProviders; private Map customActionMap; - private ErrorMessageProvider errorMessageProvider; + private @Nullable ErrorMessageProvider errorMessageProvider; private PlaybackPreparer playbackPreparer; private QueueNavigator queueNavigator; private QueueEditor queueEditor; private RatingCallback ratingCallback; - private ExoPlaybackException playbackException; /** * Creates an instance. Must be called on the same thread that is used to construct the player @@ -403,16 +402,18 @@ public final class MediaSessionConnector { /** * Sets the player to be connected to the media session. - *

- * The order in which any {@link CustomActionProvider}s are passed determines the order of the + * + *

The order in which any {@link CustomActionProvider}s are passed determines the order of the * actions published with the playback state of the session. * * @param player The player to be connected to the {@code MediaSession}. * @param playbackPreparer An optional {@link PlaybackPreparer} for preparing the player. - * @param customActionProviders An optional {@link CustomActionProvider}s to publish and handle + * @param customActionProviders Optional {@link CustomActionProvider}s to publish and handle * custom actions. */ - public void setPlayer(Player player, PlaybackPreparer playbackPreparer, + public void setPlayer( + Player player, + @Nullable PlaybackPreparer playbackPreparer, CustomActionProvider... customActionProviders) { if (this.player != null) { this.player.removeListener(exoPlayerEventListener); @@ -435,13 +436,16 @@ public final class MediaSessionConnector { } /** - * Sets the {@link ErrorMessageProvider}. + * Sets the optional {@link ErrorMessageProvider}. * * @param errorMessageProvider The error message provider. */ public void setErrorMessageProvider( - ErrorMessageProvider errorMessageProvider) { - this.errorMessageProvider = errorMessageProvider; + @Nullable ErrorMessageProvider errorMessageProvider) { + if (this.errorMessageProvider != errorMessageProvider) { + this.errorMessageProvider = errorMessageProvider; + updateMediaSessionPlaybackState(); + } } /** @@ -451,9 +455,11 @@ public final class MediaSessionConnector { * @param queueNavigator The queue navigator. */ public void setQueueNavigator(QueueNavigator queueNavigator) { - unregisterCommandReceiver(this.queueNavigator); - this.queueNavigator = queueNavigator; - registerCommandReceiver(queueNavigator); + if (this.queueNavigator != queueNavigator) { + unregisterCommandReceiver(this.queueNavigator); + this.queueNavigator = queueNavigator; + registerCommandReceiver(queueNavigator); + } } /** @@ -462,11 +468,13 @@ public final class MediaSessionConnector { * @param queueEditor The queue editor. */ public void setQueueEditor(QueueEditor queueEditor) { - unregisterCommandReceiver(this.queueEditor); - this.queueEditor = queueEditor; - registerCommandReceiver(queueEditor); - mediaSession.setFlags(queueEditor == null ? BASE_MEDIA_SESSION_FLAGS - : EDITOR_MEDIA_SESSION_FLAGS); + if (this.queueEditor != queueEditor) { + unregisterCommandReceiver(this.queueEditor); + this.queueEditor = queueEditor; + registerCommandReceiver(queueEditor); + mediaSession.setFlags( + queueEditor == null ? BASE_MEDIA_SESSION_FLAGS : EDITOR_MEDIA_SESSION_FLAGS); + } } /** @@ -475,9 +483,11 @@ public final class MediaSessionConnector { * @param ratingCallback The rating callback. */ public void setRatingCallback(RatingCallback ratingCallback) { - unregisterCommandReceiver(this.ratingCallback); - this.ratingCallback = ratingCallback; - registerCommandReceiver(this.ratingCallback); + if (this.ratingCallback != ratingCallback) { + unregisterCommandReceiver(this.ratingCallback); + this.ratingCallback = ratingCallback; + registerCommandReceiver(this.ratingCallback); + } } private void registerCommandReceiver(CommandReceiver commandReceiver) { @@ -514,16 +524,16 @@ public final class MediaSessionConnector { } customActionMap = Collections.unmodifiableMap(currentActions); - int sessionPlaybackState = playbackException != null ? PlaybackStateCompat.STATE_ERROR - : mapPlaybackState(player.getPlaybackState(), player.getPlayWhenReady()); - if (playbackException != null) { - if (errorMessageProvider != null) { - Pair message = errorMessageProvider.getErrorMessage(playbackException); - builder.setErrorMessage(message.first, message.second); - } - if (player.getPlaybackState() != Player.STATE_IDLE) { - playbackException = null; - } + int playbackState = player.getPlaybackState(); + ExoPlaybackException playbackError = + playbackState == Player.STATE_IDLE ? player.getPlaybackError() : null; + int sessionPlaybackState = + playbackError != null + ? PlaybackStateCompat.STATE_ERROR + : mapPlaybackState(player.getPlaybackState(), player.getPlayWhenReady()); + if (playbackError != null && errorMessageProvider != null) { + Pair message = errorMessageProvider.getErrorMessage(playbackError); + builder.setErrorMessage(message.first, message.second); } long activeQueueItemId = queueNavigator != null ? queueNavigator.getActiveQueueItemId(player) : MediaSessionCompat.QueueItem.UNKNOWN_ID; @@ -699,12 +709,6 @@ public final class MediaSessionConnector { updateMediaSessionPlaybackState(); } - @Override - public void onPlayerError(ExoPlaybackException error) { - playbackException = error; - updateMediaSessionPlaybackState(); - } - @Override public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { if (currentWindowIndex != player.getCurrentWindowIndex()) { diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java index 1b9bd3ecd9..26a7b6150a 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java @@ -73,10 +73,11 @@ public abstract class TimelineQueueNavigator implements MediaSessionConnector.Qu /** * Gets the {@link MediaDescriptionCompat} for a given timeline window index. * + * @param player The current player. * @param windowIndex The timeline window index for which to provide a description. * @return A {@link MediaDescriptionCompat}. */ - public abstract MediaDescriptionCompat getMediaDescription(int windowIndex); + public abstract MediaDescriptionCompat getMediaDescription(Player player, int windowIndex); @Override public long getSupportedQueueNavigatorActions(Player player) { @@ -185,7 +186,7 @@ public abstract class TimelineQueueNavigator implements MediaSessionConnector.Qu windowCount - queueSize); List queue = new ArrayList<>(); for (int i = startIndex; i < startIndex + queueSize; i++) { - queue.add(new MediaSessionCompat.QueueItem(getMediaDescription(i), i)); + queue.add(new MediaSessionCompat.QueueItem(getMediaDescription(player, i), i)); } mediaSession.setQueue(queue); activeQueueItemId = currentWindowIndex; diff --git a/extensions/mediasession/src/main/res/values-af/strings.xml b/extensions/mediasession/src/main/res/values-af/strings.xml index 4ef78cd84f..92d171cfdc 100644 --- a/extensions/mediasession/src/main/res/values-af/strings.xml +++ b/extensions/mediasession/src/main/res/values-af/strings.xml @@ -1,21 +1,6 @@ - - + - "Herhaal alles" - "Herhaal niks" - "Herhaal een" + Herhaal niks + Herhaal een + Herhaal alles diff --git a/extensions/mediasession/src/main/res/values-am/strings.xml b/extensions/mediasession/src/main/res/values-am/strings.xml index 531f605584..54509a65ab 100644 --- a/extensions/mediasession/src/main/res/values-am/strings.xml +++ b/extensions/mediasession/src/main/res/values-am/strings.xml @@ -1,21 +1,6 @@ - - + - "ሁሉንም ድገም" - "ምንም አትድገም" - "አንዱን ድገም" + ምንም አትድገም + አንድ ድገም + ሁሉንም ድገም diff --git a/extensions/mediasession/src/main/res/values-ar/strings.xml b/extensions/mediasession/src/main/res/values-ar/strings.xml index 0101a746e0..707ad41a16 100644 --- a/extensions/mediasession/src/main/res/values-ar/strings.xml +++ b/extensions/mediasession/src/main/res/values-ar/strings.xml @@ -1,21 +1,6 @@ - - + - "تكرار الكل" - "عدم التكرار" - "تكرار مقطع واحد" + عدم التكرار + تكرار مقطع صوتي واحد + تكرار الكل diff --git a/extensions/mediasession/src/main/res/values-az-rAZ/strings.xml b/extensions/mediasession/src/main/res/values-az-rAZ/strings.xml deleted file mode 100644 index 34408143fa..0000000000 --- a/extensions/mediasession/src/main/res/values-az-rAZ/strings.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - "Bütün təkrarlayın" - "Təkrar bir" - "Heç bir təkrar" - diff --git a/extensions/mediasession/src/main/res/values-az/strings.xml b/extensions/mediasession/src/main/res/values-az/strings.xml new file mode 100644 index 0000000000..33c1f341ba --- /dev/null +++ b/extensions/mediasession/src/main/res/values-az/strings.xml @@ -0,0 +1,6 @@ + + + Heç biri təkrarlanmasın + Biri təkrarlansın + Hamısı təkrarlansın + diff --git a/extensions/mediasession/src/main/res/values-b+sr+Latn/strings.xml b/extensions/mediasession/src/main/res/values-b+sr+Latn/strings.xml index 67a51cf85e..dcdcb9d977 100644 --- a/extensions/mediasession/src/main/res/values-b+sr+Latn/strings.xml +++ b/extensions/mediasession/src/main/res/values-b+sr+Latn/strings.xml @@ -1,21 +1,6 @@ - - + - "Ponovi sve" - "Ne ponavljaj nijednu" - "Ponovi jednu" + Ne ponavljaj nijednu + Ponovi jednu + Ponovi sve diff --git a/extensions/mediasession/src/main/res/values-be-rBY/strings.xml b/extensions/mediasession/src/main/res/values-be-rBY/strings.xml deleted file mode 100644 index 2f05607235..0000000000 --- a/extensions/mediasession/src/main/res/values-be-rBY/strings.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - "Паўтарыць усё" - "Паўтараць ні" - "Паўтарыць адзін" - diff --git a/extensions/mediasession/src/main/res/values-be/strings.xml b/extensions/mediasession/src/main/res/values-be/strings.xml new file mode 100644 index 0000000000..380794f281 --- /dev/null +++ b/extensions/mediasession/src/main/res/values-be/strings.xml @@ -0,0 +1,6 @@ + + + Не паўтараць нічога + Паўтарыць адзін элемент + Паўтарыць усе + diff --git a/extensions/mediasession/src/main/res/values-bg/strings.xml b/extensions/mediasession/src/main/res/values-bg/strings.xml index 16910d640a..8a639c6cff 100644 --- a/extensions/mediasession/src/main/res/values-bg/strings.xml +++ b/extensions/mediasession/src/main/res/values-bg/strings.xml @@ -1,21 +1,6 @@ - - + - "Повтаряне на всички" - "Без повтаряне" - "Повтаряне на един елемент" + Без повтаряне + Повтаряне на един елемент + Повтаряне на всички diff --git a/extensions/mediasession/src/main/res/values-bn-rBD/strings.xml b/extensions/mediasession/src/main/res/values-bn-rBD/strings.xml deleted file mode 100644 index 8872b464c6..0000000000 --- a/extensions/mediasession/src/main/res/values-bn-rBD/strings.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - "সবগুলির পুনরাবৃত্তি করুন" - "একটিরও পুনরাবৃত্তি করবেন না" - "একটির পুনরাবৃত্তি করুন" - diff --git a/extensions/mediasession/src/main/res/values-bn/strings.xml b/extensions/mediasession/src/main/res/values-bn/strings.xml new file mode 100644 index 0000000000..c39f11e570 --- /dev/null +++ b/extensions/mediasession/src/main/res/values-bn/strings.xml @@ -0,0 +1,6 @@ + + + কোনও আইটেম আবার চালাবেন না + একটি আইটেম আবার চালান + সবগুলি আইটেম আবার চালান + diff --git a/extensions/mediasession/src/main/res/values-bs-rBA/strings.xml b/extensions/mediasession/src/main/res/values-bs-rBA/strings.xml deleted file mode 100644 index d0bf068573..0000000000 --- a/extensions/mediasession/src/main/res/values-bs-rBA/strings.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - "Ponovite sve" - "Ne ponavljaju" - "Ponovite jedan" - diff --git a/extensions/mediasession/src/main/res/values-bs/strings.xml b/extensions/mediasession/src/main/res/values-bs/strings.xml new file mode 100644 index 0000000000..44b5cb5dd6 --- /dev/null +++ b/extensions/mediasession/src/main/res/values-bs/strings.xml @@ -0,0 +1,6 @@ + + + Ne ponavljaj + Ponovi jedno + Ponovi sve + diff --git a/extensions/mediasession/src/main/res/values-ca/strings.xml b/extensions/mediasession/src/main/res/values-ca/strings.xml index 89414d736e..cdb41b2b0a 100644 --- a/extensions/mediasession/src/main/res/values-ca/strings.xml +++ b/extensions/mediasession/src/main/res/values-ca/strings.xml @@ -1,21 +1,6 @@ - - + - "Repeteix-ho tot" - "No en repeteixis cap" - "Repeteix-ne un" + No en repeteixis cap + Repeteix una + Repeteix tot diff --git a/extensions/mediasession/src/main/res/values-cs/strings.xml b/extensions/mediasession/src/main/res/values-cs/strings.xml index 784d872570..4d25b3a3ba 100644 --- a/extensions/mediasession/src/main/res/values-cs/strings.xml +++ b/extensions/mediasession/src/main/res/values-cs/strings.xml @@ -1,21 +1,6 @@ - - + - "Opakovat vše" - "Neopakovat" - "Opakovat jednu položku" + Neopakovat + Opakovat jednu + Opakovat vše diff --git a/extensions/mediasession/src/main/res/values-da/strings.xml b/extensions/mediasession/src/main/res/values-da/strings.xml index 2c9784d122..f74409a50b 100644 --- a/extensions/mediasession/src/main/res/values-da/strings.xml +++ b/extensions/mediasession/src/main/res/values-da/strings.xml @@ -1,21 +1,6 @@ - - + - "Gentag alle" - "Gentag ingen" - "Gentag en" + Gentag ingen + Gentag én + Gentag alle diff --git a/extensions/mediasession/src/main/res/values-de/strings.xml b/extensions/mediasession/src/main/res/values-de/strings.xml index c11e449665..af3564cb41 100644 --- a/extensions/mediasession/src/main/res/values-de/strings.xml +++ b/extensions/mediasession/src/main/res/values-de/strings.xml @@ -1,21 +1,6 @@ - - + - "Alle wiederholen" - "Keinen Titel wiederholen" - "Einen Titel wiederholen" + Keinen wiederholen + Einen wiederholen + Alle wiederholen diff --git a/extensions/mediasession/src/main/res/values-el/strings.xml b/extensions/mediasession/src/main/res/values-el/strings.xml index 6279af5d64..e4f6666622 100644 --- a/extensions/mediasession/src/main/res/values-el/strings.xml +++ b/extensions/mediasession/src/main/res/values-el/strings.xml @@ -1,21 +1,6 @@ - - + - "Επανάληψη όλων" - "Καμία επανάληψη" - "Επανάληψη ενός στοιχείου" + Καμία επανάληψη + Επανάληψη ενός κομματιού + Επανάληψη όλων diff --git a/extensions/mediasession/src/main/res/values-en-rAU/strings.xml b/extensions/mediasession/src/main/res/values-en-rAU/strings.xml index a3fccf8b52..4170902688 100644 --- a/extensions/mediasession/src/main/res/values-en-rAU/strings.xml +++ b/extensions/mediasession/src/main/res/values-en-rAU/strings.xml @@ -1,21 +1,6 @@ - - + - "Repeat all" - "Repeat none" - "Repeat one" + Repeat none + Repeat one + Repeat all diff --git a/extensions/mediasession/src/main/res/values-en-rGB/strings.xml b/extensions/mediasession/src/main/res/values-en-rGB/strings.xml index a3fccf8b52..4170902688 100644 --- a/extensions/mediasession/src/main/res/values-en-rGB/strings.xml +++ b/extensions/mediasession/src/main/res/values-en-rGB/strings.xml @@ -1,21 +1,6 @@ - - + - "Repeat all" - "Repeat none" - "Repeat one" + Repeat none + Repeat one + Repeat all diff --git a/extensions/mediasession/src/main/res/values-en-rIN/strings.xml b/extensions/mediasession/src/main/res/values-en-rIN/strings.xml index a3fccf8b52..4170902688 100644 --- a/extensions/mediasession/src/main/res/values-en-rIN/strings.xml +++ b/extensions/mediasession/src/main/res/values-en-rIN/strings.xml @@ -1,21 +1,6 @@ - - + - "Repeat all" - "Repeat none" - "Repeat one" + Repeat none + Repeat one + Repeat all diff --git a/extensions/mediasession/src/main/res/values-es-rUS/strings.xml b/extensions/mediasession/src/main/res/values-es-rUS/strings.xml index 0fe29d3d5a..700e6de4e2 100644 --- a/extensions/mediasession/src/main/res/values-es-rUS/strings.xml +++ b/extensions/mediasession/src/main/res/values-es-rUS/strings.xml @@ -1,21 +1,6 @@ - - + - "Repetir todo" - "No repetir" - "Repetir uno" + No repetir + Repetir uno + Repetir todo diff --git a/extensions/mediasession/src/main/res/values-es/strings.xml b/extensions/mediasession/src/main/res/values-es/strings.xml index 0fe29d3d5a..700e6de4e2 100644 --- a/extensions/mediasession/src/main/res/values-es/strings.xml +++ b/extensions/mediasession/src/main/res/values-es/strings.xml @@ -1,21 +1,6 @@ - - + - "Repetir todo" - "No repetir" - "Repetir uno" + No repetir + Repetir uno + Repetir todo diff --git a/extensions/mediasession/src/main/res/values-et-rEE/strings.xml b/extensions/mediasession/src/main/res/values-et-rEE/strings.xml deleted file mode 100644 index 1bc3b59706..0000000000 --- a/extensions/mediasession/src/main/res/values-et-rEE/strings.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - "Korda kõike" - "Ära korda midagi" - "Korda ühte" - diff --git a/extensions/mediasession/src/main/res/values-et/strings.xml b/extensions/mediasession/src/main/res/values-et/strings.xml new file mode 100644 index 0000000000..1f629e68f5 --- /dev/null +++ b/extensions/mediasession/src/main/res/values-et/strings.xml @@ -0,0 +1,6 @@ + + + Ära korda ühtegi + Korda ühte + Korda kõiki + diff --git a/extensions/mediasession/src/main/res/values-eu-rES/strings.xml b/extensions/mediasession/src/main/res/values-eu-rES/strings.xml deleted file mode 100644 index f15f03160f..0000000000 --- a/extensions/mediasession/src/main/res/values-eu-rES/strings.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - "Errepikatu guztiak" - "Ez errepikatu" - "Errepikatu bat" - diff --git a/extensions/mediasession/src/main/res/values-eu/strings.xml b/extensions/mediasession/src/main/res/values-eu/strings.xml new file mode 100644 index 0000000000..34c1b9cde9 --- /dev/null +++ b/extensions/mediasession/src/main/res/values-eu/strings.xml @@ -0,0 +1,6 @@ + + + Ez errepikatu + Errepikatu bat + Errepikatu guztiak + diff --git a/extensions/mediasession/src/main/res/values-fa/strings.xml b/extensions/mediasession/src/main/res/values-fa/strings.xml index e37a08de64..96e8a1e819 100644 --- a/extensions/mediasession/src/main/res/values-fa/strings.xml +++ b/extensions/mediasession/src/main/res/values-fa/strings.xml @@ -1,21 +1,6 @@ - - + - "تکرار همه" - "تکرار هیچ‌کدام" - "یک‌بار تکرار" + تکرار هیچ‌کدام + یکبار تکرار + تکرار همه diff --git a/extensions/mediasession/src/main/res/values-fi/strings.xml b/extensions/mediasession/src/main/res/values-fi/strings.xml index c920827976..db1aca3f5c 100644 --- a/extensions/mediasession/src/main/res/values-fi/strings.xml +++ b/extensions/mediasession/src/main/res/values-fi/strings.xml @@ -1,21 +1,6 @@ - - + - "Toista kaikki" - "Toista ei mitään" - "Toista yksi" + Ei uudelleentoistoa + Toista yksi uudelleen + Toista kaikki uudelleen diff --git a/extensions/mediasession/src/main/res/values-fr-rCA/strings.xml b/extensions/mediasession/src/main/res/values-fr-rCA/strings.xml index c5191e74a9..17e17fc8b5 100644 --- a/extensions/mediasession/src/main/res/values-fr-rCA/strings.xml +++ b/extensions/mediasession/src/main/res/values-fr-rCA/strings.xml @@ -1,21 +1,6 @@ - - + - "Tout lire en boucle" - "Aucune répétition" - "Répéter un élément" + Ne rien lire en boucle + Lire une chanson en boucle + Tout lire en boucle diff --git a/extensions/mediasession/src/main/res/values-fr/strings.xml b/extensions/mediasession/src/main/res/values-fr/strings.xml index 1d76358d1f..9e35e35a0c 100644 --- a/extensions/mediasession/src/main/res/values-fr/strings.xml +++ b/extensions/mediasession/src/main/res/values-fr/strings.xml @@ -1,21 +1,6 @@ - - + - "Tout lire en boucle" - "Ne rien lire en boucle" - "Lire en boucle un élément" + Ne rien lire en boucle + Lire un titre en boucle + Tout lire en boucle diff --git a/extensions/mediasession/src/main/res/values-gl/strings.xml b/extensions/mediasession/src/main/res/values-gl/strings.xml new file mode 100644 index 0000000000..633e9669a7 --- /dev/null +++ b/extensions/mediasession/src/main/res/values-gl/strings.xml @@ -0,0 +1,6 @@ + + + Non repetir + Repetir unha pista + Repetir todas as pistas + diff --git a/extensions/mediasession/src/main/res/values-gu-rIN/strings.xml b/extensions/mediasession/src/main/res/values-gu-rIN/strings.xml deleted file mode 100644 index 0eb9cab37e..0000000000 --- a/extensions/mediasession/src/main/res/values-gu-rIN/strings.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - "બધા પુનરાવર્તન કરો" - "કંઈ પુનરાવર્તન કરો" - "એક પુનરાવર્તન કરો" - diff --git a/extensions/mediasession/src/main/res/values-gu/strings.xml b/extensions/mediasession/src/main/res/values-gu/strings.xml new file mode 100644 index 0000000000..ab17db814e --- /dev/null +++ b/extensions/mediasession/src/main/res/values-gu/strings.xml @@ -0,0 +1,6 @@ + + + કોઈ રિપીટ કરતા નહીં + એક રિપીટ કરો + બધાને રિપીટ કરો + diff --git a/extensions/mediasession/src/main/res/values-hi/strings.xml b/extensions/mediasession/src/main/res/values-hi/strings.xml index 8ce336d5e5..66415ed45d 100644 --- a/extensions/mediasession/src/main/res/values-hi/strings.xml +++ b/extensions/mediasession/src/main/res/values-hi/strings.xml @@ -1,21 +1,6 @@ - - + - "सभी को दोहराएं" - "कुछ भी न दोहराएं" - "एक दोहराएं" + किसी को न दोहराएं + एक को दोहराएं + सभी को दोहराएं diff --git a/extensions/mediasession/src/main/res/values-hr/strings.xml b/extensions/mediasession/src/main/res/values-hr/strings.xml index 9f995ec15b..3b3f8170db 100644 --- a/extensions/mediasession/src/main/res/values-hr/strings.xml +++ b/extensions/mediasession/src/main/res/values-hr/strings.xml @@ -1,21 +1,6 @@ - - + - "Ponovi sve" - "Bez ponavljanja" - "Ponovi jedno" + Bez ponavljanja + Ponovi jedno + Ponovi sve diff --git a/extensions/mediasession/src/main/res/values-hu/strings.xml b/extensions/mediasession/src/main/res/values-hu/strings.xml index 2335ade72e..392959a462 100644 --- a/extensions/mediasession/src/main/res/values-hu/strings.xml +++ b/extensions/mediasession/src/main/res/values-hu/strings.xml @@ -1,21 +1,6 @@ - - + - "Összes ismétlése" - "Nincs ismétlés" - "Egy ismétlése" + Nincs ismétlés + Egy szám ismétlése + Összes szám ismétlése diff --git a/extensions/mediasession/src/main/res/values-hy-rAM/strings.xml b/extensions/mediasession/src/main/res/values-hy-rAM/strings.xml deleted file mode 100644 index 19a89e6c87..0000000000 --- a/extensions/mediasession/src/main/res/values-hy-rAM/strings.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - "կրկնել այն ամենը" - "Չկրկնել" - "Կրկնել մեկը" - diff --git a/extensions/mediasession/src/main/res/values-hy/strings.xml b/extensions/mediasession/src/main/res/values-hy/strings.xml new file mode 100644 index 0000000000..ba4fff8fd2 --- /dev/null +++ b/extensions/mediasession/src/main/res/values-hy/strings.xml @@ -0,0 +1,6 @@ + + + Չկրկնել + Կրկնել մեկը + Կրկնել բոլորը + diff --git a/extensions/mediasession/src/main/res/values-in/strings.xml b/extensions/mediasession/src/main/res/values-in/strings.xml index 093a7f8576..1388877293 100644 --- a/extensions/mediasession/src/main/res/values-in/strings.xml +++ b/extensions/mediasession/src/main/res/values-in/strings.xml @@ -1,21 +1,6 @@ - - + - "Ulangi Semua" - "Jangan Ulangi" - "Ulangi Satu" + Jangan ulangi + Ulangi 1 + Ulangi semua diff --git a/extensions/mediasession/src/main/res/values-is-rIS/strings.xml b/extensions/mediasession/src/main/res/values-is-rIS/strings.xml deleted file mode 100644 index b200abbdb2..0000000000 --- a/extensions/mediasession/src/main/res/values-is-rIS/strings.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - "Endurtaka allt" - "Endurtaka ekkert" - "Endurtaka eitt" - diff --git a/extensions/mediasession/src/main/res/values-is/strings.xml b/extensions/mediasession/src/main/res/values-is/strings.xml new file mode 100644 index 0000000000..9db4df88dd --- /dev/null +++ b/extensions/mediasession/src/main/res/values-is/strings.xml @@ -0,0 +1,6 @@ + + + Endurtaka ekkert + Endurtaka eitt + Endurtaka allt + diff --git a/extensions/mediasession/src/main/res/values-it/strings.xml b/extensions/mediasession/src/main/res/values-it/strings.xml index c0682519f9..8922453204 100644 --- a/extensions/mediasession/src/main/res/values-it/strings.xml +++ b/extensions/mediasession/src/main/res/values-it/strings.xml @@ -1,21 +1,6 @@ - - + - "Ripeti tutti" - "Non ripetere nessuno" - "Ripeti uno" + Non ripetere nulla + Ripeti uno + Ripeti tutto diff --git a/extensions/mediasession/src/main/res/values-iw/strings.xml b/extensions/mediasession/src/main/res/values-iw/strings.xml index 5cf23d5a4c..193a3ac606 100644 --- a/extensions/mediasession/src/main/res/values-iw/strings.xml +++ b/extensions/mediasession/src/main/res/values-iw/strings.xml @@ -1,21 +1,6 @@ - - + - "חזור על הכל" - "אל תחזור על כלום" - "חזור על פריט אחד" + אל תחזור על אף פריט + חזור על פריט אחד + חזור על הכול diff --git a/extensions/mediasession/src/main/res/values-ja/strings.xml b/extensions/mediasession/src/main/res/values-ja/strings.xml index 6f543fbdee..d1cd378d53 100644 --- a/extensions/mediasession/src/main/res/values-ja/strings.xml +++ b/extensions/mediasession/src/main/res/values-ja/strings.xml @@ -1,21 +1,6 @@ - - + - "全曲を繰り返し" - "繰り返しなし" - "1曲を繰り返し" + リピートなし + 1 曲をリピート + 全曲をリピート diff --git a/extensions/mediasession/src/main/res/values-ka-rGE/strings.xml b/extensions/mediasession/src/main/res/values-ka-rGE/strings.xml deleted file mode 100644 index 96656612a7..0000000000 --- a/extensions/mediasession/src/main/res/values-ka-rGE/strings.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - "გამეორება ყველა" - "გაიმეორეთ არცერთი" - "გაიმეორეთ ერთი" - diff --git a/extensions/mediasession/src/main/res/values-ka/strings.xml b/extensions/mediasession/src/main/res/values-ka/strings.xml new file mode 100644 index 0000000000..5acf78cbf2 --- /dev/null +++ b/extensions/mediasession/src/main/res/values-ka/strings.xml @@ -0,0 +1,6 @@ + + + არცერთის გამეორება + ერთის გამეორება + ყველას გამეორება + diff --git a/extensions/mediasession/src/main/res/values-kk-rKZ/strings.xml b/extensions/mediasession/src/main/res/values-kk-rKZ/strings.xml deleted file mode 100644 index be4140120d..0000000000 --- a/extensions/mediasession/src/main/res/values-kk-rKZ/strings.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - "Барлығын қайталау" - "Ешқайсысын қайталамау" - "Біреуін қайталау" - diff --git a/extensions/mediasession/src/main/res/values-kk/strings.xml b/extensions/mediasession/src/main/res/values-kk/strings.xml new file mode 100644 index 0000000000..d13ea893a0 --- /dev/null +++ b/extensions/mediasession/src/main/res/values-kk/strings.xml @@ -0,0 +1,6 @@ + + + Ешқайсысын қайталамау + Біреуін қайталау + Барлығын қайталау + diff --git a/extensions/mediasession/src/main/res/values-km-rKH/strings.xml b/extensions/mediasession/src/main/res/values-km-rKH/strings.xml deleted file mode 100644 index dd4b734e30..0000000000 --- a/extensions/mediasession/src/main/res/values-km-rKH/strings.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - "ធ្វើ​ម្ដង​ទៀត​ទាំងអស់" - "មិន​ធ្វើ​ឡើង​វិញ" - "ធ្វើ​​ឡើងវិញ​ម្ដង" - diff --git a/extensions/mediasession/src/main/res/values-km/strings.xml b/extensions/mediasession/src/main/res/values-km/strings.xml new file mode 100644 index 0000000000..8cf4a2d344 --- /dev/null +++ b/extensions/mediasession/src/main/res/values-km/strings.xml @@ -0,0 +1,6 @@ + + + មិន​លេង​ឡើងវិញ + លេង​ឡើង​វិញ​ម្ដង + លេង​ឡើងវិញ​ទាំងអស់ + diff --git a/extensions/mediasession/src/main/res/values-kn-rIN/strings.xml b/extensions/mediasession/src/main/res/values-kn-rIN/strings.xml deleted file mode 100644 index 3d79aca9e2..0000000000 --- a/extensions/mediasession/src/main/res/values-kn-rIN/strings.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - "ಎಲ್ಲವನ್ನು ಪುನರಾವರ್ತಿಸಿ" - "ಯಾವುದನ್ನೂ ಪುನರಾವರ್ತಿಸಬೇಡಿ" - "ಒಂದನ್ನು ಪುನರಾವರ್ತಿಸಿ" - diff --git a/extensions/mediasession/src/main/res/values-kn/strings.xml b/extensions/mediasession/src/main/res/values-kn/strings.xml new file mode 100644 index 0000000000..2dea20044a --- /dev/null +++ b/extensions/mediasession/src/main/res/values-kn/strings.xml @@ -0,0 +1,6 @@ + + + ಯಾವುದನ್ನೂ ಪುನರಾವರ್ತಿಸಬೇಡಿ + ಒಂದನ್ನು ಪುನರಾವರ್ತಿಸಿ + ಎಲ್ಲವನ್ನು ಪುನರಾವರ್ತಿಸಿ + diff --git a/extensions/mediasession/src/main/res/values-ko/strings.xml b/extensions/mediasession/src/main/res/values-ko/strings.xml index d269937771..b561abc1d7 100644 --- a/extensions/mediasession/src/main/res/values-ko/strings.xml +++ b/extensions/mediasession/src/main/res/values-ko/strings.xml @@ -1,21 +1,6 @@ - - + - "전체 반복" - "반복 안함" - "한 항목 반복" + 반복 안함 + 현재 미디어 반복 + 모두 반복 diff --git a/extensions/mediasession/src/main/res/values-ky-rKG/strings.xml b/extensions/mediasession/src/main/res/values-ky-rKG/strings.xml deleted file mode 100644 index a8978ecc61..0000000000 --- a/extensions/mediasession/src/main/res/values-ky-rKG/strings.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - "Баарын кайталоо" - "Эч бирин кайталабоо" - "Бирөөнү кайталоо" - diff --git a/extensions/mediasession/src/main/res/values-ky/strings.xml b/extensions/mediasession/src/main/res/values-ky/strings.xml new file mode 100644 index 0000000000..9352c7c4ca --- /dev/null +++ b/extensions/mediasession/src/main/res/values-ky/strings.xml @@ -0,0 +1,6 @@ + + + Кайталанбасын + Бирөөнү кайталоо + Баарын кайталоо + diff --git a/extensions/mediasession/src/main/res/values-lo-rLA/strings.xml b/extensions/mediasession/src/main/res/values-lo-rLA/strings.xml deleted file mode 100644 index 950a9ba097..0000000000 --- a/extensions/mediasession/src/main/res/values-lo-rLA/strings.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - "ຫຼິ້ນ​ຊ້ຳ​ທັງ​ໝົດ" - "​ບໍ່ຫຼິ້ນ​ຊ້ຳ" - "ຫຼິ້ນ​ຊ້ຳ" - diff --git a/extensions/mediasession/src/main/res/values-lo/strings.xml b/extensions/mediasession/src/main/res/values-lo/strings.xml new file mode 100644 index 0000000000..e89ee44e5e --- /dev/null +++ b/extensions/mediasession/src/main/res/values-lo/strings.xml @@ -0,0 +1,6 @@ + + + ບໍ່ຫຼິ້ນຊ້ຳ + ຫຼິ້ນຊໍ້າ + ຫຼິ້ນຊ້ຳທັງໝົດ + diff --git a/extensions/mediasession/src/main/res/values-lt/strings.xml b/extensions/mediasession/src/main/res/values-lt/strings.xml index ae8f1cf8c3..20eb0e9b1f 100644 --- a/extensions/mediasession/src/main/res/values-lt/strings.xml +++ b/extensions/mediasession/src/main/res/values-lt/strings.xml @@ -1,21 +1,6 @@ - - + - "Kartoti viską" - "Nekartoti nieko" - "Kartoti vieną" + Nekartoti nieko + Kartoti vieną + Kartoti viską diff --git a/extensions/mediasession/src/main/res/values-lv/strings.xml b/extensions/mediasession/src/main/res/values-lv/strings.xml index a69f6a0ad5..44cddec124 100644 --- a/extensions/mediasession/src/main/res/values-lv/strings.xml +++ b/extensions/mediasession/src/main/res/values-lv/strings.xml @@ -1,21 +1,6 @@ - - + - "Atkārtot visu" - "Neatkārtot nevienu" - "Atkārtot vienu" + Neatkārtot nevienu + Atkārtot vienu + Atkārtot visu diff --git a/extensions/mediasession/src/main/res/values-mk-rMK/strings.xml b/extensions/mediasession/src/main/res/values-mk-rMK/strings.xml deleted file mode 100644 index ddf2a60c20..0000000000 --- a/extensions/mediasession/src/main/res/values-mk-rMK/strings.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - "Повтори ги сите" - "Не повторувај ниту една" - "Повтори една" - diff --git a/extensions/mediasession/src/main/res/values-mk/strings.xml b/extensions/mediasession/src/main/res/values-mk/strings.xml new file mode 100644 index 0000000000..0906c35cc3 --- /dev/null +++ b/extensions/mediasession/src/main/res/values-mk/strings.xml @@ -0,0 +1,6 @@ + + + Не повторувај ниту една + Повтори една + Повтори ги сите + diff --git a/extensions/mediasession/src/main/res/values-ml-rIN/strings.xml b/extensions/mediasession/src/main/res/values-ml-rIN/strings.xml deleted file mode 100644 index 6f869e2931..0000000000 --- a/extensions/mediasession/src/main/res/values-ml-rIN/strings.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - "എല്ലാം ആവർത്തിക്കുക" - "ഒന്നും ആവർത്തിക്കരുത്" - "ഒന്ന് ആവർത്തിക്കുക" - diff --git a/extensions/mediasession/src/main/res/values-ml/strings.xml b/extensions/mediasession/src/main/res/values-ml/strings.xml new file mode 100644 index 0000000000..1f3f023c88 --- /dev/null +++ b/extensions/mediasession/src/main/res/values-ml/strings.xml @@ -0,0 +1,6 @@ + + + ഒന്നും ആവർത്തിക്കരുത് + ഒരെണ്ണം ആവർത്തിക്കുക + എല്ലാം ആവർത്തിക്കുക + diff --git a/extensions/mediasession/src/main/res/values-mn-rMN/strings.xml b/extensions/mediasession/src/main/res/values-mn-rMN/strings.xml deleted file mode 100644 index 8d3074b91a..0000000000 --- a/extensions/mediasession/src/main/res/values-mn-rMN/strings.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - "Бүгдийг давтах" - "Алийг нь ч давтахгүй" - "Нэгийг давтах" - diff --git a/extensions/mediasession/src/main/res/values-mn/strings.xml b/extensions/mediasession/src/main/res/values-mn/strings.xml new file mode 100644 index 0000000000..4167e40548 --- /dev/null +++ b/extensions/mediasession/src/main/res/values-mn/strings.xml @@ -0,0 +1,6 @@ + + + Алийг нь ч дахин тоглуулахгүй + Одоогийн тоглуулж буй медиаг дахин тоглуулах + Бүгдийг нь дахин тоглуулах + diff --git a/extensions/mediasession/src/main/res/values-mr-rIN/strings.xml b/extensions/mediasession/src/main/res/values-mr-rIN/strings.xml deleted file mode 100644 index 6e4bfccc16..0000000000 --- a/extensions/mediasession/src/main/res/values-mr-rIN/strings.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - "सर्व पुनरावृत्ती करा" - "काहीही पुनरावृत्ती करू नका" - "एक पुनरावृत्ती करा" - diff --git a/extensions/mediasession/src/main/res/values-mr/strings.xml b/extensions/mediasession/src/main/res/values-mr/strings.xml new file mode 100644 index 0000000000..fe42b346bf --- /dev/null +++ b/extensions/mediasession/src/main/res/values-mr/strings.xml @@ -0,0 +1,6 @@ + + + रीपीट करू नका + एक रीपीट करा + सर्व रीपीट करा + diff --git a/extensions/mediasession/src/main/res/values-ms/strings.xml b/extensions/mediasession/src/main/res/values-ms/strings.xml new file mode 100644 index 0000000000..5735d50947 --- /dev/null +++ b/extensions/mediasession/src/main/res/values-ms/strings.xml @@ -0,0 +1,6 @@ + + + Jangan ulang + Ulang satu + Ulang semua + diff --git a/extensions/mediasession/src/main/res/values-my-rMM/strings.xml b/extensions/mediasession/src/main/res/values-my-rMM/strings.xml deleted file mode 100644 index aeb1375ebf..0000000000 --- a/extensions/mediasession/src/main/res/values-my-rMM/strings.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - "အားလုံး ထပ်တလဲလဲဖွင့်ရန်" - "ထပ်တလဲလဲမဖွင့်ရန်" - "တစ်ခုအား ထပ်တလဲလဲဖွင့်ရန်" - diff --git a/extensions/mediasession/src/main/res/values-my/strings.xml b/extensions/mediasession/src/main/res/values-my/strings.xml new file mode 100644 index 0000000000..11677e06f7 --- /dev/null +++ b/extensions/mediasession/src/main/res/values-my/strings.xml @@ -0,0 +1,6 @@ + + + မည်သည်ကိုမျှ ပြန်မကျော့ရန် + တစ်ခုကို ပြန်ကျော့ရန် + အားလုံး ပြန်ကျော့ရန် + diff --git a/extensions/mediasession/src/main/res/values-nb/strings.xml b/extensions/mediasession/src/main/res/values-nb/strings.xml index 10f334b226..eab972792f 100644 --- a/extensions/mediasession/src/main/res/values-nb/strings.xml +++ b/extensions/mediasession/src/main/res/values-nb/strings.xml @@ -1,21 +1,6 @@ - - + - "Gjenta alle" - "Ikke gjenta noen" - "Gjenta én" + Ikke gjenta noen + Gjenta én + Gjenta alle diff --git a/extensions/mediasession/src/main/res/values-ne-rNP/strings.xml b/extensions/mediasession/src/main/res/values-ne-rNP/strings.xml deleted file mode 100644 index 6d81ce5684..0000000000 --- a/extensions/mediasession/src/main/res/values-ne-rNP/strings.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - "सबै दोहोर्याउनुहोस्" - "कुनै पनि नदोहोर्याउनुहोस्" - "एउटा दोहोर्याउनुहोस्" - diff --git a/extensions/mediasession/src/main/res/values-ne/strings.xml b/extensions/mediasession/src/main/res/values-ne/strings.xml new file mode 100644 index 0000000000..0ef156ed57 --- /dev/null +++ b/extensions/mediasession/src/main/res/values-ne/strings.xml @@ -0,0 +1,6 @@ + + + कुनै पनि नदोहोर्‍याउनुहोस् + एउटा दोहोर्‍याउनुहोस् + सबै दोहोर्‍याउनुहोस् + diff --git a/extensions/mediasession/src/main/res/values-nl/strings.xml b/extensions/mediasession/src/main/res/values-nl/strings.xml index 55997be098..b1309f40d6 100644 --- a/extensions/mediasession/src/main/res/values-nl/strings.xml +++ b/extensions/mediasession/src/main/res/values-nl/strings.xml @@ -1,21 +1,6 @@ - - + - "Alles herhalen" - "Niet herhalen" - "Eén herhalen" + Niets herhalen + Eén herhalen + Alles herhalen diff --git a/extensions/mediasession/src/main/res/values-pa-rIN/strings.xml b/extensions/mediasession/src/main/res/values-pa-rIN/strings.xml deleted file mode 100644 index 8eee0bee16..0000000000 --- a/extensions/mediasession/src/main/res/values-pa-rIN/strings.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - "ਸਭ ਨੂੰ ਦੁਹਰਾਓ" - "ਕੋਈ ਵੀ ਨਹੀਂ ਦੁਹਰਾਓ" - "ਇੱਕ ਦੁਹਰਾਓ" - diff --git a/extensions/mediasession/src/main/res/values-pa/strings.xml b/extensions/mediasession/src/main/res/values-pa/strings.xml new file mode 100644 index 0000000000..0b7d72841c --- /dev/null +++ b/extensions/mediasession/src/main/res/values-pa/strings.xml @@ -0,0 +1,6 @@ + + + ਕਿਸੇ ਨੂੰ ਨਾ ਦੁਹਰਾਓ + ਇੱਕ ਵਾਰ ਦੁਹਰਾਓ + ਸਾਰਿਆਂ ਨੂੰ ਦੁਹਰਾਓ + diff --git a/extensions/mediasession/src/main/res/values-pl/strings.xml b/extensions/mediasession/src/main/res/values-pl/strings.xml index 6a52d58b63..5654c0f095 100644 --- a/extensions/mediasession/src/main/res/values-pl/strings.xml +++ b/extensions/mediasession/src/main/res/values-pl/strings.xml @@ -1,21 +1,6 @@ - - + - "Powtórz wszystkie" - "Nie powtarzaj" - "Powtórz jeden" + Nie powtarzaj + Powtórz jeden + Powtórz wszystkie diff --git a/extensions/mediasession/src/main/res/values-pt-rBR/strings.xml b/extensions/mediasession/src/main/res/values-pt-rBR/strings.xml deleted file mode 100644 index efb8fc433f..0000000000 --- a/extensions/mediasession/src/main/res/values-pt-rBR/strings.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - "Repetir tudo" - "Não repetir" - "Repetir um" - diff --git a/extensions/mediasession/src/main/res/values-pt-rPT/strings.xml b/extensions/mediasession/src/main/res/values-pt-rPT/strings.xml index efb8fc433f..612be4b8f4 100644 --- a/extensions/mediasession/src/main/res/values-pt-rPT/strings.xml +++ b/extensions/mediasession/src/main/res/values-pt-rPT/strings.xml @@ -1,21 +1,6 @@ - - + - "Repetir tudo" - "Não repetir" - "Repetir um" + Não repetir nenhum + Repetir um + Repetir tudo diff --git a/extensions/mediasession/src/main/res/values-pt/strings.xml b/extensions/mediasession/src/main/res/values-pt/strings.xml index aadebbb3b0..a858ea4fc6 100644 --- a/extensions/mediasession/src/main/res/values-pt/strings.xml +++ b/extensions/mediasession/src/main/res/values-pt/strings.xml @@ -1,21 +1,6 @@ - - + - "Repetir tudo" - "Não repetir" - "Repetir uma" + Não repetir + Repetir uma + Repetir tudo diff --git a/extensions/mediasession/src/main/res/values-ro/strings.xml b/extensions/mediasession/src/main/res/values-ro/strings.xml index f6aee447e5..a88088fb0c 100644 --- a/extensions/mediasession/src/main/res/values-ro/strings.xml +++ b/extensions/mediasession/src/main/res/values-ro/strings.xml @@ -1,21 +1,6 @@ - - + - "Repetați toate" - "Repetați niciuna" - "Repetați unul" + Nu repetați niciunul + Repetați unul + Repetați-le pe toate diff --git a/extensions/mediasession/src/main/res/values-ru/strings.xml b/extensions/mediasession/src/main/res/values-ru/strings.xml index 575ad9f930..f350724813 100644 --- a/extensions/mediasession/src/main/res/values-ru/strings.xml +++ b/extensions/mediasession/src/main/res/values-ru/strings.xml @@ -1,21 +1,6 @@ - - + - "Повторять все" - "Не повторять" - "Повторять один элемент" + Не повторять + Повторять трек + Повторять все diff --git a/extensions/mediasession/src/main/res/values-si-rLK/strings.xml b/extensions/mediasession/src/main/res/values-si-rLK/strings.xml deleted file mode 100644 index 8e172ac268..0000000000 --- a/extensions/mediasession/src/main/res/values-si-rLK/strings.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - "සියලු නැවත" - "කිසිවක් නැවත" - "නැවත නැවත එක්" - diff --git a/extensions/mediasession/src/main/res/values-si/strings.xml b/extensions/mediasession/src/main/res/values-si/strings.xml new file mode 100644 index 0000000000..0d86d38e7f --- /dev/null +++ b/extensions/mediasession/src/main/res/values-si/strings.xml @@ -0,0 +1,6 @@ + + + කිසිවක් පුනරාවර්තනය නොකරන්න + එකක් පුනරාවර්තනය කරන්න + සියල්ල නැවත කරන්න + diff --git a/extensions/mediasession/src/main/res/values-sk/strings.xml b/extensions/mediasession/src/main/res/values-sk/strings.xml index 5d092003e5..9c0235daec 100644 --- a/extensions/mediasession/src/main/res/values-sk/strings.xml +++ b/extensions/mediasession/src/main/res/values-sk/strings.xml @@ -1,21 +1,6 @@ - - + - "Opakovať všetko" - "Neopakovať" - "Opakovať jednu položku" + Neopakovať + Opakovať jednu + Opakovať všetko diff --git a/extensions/mediasession/src/main/res/values-sl/strings.xml b/extensions/mediasession/src/main/res/values-sl/strings.xml index ecac3800c8..9ee3add8bc 100644 --- a/extensions/mediasession/src/main/res/values-sl/strings.xml +++ b/extensions/mediasession/src/main/res/values-sl/strings.xml @@ -1,21 +1,6 @@ - - + - "Ponovi vse" - "Ne ponovi" - "Ponovi eno" + Brez ponavljanja + Ponavljanje ene + Ponavljanje vseh diff --git a/extensions/mediasession/src/main/res/values-sq-rAL/strings.xml b/extensions/mediasession/src/main/res/values-sq-rAL/strings.xml deleted file mode 100644 index 6da24cc4c7..0000000000 --- a/extensions/mediasession/src/main/res/values-sq-rAL/strings.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - "Përsërit të gjithë" - "Përsëritni asnjë" - "Përsëritni një" - diff --git a/extensions/mediasession/src/main/res/values-sq/strings.xml b/extensions/mediasession/src/main/res/values-sq/strings.xml new file mode 100644 index 0000000000..2461dcf0ca --- /dev/null +++ b/extensions/mediasession/src/main/res/values-sq/strings.xml @@ -0,0 +1,6 @@ + + + Mos përsërit asnjë + Përsërit një + Përsërit të gjitha + diff --git a/extensions/mediasession/src/main/res/values-sr/strings.xml b/extensions/mediasession/src/main/res/values-sr/strings.xml index 881cb2703b..71edd5c341 100644 --- a/extensions/mediasession/src/main/res/values-sr/strings.xml +++ b/extensions/mediasession/src/main/res/values-sr/strings.xml @@ -1,18 +1,6 @@ - - + + Не понављај ниједну + Понови једну + Понови све diff --git a/extensions/mediasession/src/main/res/values-sv/strings.xml b/extensions/mediasession/src/main/res/values-sv/strings.xml index 3a7bb630aa..0956ac9fc7 100644 --- a/extensions/mediasession/src/main/res/values-sv/strings.xml +++ b/extensions/mediasession/src/main/res/values-sv/strings.xml @@ -1,21 +1,6 @@ - - + - "Upprepa alla" - "Upprepa inga" - "Upprepa en" + Upprepa inga + Upprepa en + Upprepa alla diff --git a/extensions/mediasession/src/main/res/values-sw/strings.xml b/extensions/mediasession/src/main/res/values-sw/strings.xml index 726012ab88..0010774a6f 100644 --- a/extensions/mediasession/src/main/res/values-sw/strings.xml +++ b/extensions/mediasession/src/main/res/values-sw/strings.xml @@ -1,21 +1,6 @@ - - + - "Rudia zote" - "Usirudie Yoyote" - "Rudia Moja" + Usirudie yoyote + Rudia moja + Rudia zote diff --git a/extensions/mediasession/src/main/res/values-ta-rIN/strings.xml b/extensions/mediasession/src/main/res/values-ta-rIN/strings.xml deleted file mode 100644 index 9364bc0be2..0000000000 --- a/extensions/mediasession/src/main/res/values-ta-rIN/strings.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - "அனைத்தையும் மீண்டும் இயக்கு" - "எதையும் மீண்டும் இயக்காதே" - "ஒன்றை மட்டும் மீண்டும் இயக்கு" - diff --git a/extensions/mediasession/src/main/res/values-ta/strings.xml b/extensions/mediasession/src/main/res/values-ta/strings.xml new file mode 100644 index 0000000000..b6fbcca4a1 --- /dev/null +++ b/extensions/mediasession/src/main/res/values-ta/strings.xml @@ -0,0 +1,6 @@ + + + எதையும் மீண்டும் இயக்காதே + இதை மட்டும் மீண்டும் இயக்கு + அனைத்தையும் மீண்டும் இயக்கு + diff --git a/extensions/mediasession/src/main/res/values-te-rIN/strings.xml b/extensions/mediasession/src/main/res/values-te-rIN/strings.xml deleted file mode 100644 index b7ee7345d5..0000000000 --- a/extensions/mediasession/src/main/res/values-te-rIN/strings.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - "అన్నీ పునరావృతం చేయి" - "ఏదీ పునరావృతం చేయవద్దు" - "ఒకదాన్ని పునరావృతం చేయి" - diff --git a/extensions/mediasession/src/main/res/values-te/strings.xml b/extensions/mediasession/src/main/res/values-te/strings.xml new file mode 100644 index 0000000000..b1249c7400 --- /dev/null +++ b/extensions/mediasession/src/main/res/values-te/strings.xml @@ -0,0 +1,6 @@ + + + దేన్నీ పునరావృతం చేయకండి + ఒకదాన్ని పునరావృతం చేయండి + అన్నింటినీ పునరావృతం చేయండి + diff --git a/extensions/mediasession/src/main/res/values-th/strings.xml b/extensions/mediasession/src/main/res/values-th/strings.xml index af502b3a4c..bec0410a44 100644 --- a/extensions/mediasession/src/main/res/values-th/strings.xml +++ b/extensions/mediasession/src/main/res/values-th/strings.xml @@ -1,21 +1,6 @@ - - + - "เล่นซ้ำทั้งหมด" - "ไม่เล่นซ้ำ" - "เล่นซ้ำรายการเดียว" + ไม่เล่นซ้ำ + เล่นซ้ำเพลงเดียว + เล่นซ้ำทั้งหมด diff --git a/extensions/mediasession/src/main/res/values-tl/strings.xml b/extensions/mediasession/src/main/res/values-tl/strings.xml index 239972a4c7..6f8d8f4f88 100644 --- a/extensions/mediasession/src/main/res/values-tl/strings.xml +++ b/extensions/mediasession/src/main/res/values-tl/strings.xml @@ -1,21 +1,6 @@ - - + - "Ulitin Lahat" - "Walang Uulitin" - "Ulitin ang Isa" + Walang uulitin + Mag-ulit ng isa + Ulitin lahat diff --git a/extensions/mediasession/src/main/res/values-tr/strings.xml b/extensions/mediasession/src/main/res/values-tr/strings.xml index 89a98b1ed9..20c05d9fa6 100644 --- a/extensions/mediasession/src/main/res/values-tr/strings.xml +++ b/extensions/mediasession/src/main/res/values-tr/strings.xml @@ -1,21 +1,6 @@ - - + - "Tümünü Tekrarla" - "Hiçbirini Tekrarlama" - "Birini Tekrarla" + Hiçbirini tekrarlama + Bir şarkıyı tekrarla + Tümünü tekrarla diff --git a/extensions/mediasession/src/main/res/values-uk/strings.xml b/extensions/mediasession/src/main/res/values-uk/strings.xml index 4e1d25eb8a..44db07ef9c 100644 --- a/extensions/mediasession/src/main/res/values-uk/strings.xml +++ b/extensions/mediasession/src/main/res/values-uk/strings.xml @@ -1,21 +1,6 @@ - - + - "Повторити все" - "Не повторювати" - "Повторити один елемент" + Не повторювати + Повторити 1 + Повторити всі diff --git a/extensions/mediasession/src/main/res/values-ur-rPK/strings.xml b/extensions/mediasession/src/main/res/values-ur-rPK/strings.xml deleted file mode 100644 index ab2631a4ec..0000000000 --- a/extensions/mediasession/src/main/res/values-ur-rPK/strings.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - "سبھی کو دہرائیں" - "کسی کو نہ دہرائیں" - "ایک کو دہرائیں" - diff --git a/extensions/mediasession/src/main/res/values-ur/strings.xml b/extensions/mediasession/src/main/res/values-ur/strings.xml new file mode 100644 index 0000000000..3860986e9c --- /dev/null +++ b/extensions/mediasession/src/main/res/values-ur/strings.xml @@ -0,0 +1,6 @@ + + + کسی کو نہ دہرائیں + ایک کو دہرائیں + سبھی کو دہرائیں + diff --git a/extensions/mediasession/src/main/res/values-uz-rUZ/strings.xml b/extensions/mediasession/src/main/res/values-uz-rUZ/strings.xml deleted file mode 100644 index c32d00af8e..0000000000 --- a/extensions/mediasession/src/main/res/values-uz-rUZ/strings.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - "Barchasini takrorlash" - "Takrorlamaslik" - "Bir marta takrorlash" - diff --git a/extensions/mediasession/src/main/res/values-uz/strings.xml b/extensions/mediasession/src/main/res/values-uz/strings.xml new file mode 100644 index 0000000000..3424c9f583 --- /dev/null +++ b/extensions/mediasession/src/main/res/values-uz/strings.xml @@ -0,0 +1,6 @@ + + + Takrorlanmasin + Bittasini takrorlash + Hammasini takrorlash + diff --git a/extensions/mediasession/src/main/res/values-vi/strings.xml b/extensions/mediasession/src/main/res/values-vi/strings.xml index dabc9e05d5..9de007cdb9 100644 --- a/extensions/mediasession/src/main/res/values-vi/strings.xml +++ b/extensions/mediasession/src/main/res/values-vi/strings.xml @@ -1,21 +1,6 @@ - - + - "Lặp lại tất cả" - "Không lặp lại" - "Lặp lại một mục" + Không lặp lại + Lặp lại một + Lặp lại tất cả diff --git a/extensions/mediasession/src/main/res/values-zh-rCN/strings.xml b/extensions/mediasession/src/main/res/values-zh-rCN/strings.xml index beb3403cb9..4d1f1346b9 100644 --- a/extensions/mediasession/src/main/res/values-zh-rCN/strings.xml +++ b/extensions/mediasession/src/main/res/values-zh-rCN/strings.xml @@ -1,21 +1,6 @@ - - + - "重复播放全部" - "不重复播放" - "重复播放单个视频" + 不重复播放 + 重复播放一项 + 全部重复播放 diff --git a/extensions/mediasession/src/main/res/values-zh-rHK/strings.xml b/extensions/mediasession/src/main/res/values-zh-rHK/strings.xml index 775cd6441c..e0ec62c533 100644 --- a/extensions/mediasession/src/main/res/values-zh-rHK/strings.xml +++ b/extensions/mediasession/src/main/res/values-zh-rHK/strings.xml @@ -1,21 +1,6 @@ - - + - "重複播放所有媒體項目" - "不重複播放任何媒體項目" - "重複播放一個媒體項目" + 不重複播放 + 重複播放一個 + 全部重複播放 diff --git a/extensions/mediasession/src/main/res/values-zh-rTW/strings.xml b/extensions/mediasession/src/main/res/values-zh-rTW/strings.xml index d3789f4145..5b91fbd9fe 100644 --- a/extensions/mediasession/src/main/res/values-zh-rTW/strings.xml +++ b/extensions/mediasession/src/main/res/values-zh-rTW/strings.xml @@ -1,21 +1,6 @@ - - + - "重複播放所有媒體項目" - "不重複播放" - "重複播放單一媒體項目" + 不重複播放 + 重複播放單一項目 + 重複播放所有項目 diff --git a/extensions/mediasession/src/main/res/values-zu/strings.xml b/extensions/mediasession/src/main/res/values-zu/strings.xml index 789b6fecb4..a6299ba987 100644 --- a/extensions/mediasession/src/main/res/values-zu/strings.xml +++ b/extensions/mediasession/src/main/res/values-zu/strings.xml @@ -1,21 +1,6 @@ - - + - "Phinda konke" - "Ungaphindi lutho" - "Phida okukodwa" + Phinda okungekho + Phinda okukodwa + Phinda konke diff --git a/extensions/okhttp/build.gradle b/extensions/okhttp/build.gradle index 2f7d84d33b..2b653c3f0e 100644 --- a/extensions/okhttp/build.gradle +++ b/extensions/okhttp/build.gradle @@ -23,19 +23,12 @@ android { targetSdkVersion project.ext.targetSdkVersion consumerProguardFiles 'proguard-rules.txt' } - - lintOptions { - // See: https://github.com/square/okio/issues/58 - warning 'InvalidPackage' - } } dependencies { implementation project(modulePrefix + 'library-core') implementation 'com.android.support:support-annotations:' + supportLibraryVersion - api('com.squareup.okhttp3:okhttp:3.10.0') { - exclude group: 'org.json' - } + api 'com.squareup.okhttp3:okhttp:3.10.0' } ext { diff --git a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java index 4094b3a5d2..f2898005c1 100644 --- a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java +++ b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java @@ -325,7 +325,7 @@ public class OkHttpDataSource implements HttpDataSource { while (bytesSkipped != bytesToSkip) { int readLength = (int) Math.min(bytesToSkip - bytesSkipped, skipBuffer.length); int read = responseByteStream.read(skipBuffer, 0, readLength); - if (Thread.interrupted()) { + if (Thread.currentThread().isInterrupted()) { throw new InterruptedIOException(); } if (read == -1) { diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java index f3720fee0a..7fde7678b8 100644 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java @@ -169,8 +169,15 @@ public class LibvpxVideoRenderer extends BaseRenderer { public LibvpxVideoRenderer(boolean scaleToFit, long allowedJoiningTimeMs, Handler eventHandler, VideoRendererEventListener eventListener, int maxDroppedFramesToNotify) { - this(scaleToFit, allowedJoiningTimeMs, eventHandler, eventListener, maxDroppedFramesToNotify, - null, false, false); + this( + scaleToFit, + allowedJoiningTimeMs, + eventHandler, + eventListener, + maxDroppedFramesToNotify, + /* drmSessionManager= */ null, + /* playClearSamplesWithoutKeys= */ false, + /* disableLoopFilter= */ false); } /** diff --git a/library/core/build.gradle b/library/core/build.gradle index fe6045c2e7..52249220e0 100644 --- a/library/core/build.gradle +++ b/library/core/build.gradle @@ -42,10 +42,15 @@ android { // testCoverageEnabled = true // } } + + lintOptions { + lintConfig file("../../checker-framework-lint.xml") + } } dependencies { implementation 'com.android.support:support-annotations:' + supportLibraryVersion + implementation 'org.checkerframework:checker-qual:' + checkerframeworkVersion androidTestImplementation 'com.google.dexmaker:dexmaker:' + dexmakerVersion androidTestImplementation 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion androidTestImplementation 'com.google.truth:truth:' + truthVersion @@ -54,6 +59,8 @@ dependencies { testImplementation 'junit:junit:' + junitVersion testImplementation 'org.mockito:mockito-core:' + mockitoVersion testImplementation 'org.robolectric:robolectric:' + robolectricVersion + testImplementation 'com.google.auto.value:auto-value-annotations:' + autoValueVersion + testAnnotationProcessor 'com.google.auto.value:auto-value:' + autoValueVersion } ext { diff --git a/library/core/proguard-rules.txt b/library/core/proguard-rules.txt index 7dc81c3f73..fe204822a8 100644 --- a/library/core/proguard-rules.txt +++ b/library/core/proguard-rules.txt @@ -29,3 +29,6 @@ -keepclassmembers class com.google.android.exoplayer2.ext.rtmp.RtmpDataSource { (); } + +# Don't warn about checkerframework +-dontwarn org.checkerframework.** diff --git a/library/core/src/main/AndroidManifest.xml b/library/core/src/main/AndroidManifest.xml index 430930a3ca..1a6971fdcc 100644 --- a/library/core/src/main/AndroidManifest.xml +++ b/library/core/src/main/AndroidManifest.xml @@ -14,4 +14,7 @@ limitations under the License. --> - + + + diff --git a/library/core/src/main/java/com/google/android/exoplayer2/C.java b/library/core/src/main/java/com/google/android/exoplayer2/C.java index 045f3bfc6e..de210f5eff 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/C.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/C.java @@ -64,6 +64,9 @@ public final class C { */ public static final int LENGTH_UNSET = -1; + /** Represents an unset or unknown percentage. */ + public static final int PERCENTAGE_UNSET = -1; + /** * The number of microseconds in one second. */ @@ -490,6 +493,8 @@ public final class C { * A data type constant for time synchronization data. */ public static final int DATA_TYPE_TIME_SYNCHRONIZATION = 5; + /** A data type constant for ads loader data. */ + public static final int DATA_TYPE_AD = 6; /** * Applications or extensions may define custom {@code DATA_TYPE_*} constants greater than or * equal to this value. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java index 940d9376c1..6cab53b78a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java @@ -199,9 +199,16 @@ public class DefaultRenderersFactory implements RenderersFactory { long allowedVideoJoiningTimeMs, Handler eventHandler, VideoRendererEventListener eventListener, @ExtensionRendererMode int extensionRendererMode, ArrayList out) { - out.add(new MediaCodecVideoRenderer(context, MediaCodecSelector.DEFAULT, - allowedVideoJoiningTimeMs, drmSessionManager, false, eventHandler, eventListener, - MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY)); + out.add( + new MediaCodecVideoRenderer( + context, + MediaCodecSelector.DEFAULT, + allowedVideoJoiningTimeMs, + drmSessionManager, + /* playClearSamplesWithoutKeys= */ false, + eventHandler, + eventListener, + MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY)); if (extensionRendererMode == EXTENSION_RENDERER_MODE_OFF) { return; @@ -259,8 +266,16 @@ public class DefaultRenderersFactory implements RenderersFactory { AudioProcessor[] audioProcessors, Handler eventHandler, AudioRendererEventListener eventListener, @ExtensionRendererMode int extensionRendererMode, ArrayList out) { - out.add(new MediaCodecAudioRenderer(MediaCodecSelector.DEFAULT, drmSessionManager, true, - eventHandler, eventListener, AudioCapabilities.getCapabilities(context), audioProcessors)); + out.add( + new MediaCodecAudioRenderer( + context, + MediaCodecSelector.DEFAULT, + drmSessionManager, + /* playClearSamplesWithoutKeys= */ false, + eventHandler, + eventListener, + AudioCapabilities.getCapabilities(context), + audioProcessors)); if (extensionRendererMode == EXTENSION_RENDERER_MODE_OFF) { return; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java index 39a6243933..6d8dd5b7a8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java @@ -185,6 +185,10 @@ public interface ExoPlayer extends Player { */ Looper getPlaybackLooper(); + @Override + @Nullable + ExoPlaybackException getPlaybackError(); + /** * Prepares the player to play the provided {@link MediaSource}. Equivalent to * {@code prepare(mediaSource, true, true)}. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index daabbd5a72..5ca5994b6e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -61,6 +61,7 @@ import java.util.concurrent.CopyOnWriteArraySet; private boolean hasPendingPrepare; private boolean hasPendingSeek; private PlaybackParameters playbackParameters; + private @Nullable ExoPlaybackException playbackError; // Playback information when there is no pending seek/set source operation. private PlaybackInfo playbackInfo; @@ -156,6 +157,11 @@ import java.util.concurrent.CopyOnWriteArraySet; return playbackInfo.playbackState; } + @Override + public @Nullable ExoPlaybackException getPlaybackError() { + return playbackError; + } + @Override public void prepare(MediaSource mediaSource) { prepare(mediaSource, true, true); @@ -163,6 +169,7 @@ import java.util.concurrent.CopyOnWriteArraySet; @Override public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) { + playbackError = null; PlaybackInfo playbackInfo = getResetPlaybackInfo( resetPosition, resetState, /* playbackState= */ Player.STATE_BUFFERING); @@ -325,6 +332,9 @@ import java.util.concurrent.CopyOnWriteArraySet; @Override public void stop(boolean reset) { + if (reset) { + playbackError = null; + } PlaybackInfo playbackInfo = getResetPlaybackInfo( /* resetPosition= */ reset, @@ -560,9 +570,9 @@ import java.util.concurrent.CopyOnWriteArraySet; } break; case ExoPlayerImplInternal.MSG_ERROR: - ExoPlaybackException exception = (ExoPlaybackException) msg.obj; + playbackError = (ExoPlaybackException) msg.obj; for (Player.EventListener listener : listeners) { - listener.onPlayerError(exception); + listener.onPlayerError(playbackError); } break; default: diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java index e91495227e..98d5fe91b7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java @@ -29,11 +29,11 @@ public final class ExoPlayerLibraryInfo { /** The version of the library expressed as a string, for example "1.2.3". */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa. - public static final String VERSION = "2.7.3"; + public static final String VERSION = "2.8.0"; /** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - public static final String VERSION_SLASHY = "ExoPlayerLib/2.7.3"; + public static final String VERSION_SLASHY = "ExoPlayerLib/2.8.0"; /** * The version of the library expressed as an integer, for example 1002003. @@ -43,7 +43,7 @@ public final class ExoPlayerLibraryInfo { * integer version 123045006 (123-045-006). */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - public static final int VERSION_INT = 2007003; + public static final int VERSION_INT = 2008000; /** * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Format.java b/library/core/src/main/java/com/google/android/exoplayer2/Format.java index b18225fc70..61d416da09 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Format.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Format.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2; import android.os.Parcel; import android.os.Parcelable; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.drm.DrmInitData; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.util.MimeTypes; @@ -43,29 +44,21 @@ public final class Format implements Parcelable { */ public static final long OFFSET_SAMPLE_RELATIVE = Long.MAX_VALUE; - /** - * An identifier for the format, or null if unknown or not applicable. - */ - public final String id; + /** An identifier for the format, or null if unknown or not applicable. */ + public final @Nullable String id; /** * The average bandwidth in bits per second, or {@link #NO_VALUE} if unknown or not applicable. */ public final int bitrate; - /** - * Codecs of the format as described in RFC 6381, or null if unknown or not applicable. - */ - public final String codecs; - /** - * Metadata, or null if unknown or not applicable. - */ - public final Metadata metadata; + /** Codecs of the format as described in RFC 6381, or null if unknown or not applicable. */ + public final @Nullable String codecs; + /** Metadata, or null if unknown or not applicable. */ + public final @Nullable Metadata metadata; // Container specific. - /** - * The mime type of the container, or null if unknown or not applicable. - */ - public final String containerMimeType; + /** The mime type of the container, or null if unknown or not applicable. */ + public final @Nullable String containerMimeType; // Elementary stream specific. @@ -73,7 +66,7 @@ public final class Format implements Parcelable { * The mime type of the elementary stream (i.e. the individual samples), or null if unknown or not * applicable. */ - public final String sampleMimeType; + public final @Nullable String sampleMimeType; /** * The maximum size of a buffer of data (typically one sample), or {@link #NO_VALUE} if unknown or * not applicable. @@ -84,10 +77,8 @@ public final class Format implements Parcelable { * if initialization data is not required. */ public final List initializationData; - /** - * DRM initialization data if the stream is protected, or null otherwise. - */ - public final DrmInitData drmInitData; + /** DRM initialization data if the stream is protected, or null otherwise. */ + public final @Nullable DrmInitData drmInitData; // Video specific. @@ -117,14 +108,10 @@ public final class Format implements Parcelable { */ @C.StereoMode public final int stereoMode; - /** - * The projection data for 360/VR video, or null if not applicable. - */ - public final byte[] projectionData; - /** - * The color metadata associated with the video, helps with accurate color reproduction. - */ - public final ColorInfo colorInfo; + /** The projection data for 360/VR video, or null if not applicable. */ + public final @Nullable byte[] projectionData; + /** The color metadata associated with the video, helps with accurate color reproduction. */ + public final @Nullable ColorInfo colorInfo; // Audio specific. @@ -171,10 +158,8 @@ public final class Format implements Parcelable { @C.SelectionFlags public final int selectionFlags; - /** - * The language, or null if unknown or not applicable. - */ - public final String language; + /** The language, or null if unknown or not applicable. */ + public final @Nullable String language; /** * The Accessibility channel, or {@link #NO_VALUE} if not known or applicable. @@ -186,36 +171,72 @@ public final class Format implements Parcelable { // Video. - public static Format createVideoContainerFormat(String id, String containerMimeType, - String sampleMimeType, String codecs, int bitrate, int width, int height, - float frameRate, List initializationData, @C.SelectionFlags int selectionFlags) { + public static Format createVideoContainerFormat( + @Nullable String id, + @Nullable String containerMimeType, + String sampleMimeType, + String codecs, + int bitrate, + int width, + int height, + float frameRate, + List initializationData, + @C.SelectionFlags int selectionFlags) { return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, NO_VALUE, width, height, frameRate, NO_VALUE, NO_VALUE, null, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, selectionFlags, null, NO_VALUE, OFFSET_SAMPLE_RELATIVE, initializationData, null, null); } - public static Format createVideoSampleFormat(String id, String sampleMimeType, String codecs, - int bitrate, int maxInputSize, int width, int height, float frameRate, - List initializationData, DrmInitData drmInitData) { + public static Format createVideoSampleFormat( + @Nullable String id, + @Nullable String sampleMimeType, + @Nullable String codecs, + int bitrate, + int maxInputSize, + int width, + int height, + float frameRate, + List initializationData, + @Nullable DrmInitData drmInitData) { return createVideoSampleFormat(id, sampleMimeType, codecs, bitrate, maxInputSize, width, height, frameRate, initializationData, NO_VALUE, NO_VALUE, drmInitData); } - public static Format createVideoSampleFormat(String id, String sampleMimeType, String codecs, - int bitrate, int maxInputSize, int width, int height, float frameRate, - List initializationData, int rotationDegrees, float pixelWidthHeightRatio, - DrmInitData drmInitData) { + public static Format createVideoSampleFormat( + @Nullable String id, + @Nullable String sampleMimeType, + @Nullable String codecs, + int bitrate, + int maxInputSize, + int width, + int height, + float frameRate, + List initializationData, + int rotationDegrees, + float pixelWidthHeightRatio, + @Nullable DrmInitData drmInitData) { return createVideoSampleFormat(id, sampleMimeType, codecs, bitrate, maxInputSize, width, height, frameRate, initializationData, rotationDegrees, pixelWidthHeightRatio, null, NO_VALUE, null, drmInitData); } - public static Format createVideoSampleFormat(String id, String sampleMimeType, String codecs, - int bitrate, int maxInputSize, int width, int height, float frameRate, - List initializationData, int rotationDegrees, float pixelWidthHeightRatio, - byte[] projectionData, @C.StereoMode int stereoMode, ColorInfo colorInfo, - DrmInitData drmInitData) { + public static Format createVideoSampleFormat( + @Nullable String id, + @Nullable String sampleMimeType, + @Nullable String codecs, + int bitrate, + int maxInputSize, + int width, + int height, + float frameRate, + List initializationData, + int rotationDegrees, + float pixelWidthHeightRatio, + byte[] projectionData, + @C.StereoMode int stereoMode, + @Nullable ColorInfo colorInfo, + @Nullable DrmInitData drmInitData) { return new Format(id, null, sampleMimeType, codecs, bitrate, maxInputSize, width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode, colorInfo, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, 0, null, NO_VALUE, @@ -224,37 +245,73 @@ public final class Format implements Parcelable { // Audio. - public static Format createAudioContainerFormat(String id, String containerMimeType, - String sampleMimeType, String codecs, int bitrate, int channelCount, int sampleRate, - List initializationData, @C.SelectionFlags int selectionFlags, String language) { + public static Format createAudioContainerFormat( + @Nullable String id, + @Nullable String containerMimeType, + @Nullable String sampleMimeType, + @Nullable String codecs, + int bitrate, + int channelCount, + int sampleRate, + List initializationData, + @C.SelectionFlags int selectionFlags, + @Nullable String language) { return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, null, channelCount, sampleRate, NO_VALUE, NO_VALUE, NO_VALUE, selectionFlags, language, NO_VALUE, OFFSET_SAMPLE_RELATIVE, initializationData, null, null); } - public static Format createAudioSampleFormat(String id, String sampleMimeType, String codecs, - int bitrate, int maxInputSize, int channelCount, int sampleRate, - List initializationData, DrmInitData drmInitData, - @C.SelectionFlags int selectionFlags, String language) { + public static Format createAudioSampleFormat( + @Nullable String id, + @Nullable String sampleMimeType, + @Nullable String codecs, + int bitrate, + int maxInputSize, + int channelCount, + int sampleRate, + List initializationData, + @Nullable DrmInitData drmInitData, + @C.SelectionFlags int selectionFlags, + @Nullable String language) { return createAudioSampleFormat(id, sampleMimeType, codecs, bitrate, maxInputSize, channelCount, sampleRate, NO_VALUE, initializationData, drmInitData, selectionFlags, language); } - public static Format createAudioSampleFormat(String id, String sampleMimeType, String codecs, - int bitrate, int maxInputSize, int channelCount, int sampleRate, - @C.PcmEncoding int pcmEncoding, List initializationData, DrmInitData drmInitData, - @C.SelectionFlags int selectionFlags, String language) { + public static Format createAudioSampleFormat( + @Nullable String id, + @Nullable String sampleMimeType, + @Nullable String codecs, + int bitrate, + int maxInputSize, + int channelCount, + int sampleRate, + @C.PcmEncoding int pcmEncoding, + List initializationData, + @Nullable DrmInitData drmInitData, + @C.SelectionFlags int selectionFlags, + @Nullable String language) { return createAudioSampleFormat(id, sampleMimeType, codecs, bitrate, maxInputSize, channelCount, sampleRate, pcmEncoding, NO_VALUE, NO_VALUE, initializationData, drmInitData, selectionFlags, language, null); } - public static Format createAudioSampleFormat(String id, String sampleMimeType, String codecs, - int bitrate, int maxInputSize, int channelCount, int sampleRate, - @C.PcmEncoding int pcmEncoding, int encoderDelay, int encoderPadding, - List initializationData, DrmInitData drmInitData, - @C.SelectionFlags int selectionFlags, String language, Metadata metadata) { + public static Format createAudioSampleFormat( + @Nullable String id, + @Nullable String sampleMimeType, + @Nullable String codecs, + int bitrate, + int maxInputSize, + int channelCount, + int sampleRate, + @C.PcmEncoding int pcmEncoding, + int encoderDelay, + int encoderPadding, + List initializationData, + @Nullable DrmInitData drmInitData, + @C.SelectionFlags int selectionFlags, + @Nullable String language, + @Nullable Metadata metadata) { return new Format(id, null, sampleMimeType, codecs, bitrate, maxInputSize, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, null, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding, selectionFlags, language, NO_VALUE, OFFSET_SAMPLE_RELATIVE, @@ -263,50 +320,87 @@ public final class Format implements Parcelable { // Text. - public static Format createTextContainerFormat(String id, String containerMimeType, - String sampleMimeType, String codecs, int bitrate, @C.SelectionFlags int selectionFlags, - String language) { + public static Format createTextContainerFormat( + @Nullable String id, + @Nullable String containerMimeType, + @Nullable String sampleMimeType, + @Nullable String codecs, + int bitrate, + @C.SelectionFlags int selectionFlags, + @Nullable String language) { return createTextContainerFormat(id, containerMimeType, sampleMimeType, codecs, bitrate, selectionFlags, language, NO_VALUE); } - public static Format createTextContainerFormat(String id, String containerMimeType, - String sampleMimeType, String codecs, int bitrate, @C.SelectionFlags int selectionFlags, - String language, int accessibilityChannel) { + public static Format createTextContainerFormat( + @Nullable String id, + @Nullable String containerMimeType, + @Nullable String sampleMimeType, + @Nullable String codecs, + int bitrate, + @C.SelectionFlags int selectionFlags, + @Nullable String language, + int accessibilityChannel) { return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, selectionFlags, language, accessibilityChannel, OFFSET_SAMPLE_RELATIVE, null, null, null); } - public static Format createTextSampleFormat(String id, String sampleMimeType, - @C.SelectionFlags int selectionFlags, String language) { + public static Format createTextSampleFormat( + @Nullable String id, + String sampleMimeType, + @C.SelectionFlags int selectionFlags, + @Nullable String language) { return createTextSampleFormat(id, sampleMimeType, selectionFlags, language, null); } - public static Format createTextSampleFormat(String id, String sampleMimeType, - @C.SelectionFlags int selectionFlags, String language, DrmInitData drmInitData) { + public static Format createTextSampleFormat( + @Nullable String id, + String sampleMimeType, + @C.SelectionFlags int selectionFlags, + @Nullable String language, + @Nullable DrmInitData drmInitData) { return createTextSampleFormat(id, sampleMimeType, null, NO_VALUE, selectionFlags, language, NO_VALUE, drmInitData, OFFSET_SAMPLE_RELATIVE, Collections.emptyList()); } - public static Format createTextSampleFormat(String id, String sampleMimeType, String codecs, - int bitrate, @C.SelectionFlags int selectionFlags, String language, int accessibilityChannel, - DrmInitData drmInitData) { + public static Format createTextSampleFormat( + @Nullable String id, + @Nullable String sampleMimeType, + @Nullable String codecs, + int bitrate, + @C.SelectionFlags int selectionFlags, + @Nullable String language, + int accessibilityChannel, + @Nullable DrmInitData drmInitData) { return createTextSampleFormat(id, sampleMimeType, codecs, bitrate, selectionFlags, language, accessibilityChannel, drmInitData, OFFSET_SAMPLE_RELATIVE, Collections.emptyList()); } - public static Format createTextSampleFormat(String id, String sampleMimeType, String codecs, - int bitrate, @C.SelectionFlags int selectionFlags, String language, DrmInitData drmInitData, + public static Format createTextSampleFormat( + @Nullable String id, + @Nullable String sampleMimeType, + @Nullable String codecs, + int bitrate, + @C.SelectionFlags int selectionFlags, + @Nullable String language, + @Nullable DrmInitData drmInitData, long subsampleOffsetUs) { return createTextSampleFormat(id, sampleMimeType, codecs, bitrate, selectionFlags, language, NO_VALUE, drmInitData, subsampleOffsetUs, Collections.emptyList()); } - public static Format createTextSampleFormat(String id, String sampleMimeType, String codecs, - int bitrate, @C.SelectionFlags int selectionFlags, String language, - int accessibilityChannel, DrmInitData drmInitData, long subsampleOffsetUs, + public static Format createTextSampleFormat( + @Nullable String id, + @Nullable String sampleMimeType, + @Nullable String codecs, + int bitrate, + @C.SelectionFlags int selectionFlags, + @Nullable String language, + int accessibilityChannel, + @Nullable DrmInitData drmInitData, + long subsampleOffsetUs, List initializationData) { return new Format(id, null, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, @@ -317,14 +411,14 @@ public final class Format implements Parcelable { // Image. public static Format createImageSampleFormat( - String id, - String sampleMimeType, - String codecs, + @Nullable String id, + @Nullable String sampleMimeType, + @Nullable String codecs, int bitrate, @C.SelectionFlags int selectionFlags, List initializationData, - String language, - DrmInitData drmInitData) { + @Nullable String language, + @Nullable DrmInitData drmInitData) { return new Format( id, null, @@ -356,36 +450,65 @@ public final class Format implements Parcelable { // Generic. - public static Format createContainerFormat(String id, String containerMimeType, - String sampleMimeType, String codecs, int bitrate, @C.SelectionFlags int selectionFlags, - String language) { + public static Format createContainerFormat( + @Nullable String id, + @Nullable String containerMimeType, + @Nullable String sampleMimeType, + @Nullable String codecs, + int bitrate, + @C.SelectionFlags int selectionFlags, + @Nullable String language) { return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, selectionFlags, language, NO_VALUE, OFFSET_SAMPLE_RELATIVE, null, null, null); } - public static Format createSampleFormat(String id, String sampleMimeType, - long subsampleOffsetUs) { + public static Format createSampleFormat( + @Nullable String id, @Nullable String sampleMimeType, long subsampleOffsetUs) { return new Format(id, null, sampleMimeType, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, 0, null, NO_VALUE, subsampleOffsetUs, null, null, null); } - public static Format createSampleFormat(String id, String sampleMimeType, String codecs, - int bitrate, DrmInitData drmInitData) { + public static Format createSampleFormat( + @Nullable String id, + @Nullable String sampleMimeType, + @Nullable String codecs, + int bitrate, + @Nullable DrmInitData drmInitData) { return new Format(id, null, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, 0, null, NO_VALUE, OFFSET_SAMPLE_RELATIVE, null, drmInitData, null); } - /* package */ Format(String id, String containerMimeType, String sampleMimeType, String codecs, - int bitrate, int maxInputSize, int width, int height, float frameRate, int rotationDegrees, - float pixelWidthHeightRatio, byte[] projectionData, @C.StereoMode int stereoMode, - ColorInfo colorInfo, int channelCount, int sampleRate, @C.PcmEncoding int pcmEncoding, - int encoderDelay, int encoderPadding, @C.SelectionFlags int selectionFlags, String language, - int accessibilityChannel, long subsampleOffsetUs, List initializationData, - DrmInitData drmInitData, Metadata metadata) { + /* package */ Format( + @Nullable String id, + @Nullable String containerMimeType, + @Nullable String sampleMimeType, + @Nullable String codecs, + int bitrate, + int maxInputSize, + int width, + int height, + float frameRate, + int rotationDegrees, + float pixelWidthHeightRatio, + @Nullable byte[] projectionData, + @C.StereoMode int stereoMode, + @Nullable ColorInfo colorInfo, + int channelCount, + int sampleRate, + @C.PcmEncoding int pcmEncoding, + int encoderDelay, + int encoderPadding, + @C.SelectionFlags int selectionFlags, + @Nullable String language, + int accessibilityChannel, + long subsampleOffsetUs, + @Nullable List initializationData, + @Nullable DrmInitData drmInitData, + @Nullable Metadata metadata) { this.id = id; this.containerMimeType = containerMimeType; this.sampleMimeType = sampleMimeType; @@ -468,14 +591,14 @@ public final class Format implements Parcelable { } public Format copyWithContainerInfo( - String id, - String sampleMimeType, - String codecs, + @Nullable String id, + @Nullable String sampleMimeType, + @Nullable String codecs, int bitrate, int width, int height, @C.SelectionFlags int selectionFlags, - String language) { + @Nullable String language) { return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode, colorInfo, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding, @@ -512,7 +635,7 @@ public final class Format implements Parcelable { drmInitData, metadata); } - public Format copyWithDrmInitData(DrmInitData drmInitData) { + public Format copyWithDrmInitData(@Nullable DrmInitData drmInitData) { return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode, colorInfo, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding, @@ -520,7 +643,7 @@ public final class Format implements Parcelable { drmInitData, metadata); } - public Format copyWithMetadata(Metadata metadata) { + public Format copyWithMetadata(@Nullable Metadata metadata) { return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode, colorInfo, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding, @@ -574,7 +697,7 @@ public final class Format implements Parcelable { } @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Player.java b/library/core/src/main/java/com/google/android/exoplayer2/Player.java index 85872339a3..328816d709 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Player.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Player.java @@ -459,6 +459,17 @@ public interface Player { */ int getPlaybackState(); + /** + * Returns the error that caused playback to fail. This is the same error that will have been + * reported via {@link Player.EventListener#onPlayerError(ExoPlaybackException)} at the time of + * failure. It can be queried using this method until {@code stop(true)} is called or the player + * is re-prepared. + * + * @return The error, or {@code null}. + */ + @Nullable + ExoPlaybackException getPlaybackError(); + /** * Sets whether playback should proceed when {@link #getPlaybackState()} == {@link #STATE_READY}. *

diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index b998027eb3..482e2c970a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -47,6 +47,7 @@ import com.google.android.exoplayer2.util.Clock; import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.video.VideoRendererEventListener; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.concurrent.CopyOnWriteArraySet; @@ -181,8 +182,8 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player player = createExoPlayerImpl(renderers, trackSelector, loadControl, clock); analyticsCollector = analyticsCollectorFactory.createAnalyticsCollector(player, clock); addListener(analyticsCollector); - addVideoDebugListener(analyticsCollector); - addAudioDebugListener(analyticsCollector); + videoDebugListeners.add(analyticsCollector); + audioDebugListeners.add(analyticsCollector); addMetadataOutput(analyticsCollector); if (drmSessionManager instanceof DefaultDrmSessionManager) { ((DefaultDrmSessionManager) drmSessionManager).addListener(eventHandler, analyticsCollector); @@ -534,10 +535,20 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player removeTextOutput(output); } + /** + * Adds a {@link MetadataOutput} to receive metadata. + * + * @param listener The output to register. + */ public void addMetadataOutput(MetadataOutput listener) { metadataOutputs.add(listener); } + /** + * Removes a {@link MetadataOutput}. + * + * @param listener The output to remove. + */ public void removeMetadataOutput(MetadataOutput listener) { metadataOutputs.remove(listener); } @@ -550,7 +561,7 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player */ @Deprecated public void setMetadataOutput(MetadataOutput output) { - metadataOutputs.clear(); + metadataOutputs.retainAll(Collections.singleton(analyticsCollector)); if (output != null) { addMetadataOutput(output); } @@ -568,65 +579,61 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player } /** - * Sets a listener to receive debug events from the video renderer. - * - * @param listener The listener. - * @deprecated Use {@link #addVideoDebugListener(VideoRendererEventListener)}. + * @deprecated Use {@link #addAnalyticsListener(AnalyticsListener)} to get more detailed debug + * information. */ @Deprecated public void setVideoDebugListener(VideoRendererEventListener listener) { - videoDebugListeners.clear(); + videoDebugListeners.retainAll(Collections.singleton(analyticsCollector)); if (listener != null) { addVideoDebugListener(listener); } } /** - * Adds a listener to receive debug events from the video renderer. - * - * @param listener The listener. + * @deprecated Use {@link #addAnalyticsListener(AnalyticsListener)} to get more detailed debug + * information. */ + @Deprecated public void addVideoDebugListener(VideoRendererEventListener listener) { videoDebugListeners.add(listener); } /** - * Removes a listener to receive debug events from the video renderer. - * - * @param listener The listener. + * @deprecated Use {@link #addAnalyticsListener(AnalyticsListener)} and {@link + * #removeAnalyticsListener(AnalyticsListener)} to get more detailed debug information. */ + @Deprecated public void removeVideoDebugListener(VideoRendererEventListener listener) { videoDebugListeners.remove(listener); } /** - * Sets a listener to receive debug events from the audio renderer. - * - * @param listener The listener. - * @deprecated Use {@link #addAudioDebugListener(AudioRendererEventListener)}. + * @deprecated Use {@link #addAnalyticsListener(AnalyticsListener)} to get more detailed debug + * information. */ @Deprecated public void setAudioDebugListener(AudioRendererEventListener listener) { - audioDebugListeners.clear(); + audioDebugListeners.retainAll(Collections.singleton(analyticsCollector)); if (listener != null) { addAudioDebugListener(listener); } } /** - * Adds a listener to receive debug events from the audio renderer. - * - * @param listener The listener. + * @deprecated Use {@link #addAnalyticsListener(AnalyticsListener)} to get more detailed debug + * information. */ + @Deprecated public void addAudioDebugListener(AudioRendererEventListener listener) { audioDebugListeners.add(listener); } /** - * Removes a listener to receive debug events from the audio renderer. - * - * @param listener The listener. + * @deprecated Use {@link #addAnalyticsListener(AnalyticsListener)} and {@link + * #removeAnalyticsListener(AnalyticsListener)} to get more detailed debug information. */ + @Deprecated public void removeAudioDebugListener(AudioRendererEventListener listener) { audioDebugListeners.remove(listener); } @@ -653,6 +660,11 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player return player.getPlaybackState(); } + @Override + public ExoPlaybackException getPlaybackError() { + return player.getPlaybackError(); + } + @Override public void prepare(MediaSource mediaSource) { prepare(mediaSource, /* resetPosition= */ true, /* resetState= */ true); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java index 4397b945b4..43ef308f27 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java @@ -35,7 +35,6 @@ import com.google.android.exoplayer2.metadata.MetadataOutput; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.source.MediaSourceEventListener; import com.google.android.exoplayer2.source.TrackGroupArray; -import com.google.android.exoplayer2.source.ads.AdsMediaSource; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.upstream.BandwidthMeter; import com.google.android.exoplayer2.util.Assertions; @@ -59,7 +58,6 @@ public class AnalyticsCollector VideoRendererEventListener, MediaSourceEventListener, BandwidthMeter.EventListener, - AdsMediaSource.EventListener, DefaultDrmSessionEventListener { /** Factory for an analytics collector. */ @@ -80,7 +78,6 @@ public class AnalyticsCollector private final CopyOnWriteArraySet listeners; private final Player player; private final Clock clock; - private final Period period; private final Window window; private final MediaPeriodQueueTracker mediaPeriodQueueTracker; @@ -95,7 +92,6 @@ public class AnalyticsCollector this.clock = Assertions.checkNotNull(clock); listeners = new CopyOnWriteArraySet<>(); mediaPeriodQueueTracker = new MediaPeriodQueueTracker(); - period = new Period(); window = new Window(); } @@ -164,13 +160,10 @@ public class AnalyticsCollector */ public final void resetForNewMediaSource() { // Copying the list is needed because onMediaPeriodReleased will modify the list. - List activeMediaPeriods = + List activeMediaPeriods = new ArrayList<>(mediaPeriodQueueTracker.activeMediaPeriods); - Timeline timeline = mediaPeriodQueueTracker.timeline; - for (MediaPeriodId mediaPeriod : activeMediaPeriods) { - int windowIndex = - timeline.isEmpty() ? 0 : timeline.getPeriod(mediaPeriod.periodIndex, period).windowIndex; - onMediaPeriodReleased(windowIndex, mediaPeriod); + for (WindowAndMediaPeriodId mediaPeriod : activeMediaPeriods) { + onMediaPeriodReleased(mediaPeriod.windowIndex, mediaPeriod.mediaPeriodId); } } @@ -309,7 +302,7 @@ public class AnalyticsCollector @Override public final void onMediaPeriodCreated(int windowIndex, MediaPeriodId mediaPeriodId) { - mediaPeriodQueueTracker.onMediaPeriodCreated(mediaPeriodId); + mediaPeriodQueueTracker.onMediaPeriodCreated(windowIndex, mediaPeriodId); EventTime eventTime = generateEventTime(windowIndex, mediaPeriodId); for (AnalyticsListener listener : listeners) { listener.onMediaPeriodCreated(eventTime); @@ -318,7 +311,7 @@ public class AnalyticsCollector @Override public final void onMediaPeriodReleased(int windowIndex, MediaPeriodId mediaPeriodId) { - mediaPeriodQueueTracker.onMediaPeriodReleased(mediaPeriodId); + mediaPeriodQueueTracker.onMediaPeriodReleased(windowIndex, mediaPeriodId); EventTime eventTime = generateEventTime(windowIndex, mediaPeriodId); for (AnalyticsListener listener : listeners) { listener.onMediaPeriodReleased(eventTime); @@ -377,7 +370,7 @@ public class AnalyticsCollector @Override public final void onReadingStarted(int windowIndex, MediaPeriodId mediaPeriodId) { - mediaPeriodQueueTracker.onReadingStarted(mediaPeriodId); + mediaPeriodQueueTracker.onReadingStarted(windowIndex, mediaPeriodId); EventTime eventTime = generateEventTime(windowIndex, mediaPeriodId); for (AnalyticsListener listener : listeners) { listener.onReadingStarted(eventTime); @@ -539,40 +532,6 @@ public class AnalyticsCollector } } - // AdsMediaSource.EventListener implementation. - - @Override - public final void onAdLoadError(IOException error) { - EventTime eventTime = generatePlayingMediaPeriodEventTime(); - for (AnalyticsListener listener : listeners) { - listener.onAdLoadError(eventTime, error); - } - } - - @Override - public final void onInternalAdLoadError(RuntimeException error) { - EventTime eventTime = generatePlayingMediaPeriodEventTime(); - for (AnalyticsListener listener : listeners) { - listener.onInternalAdLoadError(eventTime, error); - } - } - - @Override - public final void onAdClicked() { - EventTime eventTime = generatePlayingMediaPeriodEventTime(); - for (AnalyticsListener listener : listeners) { - listener.onAdClicked(eventTime); - } - } - - @Override - public final void onAdTapped() { - EventTime eventTime = generatePlayingMediaPeriodEventTime(); - for (AnalyticsListener listener : listeners) { - listener.onAdTapped(eventTime); - } - } - // Internal methods. /** Returns read-only set of registered listeners. */ @@ -597,7 +556,8 @@ public class AnalyticsCollector // This event is for content in the currently playing window. eventPositionMs = player.getContentPosition(); } - } else if (timeline.isEmpty() || (mediaPeriodId != null && mediaPeriodId.isAd())) { + } else if (windowIndex >= timeline.getWindowCount() + || (mediaPeriodId != null && mediaPeriodId.isAd())) { // This event is for an unknown future window or for an ad in a future window. // Assume start position of zero. eventPositionMs = 0; @@ -617,16 +577,13 @@ public class AnalyticsCollector bufferedDurationMs); } - private EventTime generateEventTime(@Nullable MediaPeriodId mediaPeriodId) { - Timeline timeline = player.getCurrentTimeline(); - if (mediaPeriodId == null) { - mediaPeriodId = mediaPeriodQueueTracker.tryResolveWindowIndex(player.getCurrentWindowIndex()); + private EventTime generateEventTime(@Nullable WindowAndMediaPeriodId mediaPeriod) { + if (mediaPeriod == null) { + int windowIndex = player.getCurrentWindowIndex(); + MediaPeriodId mediaPeriodId = mediaPeriodQueueTracker.tryResolveWindowIndex(windowIndex); + return generateEventTime(windowIndex, mediaPeriodId); } - int windowIndex = - mediaPeriodId == null || timeline.isEmpty() - ? player.getCurrentWindowIndex() - : timeline.getPeriod(mediaPeriodId.periodIndex, period).windowIndex; - return generateEventTime(windowIndex, mediaPeriodId); + return generateEventTime(mediaPeriod.windowIndex, mediaPeriod.mediaPeriodId); } private EventTime generateLastReportedPlayingMediaPeriodEventTime() { @@ -651,11 +608,11 @@ public class AnalyticsCollector // TODO: Investigate reporting MediaPeriodId in renderer events and adding a listener of queue // changes, which would hopefully remove the need to track the queue here. - private final ArrayList activeMediaPeriods; + private final ArrayList activeMediaPeriods; private final Period period; - private MediaPeriodId lastReportedPlayingMediaPeriod; - private MediaPeriodId readingMediaPeriod; + private WindowAndMediaPeriodId lastReportedPlayingMediaPeriod; + private WindowAndMediaPeriodId readingMediaPeriod; private Timeline timeline; private boolean isSeeking; @@ -666,33 +623,34 @@ public class AnalyticsCollector } /** - * Returns the {@link MediaPeriodId} of the media period in the front of the queue. This is the - * playing media period unless the player hasn't started playing yet (in which case it is the - * loading media period or null). While the player is seeking or preparing, this method will - * always return null to reflect the uncertainty about the current playing period. May also be - * null, if the timeline is empty or no media period is active yet. + * Returns the {@link WindowAndMediaPeriodId} of the media period in the front of the queue. + * This is the playing media period unless the player hasn't started playing yet (in which case + * it is the loading media period or null). While the player is seeking or preparing, this + * method will always return null to reflect the uncertainty about the current playing period. + * May also be null, if the timeline is empty or no media period is active yet. */ - public @Nullable MediaPeriodId getPlayingMediaPeriod() { + public @Nullable WindowAndMediaPeriodId getPlayingMediaPeriod() { return activeMediaPeriods.isEmpty() || timeline.isEmpty() || isSeeking ? null : activeMediaPeriods.get(0); } /** - * Returns the {@link MediaPeriodId} of the currently playing media period. This is the publicly - * reported period which should always match {@link Player#getCurrentPeriodIndex()} unless the - * player is currently seeking or being prepared in which case the previous period is reported - * until the seek or preparation is processed. May be null, if no media period is active yet. + * Returns the {@link WindowAndMediaPeriodId} of the currently playing media period. This is the + * publicly reported period which should always match {@link Player#getCurrentPeriodIndex()} + * unless the player is currently seeking or being prepared in which case the previous period is + * reported until the seek or preparation is processed. May be null, if no media period is + * active yet. */ - public @Nullable MediaPeriodId getLastReportedPlayingMediaPeriod() { + public @Nullable WindowAndMediaPeriodId getLastReportedPlayingMediaPeriod() { return lastReportedPlayingMediaPeriod; } /** - * Returns the {@link MediaPeriodId} of the media period currently being read by the player. May - * be null, if the player is not reading a media period. + * Returns the {@link WindowAndMediaPeriodId} of the media period currently being read by the + * player. May be null, if the player is not reading a media period. */ - public @Nullable MediaPeriodId getReadingMediaPeriod() { + public @Nullable WindowAndMediaPeriodId getReadingMediaPeriod() { return readingMediaPeriod; } @@ -701,7 +659,7 @@ public class AnalyticsCollector * currently loading or will be the next one loading. May be null, if no media period is active * yet. */ - public @Nullable MediaPeriodId getLoadingMediaPeriod() { + public @Nullable WindowAndMediaPeriodId getLoadingMediaPeriod() { return activeMediaPeriods.isEmpty() ? null : activeMediaPeriods.get(activeMediaPeriods.size() - 1); @@ -713,20 +671,23 @@ public class AnalyticsCollector } /** - * Tries to find an existing media period from the specified window index. Only returns a + * Tries to find an existing media period id from the specified window index. Only returns a * non-null media period id if there is a unique, unambiguous match. */ public @Nullable MediaPeriodId tryResolveWindowIndex(int windowIndex) { MediaPeriodId match = null; - if (timeline != null && !timeline.isEmpty()) { + if (timeline != null) { + int timelinePeriodCount = timeline.getPeriodCount(); for (int i = 0; i < activeMediaPeriods.size(); i++) { - MediaPeriodId mediaPeriodId = activeMediaPeriods.get(i); - if (timeline.getPeriod(mediaPeriodId.periodIndex, period).windowIndex == windowIndex) { + WindowAndMediaPeriodId mediaPeriod = activeMediaPeriods.get(i); + int periodIndex = mediaPeriod.mediaPeriodId.periodIndex; + if (periodIndex < timelinePeriodCount + && timeline.getPeriod(periodIndex, period).windowIndex == windowIndex) { if (match != null) { // Ambiguous match. return null; } - match = mediaPeriodId; + match = mediaPeriod.mediaPeriodId; } } } @@ -742,10 +703,10 @@ public class AnalyticsCollector public void onTimelineChanged(Timeline timeline) { for (int i = 0; i < activeMediaPeriods.size(); i++) { activeMediaPeriods.set( - i, updateMediaPeriodIdToNewTimeline(activeMediaPeriods.get(i), timeline)); + i, updateMediaPeriodToNewTimeline(activeMediaPeriods.get(i), timeline)); } if (readingMediaPeriod != null) { - readingMediaPeriod = updateMediaPeriodIdToNewTimeline(readingMediaPeriod, timeline); + readingMediaPeriod = updateMediaPeriodToNewTimeline(readingMediaPeriod, timeline); } this.timeline = timeline; updateLastReportedPlayingMediaPeriod(); @@ -763,24 +724,25 @@ public class AnalyticsCollector } /** Updates the queue with a newly created media period. */ - public void onMediaPeriodCreated(MediaPeriodId mediaPeriodId) { - activeMediaPeriods.add(mediaPeriodId); + public void onMediaPeriodCreated(int windowIndex, MediaPeriodId mediaPeriodId) { + activeMediaPeriods.add(new WindowAndMediaPeriodId(windowIndex, mediaPeriodId)); if (activeMediaPeriods.size() == 1 && !timeline.isEmpty()) { updateLastReportedPlayingMediaPeriod(); } } /** Updates the queue with a released media period. */ - public void onMediaPeriodReleased(MediaPeriodId mediaPeriodId) { - activeMediaPeriods.remove(mediaPeriodId); - if (mediaPeriodId.equals(readingMediaPeriod)) { + public void onMediaPeriodReleased(int windowIndex, MediaPeriodId mediaPeriodId) { + WindowAndMediaPeriodId mediaPeriod = new WindowAndMediaPeriodId(windowIndex, mediaPeriodId); + activeMediaPeriods.remove(mediaPeriod); + if (mediaPeriod.equals(readingMediaPeriod)) { readingMediaPeriod = activeMediaPeriods.isEmpty() ? null : activeMediaPeriods.get(0); } } /** Update the queue with a change in the reading media period. */ - public void onReadingStarted(MediaPeriodId mediaPeriodId) { - readingMediaPeriod = mediaPeriodId; + public void onReadingStarted(int windowIndex, MediaPeriodId mediaPeriodId) { + readingMediaPeriod = new WindowAndMediaPeriodId(windowIndex, mediaPeriodId); } private void updateLastReportedPlayingMediaPeriod() { @@ -789,16 +751,48 @@ public class AnalyticsCollector } } - private MediaPeriodId updateMediaPeriodIdToNewTimeline( - MediaPeriodId mediaPeriodId, Timeline newTimeline) { + private WindowAndMediaPeriodId updateMediaPeriodToNewTimeline( + WindowAndMediaPeriodId mediaPeriod, Timeline newTimeline) { if (newTimeline.isEmpty() || timeline.isEmpty()) { - return mediaPeriodId; + return mediaPeriod; } - Object uid = timeline.getPeriod(mediaPeriodId.periodIndex, period, /* setIds= */ true).uid; - int newIndex = newTimeline.getIndexOfPeriod(uid); - return newIndex == C.INDEX_UNSET - ? mediaPeriodId - : mediaPeriodId.copyWithPeriodIndex(newIndex); + Object uid = + timeline.getPeriod(mediaPeriod.mediaPeriodId.periodIndex, period, /* setIds= */ true).uid; + int newPeriodIndex = newTimeline.getIndexOfPeriod(uid); + if (newPeriodIndex == C.INDEX_UNSET) { + return mediaPeriod; + } + int newWindowIndex = newTimeline.getPeriod(newPeriodIndex, period).windowIndex; + return new WindowAndMediaPeriodId( + newWindowIndex, mediaPeriod.mediaPeriodId.copyWithPeriodIndex(newPeriodIndex)); + } + } + + private static final class WindowAndMediaPeriodId { + + public final int windowIndex; + public final MediaPeriodId mediaPeriodId; + + public WindowAndMediaPeriodId(int windowIndex, MediaPeriodId mediaPeriodId) { + this.windowIndex = windowIndex; + this.mediaPeriodId = mediaPeriodId; + } + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (other == null || getClass() != other.getClass()) { + return false; + } + WindowAndMediaPeriodId that = (WindowAndMediaPeriodId) other; + return windowIndex == that.windowIndex && mediaPeriodId.equals(that.mediaPeriodId); + } + + @Override + public int hashCode() { + return 31 * windowIndex + mediaPeriodId.hashCode(); } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsListener.java b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsListener.java index 30357b08ef..48057f2bff 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsListener.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsListener.java @@ -433,38 +433,6 @@ public interface AnalyticsListener { */ void onRenderedFirstFrame(EventTime eventTime, Surface surface); - /** - * Called if there was an error loading one or more ads. The ads loader will skip the problematic - * ad(s). - * - * @param eventTime The event time. - * @param error The error. - */ - void onAdLoadError(EventTime eventTime, IOException error); - - /** - * Called when an unexpected internal error is encountered while loading ads. The ads loader will - * skip all remaining ads, as the error is not recoverable. - * - * @param eventTime The event time. - * @param error The error. - */ - void onInternalAdLoadError(EventTime eventTime, RuntimeException error); - - /** - * Called when the user clicks through an ad (for example, following a 'learn more' link). - * - * @param eventTime The event time. - */ - void onAdClicked(EventTime eventTime); - - /** - * Called when the user taps a non-clickthrough part of an ad. - * - * @param eventTime The event time. - */ - void onAdTapped(EventTime eventTime); - /** * Called each time drm keys are loaded. * diff --git a/library/core/src/main/java/com/google/android/exoplayer2/analytics/DefaultAnalyticsListener.java b/library/core/src/main/java/com/google/android/exoplayer2/analytics/DefaultAnalyticsListener.java index a53a85ed71..4a49de56b0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/analytics/DefaultAnalyticsListener.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/DefaultAnalyticsListener.java @@ -152,18 +152,6 @@ public abstract class DefaultAnalyticsListener implements AnalyticsListener { @Override public void onRenderedFirstFrame(EventTime eventTime, Surface surface) {} - @Override - public void onAdLoadError(EventTime eventTime, IOException error) {} - - @Override - public void onInternalAdLoadError(EventTime eventTime, RuntimeException error) {} - - @Override - public void onAdClicked(EventTime eventTime) {} - - @Override - public void onAdTapped(EventTime eventTime) {} - @Override public void onDrmKeysLoaded(EventTime eventTime) {} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java index a206d2c1ec..1025cb953b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java @@ -35,6 +35,8 @@ import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.ArrayDeque; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; /** * Plays audio data. The implementation delegates to an {@link AudioTrack} and handles playback @@ -64,6 +66,92 @@ public final class DefaultAudioSink implements AudioSink { } + /** + * Provides a chain of audio processors, which are used for any user-defined processing and + * applying playback parameters (if supported). Because applying playback parameters can skip and + * stretch/compress audio, the sink will query the chain for information on how to transform its + * output position to map it onto a media position, via {@link #getMediaDuration(long)} and {@link + * #getSkippedOutputFrameCount()}. + */ + public interface AudioProcessorChain { + + /** + * Returns the fixed chain of audio processors that will process audio. This method is called + * once during initialization, but audio processors may change state to become active/inactive + * during playback. + */ + AudioProcessor[] getAudioProcessors(); + + /** + * Configures audio processors to apply the specified playback parameters immediately, returning + * the new parameters, which may differ from those passed in. Only called when processors have + * no input pending. + * + * @param playbackParameters The playback parameters to try to apply. + * @return The playback parameters that were actually applied. + */ + PlaybackParameters applyPlaybackParameters(PlaybackParameters playbackParameters); + + /** + * Scales the specified playout duration to take into account speedup due to audio processing, + * returning an input media duration, in arbitrary units. + */ + long getMediaDuration(long playoutDuration); + + /** + * Returns the number of output audio frames skipped since the audio processors were last + * flushed. + */ + long getSkippedOutputFrameCount(); + } + + /** + * The default audio processor chain, which applies a (possibly empty) chain of user-defined audio + * processors followed by {@link SilenceSkippingAudioProcessor} and {@link SonicAudioProcessor}. + */ + public static class DefaultAudioProcessorChain implements AudioProcessorChain { + + private final AudioProcessor[] audioProcessors; + private final SilenceSkippingAudioProcessor silenceSkippingAudioProcessor; + private final SonicAudioProcessor sonicAudioProcessor; + + /** + * Creates a new default chain of audio processors, with the user-defined {@code + * audioProcessors} applied before silence skipping and playback parameters. + */ + public DefaultAudioProcessorChain(AudioProcessor... audioProcessors) { + this.audioProcessors = Arrays.copyOf(audioProcessors, audioProcessors.length + 2); + silenceSkippingAudioProcessor = new SilenceSkippingAudioProcessor(); + sonicAudioProcessor = new SonicAudioProcessor(); + this.audioProcessors[audioProcessors.length] = silenceSkippingAudioProcessor; + this.audioProcessors[audioProcessors.length + 1] = sonicAudioProcessor; + } + + @Override + public AudioProcessor[] getAudioProcessors() { + return audioProcessors; + } + + @Override + public PlaybackParameters applyPlaybackParameters(PlaybackParameters playbackParameters) { + silenceSkippingAudioProcessor.setEnabled(playbackParameters.skipSilence); + return new PlaybackParameters( + sonicAudioProcessor.setSpeed(playbackParameters.speed), + sonicAudioProcessor.setPitch(playbackParameters.pitch), + playbackParameters.skipSilence); + } + + @Override + public long getMediaDuration(long playoutDuration) { + return sonicAudioProcessor.scaleDurationForSpeedup(playoutDuration); + } + + @Override + public long getSkippedOutputFrameCount() { + return silenceSkippingAudioProcessor.getSkippedFrames(); + } + } + /** * A minimum length for the {@link AudioTrack} buffer, in microseconds. */ @@ -135,11 +223,10 @@ public final class DefaultAudioSink implements AudioSink { public static boolean failOnSpuriousAudioTimestamp = false; @Nullable private final AudioCapabilities audioCapabilities; + private final AudioProcessorChain audioProcessorChain; private final boolean enableConvertHighResIntPcmToFloat; private final ChannelMappingAudioProcessor channelMappingAudioProcessor; private final TrimmingAudioProcessor trimmingAudioProcessor; - private final SilenceSkippingAudioProcessor silenceSkippingAudioProcessor; - private final SonicAudioProcessor sonicAudioProcessor; private final AudioProcessor[] toIntPcmAvailableAudioProcessors; private final AudioProcessor[] toFloatPcmAvailableAudioProcessors; private final ConditionVariable releasingConditionVariable; @@ -181,7 +268,7 @@ public final class DefaultAudioSink implements AudioSink { private long startMediaTimeUs; private float volume; - private AudioProcessor[] audioProcessors; + private AudioProcessor[] activeAudioProcessors; private ByteBuffer[] outputBuffers; @Nullable private ByteBuffer inputBuffer; @Nullable private ByteBuffer outputBuffer; @@ -196,17 +283,21 @@ public final class DefaultAudioSink implements AudioSink { private long lastFeedElapsedRealtimeMs; /** + * Creates a new default audio sink. + * * @param audioCapabilities The audio capabilities for playback on this device. May be null if the * default capabilities (no encoded audio passthrough support) should be assumed. * @param audioProcessors An array of {@link AudioProcessor}s that will process PCM audio before * output. May be empty. */ - public DefaultAudioSink(@Nullable AudioCapabilities audioCapabilities, - AudioProcessor[] audioProcessors) { + public DefaultAudioSink( + @Nullable AudioCapabilities audioCapabilities, AudioProcessor[] audioProcessors) { this(audioCapabilities, audioProcessors, /* enableConvertHighResIntPcmToFloat= */ false); } /** + * Creates a new default audio sink, optionally using float output for high resolution PCM. + * * @param audioCapabilities The audio capabilities for playback on this device. May be null if the * default capabilities (no encoded audio passthrough support) should be assumed. * @param audioProcessors An array of {@link AudioProcessor}s that will process PCM audio before @@ -220,22 +311,45 @@ public final class DefaultAudioSink implements AudioSink { @Nullable AudioCapabilities audioCapabilities, AudioProcessor[] audioProcessors, boolean enableConvertHighResIntPcmToFloat) { + this( + audioCapabilities, + new DefaultAudioProcessorChain(audioProcessors), + enableConvertHighResIntPcmToFloat); + } + + /** + * Creates a new default audio sink, optionally using float output for high resolution PCM and + * with the specified {@code audioProcessorChain}. + * + * @param audioCapabilities The audio capabilities for playback on this device. May be null if the + * default capabilities (no encoded audio passthrough support) should be assumed. + * @param audioProcessorChain An {@link AudioProcessorChain} which is used to apply playback + * parameters adjustments. The instance passed in must not be reused in other sinks. + * @param enableConvertHighResIntPcmToFloat Whether to enable conversion of high resolution + * integer PCM to 32-bit float for output, if possible. Functionality that uses 16-bit integer + * audio processing (for example, speed and pitch adjustment) will not be available when float + * output is in use. + */ + public DefaultAudioSink( + @Nullable AudioCapabilities audioCapabilities, + AudioProcessorChain audioProcessorChain, + boolean enableConvertHighResIntPcmToFloat) { this.audioCapabilities = audioCapabilities; + this.audioProcessorChain = Assertions.checkNotNull(audioProcessorChain); this.enableConvertHighResIntPcmToFloat = enableConvertHighResIntPcmToFloat; releasingConditionVariable = new ConditionVariable(true); audioTrackPositionTracker = new AudioTrackPositionTracker(new PositionTrackerListener()); channelMappingAudioProcessor = new ChannelMappingAudioProcessor(); trimmingAudioProcessor = new TrimmingAudioProcessor(); - silenceSkippingAudioProcessor = new SilenceSkippingAudioProcessor(); - sonicAudioProcessor = new SonicAudioProcessor(); - toIntPcmAvailableAudioProcessors = new AudioProcessor[5 + audioProcessors.length]; - toIntPcmAvailableAudioProcessors[0] = new ResamplingAudioProcessor(); - toIntPcmAvailableAudioProcessors[1] = channelMappingAudioProcessor; - toIntPcmAvailableAudioProcessors[2] = trimmingAudioProcessor; - System.arraycopy( - audioProcessors, 0, toIntPcmAvailableAudioProcessors, 3, audioProcessors.length); - toIntPcmAvailableAudioProcessors[3 + audioProcessors.length] = silenceSkippingAudioProcessor; - toIntPcmAvailableAudioProcessors[4 + audioProcessors.length] = sonicAudioProcessor; + ArrayList toIntPcmAudioProcessors = new ArrayList<>(); + Collections.addAll( + toIntPcmAudioProcessors, + new ResamplingAudioProcessor(), + channelMappingAudioProcessor, + trimmingAudioProcessor); + Collections.addAll(toIntPcmAudioProcessors, audioProcessorChain.getAudioProcessors()); + toIntPcmAvailableAudioProcessors = + toIntPcmAudioProcessors.toArray(new AudioProcessor[toIntPcmAudioProcessors.size()]); toFloatPcmAvailableAudioProcessors = new AudioProcessor[] {new FloatResamplingAudioProcessor()}; volume = 1.0f; startMediaTimeState = START_NOT_SET; @@ -243,7 +357,7 @@ public final class DefaultAudioSink implements AudioSink { audioSessionId = C.AUDIO_SESSION_ID_UNSET; playbackParameters = PlaybackParameters.DEFAULT; drainingAudioProcessorIndex = C.INDEX_UNSET; - this.audioProcessors = new AudioProcessor[0]; + activeAudioProcessors = new AudioProcessor[0]; outputBuffers = new ByteBuffer[0]; playbackParametersCheckpoints = new ArrayDeque<>(); } @@ -423,14 +537,14 @@ public final class DefaultAudioSink implements AudioSink { } } int count = newAudioProcessors.size(); - audioProcessors = newAudioProcessors.toArray(new AudioProcessor[count]); + activeAudioProcessors = newAudioProcessors.toArray(new AudioProcessor[count]); outputBuffers = new ByteBuffer[count]; flushAudioProcessors(); } private void flushAudioProcessors() { - for (int i = 0; i < audioProcessors.length; i++) { - AudioProcessor audioProcessor = audioProcessors[i]; + for (int i = 0; i < activeAudioProcessors.length; i++) { + AudioProcessor audioProcessor = activeAudioProcessors[i]; audioProcessor.flush(); outputBuffers[i] = audioProcessor.getOutput(); } @@ -468,7 +582,7 @@ public final class DefaultAudioSink implements AudioSink { playbackParameters = canApplyPlaybackParameters - ? applyPlaybackParameters(playbackParameters) + ? audioProcessorChain.applyPlaybackParameters(playbackParameters) : PlaybackParameters.DEFAULT; setupAudioProcessors(); @@ -536,7 +650,7 @@ public final class DefaultAudioSink implements AudioSink { } PlaybackParameters newPlaybackParameters = afterDrainPlaybackParameters; afterDrainPlaybackParameters = null; - newPlaybackParameters = applyPlaybackParameters(newPlaybackParameters); + newPlaybackParameters = audioProcessorChain.applyPlaybackParameters(newPlaybackParameters); // Store the position and corresponding media time from which the parameters will apply. playbackParametersCheckpoints.add( new PlaybackParametersCheckpoint( @@ -601,7 +715,7 @@ public final class DefaultAudioSink implements AudioSink { } private void processBuffers(long avSyncPresentationTimeUs) throws WriteException { - int count = audioProcessors.length; + int count = activeAudioProcessors.length; int index = count; while (index >= 0) { ByteBuffer input = index > 0 ? outputBuffers[index - 1] @@ -609,7 +723,7 @@ public final class DefaultAudioSink implements AudioSink { if (index == count) { writeBuffer(input, avSyncPresentationTimeUs); } else { - AudioProcessor audioProcessor = audioProcessors[index]; + AudioProcessor audioProcessor = activeAudioProcessors[index]; audioProcessor.queueInput(input); ByteBuffer output = audioProcessor.getOutput(); outputBuffers[index] = output; @@ -706,11 +820,11 @@ public final class DefaultAudioSink implements AudioSink { private boolean drainAudioProcessorsToEndOfStream() throws WriteException { boolean audioProcessorNeedsEndOfStream = false; if (drainingAudioProcessorIndex == C.INDEX_UNSET) { - drainingAudioProcessorIndex = processingEnabled ? 0 : audioProcessors.length; + drainingAudioProcessorIndex = processingEnabled ? 0 : activeAudioProcessors.length; audioProcessorNeedsEndOfStream = true; } - while (drainingAudioProcessorIndex < audioProcessors.length) { - AudioProcessor audioProcessor = audioProcessors[drainingAudioProcessorIndex]; + while (drainingAudioProcessorIndex < activeAudioProcessors.length) { + AudioProcessor audioProcessor = activeAudioProcessors[drainingAudioProcessorIndex]; if (audioProcessorNeedsEndOfStream) { audioProcessor.queueEndOfStream(); } @@ -762,7 +876,7 @@ public final class DefaultAudioSink implements AudioSink { afterDrainPlaybackParameters = playbackParameters; } else { // Update the playback parameters now. - this.playbackParameters = applyPlaybackParameters(playbackParameters); + this.playbackParameters = audioProcessorChain.applyPlaybackParameters(playbackParameters); } } return this.playbackParameters; @@ -920,29 +1034,14 @@ public final class DefaultAudioSink implements AudioSink { }.start(); } - /** - * Configures audio processors to apply the specified playback parameters, returning the new - * parameters, which may differ from those passed in. - * - * @param playbackParameters The playback parameters to try to apply. - * @return The playback parameters that were actually applied. - */ - private PlaybackParameters applyPlaybackParameters(PlaybackParameters playbackParameters) { - silenceSkippingAudioProcessor.setEnabled(playbackParameters.skipSilence); - return new PlaybackParameters( - sonicAudioProcessor.setSpeed(playbackParameters.speed), - sonicAudioProcessor.setPitch(playbackParameters.pitch), - playbackParameters.skipSilence); - } - - /** - * Returns the underlying audio track {@code positionUs} with any applicable speedup applied. - */ private long applySpeedup(long positionUs) { + @Nullable PlaybackParametersCheckpoint checkpoint = null; while (!playbackParametersCheckpoints.isEmpty() && positionUs >= playbackParametersCheckpoints.getFirst().positionUs) { + checkpoint = playbackParametersCheckpoints.remove(); + } + if (checkpoint != null) { // We are playing (or about to play) media with the new playback parameters, so update them. - PlaybackParametersCheckpoint checkpoint = playbackParametersCheckpoints.remove(); playbackParameters = checkpoint.playbackParameters; playbackParametersPositionUs = checkpoint.positionUs; playbackParametersOffsetUs = checkpoint.mediaTimeUs - startMediaTimeUs; @@ -954,8 +1053,9 @@ public final class DefaultAudioSink implements AudioSink { if (playbackParametersCheckpoints.isEmpty()) { return playbackParametersOffsetUs - + sonicAudioProcessor.scaleDurationForSpeedup(positionUs - playbackParametersPositionUs); + + audioProcessorChain.getMediaDuration(positionUs - playbackParametersPositionUs); } + // We are playing data at a previous playback speed, so fall back to multiplying by the speed. return playbackParametersOffsetUs + Util.getMediaDurationForPlayoutDuration( @@ -963,7 +1063,7 @@ public final class DefaultAudioSink implements AudioSink { } private long applySkipping(long positionUs) { - return positionUs + framesToDurationUs(silenceSkippingAudioProcessor.getSkippedFrames()); + return positionUs + framesToDurationUs(audioProcessorChain.getSkippedOutputFrameCount()); } private boolean isInitialized() { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java index c73081e2ab..9ab066ee7d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java @@ -17,6 +17,8 @@ package com.google.android.exoplayer2.audio; import android.annotation.SuppressLint; import android.annotation.TargetApi; +import android.content.Context; +import android.content.pm.PackageManager; import android.media.MediaCodec; import android.media.MediaCrypto; import android.media.MediaFormat; @@ -61,6 +63,7 @@ import java.nio.ByteBuffer; @TargetApi(16) public class MediaCodecAudioRenderer extends MediaCodecRenderer implements MediaClock { + private final Context context; private final EventDispatcher eventDispatcher; private final AudioSink audioSink; @@ -78,13 +81,19 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media private boolean allowPositionDiscontinuity; /** + * @param context A context. * @param mediaCodecSelector A decoder selector. */ - public MediaCodecAudioRenderer(MediaCodecSelector mediaCodecSelector) { - this(mediaCodecSelector, null, true); + public MediaCodecAudioRenderer(Context context, MediaCodecSelector mediaCodecSelector) { + this( + context, + mediaCodecSelector, + /* drmSessionManager= */ null, + /* playClearSamplesWithoutKeys= */ false); } /** + * @param context A context. * @param mediaCodecSelector A decoder selector. * @param drmSessionManager For use with encrypted content. May be null if support for encrypted * content is not required. @@ -94,24 +103,43 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media * permitted to play clear regions of encrypted media files before {@code drmSessionManager} * has obtained the keys necessary to decrypt encrypted regions of the media. */ - public MediaCodecAudioRenderer(MediaCodecSelector mediaCodecSelector, + public MediaCodecAudioRenderer( + Context context, + MediaCodecSelector mediaCodecSelector, @Nullable DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys) { - this(mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, null, null); + this( + context, + mediaCodecSelector, + drmSessionManager, + playClearSamplesWithoutKeys, + /* eventHandler= */ null, + /* eventListener= */ null); } /** + * @param context A context. * @param mediaCodecSelector A decoder selector. * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be * null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. */ - public MediaCodecAudioRenderer(MediaCodecSelector mediaCodecSelector, - @Nullable Handler eventHandler, @Nullable AudioRendererEventListener eventListener) { - this(mediaCodecSelector, null, true, eventHandler, eventListener); + public MediaCodecAudioRenderer( + Context context, + MediaCodecSelector mediaCodecSelector, + @Nullable Handler eventHandler, + @Nullable AudioRendererEventListener eventListener) { + this( + context, + mediaCodecSelector, + /* drmSessionManager= */ null, + /* playClearSamplesWithoutKeys= */ false, + eventHandler, + eventListener); } /** + * @param context A context. * @param mediaCodecSelector A decoder selector. * @param drmSessionManager For use with encrypted content. May be null if support for encrypted * content is not required. @@ -124,15 +152,25 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media * null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. */ - public MediaCodecAudioRenderer(MediaCodecSelector mediaCodecSelector, + public MediaCodecAudioRenderer( + Context context, + MediaCodecSelector mediaCodecSelector, @Nullable DrmSessionManager drmSessionManager, - boolean playClearSamplesWithoutKeys, @Nullable Handler eventHandler, + boolean playClearSamplesWithoutKeys, + @Nullable Handler eventHandler, @Nullable AudioRendererEventListener eventListener) { - this(mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, eventHandler, - eventListener, (AudioCapabilities) null); + this( + context, + mediaCodecSelector, + drmSessionManager, + playClearSamplesWithoutKeys, + eventHandler, + eventListener, + (AudioCapabilities) null); } /** + * @param context A context. * @param mediaCodecSelector A decoder selector. * @param drmSessionManager For use with encrypted content. May be null if support for encrypted * content is not required. @@ -149,16 +187,27 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media * @param audioProcessors Optional {@link AudioProcessor}s that will process PCM audio before * output. */ - public MediaCodecAudioRenderer(MediaCodecSelector mediaCodecSelector, + public MediaCodecAudioRenderer( + Context context, + MediaCodecSelector mediaCodecSelector, @Nullable DrmSessionManager drmSessionManager, - boolean playClearSamplesWithoutKeys, @Nullable Handler eventHandler, + boolean playClearSamplesWithoutKeys, + @Nullable Handler eventHandler, @Nullable AudioRendererEventListener eventListener, - @Nullable AudioCapabilities audioCapabilities, AudioProcessor... audioProcessors) { - this(mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, - eventHandler, eventListener, new DefaultAudioSink(audioCapabilities, audioProcessors)); + @Nullable AudioCapabilities audioCapabilities, + AudioProcessor... audioProcessors) { + this( + context, + mediaCodecSelector, + drmSessionManager, + playClearSamplesWithoutKeys, + eventHandler, + eventListener, + new DefaultAudioSink(audioCapabilities, audioProcessors)); } /** + * @param context A context. * @param mediaCodecSelector A decoder selector. * @param drmSessionManager For use with encrypted content. May be null if support for encrypted * content is not required. @@ -172,13 +221,18 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media * @param eventListener A listener of events. May be null if delivery of events is not required. * @param audioSink The sink to which audio will be output. */ - public MediaCodecAudioRenderer(MediaCodecSelector mediaCodecSelector, + public MediaCodecAudioRenderer( + Context context, + MediaCodecSelector mediaCodecSelector, @Nullable DrmSessionManager drmSessionManager, - boolean playClearSamplesWithoutKeys, @Nullable Handler eventHandler, - @Nullable AudioRendererEventListener eventListener, AudioSink audioSink) { + boolean playClearSamplesWithoutKeys, + @Nullable Handler eventHandler, + @Nullable AudioRendererEventListener eventListener, + AudioSink audioSink) { super(C.TRACK_TYPE_AUDIO, mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys); - eventDispatcher = new EventDispatcher(eventHandler, eventListener); + this.context = context.getApplicationContext(); this.audioSink = audioSink; + eventDispatcher = new EventDispatcher(eventHandler, eventListener); audioSink.setListener(new AudioSinkListener()); } @@ -233,11 +287,9 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media if (allowPassthrough(format.sampleMimeType)) { MediaCodecInfo passthroughDecoderInfo = mediaCodecSelector.getPassthroughDecoderInfo(); if (passthroughDecoderInfo != null) { - passthroughEnabled = true; return passthroughDecoderInfo; } } - passthroughEnabled = false; return super.getDecoderInfo(mediaCodecSelector, format, requiresSecureDecoder); } @@ -257,28 +309,31 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media @Override protected void configureCodec(MediaCodecInfo codecInfo, MediaCodec codec, Format format, MediaCrypto crypto) { - codecMaxInputSize = getCodecMaxInputSize(format, getStreamFormats()); + codecMaxInputSize = getCodecMaxInputSize(codecInfo, format, getStreamFormats()); codecNeedsDiscardChannelsWorkaround = codecNeedsDiscardChannelsWorkaround(codecInfo.name); - MediaFormat mediaFormat = getMediaFormat(format, codecMaxInputSize); + passthroughEnabled = codecInfo.passthrough; + String codecMimeType = codecInfo.mimeType == null ? MimeTypes.AUDIO_RAW : codecInfo.mimeType; + MediaFormat mediaFormat = getMediaFormat(format, codecMimeType, codecMaxInputSize); + codec.configure(mediaFormat, /* surface= */ null, crypto, /* flags= */ 0); if (passthroughEnabled) { - // Override the MIME type used to configure the codec if we are using a passthrough decoder. + // Store the input MIME type if we're using the passthrough codec. passthroughMediaFormat = mediaFormat; - passthroughMediaFormat.setString(MediaFormat.KEY_MIME, MimeTypes.AUDIO_RAW); - codec.configure(passthroughMediaFormat, null, crypto, 0); passthroughMediaFormat.setString(MediaFormat.KEY_MIME, format.sampleMimeType); } else { - codec.configure(mediaFormat, null, crypto, 0); passthroughMediaFormat = null; } } @Override protected @KeepCodecResult int canKeepCodec( - MediaCodec codec, boolean codecIsAdaptive, Format oldFormat, Format newFormat) { - return newFormat.maxInputSize <= codecMaxInputSize - && areAdaptationCompatible(oldFormat, newFormat) - ? KEEP_CODEC_RESULT_YES_WITHOUT_RECONFIGURATION - : KEEP_CODEC_RESULT_NO; + MediaCodec codec, MediaCodecInfo codecInfo, Format oldFormat, Format newFormat) { + return KEEP_CODEC_RESULT_NO; + // TODO: Determine when codecs can be safely kept. When doing so, also uncomment the commented + // out code in getCodecMaxInputSize. + // return getCodecMaxInputSize(codecInfo, newFormat) <= codecMaxInputSize + // && areAdaptationCompatible(oldFormat, newFormat) + // ? KEEP_CODEC_RESULT_YES_WITHOUT_RECONFIGURATION + // : KEEP_CODEC_RESULT_NO; } @Override @@ -511,23 +566,55 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media * Returns a maximum input size suitable for configuring a codec for {@code format} in a way that * will allow possible adaptation to other compatible formats in {@code streamFormats}. * + * @param codecInfo A {@link MediaCodecInfo} describing the decoder. * @param format The format for which the codec is being configured. * @param streamFormats The possible stream formats. * @return A suitable maximum input size. */ - protected int getCodecMaxInputSize(Format format, Format[] streamFormats) { - int maxInputSize = format.maxInputSize; - if (streamFormats.length == 1) { - // The single entry in streamFormats must correspond to the format for which the codec is - // being configured. - return maxInputSize; - } - for (Format streamFormat : streamFormats) { - if (areAdaptationCompatible(format, streamFormat)) { - maxInputSize = Math.max(maxInputSize, streamFormat.maxInputSize); + protected int getCodecMaxInputSize( + MediaCodecInfo codecInfo, Format format, Format[] streamFormats) { + int maxInputSize = getCodecMaxInputSize(codecInfo, format); + // if (streamFormats.length == 1) { + // // The single entry in streamFormats must correspond to the format for which the codec is + // // being configured. + // return maxInputSize; + // } + // for (Format streamFormat : streamFormats) { + // if (areAdaptationCompatible(format, streamFormat)) { + // maxInputSize = Math.max(maxInputSize, getCodecMaxInputSize(codecInfo, streamFormat)); + // } + // } + return maxInputSize; + } + + /** + * Returns a maximum input buffer size for a given format. + * + * @param codecInfo A {@link MediaCodecInfo} describing the decoder. + * @param format The format. + * @return A maximum input buffer size in bytes, or {@link Format#NO_VALUE} if a maximum could not + * be determined. + */ + private int getCodecMaxInputSize(MediaCodecInfo codecInfo, Format format) { + if (Util.SDK_INT < 24 && "OMX.google.raw.decoder".equals(codecInfo.name)) { + // OMX.google.raw.decoder didn't resize its output buffers correctly prior to N, so there's no + // point requesting a non-default input size. Doing so may cause a native crash, where-as not + // doing so will cause a more controlled failure when attempting to fill an input buffer. See: + // https://github.com/google/ExoPlayer/issues/4057. + boolean needsRawDecoderWorkaround = true; + if (Util.SDK_INT == 23) { + PackageManager packageManager = context.getPackageManager(); + if (packageManager != null + && packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)) { + // The workaround is not required for AndroidTV devices running M. + needsRawDecoderWorkaround = false; + } + } + if (needsRawDecoderWorkaround) { + return Format.NO_VALUE; } } - return maxInputSize; + return format.maxInputSize; } /** @@ -535,13 +622,15 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media * for decoding the given {@link Format} for playback. * * @param format The format of the media. + * @param codecMimeType The MIME type handled by the codec. + * @param codecMaxInputSize The maximum input size supported by the codec. * @return The framework media format. */ @SuppressLint("InlinedApi") - protected MediaFormat getMediaFormat(Format format, int codecMaxInputSize) { + protected MediaFormat getMediaFormat(Format format, String codecMimeType, int codecMaxInputSize) { MediaFormat mediaFormat = new MediaFormat(); // Set format parameters that should always be set. - mediaFormat.setString(MediaFormat.KEY_MIME, format.sampleMimeType); + mediaFormat.setString(MediaFormat.KEY_MIME, codecMimeType); mediaFormat.setInteger(MediaFormat.KEY_CHANNEL_COUNT, format.channelCount); mediaFormat.setInteger(MediaFormat.KEY_SAMPLE_RATE, format.sampleRate); MediaFormatUtil.setCsdBuffers(mediaFormat, format.initializationData); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/SilenceSkippingAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/SilenceSkippingAudioProcessor.java index b748dbab5d..a289ced128 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/SilenceSkippingAudioProcessor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/SilenceSkippingAudioProcessor.java @@ -27,7 +27,7 @@ import java.nio.ByteOrder; * An {@link AudioProcessor} that skips silence in the input stream. Input and output are 16-bit * PCM. */ -/* package */ final class SilenceSkippingAudioProcessor implements AudioProcessor { +public final class SilenceSkippingAudioProcessor implements AudioProcessor { /** * The minimum duration of audio that must be below {@link #SILENCE_THRESHOLD_LEVEL} to classify diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java index 89560f2977..c404912882 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java @@ -112,7 +112,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements private boolean waitingForKeys; public SimpleDecoderAudioRenderer() { - this(null, null); + this(/* eventHandler= */ null, /* eventListener= */ null); } /** @@ -123,7 +123,13 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements */ public SimpleDecoderAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener, AudioProcessor... audioProcessors) { - this(eventHandler, eventListener, null, null, false, audioProcessors); + this( + eventHandler, + eventListener, + /* audioCapabilities= */ null, + /* drmSessionManager= */ null, + /* playClearSamplesWithoutKeys= */ false, + audioProcessors); } /** @@ -135,7 +141,12 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements */ public SimpleDecoderAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener, AudioCapabilities audioCapabilities) { - this(eventHandler, eventListener, audioCapabilities, null, false); + this( + eventHandler, + eventListener, + audioCapabilities, + /* drmSessionManager= */ null, + /* playClearSamplesWithoutKeys= */ false); } /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/ExoMediaDrm.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/ExoMediaDrm.java index cecc840511..2699559c5f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/ExoMediaDrm.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/ExoMediaDrm.java @@ -15,13 +15,13 @@ */ package com.google.android.exoplayer2.drm; -import android.annotation.TargetApi; import android.media.DeniedByServerException; import android.media.MediaCryptoException; import android.media.MediaDrm; import android.media.MediaDrmException; import android.media.NotProvisionedException; import android.os.Handler; +import android.support.annotation.Nullable; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -30,7 +30,6 @@ import java.util.UUID; /** * Used to obtain keys for decrypting protected media streams. See {@link android.media.MediaDrm}. */ -@TargetApi(18) public interface ExoMediaDrm { /** @@ -72,14 +71,18 @@ public interface ExoMediaDrm { /** * Called when an event occurs that requires the app to be notified * - * @param mediaDrm the {@link ExoMediaDrm} object on which the event occurred. - * @param sessionId the DRM session ID on which the event occurred - * @param event indicates the event type - * @param extra an secondary error code - * @param data optional byte array of data that may be associated with the event + * @param mediaDrm The {@link ExoMediaDrm} object on which the event occurred. + * @param sessionId The DRM session ID on which the event occurred. + * @param event Indicates the event type. + * @param extra A secondary error code. + * @param data Optional byte array of data that may be associated with the event. */ - void onEvent(ExoMediaDrm mediaDrm, byte[] sessionId, int event, int extra, - byte[] data); + void onEvent( + ExoMediaDrm mediaDrm, + byte[] sessionId, + int event, + int extra, + @Nullable byte[] data); } /** @@ -90,20 +93,25 @@ public interface ExoMediaDrm { * Called when the keys in a session change status, such as when the license is renewed or * expires. * - * @param mediaDrm the {@link ExoMediaDrm} object on which the event occurred. - * @param sessionId the DRM session ID on which the event occurred. - * @param exoKeyInfo a list of {@link KeyStatus} that contains key ID and status. - * @param hasNewUsableKey true if new key becomes usable. + * @param mediaDrm The {@link ExoMediaDrm} object on which the event occurred. + * @param sessionId The DRM session ID on which the event occurred. + * @param exoKeyInformation A list of {@link KeyStatus} that contains key ID and status. + * @param hasNewUsableKey Whether a new key became usable. */ - void onKeyStatusChange(ExoMediaDrm mediaDrm, byte[] sessionId, - List exoKeyInfo, boolean hasNewUsableKey); + void onKeyStatusChange( + ExoMediaDrm mediaDrm, + byte[] sessionId, + List exoKeyInformation, + boolean hasNewUsableKey); } /** * @see android.media.MediaDrm.KeyStatus */ interface KeyStatus { + /** Returns the status code for the key. */ int getStatusCode(); + /** Returns the id for the key. */ byte[] getKeyId(); } @@ -218,15 +226,16 @@ public interface ExoMediaDrm { */ void closeSession(byte[] sessionId); - /** - * @see MediaDrm#getKeyRequest(byte[], byte[], String, int, HashMap) - */ - KeyRequest getKeyRequest(byte[] scope, byte[] init, String mimeType, int keyType, - HashMap optionalParameters) throws NotProvisionedException; + /** @see MediaDrm#getKeyRequest(byte[], byte[], String, int, HashMap) */ + KeyRequest getKeyRequest( + byte[] scope, + byte[] init, + String mimeType, + int keyType, + HashMap optionalParameters) + throws NotProvisionedException; - /** - * @see MediaDrm#provideKeyResponse(byte[], byte[]) - */ + /** @see MediaDrm#provideKeyResponse(byte[], byte[]) */ byte[] provideKeyResponse(byte[] scope, byte[] response) throws NotProvisionedException, DeniedByServerException; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ChunkIndex.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ChunkIndex.java index d0c66f930a..7ddd03bbd5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ChunkIndex.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ChunkIndex.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.extractor; import com.google.android.exoplayer2.util.Util; +import java.util.Arrays; /** * Defines chunks of samples within a media stream. @@ -102,4 +103,19 @@ public final class ChunkIndex implements SeekMap { } } + @Override + public String toString() { + return "ChunkIndex(" + + "length=" + + length + + ", sizes=" + + Arrays.toString(sizes) + + ", offsets=" + + Arrays.toString(offsets) + + ", timeUs=" + + Arrays.toString(timesUs) + + ", durationsUs=" + + Arrays.toString(durationsUs) + + ")"; + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java index b85ecba3a4..425f2b77cd 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.extractor; +import com.google.android.exoplayer2.extractor.amr.AmrExtractor; import com.google.android.exoplayer2.extractor.flv.FlvExtractor; import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor; import com.google.android.exoplayer2.extractor.mp3.Mp3Extractor; @@ -35,18 +36,19 @@ import java.lang.reflect.Constructor; * An {@link ExtractorsFactory} that provides an array of extractors for the following formats: * *

    - *
  • MP4, including M4A ({@link Mp4Extractor})
  • - *
  • fMP4 ({@link FragmentedMp4Extractor})
  • - *
  • Matroska and WebM ({@link MatroskaExtractor})
  • - *
  • Ogg Vorbis/FLAC ({@link OggExtractor}
  • - *
  • MP3 ({@link Mp3Extractor})
  • - *
  • AAC ({@link AdtsExtractor})
  • - *
  • MPEG TS ({@link TsExtractor})
  • - *
  • MPEG PS ({@link PsExtractor})
  • - *
  • FLV ({@link FlvExtractor})
  • - *
  • WAV ({@link WavExtractor})
  • - *
  • AC3 ({@link Ac3Extractor})
  • - *
  • FLAC (only available if the FLAC extension is built and included)
  • + *
  • MP4, including M4A ({@link Mp4Extractor}) + *
  • fMP4 ({@link FragmentedMp4Extractor}) + *
  • Matroska and WebM ({@link MatroskaExtractor}) + *
  • Ogg Vorbis/FLAC ({@link OggExtractor} + *
  • MP3 ({@link Mp3Extractor}) + *
  • AAC ({@link AdtsExtractor}) + *
  • MPEG TS ({@link TsExtractor}) + *
  • MPEG PS ({@link PsExtractor}) + *
  • FLV ({@link FlvExtractor}) + *
  • WAV ({@link WavExtractor}) + *
  • AC3 ({@link Ac3Extractor}) + *
  • AMR ({@link AmrExtractor}) + *
  • FLAC (only available if the FLAC extension is built and included) *
*/ public final class DefaultExtractorsFactory implements ExtractorsFactory { @@ -159,7 +161,7 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory { @Override public synchronized Extractor[] createExtractors() { - Extractor[] extractors = new Extractor[FLAC_EXTRACTOR_CONSTRUCTOR == null ? 11 : 12]; + Extractor[] extractors = new Extractor[FLAC_EXTRACTOR_CONSTRUCTOR == null ? 12 : 13]; extractors[0] = new MatroskaExtractor(matroskaFlags); extractors[1] = new FragmentedMp4Extractor(fragmentedMp4Flags); extractors[2] = new Mp4Extractor(mp4Flags); @@ -171,9 +173,10 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory { extractors[8] = new OggExtractor(); extractors[9] = new PsExtractor(); extractors[10] = new WavExtractor(); + extractors[11] = new AmrExtractor(); if (FLAC_EXTRACTOR_CONSTRUCTOR != null) { try { - extractors[11] = FLAC_EXTRACTOR_CONSTRUCTOR.newInstance(); + extractors[12] = FLAC_EXTRACTOR_CONSTRUCTOR.newInstance(); } catch (Exception e) { // Should never happen. throw new IllegalStateException("Unexpected error creating FLAC extractor", e); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/amr/AmrExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/amr/AmrExtractor.java new file mode 100644 index 0000000000..b58e979c26 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/amr/AmrExtractor.java @@ -0,0 +1,299 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.extractor.amr; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.ParserException; +import com.google.android.exoplayer2.extractor.Extractor; +import com.google.android.exoplayer2.extractor.ExtractorInput; +import com.google.android.exoplayer2.extractor.ExtractorOutput; +import com.google.android.exoplayer2.extractor.ExtractorsFactory; +import com.google.android.exoplayer2.extractor.PositionHolder; +import com.google.android.exoplayer2.extractor.SeekMap; +import com.google.android.exoplayer2.extractor.TrackOutput; +import com.google.android.exoplayer2.util.MimeTypes; +import com.google.android.exoplayer2.util.Util; +import java.io.EOFException; +import java.io.IOException; +import java.util.Arrays; + +/** + * Extracts data from the AMR containers format (either AMR or AMR-WB). This follows RFC-4867, + * section 5. + * + *

This extractor only supports single-channel AMR container formats. + */ +public final class AmrExtractor implements Extractor { + + /** Factory for {@link AmrExtractor} instances. */ + public static final ExtractorsFactory FACTORY = + new ExtractorsFactory() { + + @Override + public Extractor[] createExtractors() { + return new Extractor[] {new AmrExtractor()}; + } + }; + + /** + * The frame size in bytes, including header (1 byte), for each of the 16 frame types for AMR + * narrow band. + */ + private static final int[] frameSizeBytesByTypeNb = { + 13, + 14, + 16, + 18, + 20, + 21, + 27, + 32, + 6, // AMR SID + 7, // GSM-EFR SID + 6, // TDMA-EFR SID + 6, // PDC-EFR SID + 1, // Future use + 1, // Future use + 1, // Future use + 1 // No data + }; + + /** + * The frame size in bytes, including header (1 byte), for each of the 16 frame types for AMR wide + * band. + */ + private static final int[] frameSizeBytesByTypeWb = { + 18, + 24, + 33, + 37, + 41, + 47, + 51, + 59, + 61, + 6, // AMR-WB SID + 1, // Future use + 1, // Future use + 1, // Future use + 1, // Future use + 1, // speech lost + 1 // No data + }; + + private static final byte[] amrSignatureNb = Util.getUtf8Bytes("#!AMR\n"); + private static final byte[] amrSignatureWb = Util.getUtf8Bytes("#!AMR-WB\n"); + + /** Theoretical maximum frame size for a AMR frame. */ + private static final int MAX_FRAME_SIZE_BYTES = frameSizeBytesByTypeWb[8]; + + private static final int SAMPLE_RATE_WB = 16_000; + private static final int SAMPLE_RATE_NB = 8_000; + private static final int SAMPLE_TIME_PER_FRAME_US = 20_000; + + private final byte[] scratch; + + private boolean isWideBand; + private long currentSampleTimeUs; + private int currentSampleTotalBytes; + private int currentSampleBytesRemaining; + + private TrackOutput trackOutput; + private boolean hasOutputFormat; + + public AmrExtractor() { + scratch = new byte[1]; + } + + // Extractor implementation. + + @Override + public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { + return readAmrHeader(input); + } + + @Override + public void init(ExtractorOutput output) { + output.seekMap(new SeekMap.Unseekable(C.TIME_UNSET)); + trackOutput = output.track(/* id= */ 0, C.TRACK_TYPE_AUDIO); + output.endTracks(); + } + + @Override + public int read(ExtractorInput input, PositionHolder seekPosition) + throws IOException, InterruptedException { + if (input.getPosition() == 0) { + if (!readAmrHeader(input)) { + throw new ParserException("Could not find AMR header."); + } + } + maybeOutputFormat(); + return readSample(input); + } + + @Override + public void seek(long position, long timeUs) { + currentSampleTimeUs = 0; + currentSampleTotalBytes = 0; + currentSampleBytesRemaining = 0; + } + + @Override + public void release() { + // Do nothing + } + + /* package */ static int frameSizeBytesByTypeNb(int frameType) { + return frameSizeBytesByTypeNb[frameType]; + } + + /* package */ static int frameSizeBytesByTypeWb(int frameType) { + return frameSizeBytesByTypeWb[frameType]; + } + + /* package */ static byte[] amrSignatureNb() { + return Arrays.copyOf(amrSignatureNb, amrSignatureNb.length); + } + + /* package */ static byte[] amrSignatureWb() { + return Arrays.copyOf(amrSignatureWb, amrSignatureWb.length); + } + + // Internal methods. + + /** + * Peeks the AMR header from the beginning of the input, and consumes it if it exists. + * + * @param input The {@link ExtractorInput} from which data should be peeked/read. + * @return Whether the AMR header has been read. + */ + private boolean readAmrHeader(ExtractorInput input) throws IOException, InterruptedException { + if (peekAmrSignature(input, amrSignatureNb)) { + isWideBand = false; + input.skipFully(amrSignatureNb.length); + return true; + } else if (peekAmrSignature(input, amrSignatureWb)) { + isWideBand = true; + input.skipFully(amrSignatureWb.length); + return true; + } + return false; + } + + /** Peeks from the beginning of the input to see if the given AMR signature exists. */ + private boolean peekAmrSignature(ExtractorInput input, byte[] amrSignature) + throws IOException, InterruptedException { + input.resetPeekPosition(); + byte[] header = new byte[amrSignature.length]; + input.peekFully(header, 0, amrSignature.length); + return Arrays.equals(header, amrSignature); + } + + private void maybeOutputFormat() { + if (!hasOutputFormat) { + hasOutputFormat = true; + String mimeType = isWideBand ? MimeTypes.AUDIO_AMR_WB : MimeTypes.AUDIO_AMR_NB; + int sampleRate = isWideBand ? SAMPLE_RATE_WB : SAMPLE_RATE_NB; + trackOutput.format( + Format.createAudioSampleFormat( + /* id= */ null, + mimeType, + /* codecs= */ null, + /* bitrate= */ Format.NO_VALUE, + MAX_FRAME_SIZE_BYTES, + /* channelCount= */ 1, + sampleRate, + /* pcmEncoding= */ Format.NO_VALUE, + /* initializationData= */ null, + /* drmInitData= */ null, + /* selectionFlags= */ 0, + /* language= */ null)); + } + } + + private int readSample(ExtractorInput extractorInput) throws IOException, InterruptedException { + if (currentSampleBytesRemaining == 0) { + try { + currentSampleTotalBytes = readNextSampleSize(extractorInput); + } catch (EOFException e) { + return RESULT_END_OF_INPUT; + } + currentSampleBytesRemaining = currentSampleTotalBytes; + } + + int bytesAppended = + trackOutput.sampleData( + extractorInput, currentSampleBytesRemaining, /* allowEndOfInput= */ true); + if (bytesAppended == C.RESULT_END_OF_INPUT) { + return RESULT_END_OF_INPUT; + } + currentSampleBytesRemaining -= bytesAppended; + if (currentSampleBytesRemaining > 0) { + return RESULT_CONTINUE; + } + + trackOutput.sampleMetadata( + currentSampleTimeUs, + C.BUFFER_FLAG_KEY_FRAME, + currentSampleTotalBytes, + /* offset= */ 0, + /* encryptionData= */ null); + currentSampleTimeUs += SAMPLE_TIME_PER_FRAME_US; + return RESULT_CONTINUE; + } + + private int readNextSampleSize(ExtractorInput extractorInput) + throws IOException, InterruptedException { + extractorInput.resetPeekPosition(); + extractorInput.peekFully(scratch, /* offset= */ 0, /* length= */ 1); + + byte frameHeader = scratch[0]; + if ((frameHeader & 0x83) > 0) { + // The padding bits are at bit-1 positions in the following pattern: 1000 0011 + // Padding bits must be 0. + throw new ParserException("Invalid padding bits for frame header " + frameHeader); + } + + int frameType = (frameHeader >> 3) & 0x0f; + return getFrameSizeInBytes(frameType); + } + + private int getFrameSizeInBytes(int frameType) throws ParserException { + if (!isValidFrameType(frameType)) { + throw new ParserException( + "Illegal AMR " + (isWideBand ? "WB" : "NB") + " frame type " + frameType); + } + + return isWideBand ? frameSizeBytesByTypeWb[frameType] : frameSizeBytesByTypeNb[frameType]; + } + + private boolean isValidFrameType(int frameType) { + return frameType >= 0 + && frameType <= 15 + && (isWideBandValidFrameType(frameType) || isNarrowBandValidFrameType(frameType)); + } + + private boolean isWideBandValidFrameType(int frameType) { + // For wide band, type 10-13 are for future use. + return isWideBand && (frameType < 10 || frameType > 13); + } + + private boolean isNarrowBandValidFrameType(int frameType) { + // For narrow band, type 12-14 are for future use. + return !isWideBand && (frameType < 12 || frameType > 14); + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java index d30355699c..d1134dc3f6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java @@ -121,11 +121,11 @@ public final class FragmentedMp4Extractor implements Extractor { // Workarounds. @Flags private final int flags; - private final Track sideloadedTrack; + private final @Nullable Track sideloadedTrack; // Sideloaded data. private final List closedCaptionFormats; - private final DrmInitData sideloadedDrmInitData; + private final @Nullable DrmInitData sideloadedDrmInitData; // Track-linked data bundle, accessible as a whole through trackID. private final SparseArray trackBundles; @@ -136,7 +136,7 @@ public final class FragmentedMp4Extractor implements Extractor { private final ParsableByteArray nalBuffer; // Adjusts sample timestamps. - private final TimestampAdjuster timestampAdjuster; + private final @Nullable TimestampAdjuster timestampAdjuster; // Parser state. private final ParsableByteArray atomHeader; @@ -185,20 +185,23 @@ public final class FragmentedMp4Extractor implements Extractor { * @param flags Flags that control the extractor's behavior. * @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed. */ - public FragmentedMp4Extractor(@Flags int flags, TimestampAdjuster timestampAdjuster) { + public FragmentedMp4Extractor(@Flags int flags, @Nullable TimestampAdjuster timestampAdjuster) { this(flags, timestampAdjuster, null, null); } /** * @param flags Flags that control the extractor's behavior. * @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed. - * @param sideloadedTrack Sideloaded track information, in the case that the extractor - * will not receive a moov box in the input data. Null if a moov box is expected. + * @param sideloadedTrack Sideloaded track information, in the case that the extractor will not + * receive a moov box in the input data. Null if a moov box is expected. * @param sideloadedDrmInitData The {@link DrmInitData} to use for encrypted tracks. If null, the * pssh boxes (if present) will be used. */ - public FragmentedMp4Extractor(@Flags int flags, TimestampAdjuster timestampAdjuster, - Track sideloadedTrack, DrmInitData sideloadedDrmInitData) { + public FragmentedMp4Extractor( + @Flags int flags, + @Nullable TimestampAdjuster timestampAdjuster, + @Nullable Track sideloadedTrack, + @Nullable DrmInitData sideloadedDrmInitData) { this(flags, timestampAdjuster, sideloadedTrack, sideloadedDrmInitData, Collections.emptyList()); } @@ -206,15 +209,19 @@ public final class FragmentedMp4Extractor implements Extractor { /** * @param flags Flags that control the extractor's behavior. * @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed. - * @param sideloadedTrack Sideloaded track information, in the case that the extractor - * will not receive a moov box in the input data. Null if a moov box is expected. + * @param sideloadedTrack Sideloaded track information, in the case that the extractor will not + * receive a moov box in the input data. Null if a moov box is expected. * @param sideloadedDrmInitData The {@link DrmInitData} to use for encrypted tracks. If null, the * pssh boxes (if present) will be used. * @param closedCaptionFormats For tracks that contain SEI messages, the formats of the closed * caption channels to expose. */ - public FragmentedMp4Extractor(@Flags int flags, TimestampAdjuster timestampAdjuster, - Track sideloadedTrack, DrmInitData sideloadedDrmInitData, List closedCaptionFormats) { + public FragmentedMp4Extractor( + @Flags int flags, + @Nullable TimestampAdjuster timestampAdjuster, + @Nullable Track sideloadedTrack, + @Nullable DrmInitData sideloadedDrmInitData, + List closedCaptionFormats) { this(flags, timestampAdjuster, sideloadedTrack, sideloadedDrmInitData, closedCaptionFormats, null); } @@ -222,8 +229,8 @@ public final class FragmentedMp4Extractor implements Extractor { /** * @param flags Flags that control the extractor's behavior. * @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed. - * @param sideloadedTrack Sideloaded track information, in the case that the extractor - * will not receive a moov box in the input data. Null if a moov box is expected. + * @param sideloadedTrack Sideloaded track information, in the case that the extractor will not + * receive a moov box in the input data. Null if a moov box is expected. * @param sideloadedDrmInitData The {@link DrmInitData} to use for encrypted tracks. If null, the * pssh boxes (if present) will be used. * @param closedCaptionFormats For tracks that contain SEI messages, the formats of the closed @@ -232,8 +239,12 @@ public final class FragmentedMp4Extractor implements Extractor { * targeting the player, even if {@link #FLAG_ENABLE_EMSG_TRACK} is not set. Null if special * handling of emsg messages for players is not required. */ - public FragmentedMp4Extractor(@Flags int flags, TimestampAdjuster timestampAdjuster, - Track sideloadedTrack, DrmInitData sideloadedDrmInitData, List closedCaptionFormats, + public FragmentedMp4Extractor( + @Flags int flags, + @Nullable TimestampAdjuster timestampAdjuster, + @Nullable Track sideloadedTrack, + @Nullable DrmInitData sideloadedDrmInitData, + List closedCaptionFormats, @Nullable TrackOutput additionalEmsgTrackOutput) { this.flags = flags | (sideloadedTrack != null ? FLAG_SIDELOADED : 0); this.timestampAdjuster = timestampAdjuster; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtil.java index 55ce41e4b1..84513ef4d3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtil.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.extractor.mp4; +import android.support.annotation.Nullable; import android.util.Log; import com.google.android.exoplayer2.util.ParsableByteArray; import java.nio.ByteBuffer; @@ -36,7 +37,7 @@ public final class PsshAtomUtil { * @param data The scheme specific data. * @return The PSSH atom. */ - public static byte[] buildPsshAtom(UUID systemId, byte[] data) { + public static byte[] buildPsshAtom(UUID systemId, @Nullable byte[] data) { return buildPsshAtom(systemId, null, data); } @@ -48,7 +49,8 @@ public final class PsshAtomUtil { * @param data The scheme specific data. * @return The PSSH atom. */ - public static byte[] buildPsshAtom(UUID systemId, UUID[] keyIds, byte[] data) { + public static byte[] buildPsshAtom( + UUID systemId, @Nullable UUID[] keyIds, @Nullable byte[] data) { boolean buildV1Atom = keyIds != null; int dataLength = data != null ? data.length : 0; int psshBoxLength = Atom.FULL_HEADER_SIZE + 16 /* SystemId */ + 4 /* DataSize */ + dataLength; @@ -77,14 +79,14 @@ public final class PsshAtomUtil { /** * Parses the UUID from a PSSH atom. Version 0 and 1 PSSH atoms are supported. - *

- * The UUID is only parsed if the data is a valid PSSH atom. + * + *

The UUID is only parsed if the data is a valid PSSH atom. * * @param atom The atom to parse. - * @return The parsed UUID. Null if the input is not a valid PSSH atom, or if the PSSH atom has - * an unsupported version. + * @return The parsed UUID. Null if the input is not a valid PSSH atom, or if the PSSH atom has an + * unsupported version. */ - public static UUID parseUuid(byte[] atom) { + public static @Nullable UUID parseUuid(byte[] atom) { PsshAtom parsedAtom = parsePsshAtom(atom); if (parsedAtom == null) { return null; @@ -111,8 +113,8 @@ public final class PsshAtomUtil { /** * Parses the scheme specific data from a PSSH atom. Version 0 and 1 PSSH atoms are supported. - *

- * The scheme specific data is only parsed if the data is a valid PSSH atom matching the given + * + *

The scheme specific data is only parsed if the data is a valid PSSH atom matching the given * UUID, or if the data is a valid PSSH atom of any type in the case that the passed UUID is null. * * @param atom The atom to parse. @@ -120,7 +122,7 @@ public final class PsshAtomUtil { * @return The parsed scheme specific data. Null if the input is not a valid PSSH atom, or if the * PSSH atom has an unsupported version, or if the PSSH atom does not match the passed UUID. */ - public static byte[] parseSchemeSpecificData(byte[] atom, UUID uuid) { + public static @Nullable byte[] parseSchemeSpecificData(byte[] atom, UUID uuid) { PsshAtom parsedAtom = parsePsshAtom(atom); if (parsedAtom == null) { return null; @@ -140,7 +142,7 @@ public final class PsshAtomUtil { * has an unsupported version. */ // TODO: Support parsing of the key ids for version 1 PSSH atoms. - private static PsshAtom parsePsshAtom(byte[] atom) { + private static @Nullable PsshAtom parsePsshAtom(byte[] atom) { ParsableByteArray atomData = new ParsableByteArray(atom); if (atomData.limit() < Atom.FULL_HEADER_SIZE + 16 /* UUID */ + 4 /* DataSize */) { // Data too short. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java index cd33051bd2..d822916bce 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java @@ -52,6 +52,15 @@ public final class MediaCodecInfo { */ public final String name; + /** The MIME type handled by the codec, or {@code null} if this is a passthrough codec. */ + public final @Nullable String mimeType; + + /** + * The capabilities of the decoder, like the profiles/levels it supports, or {@code null} if this + * is a passthrough codec. + */ + public final @Nullable CodecCapabilities capabilities; + /** * Whether the decoder supports seamless resolution switches. * @@ -76,13 +85,8 @@ public final class MediaCodecInfo { */ public final boolean secure; - /** - * The capabilities of the decoder, like the profiles/levels it supports, or {@code null} if this - * is a passthrough codec. - */ - public final @Nullable CodecCapabilities capabilities; - - private final String mimeType; + /** Whether this instance describes a passthrough codec. */ + public final boolean passthrough; /** * Creates an instance representing an audio passthrough decoder. @@ -95,6 +99,7 @@ public final class MediaCodecInfo { name, /* mimeType= */ null, /* capabilities= */ null, + /* passthrough= */ true, /* forceDisableAdaptive= */ false, /* forceSecure= */ false); } @@ -110,7 +115,12 @@ public final class MediaCodecInfo { public static MediaCodecInfo newInstance(String name, String mimeType, CodecCapabilities capabilities) { return new MediaCodecInfo( - name, mimeType, capabilities, /* forceDisableAdaptive= */ false, /* forceSecure= */ false); + name, + mimeType, + capabilities, + /* passthrough= */ false, + /* forceDisableAdaptive= */ false, + /* forceSecure= */ false); } /** @@ -129,18 +139,21 @@ public final class MediaCodecInfo { CodecCapabilities capabilities, boolean forceDisableAdaptive, boolean forceSecure) { - return new MediaCodecInfo(name, mimeType, capabilities, forceDisableAdaptive, forceSecure); + return new MediaCodecInfo( + name, mimeType, capabilities, /* passthrough= */ false, forceDisableAdaptive, forceSecure); } private MediaCodecInfo( String name, - String mimeType, + @Nullable String mimeType, @Nullable CodecCapabilities capabilities, + boolean passthrough, boolean forceDisableAdaptive, boolean forceSecure) { this.name = Assertions.checkNotNull(name); - this.capabilities = capabilities; this.mimeType = mimeType; + this.capabilities = capabilities; + this.passthrough = passthrough; adaptive = !forceDisableAdaptive && capabilities != null && isAdaptive(capabilities); tunneling = capabilities != null && isTunneling(capabilities); secure = forceSecure || (capabilities != null && isSecure(capabilities)); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java index 2f95d15f12..03a0b66661 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java @@ -129,7 +129,10 @@ public abstract class MediaCodecRenderer extends BaseRenderer { */ private static final long MAX_CODEC_HOTSWAP_TIME_MS = 1000; - /** The possible return values for {@link #canKeepCodec(MediaCodec, boolean, Format, Format)}. */ + /** + * The possible return values for {@link #canKeepCodec(MediaCodec, MediaCodecInfo, Format, + * Format)}. + */ @Retention(RetentionPolicy.SOURCE) @IntDef({ KEEP_CODEC_RESULT_NO, @@ -885,7 +888,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { boolean keepingCodec = false; if (pendingDrmSession == drmSession && codec != null) { - switch (canKeepCodec(codec, codecInfo.adaptive, oldFormat, format)) { + switch (canKeepCodec(codec, codecInfo, oldFormat, format)) { case KEEP_CODEC_RESULT_NO: // Do nothing. break; @@ -962,13 +965,13 @@ public abstract class MediaCodecRenderer extends BaseRenderer { *

The default implementation returns {@link #KEEP_CODEC_RESULT_NO}. * * @param codec The existing {@link MediaCodec} instance. - * @param codecIsAdaptive Whether the codec is adaptive. + * @param codecInfo A {@link MediaCodecInfo} describing the decoder. * @param oldFormat The format for which the existing instance is configured. * @param newFormat The new format. * @return Whether the instance can be kept, and if it can whether it requires reconfiguration. */ protected @KeepCodecResult int canKeepCodec( - MediaCodec codec, boolean codecIsAdaptive, Format oldFormat, Format newFormat) { + MediaCodec codec, MediaCodecInfo codecInfo, Format oldFormat, Format newFormat) { return KEEP_CODEC_RESULT_NO; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/ActionFile.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/ActionFile.java index 0724755ac7..e37e09a090 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/ActionFile.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/ActionFile.java @@ -29,6 +29,8 @@ import java.io.InputStream; */ public final class ActionFile { + /* package */ static final int VERSION = 0; + private final AtomicFile atomicFile; private final File actionFile; @@ -56,13 +58,13 @@ public final class ActionFile { inputStream = atomicFile.openRead(); DataInputStream dataInputStream = new DataInputStream(inputStream); int version = dataInputStream.readInt(); - if (version > DownloadAction.MASTER_VERSION) { - throw new IOException("Not supported action file version: " + version); + if (version > VERSION) { + throw new IOException("Unsupported action file version: " + version); } int actionCount = dataInputStream.readInt(); DownloadAction[] actions = new DownloadAction[actionCount]; for (int i = 0; i < actionCount; i++) { - actions[i] = DownloadAction.deserializeFromStream(deserializers, dataInputStream, version); + actions[i] = DownloadAction.deserializeFromStream(deserializers, dataInputStream); } return actions; } finally { @@ -80,7 +82,7 @@ public final class ActionFile { DataOutputStream output = null; try { output = new DataOutputStream(atomicFile.startWrite()); - output.writeInt(DownloadAction.MASTER_VERSION); + output.writeInt(VERSION); output.writeInt(downloadActions.length); for (DownloadAction action : downloadActions) { DownloadAction.serializeToStream(action, output); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadAction.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadAction.java index cec46c3664..cf061f3745 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadAction.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadAction.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.offline; +import android.net.Uri; import android.support.annotation.Nullable; import java.io.ByteArrayOutputStream; import java.io.DataInputStream; @@ -22,78 +23,58 @@ import java.io.DataOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.util.Arrays; /** Contains the necessary parameters for a download or remove action. */ public abstract class DownloadAction { - /** - * Master version for all {@link DownloadAction} serialization/deserialization implementations. On - * each change on any {@link DownloadAction} serialization format this version needs to be - * increased. - */ - public static final int MASTER_VERSION = 0; - /** Used to deserialize {@link DownloadAction}s. */ - public interface Deserializer { + public abstract static class Deserializer { - /** Returns the type string of the {@link DownloadAction}. This string should be unique. */ - String getType(); + public final String type; + public final int version; + + public Deserializer(String type, int version) { + this.type = type; + this.version = version; + } /** - * Deserializes a {@link DownloadAction} from the {@code input}. + * Deserializes an action from the {@code input}. * - * @param version Version of the data. - * @param input DataInputStream to read data from. + * @param version The version of the serialized action. + * @param input The stream from which to read the action. * @see DownloadAction#writeToStream(DataOutputStream) - * @see DownloadAction#MASTER_VERSION */ - DownloadAction readFromStream(int version, DataInputStream input) throws IOException; + public abstract DownloadAction readFromStream(int version, DataInputStream input) + throws IOException; } /** - * Deserializes one {@code action} which was serialized by {@link - * #serializeToStream(DownloadAction, OutputStream)} from the {@code input} using one of the - * {@link Deserializer}s which supports the type of the action. + * Deserializes one action that was serialized with {@link #serializeToStream(DownloadAction, + * OutputStream)} from the {@code input}, using the {@link Deserializer}s that supports the + * action's type. * *

The caller is responsible for closing the given {@link InputStream}. * - * @param deserializers Array of {@link Deserializer}s to deserialize a {@link DownloadAction}. - * @param input Input stream to read serialized data. - * @return The deserialized {@link DownloadAction}. - * @throws IOException If there is an IO error from {@code input} or the action type isn't - * supported by any of the {@code deserializers}. + * @param deserializers {@link Deserializer}s for supported actions. + * @param input The stream from which to read the action. + * @return The deserialized action. + * @throws IOException If there is an IO error reading from {@code input}, or if the action type + * isn't supported by any of the {@code deserializers}. */ public static DownloadAction deserializeFromStream( Deserializer[] deserializers, InputStream input) throws IOException { - return deserializeFromStream(deserializers, input, MASTER_VERSION); - } - - /** - * Deserializes one {@code action} which was serialized by {@link - * #serializeToStream(DownloadAction, OutputStream)} from the {@code input} using one of the - * {@link Deserializer}s which supports the type of the action. - * - *

The caller is responsible for closing the given {@link InputStream}. - * - * @param deserializers Array of {@link Deserializer}s to deserialize a {@link DownloadAction}. - * @param input Input stream to read serialized data. - * @param version Master version of the serialization. See {@link DownloadAction#MASTER_VERSION}. - * @return The deserialized {@link DownloadAction}. - * @throws IOException If there is an IO error from {@code input}. - * @throws DownloadException If the action type isn't supported by any of the {@code - * deserializers}. - */ - public static DownloadAction deserializeFromStream( - Deserializer[] deserializers, InputStream input, int version) throws IOException { // Don't close the stream as it closes the underlying stream too. DataInputStream dataInputStream = new DataInputStream(input); String type = dataInputStream.readUTF(); + int version = dataInputStream.readInt(); for (Deserializer deserializer : deserializers) { - if (type.equals(deserializer.getType())) { + if (type.equals(deserializer.type) && deserializer.version >= version) { return deserializer.readFromStream(version, dataInputStream); } } - throw new DownloadException("No Deserializer can be found to parse the data."); + throw new DownloadException("No deserializer found for:" + type + ", " + version); } /** Serializes {@code action} type and data into the {@code output}. */ @@ -101,16 +82,37 @@ public abstract class DownloadAction { throws IOException { // Don't close the stream as it closes the underlying stream too. DataOutputStream dataOutputStream = new DataOutputStream(output); - dataOutputStream.writeUTF(action.getType()); + dataOutputStream.writeUTF(action.type); + dataOutputStream.writeInt(action.version); action.writeToStream(dataOutputStream); dataOutputStream.flush(); } - private final String data; + /** The type of the action. */ + public final String type; + /** The action version. */ + public final int version; + /** The uri being downloaded or removed. */ + public final Uri uri; + /** Whether this is a remove action. If false, this is a download action. */ + public final boolean isRemoveAction; + /** Custom data for this action. May be empty. */ + public final byte[] data; - /** @param data Optional custom data for this action. If null, an empty string is used. */ - protected DownloadAction(@Nullable String data) { - this.data = data != null ? data : ""; + /** + * @param type The type of the action. + * @param version The action version. + * @param uri The uri being downloaded or removed. + * @param isRemoveAction Whether this is a remove action. If false, this is a download action. + * @param data Optional custom data for this action. + */ + protected DownloadAction( + String type, int version, Uri uri, boolean isRemoveAction, @Nullable byte[] data) { + this.type = type; + this.version = version; + this.uri = uri; + this.isRemoveAction = isRemoveAction; + this.data = data != null ? data : new byte[0]; } /** Serializes itself into a byte array. */ @@ -125,23 +127,14 @@ public abstract class DownloadAction { return output.toByteArray(); } - /** Returns custom data for this action. */ - public final String getData() { - return data; + /** Returns whether this is an action for the same media as the {@code other}. */ + public boolean isSameMedia(DownloadAction other) { + return uri.equals(other.uri); } - /** Returns whether this is a remove action or a download action. */ - public abstract boolean isRemoveAction(); - - /** Returns the type string of the {@link DownloadAction}. This string should be unique. */ - protected abstract String getType(); - /** Serializes itself into the {@code output}. */ protected abstract void writeToStream(DataOutputStream output) throws IOException; - /** Returns whether this is an action for the same media as the {@code other}. */ - protected abstract boolean isSameMedia(DownloadAction other); - /** Creates a {@link Downloader} with the given parameters. */ protected abstract Downloader createDownloader( DownloaderConstructorHelper downloaderConstructorHelper); @@ -152,13 +145,18 @@ public abstract class DownloadAction { return false; } DownloadAction that = (DownloadAction) o; - return data.equals(that.data) && isRemoveAction() == that.isRemoveAction(); + return type.equals(that.type) + && version == that.version + && uri.equals(that.uri) + && isRemoveAction == that.isRemoveAction + && Arrays.equals(data, that.data); } @Override public int hashCode() { - int result = data.hashCode(); - result = 31 * result + (isRemoveAction() ? 1 : 0); + int result = uri.hashCode(); + result = 31 * result + (isRemoveAction ? 1 : 0); + result = 31 * result + Arrays.hashCode(data); return result; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java new file mode 100644 index 0000000000..f6157c1dc3 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.offline; + +import android.os.Handler; +import android.os.Looper; +import android.support.annotation.Nullable; +import com.google.android.exoplayer2.source.TrackGroupArray; +import java.io.IOException; +import java.util.List; + +/** A helper for initializing and removing downloads. */ +public abstract class DownloadHelper { + + /** A callback to be notified when the {@link DownloadHelper} is prepared. */ + public interface Callback { + + /** + * Called when preparation completes. + * + * @param helper The reporting {@link DownloadHelper}. + */ + void onPrepared(DownloadHelper helper); + + /** + * Called when preparation fails. + * + * @param helper The reporting {@link DownloadHelper}. + * @param e The error. + */ + void onPrepareError(DownloadHelper helper, IOException e); + } + + /** + * Initializes the helper for starting a download. + * + * @param callback A callback to be notified when preparation completes or fails. The callback + * will be invoked on the calling thread unless that thread does not have an associated {@link + * Looper}, in which case it will be called on the application's main thread. + */ + public void prepare(final Callback callback) { + final Handler handler = + new Handler(Looper.myLooper() != null ? Looper.myLooper() : Looper.getMainLooper()); + new Thread() { + @Override + public void run() { + try { + prepareInternal(); + handler.post( + new Runnable() { + @Override + public void run() { + callback.onPrepared(DownloadHelper.this); + } + }); + } catch (final IOException e) { + handler.post( + new Runnable() { + @Override + public void run() { + callback.onPrepareError(DownloadHelper.this, e); + } + }); + } + } + }.start(); + } + + /** + * Called on a background thread during preparation. + * + * @throws IOException If preparation fails. + */ + protected abstract void prepareInternal() throws IOException; + + /** + * Returns the number of periods for which media is available. Must not be called until after + * preparation completes. + */ + public abstract int getPeriodCount(); + + /** + * Returns the track groups for the given period. Must not be called until after preparation + * completes. + * + * @param periodIndex The period index. + * @return The track groups for the period. May be {@link TrackGroupArray#EMPTY} for single stream + * content. + */ + public abstract TrackGroupArray getTrackGroups(int periodIndex); + + /** + * Builds a {@link DownloadAction} for downloading the specified tracks. Must not be called until + * after preparation completes. + * + * @param data Application provided data to store in {@link DownloadAction#data}. + * @param trackKeys The selected tracks. If empty, all streams will be downloaded. + * @return The built {@link DownloadAction}. + */ + public abstract DownloadAction getDownloadAction(@Nullable byte[] data, List trackKeys); + + /** + * Builds a {@link DownloadAction} for removing the media. May be called in any state. + * + * @param data Application provided data to store in {@link DownloadAction#data}. + * @return The built {@link DownloadAction}. + */ + public abstract DownloadAction getRemoveAction(@Nullable byte[] data); +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java index 9be42ac5bb..8be822b6ca 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java @@ -15,20 +15,23 @@ */ package com.google.android.exoplayer2.offline; -import static com.google.android.exoplayer2.offline.DownloadManager.DownloadState.STATE_CANCELED; -import static com.google.android.exoplayer2.offline.DownloadManager.DownloadState.STATE_ENDED; -import static com.google.android.exoplayer2.offline.DownloadManager.DownloadState.STATE_ERROR; -import static com.google.android.exoplayer2.offline.DownloadManager.DownloadState.STATE_QUEUED; -import static com.google.android.exoplayer2.offline.DownloadManager.DownloadState.STATE_STARTED; +import static com.google.android.exoplayer2.offline.DownloadManager.TaskState.STATE_CANCELED; +import static com.google.android.exoplayer2.offline.DownloadManager.TaskState.STATE_COMPLETED; +import static com.google.android.exoplayer2.offline.DownloadManager.TaskState.STATE_FAILED; +import static com.google.android.exoplayer2.offline.DownloadManager.TaskState.STATE_QUEUED; +import static com.google.android.exoplayer2.offline.DownloadManager.TaskState.STATE_STARTED; import android.os.ConditionVariable; import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; import android.support.annotation.IntDef; +import android.support.annotation.Nullable; import android.util.Log; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.offline.DownloadAction.Deserializer; +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.cache.Cache; import com.google.android.exoplayer2.util.Assertions; import java.io.ByteArrayInputStream; import java.io.File; @@ -36,29 +39,33 @@ import java.io.IOException; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; +import java.util.List; import java.util.concurrent.CopyOnWriteArraySet; /** * Manages multiple stream download and remove requests. * - *

By default downloads are stopped. Call {@link #startDownloads()} to start downloads. - * - *

WARNING: Methods of this class must be called only on the main thread of the application. + *

A download manager instance must be accessed only from the thread that created it, unless that + * thread does not have a {@link Looper}. In that case, it must be accessed only from the + * application's main thread. Registered listeners will be called on the same thread. */ public final class DownloadManager { - /** - * Listener for download events. Listener methods are called on the main thread of the - * application. - */ - public interface DownloadListener { + /** Listener for {@link DownloadManager} events. */ + public interface Listener { /** - * Called on download state change. + * Called when all actions have been restored. * * @param downloadManager The reporting instance. - * @param downloadState The download task. */ - void onStateChange(DownloadManager downloadManager, DownloadState downloadState); + void onInitialized(DownloadManager downloadManager); + /** + * Called when the state of a task changes. + * + * @param downloadManager The reporting instance. + * @param taskState The state of the task. + */ + void onTaskStateChanged(DownloadManager downloadManager, TaskState taskState); /** * Called when there is no active task left. @@ -68,9 +75,9 @@ public final class DownloadManager { void onIdle(DownloadManager downloadManager); } - /** The default maximum number of simultaneous downloads. */ + /** The default maximum number of simultaneous download tasks. */ public static final int DEFAULT_MAX_SIMULTANEOUS_DOWNLOADS = 1; - /** The default minimum number of times the downloads must be retried before failing. */ + /** The default minimum number of times a task must be retried before failing. */ public static final int DEFAULT_MIN_RETRY_COUNT = 5; private static final String TAG = "DownloadManager"; @@ -81,34 +88,34 @@ public final class DownloadManager { private final int minRetryCount; private final ActionFile actionFile; private final DownloadAction.Deserializer[] deserializers; - private final ArrayList tasks; - private final ArrayList activeDownloadTasks; + private final ArrayList tasks; + private final ArrayList activeDownloadTasks; private final Handler handler; private final HandlerThread fileIOThread; private final Handler fileIOHandler; - private final CopyOnWriteArraySet listeners; + private final CopyOnWriteArraySet listeners; private int nextTaskId; - private boolean actionFileLoadCompleted; + private boolean initialized; private boolean released; private boolean downloadsStopped; /** - * Constructs a {@link DownloadManager}. + * Creates a {@link DownloadManager}. * - * @param constructorHelper A {@link DownloaderConstructorHelper} to create {@link Downloader}s - * for downloading data. + * @param cache Cache instance to be used to store downloaded data. + * @param upstreamDataSourceFactory A {@link DataSource.Factory} for creating data sources for + * downloading upstream data. * @param actionSaveFile File to save active actions. * @param deserializers Used to deserialize {@link DownloadAction}s. */ public DownloadManager( - DownloaderConstructorHelper constructorHelper, - String actionSaveFile, + Cache cache, + DataSource.Factory upstreamDataSourceFactory, + File actionSaveFile, Deserializer... deserializers) { this( - constructorHelper, - DEFAULT_MAX_SIMULTANEOUS_DOWNLOADS, - DEFAULT_MIN_RETRY_COUNT, + new DownloaderConstructorHelper(cache, upstreamDataSourceFactory), actionSaveFile, deserializers); } @@ -118,24 +125,43 @@ public final class DownloadManager { * * @param constructorHelper A {@link DownloaderConstructorHelper} to create {@link Downloader}s * for downloading data. - * @param maxSimultaneousDownloads The maximum number of simultaneous downloads. - * @param minRetryCount The minimum number of times the downloads must be retried before failing. - * @param actionSaveFile File to save active actions. + * @param actionFile The file in which active actions are saved. + * @param deserializers Used to deserialize {@link DownloadAction}s. + */ + public DownloadManager( + DownloaderConstructorHelper constructorHelper, + File actionFile, + Deserializer... deserializers) { + this( + constructorHelper, + DEFAULT_MAX_SIMULTANEOUS_DOWNLOADS, + DEFAULT_MIN_RETRY_COUNT, + actionFile, + deserializers); + } + + /** + * Constructs a {@link DownloadManager}. + * + * @param constructorHelper A {@link DownloaderConstructorHelper} to create {@link Downloader}s + * for downloading data. + * @param maxSimultaneousDownloads The maximum number of simultaneous download tasks. + * @param minRetryCount The minimum number of times a task must be retried before failing. + * @param actionFile The file in which active actions are saved. * @param deserializers Used to deserialize {@link DownloadAction}s. */ public DownloadManager( DownloaderConstructorHelper constructorHelper, int maxSimultaneousDownloads, int minRetryCount, - String actionSaveFile, + File actionFile, Deserializer... deserializers) { - Assertions.checkArgument( - deserializers.length > 0, "At least one Deserializer should be given."); + Assertions.checkArgument(deserializers.length > 0, "At least one Deserializer is required."); this.downloaderConstructorHelper = constructorHelper; this.maxActiveDownloadTasks = maxSimultaneousDownloads; this.minRetryCount = minRetryCount; - this.actionFile = new ActionFile(new File(actionSaveFile)); + this.actionFile = new ActionFile(actionFile); this.deserializers = deserializers; this.downloadsStopped = true; @@ -155,14 +181,143 @@ public final class DownloadManager { listeners = new CopyOnWriteArraySet<>(); loadActions(); - logd("DownloadManager is created"); + logd("Created"); } /** - * Stops all of the tasks and releases resources. If the action file isn't up to date, - * waits for the changes to be written. + * Adds a {@link Listener}. + * + * @param listener The listener to be added. + */ + public void addListener(Listener listener) { + listeners.add(listener); + } + + /** + * Removes a {@link Listener}. + * + * @param listener The listener to be removed. + */ + public void removeListener(Listener listener) { + listeners.remove(listener); + } + + /** Starts the download tasks. */ + public void startDownloads() { + Assertions.checkState(!released); + if (downloadsStopped) { + downloadsStopped = false; + maybeStartTasks(); + logd("Downloads are started"); + } + } + + /** Stops all of the download tasks. Call {@link #startDownloads()} to restart tasks. */ + public void stopDownloads() { + Assertions.checkState(!released); + if (!downloadsStopped) { + downloadsStopped = true; + for (int i = 0; i < activeDownloadTasks.size(); i++) { + activeDownloadTasks.get(i).stop(); + } + logd("Downloads are stopping"); + } + } + + /** + * Deserializes an action from {@code actionData}, and calls {@link + * #handleAction(DownloadAction)}. + * + * @param actionData Serialized version of the action to be executed. + * @return The id of the newly created task. + * @throws IOException If an error occurs deserializing the action. + */ + public int handleAction(byte[] actionData) throws IOException { + Assertions.checkState(!released); + ByteArrayInputStream input = new ByteArrayInputStream(actionData); + DownloadAction action = DownloadAction.deserializeFromStream(deserializers, input); + return handleAction(action); + } + + /** + * Handles the given action. A task is created and added to the task queue. If it's a remove + * action then any download tasks for the same media are immediately canceled. + * + * @param action The action to be executed. + * @return The id of the newly created task. + */ + public int handleAction(DownloadAction action) { + Assertions.checkState(!released); + Task task = addTaskForAction(action); + if (initialized) { + notifyListenersTaskStateChange(task); + saveActions(); + maybeStartTasks(); + if (task.currentState == STATE_QUEUED) { + // Task did not change out of its initial state, and so its initial state won't have been + // reported to listeners. Do so now. + notifyListenersTaskStateChange(task); + } + } + return task.id; + } + + /** Returns the current number of tasks. */ + public int getTaskCount() { + Assertions.checkState(!released); + return tasks.size(); + } + + /** Returns the state of a task, or null if no such task exists */ + public @Nullable TaskState getTaskState(int taskId) { + Assertions.checkState(!released); + for (int i = 0; i < tasks.size(); i++) { + Task task = tasks.get(i); + if (task.id == taskId) { + return task.getDownloadState(); + } + } + return null; + } + + /** Returns the states of all current tasks. */ + public TaskState[] getAllTaskStates() { + Assertions.checkState(!released); + TaskState[] states = new TaskState[tasks.size()]; + for (int i = 0; i < states.length; i++) { + states[i] = tasks.get(i).getDownloadState(); + } + return states; + } + + /** Returns whether the manager has completed initialization. */ + public boolean isInitialized() { + Assertions.checkState(!released); + return initialized; + } + + /** Returns whether there are no active tasks. */ + public boolean isIdle() { + Assertions.checkState(!released); + if (!initialized) { + return false; + } + for (int i = 0; i < tasks.size(); i++) { + if (tasks.get(i).isActive()) { + return false; + } + } + return true; + } + + /** + * Stops all of the tasks and releases resources. If the action file isn't up to date, waits for + * the changes to be written. The manager must not be accessed after this method has been called. */ public void release() { + if (released) { + return; + } released = true; for (int i = 0; i < tasks.size(); i++) { tasks.get(i).stop(); @@ -176,125 +331,14 @@ public final class DownloadManager { }); fileIOFinishedCondition.block(); fileIOThread.quit(); - logd("DownloadManager is released"); + logd("Released"); } - /** Stops all of the download tasks. Call {@link #startDownloads()} to restart tasks. */ - public void stopDownloads() { - if (!downloadsStopped) { - downloadsStopped = true; - for (int i = 0; i < activeDownloadTasks.size(); i++) { - activeDownloadTasks.get(i).stop(); - } - logd("Downloads are stopping"); - } - } - - /** Starts the download tasks. */ - public void startDownloads() { - if (downloadsStopped) { - downloadsStopped = false; - maybeStartTasks(); - logd("Downloads are started"); - } - } - - /** - * Adds a {@link DownloadListener}. - * - * @param listener The listener to be added. - */ - public void addListener(DownloadListener listener) { - listeners.add(listener); - } - - /** - * Removes a {@link DownloadListener}. - * - * @param listener The listener to be removed. - */ - public void removeListener(DownloadListener listener) { - listeners.remove(listener); - } - - /** - * Deserializes one {@link DownloadAction} from {@code actionData} and calls {@link - * #handleAction(DownloadAction)}. - * - * @param actionData Serialized {@link DownloadAction} data. - * @return The task id. - * @throws IOException If an error occurs during handling action. - */ - public int handleAction(byte[] actionData) throws IOException { - ByteArrayInputStream input = new ByteArrayInputStream(actionData); - DownloadAction action = DownloadAction.deserializeFromStream(deserializers, input); - return handleAction(action); - } - - /** - * Handles the given {@link DownloadAction}. A task is created and added to the task queue. If - * it's a remove action then this method cancels any download tasks which works on the same media - * immediately. - * - * @param downloadAction Action to be executed. - * @return The task id. - */ - public int handleAction(DownloadAction downloadAction) { - DownloadTask downloadTask = createDownloadTask(downloadAction); - saveActions(); - if (downloadsStopped && !downloadAction.isRemoveAction()) { - logd("Can't start the task as downloads are stopped", downloadTask); - } else { - maybeStartTasks(); - } - return downloadTask.id; - } - - private DownloadTask createDownloadTask(DownloadAction downloadAction) { - DownloadTask downloadTask = new DownloadTask(nextTaskId++, this, downloadAction, minRetryCount); - tasks.add(downloadTask); - logd("Task is added", downloadTask); - notifyListenersTaskStateChange(downloadTask); - return downloadTask; - } - - /** Returns number of tasks. */ - public int getTaskCount() { - return tasks.size(); - } - - /** Returns a {@link DownloadTask} for a task. */ - public DownloadState getDownloadState(int taskId) { - for (int i = 0; i < tasks.size(); i++) { - DownloadTask task = tasks.get(i); - if (task.id == taskId) { - return task.getDownloadState(); - } - } - return null; - } - - /** Returns {@link DownloadState}s for all tasks. */ - public DownloadState[] getDownloadStates() { - return getDownloadStates(tasks); - } - - /** Returns an array of {@link DownloadState}s for active download tasks. */ - public DownloadState[] getActiveDownloadStates() { - return getDownloadStates(activeDownloadTasks); - } - - /** Returns whether there are no active tasks. */ - public boolean isIdle() { - if (!actionFileLoadCompleted) { - return false; - } - for (int i = 0; i < tasks.size(); i++) { - if (tasks.get(i).isActive()) { - return false; - } - } - return true; + private Task addTaskForAction(DownloadAction action) { + Task task = new Task(nextTaskId++, this, action, minRetryCount); + tasks.add(task); + logd("Task is added", task); + return task; } /** @@ -310,34 +354,34 @@ public final class DownloadManager { * If the task is a remove action then preceding conflicting tasks are canceled. */ private void maybeStartTasks() { - if (released) { + if (!initialized || released) { return; } boolean skipDownloadActions = downloadsStopped || activeDownloadTasks.size() == maxActiveDownloadTasks; for (int i = 0; i < tasks.size(); i++) { - DownloadTask downloadTask = tasks.get(i); - if (!downloadTask.canStart()) { + Task task = tasks.get(i); + if (!task.canStart()) { continue; } - DownloadAction downloadAction = downloadTask.downloadAction; - boolean removeAction = downloadAction.isRemoveAction(); - if (!removeAction && skipDownloadActions) { + DownloadAction action = task.action; + boolean isRemoveAction = action.isRemoveAction; + if (!isRemoveAction && skipDownloadActions) { continue; } boolean canStartTask = true; for (int j = 0; j < i; j++) { - DownloadTask task = tasks.get(j); - if (task.downloadAction.isSameMedia(downloadAction)) { - if (removeAction) { + Task otherTask = tasks.get(j); + if (otherTask.action.isSameMedia(action)) { + if (isRemoveAction) { canStartTask = false; - logd(downloadTask + " clashes with " + task); - task.cancel(); + logd(task + " clashes with " + otherTask); + otherTask.cancel(); // Continue loop to cancel any other preceding clashing tasks. - } else if (task.downloadAction.isRemoveAction()) { + } else if (otherTask.action.isRemoveAction) { canStartTask = false; skipDownloadActions = true; break; @@ -346,9 +390,9 @@ public final class DownloadManager { } if (canStartTask) { - downloadTask.start(); - if (!removeAction) { - activeDownloadTasks.add(downloadTask); + task.start(); + if (!isRemoveAction) { + activeDownloadTasks.add(task); skipDownloadActions = activeDownloadTasks.size() == maxActiveDownloadTasks; } } @@ -360,23 +404,23 @@ public final class DownloadManager { return; } logd("Notify idle state"); - for (DownloadListener listener : listeners) { + for (Listener listener : listeners) { listener.onIdle(this); } } - private void onTaskStateChange(DownloadTask downloadTask) { + private void onTaskStateChange(Task task) { if (released) { return; } - logd("Task state is changed", downloadTask); - boolean stopped = !downloadTask.isActive(); + logd("Task state is changed", task); + boolean stopped = !task.isActive(); if (stopped) { - activeDownloadTasks.remove(downloadTask); + activeDownloadTasks.remove(task); } - notifyListenersTaskStateChange(downloadTask); - if (downloadTask.isFinished()) { - tasks.remove(downloadTask); + notifyListenersTaskStateChange(task); + if (task.isFinished()) { + tasks.remove(task); saveActions(); } if (stopped) { @@ -385,10 +429,10 @@ public final class DownloadManager { } } - private void notifyListenersTaskStateChange(DownloadTask downloadTask) { - DownloadState downloadState = downloadTask.getDownloadState(); - for (DownloadListener listener : listeners) { - listener.onStateChange(this, downloadState); + private void notifyListenersTaskStateChange(Task task) { + TaskState taskState = task.getDownloadState(); + for (Listener listener : listeners) { + listener.onTaskStateChanged(this, taskState); } } @@ -410,15 +454,33 @@ public final class DownloadManager { new Runnable() { @Override public void run() { - try { - for (DownloadAction action : actions) { - createDownloadTask(action); + if (released) { + return; + } + List pendingTasks = new ArrayList<>(tasks); + tasks.clear(); + for (DownloadAction action : actions) { + addTaskForAction(action); + } + logd("Tasks are created."); + initialized = true; + for (Listener listener : listeners) { + listener.onInitialized(DownloadManager.this); + } + if (!pendingTasks.isEmpty()) { + for (int i = 0; i < pendingTasks.size(); i++) { + tasks.add(pendingTasks.get(i)); + } + saveActions(); + } + maybeStartTasks(); + for (int i = 0; i < pendingTasks.size(); i++) { + Task pendingTask = pendingTasks.get(i); + if (pendingTask.currentState == STATE_QUEUED) { + // Task did not change out of its initial state, and so its initial state + // won't have been reported to listeners. Do so now. + notifyListenersTaskStateChange(pendingTask); } - logd("Tasks are created."); - maybeStartTasks(); - } finally { - actionFileLoadCompleted = true; - maybeNotifyListenersIdle(); } } }); @@ -427,12 +489,12 @@ public final class DownloadManager { } private void saveActions() { - if (!actionFileLoadCompleted || released) { + if (released) { return; } final DownloadAction[] actions = new DownloadAction[tasks.size()]; for (int i = 0; i < tasks.size(); i++) { - actions[i] = tasks.get(i).downloadAction; + actions[i] = tasks.get(i).action; } fileIOHandler.post(new Runnable() { @Override @@ -447,27 +509,18 @@ public final class DownloadManager { }); } - private void logd(String message) { + private static void logd(String message) { if (DEBUG) { Log.d(TAG, message); } } - private void logd(String message, DownloadTask task) { + private static void logd(String message, Task task) { logd(message + ": " + task); } - private static DownloadState[] getDownloadStates(ArrayList tasks) { - DownloadState[] states = new DownloadState[tasks.size()]; - for (int i = 0; i < tasks.size(); i++) { - DownloadTask task = tasks.get(i); - states[i] = task.getDownloadState(); - } - return states; - } - - /** Represents state of a download task. */ - public static final class DownloadState { + /** Represents state of a task. */ + public static final class TaskState { /** * Task states. @@ -476,23 +529,23 @@ public final class DownloadManager { * *

      *                    -> canceled
-     * queued <-> started -> ended
-     *                    -> error
+     * queued <-> started -> completed
+     *                    -> failed
      * 
*/ @Retention(RetentionPolicy.SOURCE) - @IntDef({STATE_QUEUED, STATE_STARTED, STATE_ENDED, STATE_CANCELED, STATE_ERROR}) + @IntDef({STATE_QUEUED, STATE_STARTED, STATE_COMPLETED, STATE_CANCELED, STATE_FAILED}) public @interface State {} /** The task is waiting to be started. */ public static final int STATE_QUEUED = 0; /** The task is currently started. */ public static final int STATE_STARTED = 1; /** The task completed. */ - public static final int STATE_ENDED = 2; + public static final int STATE_COMPLETED = 2; /** The task was canceled. */ public static final int STATE_CANCELED = 3; /** The task failed. */ - public static final int STATE_ERROR = 4; + public static final int STATE_FAILED = 4; /** Returns the state string for the given state value. */ public static String getStateString(@State int state) { @@ -501,45 +554,44 @@ public final class DownloadManager { return "QUEUED"; case STATE_STARTED: return "STARTED"; - case STATE_ENDED: - return "ENDED"; + case STATE_COMPLETED: + return "COMPLETED"; case STATE_CANCELED: return "CANCELED"; - case STATE_ERROR: - return "ERROR"; + case STATE_FAILED: + return "FAILED"; default: throw new IllegalStateException(); } } - /** Unique id of the task. */ + /** The unique task id. */ public final int taskId; - /** The {@link DownloadAction} which is being executed. */ - public final DownloadAction downloadAction; - /** The state of the task. See {@link State}. */ + /** The action being executed. */ + public final DownloadAction action; + /** The state of the task. */ public final @State int state; + /** - * The download percentage, or {@link Float#NaN} if it can't be calculated or the task is for - * removing. + * The estimated download percentage, or {@link C#PERCENTAGE_UNSET} if no estimate is available + * or if this is a removal task. */ public final float downloadPercentage; - /** - * The downloaded bytes, or {@link C#LENGTH_UNSET} if it hasn't been calculated yet or the task - * is for removing. - */ + /** The total number of downloaded bytes. */ public final long downloadedBytes; - /** If {@link #state} is {@link #STATE_ERROR} then this is the cause, otherwise null. */ + + /** If {@link #state} is {@link #STATE_FAILED} then this is the cause, otherwise null. */ public final Throwable error; - private DownloadState( + private TaskState( int taskId, - DownloadAction downloadAction, + DownloadAction action, @State int state, float downloadPercentage, long downloadedBytes, Throwable error) { this.taskId = taskId; - this.downloadAction = downloadAction; + this.action = action; this.state = state; this.downloadPercentage = downloadPercentage; this.downloadedBytes = downloadedBytes; @@ -548,7 +600,7 @@ public final class DownloadManager { } - private static final class DownloadTask implements Runnable { + private static final class Task implements Runnable { /** * Task states. @@ -556,24 +608,24 @@ public final class DownloadManager { *

Transition map (vertical states are source states): * *

-     *             +------+-------+-----+-----------+-----------+--------+--------+-----+
-     *             |queued|started|ended|q_canceling|s_canceling|canceled|stopping|error|
-     * +-----------+------+-------+-----+-----------+-----------+--------+--------+-----+
-     * |queued     |      |   X   |     |     X     |           |        |        |     |
-     * |started    |      |       |  X  |           |     X     |        |   X    |  X  |
-     * |q_canceling|      |       |     |           |           |   X    |        |     |
-     * |s_canceling|      |       |     |           |           |   X    |        |     |
-     * |stopping   |   X  |       |     |           |           |        |        |     |
-     * +-----------+------+-------+-----+-----------+-----------+--------+--------+-----+
+     *             +------+-------+---------+-----------+-----------+--------+--------+------+
+     *             |queued|started|completed|q_canceling|s_canceling|canceled|stopping|failed|
+     * +-----------+------+-------+---------+-----------+-----------+--------+--------+------+
+     * |queued     |      |   X   |         |     X     |           |        |        |      |
+     * |started    |      |       |    X    |           |     X     |        |   X    |   X  |
+     * |q_canceling|      |       |         |           |           |   X    |        |      |
+     * |s_canceling|      |       |         |           |           |   X    |        |      |
+     * |stopping   |   X  |       |         |           |           |        |        |      |
+     * +-----------+------+-------+---------+-----------+-----------+--------+--------+------+
      * 
*/ @Retention(RetentionPolicy.SOURCE) @IntDef({ STATE_QUEUED, STATE_STARTED, - STATE_ENDED, + STATE_COMPLETED, STATE_CANCELED, - STATE_ERROR, + STATE_FAILED, STATE_QUEUED_CANCELING, STATE_STARTED_CANCELING, STATE_STARTED_STOPPING @@ -588,32 +640,32 @@ public final class DownloadManager { private final int id; private final DownloadManager downloadManager; - private final DownloadAction downloadAction; + private final DownloadAction action; private final int minRetryCount; private volatile @InternalState int currentState; private volatile Downloader downloader; private Thread thread; private Throwable error; - private DownloadTask( - int id, DownloadManager downloadManager, DownloadAction downloadAction, int minRetryCount) { + private Task( + int id, DownloadManager downloadManager, DownloadAction action, int minRetryCount) { this.id = id; this.downloadManager = downloadManager; - this.downloadAction = downloadAction; + this.action = action; this.currentState = STATE_QUEUED; this.minRetryCount = minRetryCount; } - public DownloadState getDownloadState() { + public TaskState getDownloadState() { int externalState = getExternalState(); - return new DownloadState( - id, downloadAction, externalState, getDownloadPercentage(), getDownloadedBytes(), error); + return new TaskState( + id, action, externalState, getDownloadPercentage(), getDownloadedBytes(), error); } /** Returns whether the task is finished. */ public boolean isFinished() { - return currentState == STATE_ERROR - || currentState == STATE_ENDED + return currentState == STATE_FAILED + || currentState == STATE_COMPLETED || currentState == STATE_CANCELED; } @@ -626,19 +678,16 @@ public final class DownloadManager { } /** - * Returns the download percentage, or {@link Float#NaN} if it can't be calculated yet. This - * value can be an estimation. + * Returns the estimated download percentage, or {@link C#PERCENTAGE_UNSET} if no estimate is + * available. */ public float getDownloadPercentage() { - return downloader != null ? downloader.getDownloadPercentage() : Float.NaN; + return downloader != null ? downloader.getDownloadPercentage() : C.PERCENTAGE_UNSET; } - /** - * Returns the total number of downloaded bytes, or {@link C#LENGTH_UNSET} if it hasn't been - * calculated yet. - */ + /** Returns the total number of downloaded bytes. */ public long getDownloadedBytes() { - return downloader != null ? downloader.getDownloadedBytes() : C.LENGTH_UNSET; + return downloader != null ? downloader.getDownloadedBytes() : 0; } @Override @@ -646,11 +695,9 @@ public final class DownloadManager { if (!DEBUG) { return super.toString(); } - return downloadAction.getType() + return action.type + ' ' - + (downloadAction.isRemoveAction() ? "remove" : "download") - + ' ' - + downloadAction.getData() + + (action.isRemoveAction ? "remove" : "download") + ' ' + getStateString(); } @@ -663,7 +710,7 @@ public final class DownloadManager { case STATE_STARTED_STOPPING: return "STOPPING"; default: - return DownloadState.getStateString(currentState); + return TaskState.getStateString(currentState); } } @@ -700,13 +747,13 @@ public final class DownloadManager { } }); } else if (changeStateAndNotify(STATE_STARTED, STATE_STARTED_CANCELING)) { - thread.interrupt(); + cancelDownload(); } } private void stop() { if (changeStateAndNotify(STATE_STARTED, STATE_STARTED_STOPPING)) { - downloadManager.logd("Stopping", this); + logd("Stopping", this); thread.interrupt(); } } @@ -724,40 +771,46 @@ public final class DownloadManager { this.error = error; boolean isInternalState = currentState != getExternalState(); if (!isInternalState) { - downloadManager.onTaskStateChange(DownloadTask.this); + downloadManager.onTaskStateChange(this); } return true; } - /* Methods running on download thread. */ + private void cancelDownload() { + if (downloader != null) { + downloader.cancel(); + } + thread.interrupt(); + } + + // Methods running on download thread. @Override public void run() { - downloadManager.logd("Task is started", DownloadTask.this); + logd("Task is started", this); Throwable error = null; try { - downloader = downloadAction.createDownloader(downloadManager.downloaderConstructorHelper); - if (downloadAction.isRemoveAction()) { + downloader = action.createDownloader(downloadManager.downloaderConstructorHelper); + if (action.isRemoveAction) { downloader.remove(); } else { int errorCount = 0; long errorPosition = C.LENGTH_UNSET; - while (true) { + while (!Thread.interrupted()) { try { - downloader.download(null); + downloader.download(); break; } catch (IOException e) { long downloadedBytes = downloader.getDownloadedBytes(); if (downloadedBytes != errorPosition) { - downloadManager.logd( - "Reset error count. downloadedBytes = " + downloadedBytes, this); + logd("Reset error count. downloadedBytes = " + downloadedBytes, this); errorPosition = downloadedBytes; errorCount = 0; } if (currentState != STATE_STARTED || ++errorCount > minRetryCount) { throw e; } - downloadManager.logd("Download error. Retry " + errorCount, this); + logd("Download error. Retry " + errorCount, this); Thread.sleep(getRetryDelayMillis(errorCount)); } } @@ -771,7 +824,9 @@ public final class DownloadManager { @Override public void run() { if (changeStateAndNotify( - STATE_STARTED, finalError != null ? STATE_ERROR : STATE_ENDED, finalError) + STATE_STARTED, + finalError != null ? STATE_FAILED : STATE_COMPLETED, + finalError) || changeStateAndNotify(STATE_STARTED_CANCELING, STATE_CANCELED) || changeStateAndNotify(STATE_STARTED_STOPPING, STATE_QUEUED)) { return; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java index a5656ec109..908aae481a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java @@ -16,150 +16,244 @@ package com.google.android.exoplayer2.offline; import android.app.Notification; -import android.app.Notification.Builder; -import android.app.NotificationChannel; -import android.app.NotificationManager; import android.app.Service; import android.content.Context; import android.content.Intent; import android.os.Handler; import android.os.IBinder; import android.os.Looper; -import android.support.annotation.CallSuper; import android.support.annotation.Nullable; +import android.support.annotation.StringRes; import android.util.Log; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.offline.DownloadManager.DownloadState; +import com.google.android.exoplayer2.offline.DownloadManager.TaskState; import com.google.android.exoplayer2.scheduler.Requirements; import com.google.android.exoplayer2.scheduler.RequirementsWatcher; import com.google.android.exoplayer2.scheduler.Scheduler; +import com.google.android.exoplayer2.util.NotificationUtil; import com.google.android.exoplayer2.util.Util; import java.io.IOException; +import java.util.HashMap; -/** - * A {@link Service} that downloads streams in the background. - * - *

To start the service, create an instance of one of the subclasses of {@link DownloadAction} - * and call {@link #addDownloadAction(Context, Class, DownloadAction)} with it. - */ -public abstract class DownloadService extends Service implements DownloadManager.DownloadListener { +/** A {@link Service} for downloading media. */ +public abstract class DownloadService extends Service { - /** Use this action to initialize {@link DownloadManager}. */ + /** Starts a download service without adding a new {@link DownloadAction}. */ public static final String ACTION_INIT = "com.google.android.exoplayer.downloadService.action.INIT"; - /** Use this action to add a {@link DownloadAction} to {@link DownloadManager} action queue. */ + /** Starts a download service, adding a new {@link DownloadAction} to be executed. */ public static final String ACTION_ADD = "com.google.android.exoplayer.downloadService.action.ADD"; - /** Use this action to make {@link DownloadManager} stop download tasks. */ - private static final String ACTION_STOP = - "com.google.android.exoplayer.downloadService.action.STOP"; + /** Like {@link #ACTION_INIT}, but with {@link #KEY_FOREGROUND} implicitly set to true. */ + private static final String ACTION_RESTART = + "com.google.android.exoplayer.downloadService.action.RESTART"; - /** Use this action to make {@link DownloadManager} start download tasks. */ - private static final String ACTION_START = - "com.google.android.exoplayer.downloadService.action.START"; + /** Starts download tasks. */ + private static final String ACTION_START_DOWNLOADS = + "com.google.android.exoplayer.downloadService.action.START_DOWNLOADS"; - /** A {@link DownloadAction} to be executed. */ - public static final String DOWNLOAD_ACTION = "DownloadAction"; + /** Stops download tasks. */ + private static final String ACTION_STOP_DOWNLOADS = + "com.google.android.exoplayer.downloadService.action.STOP_DOWNLOADS"; - /** Default progress update interval in milliseconds. */ - public static final long DEFAULT_PROGRESS_UPDATE_INTERVAL_MILLIS = 1000; + /** Key for the {@link DownloadAction} in an {@link #ACTION_ADD} intent. */ + public static final String KEY_DOWNLOAD_ACTION = "download_action"; + + /** + * Key for a boolean flag in any intent to indicate whether the service was started in the + * foreground. If set, the service is guaranteed to call {@link #startForeground(int, + * Notification)}. + */ + public static final String KEY_FOREGROUND = "foreground"; + + /** Default foreground notification update interval in milliseconds. */ + public static final long DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL = 1000; private static final String TAG = "DownloadService"; private static final boolean DEBUG = false; - // Keep requirementsWatcher and scheduler alive beyond DownloadService life span (until the app is - // killed) because it may take long time for Scheduler to start the service. - private static RequirementsWatcher requirementsWatcher; - private static Scheduler scheduler; + // Keep the requirements helper for each DownloadService as long as there are tasks (and the + // process is running). This allows tasks to resume when there's no scheduler. It may also allow + // tasks the resume more quickly than when relying on the scheduler alone. + private static final HashMap, RequirementsHelper> + requirementsHelpers = new HashMap<>(); - private final int notificationIdOffset; - private final long progressUpdateIntervalMillis; + private final ForegroundNotificationUpdater foregroundNotificationUpdater; + private final @Nullable String channelId; + private final @StringRes int channelName; private DownloadManager downloadManager; - private ProgressUpdater progressUpdater; + private DownloadManagerListener downloadManagerListener; private int lastStartId; - - /** @param notificationIdOffset Value to offset notification ids. Must be greater than 0. */ - protected DownloadService(int notificationIdOffset) { - this(notificationIdOffset, DEFAULT_PROGRESS_UPDATE_INTERVAL_MILLIS); - } + private boolean startedInForeground; /** - * @param notificationIdOffset Value to offset notification ids. Must be greater than 0. - * @param progressUpdateIntervalMillis {@link #onProgressUpdate(DownloadState[])} is called using - * this interval. If it's {@link C#TIME_UNSET}, then {@link - * #onProgressUpdate(DownloadState[])} isn't called. - */ - protected DownloadService(int notificationIdOffset, long progressUpdateIntervalMillis) { - this.notificationIdOffset = notificationIdOffset; - this.progressUpdateIntervalMillis = progressUpdateIntervalMillis; - } - - /** - * Creates an {@link Intent} to be used to start this service and adds the {@link DownloadAction} - * to the {@link DownloadManager}. + * Creates a DownloadService with {@link #DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL}. * - * @param context A {@link Context} of the application calling this service. - * @param clazz Class object of DownloadService or subclass. - * @param downloadAction A {@link DownloadAction} to be executed. + * @param foregroundNotificationId The notification id for the foreground notification, must not + * be 0. + */ + protected DownloadService(int foregroundNotificationId) { + this(foregroundNotificationId, DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL); + } + + /** + * @param foregroundNotificationId The notification id for the foreground notification, must not + * be 0. + * @param foregroundNotificationUpdateInterval The maximum interval to update foreground + * notification, in milliseconds. + */ + protected DownloadService( + int foregroundNotificationId, long foregroundNotificationUpdateInterval) { + this( + foregroundNotificationId, + foregroundNotificationUpdateInterval, + /* channelId= */ null, + /* channelName= */ 0); + } + + /** + * @param foregroundNotificationId The notification id for the foreground notification. Must not + * be 0. + * @param foregroundNotificationUpdateInterval The maximum interval between updates to the + * foreground notification, in milliseconds. + * @param channelId An id for a low priority notification channel to create, or {@code null} if + * the app will take care of creating a notification channel if needed. If specified, must be + * unique per package and the value may be truncated if it is too long. + * @param channelName A string resource identifier for the user visible name of the channel, if + * {@code channelId} is specified. The recommended maximum length is 40 characters; the value + * may be truncated if it is too long. + */ + protected DownloadService( + int foregroundNotificationId, + long foregroundNotificationUpdateInterval, + @Nullable String channelId, + @StringRes int channelName) { + foregroundNotificationUpdater = + new ForegroundNotificationUpdater( + foregroundNotificationId, foregroundNotificationUpdateInterval); + this.channelId = channelId; + this.channelName = channelName; + } + + /** + * Builds an {@link Intent} for adding an action to be executed by the service. + * + * @param context A {@link Context}. + * @param clazz The concrete download service being targeted by the intent. + * @param downloadAction The action to be executed. + * @param foreground Whether this intent will be used to start the service in the foreground. * @return Created Intent. */ - public static Intent createAddDownloadActionIntent( - Context context, Class clazz, DownloadAction downloadAction) { + public static Intent buildAddActionIntent( + Context context, + Class clazz, + DownloadAction downloadAction, + boolean foreground) { return new Intent(context, clazz) .setAction(ACTION_ADD) - .putExtra(DOWNLOAD_ACTION, downloadAction.toByteArray()); + .putExtra(KEY_DOWNLOAD_ACTION, downloadAction.toByteArray()) + .putExtra(KEY_FOREGROUND, foreground); } /** - * Adds a {@link DownloadAction} to the {@link DownloadManager}. This will start the download - * service if it was not running. + * Starts the service, adding an action to be executed. * - * @param context A {@link Context} of the application calling this service. - * @param clazz Class object of DownloadService or subclass. - * @param downloadAction A {@link DownloadAction} to be executed. - * @see #createAddDownloadActionIntent(Context, Class, DownloadAction) + * @param context A {@link Context}. + * @param clazz The concrete download service being targeted by the intent. + * @param downloadAction The action to be executed. + * @param foreground Whether this intent will be used to start the service in the foreground. */ - public static void addDownloadAction( - Context context, Class clazz, DownloadAction downloadAction) { - context.startService(createAddDownloadActionIntent(context, clazz, downloadAction)); + public static void startWithAction( + Context context, + Class clazz, + DownloadAction downloadAction, + boolean foreground) { + Intent intent = buildAddActionIntent(context, clazz, downloadAction, foreground); + if (foreground) { + Util.startForegroundService(context, intent); + } else { + context.startService(intent); + } } @Override public void onCreate() { logd("onCreate"); + if (channelId != null) { + NotificationUtil.createNotificationChannel( + this, channelId, channelName, NotificationUtil.IMPORTANCE_LOW); + } downloadManager = getDownloadManager(); - downloadManager.addListener(this); + downloadManagerListener = new DownloadManagerListener(); + downloadManager.addListener(downloadManagerListener); - if (requirementsWatcher == null) { - Requirements requirements = getRequirements(); - if (requirements != null) { - scheduler = getScheduler(); - RequirementsListener listener = - new RequirementsListener(getApplicationContext(), getClass(), scheduler); - requirementsWatcher = - new RequirementsWatcher(getApplicationContext(), listener, requirements); - requirementsWatcher.start(); + RequirementsHelper requirementsHelper; + synchronized (requirementsHelpers) { + Class clazz = getClass(); + requirementsHelper = requirementsHelpers.get(clazz); + if (requirementsHelper == null) { + requirementsHelper = new RequirementsHelper(this, getRequirements(), getScheduler(), clazz); + requirementsHelpers.put(clazz, requirementsHelper); } } + requirementsHelper.start(); + } - progressUpdater = new ProgressUpdater(this, progressUpdateIntervalMillis); + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + lastStartId = startId; + String intentAction = null; + if (intent != null) { + intentAction = intent.getAction(); + startedInForeground |= + intent.getBooleanExtra(KEY_FOREGROUND, false) || ACTION_RESTART.equals(intentAction); + } + logd("onStartCommand action: " + intentAction + " startId: " + startId); + switch (intentAction) { + case ACTION_INIT: + case ACTION_RESTART: + // Do nothing. The RequirementsWatcher will start downloads when possible. + break; + case ACTION_ADD: + byte[] actionData = intent.getByteArrayExtra(KEY_DOWNLOAD_ACTION); + if (actionData == null) { + Log.e(TAG, "Ignoring ADD action with no action data"); + } else { + try { + downloadManager.handleAction(actionData); + } catch (IOException e) { + Log.e(TAG, "Failed to handle ADD action", e); + } + } + break; + case ACTION_STOP_DOWNLOADS: + downloadManager.stopDownloads(); + break; + case ACTION_START_DOWNLOADS: + downloadManager.startDownloads(); + break; + default: + Log.e(TAG, "Ignoring unrecognized action: " + intentAction); + break; + } + if (downloadManager.isIdle()) { + stop(); + } + return START_STICKY; } @Override public void onDestroy() { logd("onDestroy"); - progressUpdater.stop(); - downloadManager.removeListener(this); + foregroundNotificationUpdater.stopPeriodicUpdates(); + downloadManager.removeListener(downloadManagerListener); if (downloadManager.getTaskCount() == 0) { - if (requirementsWatcher != null) { - requirementsWatcher.stop(); - requirementsWatcher = null; - } - if (scheduler != null) { - scheduler.cancel(); - scheduler = null; + synchronized (requirementsHelpers) { + RequirementsHelper requirementsHelper = requirementsHelpers.remove(getClass()); + if (requirementsHelper != null) { + requirementsHelper.stop(); + } } } } @@ -170,206 +264,173 @@ public abstract class DownloadService extends Service implements DownloadManager return null; } - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - this.lastStartId = startId; - String intentAction = intent != null ? intent.getAction() : null; - if (intentAction == null) { - intentAction = ACTION_INIT; - } - logd("onStartCommand action: " + intentAction + " startId: " + startId); - switch (intentAction) { - case ACTION_INIT: - // Do nothing. DownloadManager and RequirementsWatcher is initialized. If there are download - // or remove tasks loaded from file, they will start if the requirements are met. - break; - case ACTION_ADD: - byte[] actionData = intent.getByteArrayExtra(DOWNLOAD_ACTION); - if (actionData == null) { - onCommandError(intent, new IllegalArgumentException("DownloadAction is missing.")); - } else { - try { - onNewTask(intent, downloadManager.handleAction(actionData)); - } catch (IOException e) { - onCommandError(intent, e); - } - } - break; - case ACTION_STOP: - downloadManager.stopDownloads(); - break; - case ACTION_START: - downloadManager.startDownloads(); - break; - default: - onCommandError(intent, new IllegalArgumentException("Unknown action: " + intentAction)); - break; - } - if (downloadManager.isIdle()) { - onIdle(null); - } - return START_STICKY; - } - /** * Returns a {@link DownloadManager} to be used to downloaded content. Called only once in the - * life cycle of the service. + * life cycle of the service. The service will call {@link DownloadManager#startDownloads()} and + * {@link DownloadManager#stopDownloads} as necessary when requirements returned by {@link + * #getRequirements()} are met or stop being met. */ protected abstract DownloadManager getDownloadManager(); /** - * Returns a {@link Scheduler} which contains a job to initialize {@link DownloadService} when the - * requirements are met, or null. If not null, scheduler is used to start downloads even when the - * app isn't running. + * Returns a {@link Scheduler} to restart the service when requirements allowing downloads to take + * place are met. If {@code null}, the service will only be restarted if the process is still in + * memory when the requirements are met. */ protected abstract @Nullable Scheduler getScheduler(); - /** Returns requirements for downloads to take place, or null. */ - protected abstract @Nullable Requirements getRequirements(); + /** + * Returns requirements for downloads to take place. By default the only requirement is that the + * device has network connectivity. + */ + protected Requirements getRequirements() { + return new Requirements(Requirements.NETWORK_TYPE_ANY, false, false); + } - /** Called on error in start command. */ - protected void onCommandError(Intent intent, Exception error) { + /** + * Returns a notification to be displayed when this service running in the foreground. + * + *

This method is called when there is a task state change and periodically while there are + * active tasks. The periodic update interval can be set using {@link #DownloadService(int, + * long)}. + * + *

On API level 26 and above, this method may also be called just before the service stops, + * with an empty {@code taskStates} array. The returned notification is used to satisfy system + * requirements for foreground services. + * + * @param taskStates The states of all current tasks. + * @return The foreground notification to display. + */ + protected abstract Notification getForegroundNotification(TaskState[] taskStates); + + /** + * Called when the state of a task changes. + * + * @param taskState The state of the task. + */ + protected void onTaskStateChanged(TaskState taskState) { // Do nothing. } - /** Called when a new task is added to the {@link DownloadManager}. */ - protected void onNewTask(Intent intent, int taskId) { - // Do nothing. - } - - /** Returns a notification channelId. See {@link NotificationChannel}. */ - protected abstract String getNotificationChannelId(); - - /** - * Helper method which calls {@link #startForeground(int, Notification)} with {@code - * notificationIdOffset} and {@code foregroundNotification}. - */ - public void startForeground(Notification foregroundNotification) { - // logd("start foreground"); - startForeground(notificationIdOffset, foregroundNotification); - } - - /** - * Sets/replaces or cancels the notification for the given id. - * - * @param id A unique id for the notification. This value is offset by {@code - * notificationIdOffset}. - * @param notification If not null, it's showed, replacing any previous notification. Otherwise - * any previous notification is canceled. - */ - public void setNotification(int id, @Nullable Notification notification) { - NotificationManager notificationManager = - (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - if (notification != null) { - notificationManager.notify(notificationIdOffset + 1 + id, notification); - } else { - notificationManager.cancel(notificationIdOffset + 1 + id); - } - } - - /** - * Override this method to get notified. - * - *

{@inheritDoc} - */ - @CallSuper - @Override - public void onStateChange(DownloadManager downloadManager, DownloadState downloadState) { - if (downloadState.state == DownloadState.STATE_STARTED) { - progressUpdater.start(); - } - } - - /** - * Override this method to get notified. - * - *

{@inheritDoc} - */ - @CallSuper - @Override - public void onIdle(DownloadManager downloadManager) { - // Make sure startForeground is called before stopping. - // Workaround for https://buganizer.corp.google.com/issues/69424260 - if (Util.SDK_INT >= 26) { - Builder notificationBuilder = new Builder(this, getNotificationChannelId()); - Notification foregroundNotification = notificationBuilder.build(); - startForeground(foregroundNotification); + private void stop() { + foregroundNotificationUpdater.stopPeriodicUpdates(); + // Make sure startForeground is called before stopping. Workaround for [Internal: b/69424260]. + if (startedInForeground && Util.SDK_INT >= 26) { + foregroundNotificationUpdater.showNotificationIfNotAlready(); } boolean stopSelfResult = stopSelfResult(lastStartId); logd("stopSelf(" + lastStartId + ") result: " + stopSelfResult); } - /** Override this method to get notified on every second while there are active downloads. */ - protected void onProgressUpdate(DownloadState[] activeDownloadTasks) { - // Do nothing. - } - private void logd(String message) { if (DEBUG) { Log.d(TAG, message); } } - private static final class ProgressUpdater implements Runnable { + private final class DownloadManagerListener implements DownloadManager.Listener { + @Override + public void onInitialized(DownloadManager downloadManager) { + // Do nothing. + } - private final DownloadService downloadService; - private final long progressUpdateIntervalMillis; + @Override + public void onTaskStateChanged(DownloadManager downloadManager, TaskState taskState) { + DownloadService.this.onTaskStateChanged(taskState); + if (taskState.state == TaskState.STATE_STARTED) { + foregroundNotificationUpdater.startPeriodicUpdates(); + } else { + foregroundNotificationUpdater.update(); + } + } + + @Override + public final void onIdle(DownloadManager downloadManager) { + stop(); + } + } + + private final class ForegroundNotificationUpdater implements Runnable { + + private final int notificationId; + private final long updateInterval; private final Handler handler; - private boolean stopped; - public ProgressUpdater(DownloadService downloadService, long progressUpdateIntervalMillis) { - this.downloadService = downloadService; - this.progressUpdateIntervalMillis = progressUpdateIntervalMillis; + private boolean periodicUpdatesStarted; + private boolean notificationDisplayed; + + public ForegroundNotificationUpdater(int notificationId, long updateInterval) { + this.notificationId = notificationId; + this.updateInterval = updateInterval; this.handler = new Handler(Looper.getMainLooper()); - stopped = true; + } + + public void startPeriodicUpdates() { + periodicUpdatesStarted = true; + update(); + } + + public void stopPeriodicUpdates() { + periodicUpdatesStarted = false; + handler.removeCallbacks(this); + } + + public void update() { + TaskState[] taskStates = downloadManager.getAllTaskStates(); + startForeground(notificationId, getForegroundNotification(taskStates)); + notificationDisplayed = true; + if (periodicUpdatesStarted) { + handler.removeCallbacks(this); + handler.postDelayed(this, updateInterval); + } + } + + public void showNotificationIfNotAlready() { + if (!notificationDisplayed) { + update(); + } } @Override public void run() { - DownloadState[] activeDownloadTasks = - downloadService.downloadManager.getActiveDownloadStates(); - if (activeDownloadTasks.length > 0) { - downloadService.onProgressUpdate(activeDownloadTasks); - if (progressUpdateIntervalMillis != C.TIME_UNSET) { - handler.postDelayed(this, progressUpdateIntervalMillis); - } - } else { - stop(); - } + update(); } + } - public void stop() { - stopped = true; - handler.removeCallbacks(this); + private static final class RequirementsHelper implements RequirementsWatcher.Listener { + + private final Context context; + private final Requirements requirements; + private final @Nullable Scheduler scheduler; + private final Class serviceClass; + private final RequirementsWatcher requirementsWatcher; + + private RequirementsHelper( + Context context, + Requirements requirements, + @Nullable Scheduler scheduler, + Class serviceClass) { + this.context = context; + this.requirements = requirements; + this.scheduler = scheduler; + this.serviceClass = serviceClass; + requirementsWatcher = new RequirementsWatcher(context, this, requirements); } public void start() { - if (stopped) { - stopped = false; - if (progressUpdateIntervalMillis != C.TIME_UNSET) { - handler.post(this); - } - } + requirementsWatcher.start(); } - } - - private static final class RequirementsListener implements RequirementsWatcher.Listener { - - private final Context context; - private final Class serviceClass; - private final Scheduler scheduler; - - private RequirementsListener( - Context context, Class serviceClass, Scheduler scheduler) { - this.context = context; - this.serviceClass = serviceClass; - this.scheduler = scheduler; + public void stop() { + requirementsWatcher.stop(); + if (scheduler != null) { + scheduler.cancel(); + } } @Override public void requirementsMet(RequirementsWatcher requirementsWatcher) { - startServiceWithAction(DownloadService.ACTION_START); + startServiceWithAction(DownloadService.ACTION_START_DOWNLOADS); if (scheduler != null) { scheduler.cancel(); } @@ -377,21 +438,20 @@ public abstract class DownloadService extends Service implements DownloadManager @Override public void requirementsNotMet(RequirementsWatcher requirementsWatcher) { - startServiceWithAction(DownloadService.ACTION_STOP); + startServiceWithAction(DownloadService.ACTION_STOP_DOWNLOADS); if (scheduler != null) { - if (!scheduler.schedule()) { + String servicePackage = context.getPackageName(); + boolean success = scheduler.schedule(requirements, servicePackage, ACTION_RESTART); + if (!success) { Log.e(TAG, "Scheduling downloads failed."); } } } private void startServiceWithAction(String action) { - Intent intent = new Intent(context, serviceClass).setAction(action); - if (Util.SDK_INT >= 26) { - context.startForegroundService(intent); - } else { - context.startService(intent); - } + Intent intent = + new Intent(context, serviceClass).setAction(action).putExtra(KEY_FOREGROUND, true); + Util.startForegroundService(context, intent); } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/Downloader.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/Downloader.java index b8d9432c63..10523d6bc6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/Downloader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/Downloader.java @@ -15,7 +15,6 @@ */ package com.google.android.exoplayer2.offline; -import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import java.io.IOException; @@ -24,69 +23,31 @@ import java.io.IOException; */ public interface Downloader { - /** - * Listener notified when download progresses. - *

- * No guarantees are made about the thread or threads on which the listener is called, but it is - * guaranteed that listener methods will be called in a serial fashion (i.e. one at a time) and in - * the same order as events occurred. - */ - interface ProgressListener { - /** - * Called during the download. Calling intervals depend on the {@link Downloader} - * implementation. - * - * @param downloader The reporting instance. - * @param downloadPercentage The download percentage. This value can be an estimation. - * @param downloadedBytes Total number of downloaded bytes. - * @see #download(ProgressListener) - */ - void onDownloadProgress(Downloader downloader, float downloadPercentage, long downloadedBytes); - } - - /** - * Initializes the downloader. - * - * @throws DownloadException Thrown if the media cannot be downloaded. - * @throws InterruptedException If the thread has been interrupted. - * @throws IOException Thrown when there is an io error while reading from cache. - * @see #getDownloadedBytes() - * @see #getDownloadPercentage() - */ - void init() throws InterruptedException, IOException; - /** * Downloads the media. * - * @param listener If not null, called during download. * @throws DownloadException Thrown if the media cannot be downloaded. * @throws InterruptedException If the thread has been interrupted. * @throws IOException Thrown when there is an io error while downloading. */ - void download(@Nullable ProgressListener listener) - throws InterruptedException, IOException; + void download() throws InterruptedException, IOException; + + /** Interrupts any current download operation and prevents future operations from running. */ + void cancel(); + + /** Returns the total number of downloaded bytes. */ + long getDownloadedBytes(); /** - * Removes all of the downloaded data of the media. + * Returns the estimated download percentage, or {@link C#PERCENTAGE_UNSET} if no estimate is + * available. + */ + float getDownloadPercentage(); + + /** + * Removes the media. * * @throws InterruptedException Thrown if the thread was interrupted. */ void remove() throws InterruptedException; - - /** - * Returns the total number of downloaded bytes, or {@link C#LENGTH_UNSET} if it hasn't been - * calculated yet. - * - * @see #init() - */ - long getDownloadedBytes(); - - /** - * Returns the download percentage, or {@link Float#NaN} if it can't be calculated yet. This - * value can be an estimation. - * - * @see #init() - */ - float getDownloadPercentage(); - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloaderConstructorHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloaderConstructorHelper.java index 9ef9366397..18387b9d92 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloaderConstructorHelper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloaderConstructorHelper.java @@ -49,14 +49,17 @@ public final class DownloaderConstructorHelper { /** * @param cache Cache instance to be used to store downloaded data. * @param upstreamDataSourceFactory A {@link Factory} for downloading data. - * @param cacheReadDataSourceFactory A {@link Factory} for reading data from the cache. - * If null, null is passed to {@link Downloader} constructor. + * @param cacheReadDataSourceFactory A {@link Factory} for reading data from the cache. If null + * then standard {@link FileDataSource} instances will be used. * @param cacheWriteDataSinkFactory A {@link DataSink.Factory} for writing data to the cache. If - * null, null is passed to {@link Downloader} constructor. - * @param priorityTaskManager If one is given then the download priority is set lower than - * loading. If null, null is passed to {@link Downloader} constructor. + * null then standard {@link CacheDataSink} instances will be used. + * @param priorityTaskManager A {@link PriorityTaskManager} to use when downloading. If non-null, + * downloaders will register as tasks with priority {@link C#PRIORITY_DOWNLOAD} whilst + * downloading. */ - public DownloaderConstructorHelper(Cache cache, Factory upstreamDataSourceFactory, + public DownloaderConstructorHelper( + Cache cache, + Factory upstreamDataSourceFactory, @Nullable Factory cacheReadDataSourceFactory, @Nullable DataSink.Factory cacheWriteDataSinkFactory, @Nullable PriorityTaskManager priorityTaskManager) { @@ -73,7 +76,7 @@ public final class DownloaderConstructorHelper { return cache; } - /** Returns a {@link PriorityTaskManager} instance.*/ + /** Returns a {@link PriorityTaskManager} instance. */ public PriorityTaskManager getPriorityTaskManager() { // Return a dummy PriorityTaskManager if none is provided. Create a new PriorityTaskManager // each time so clients don't affect each other over the dummy PriorityTaskManager instance. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/FilterableManifest.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/FilterableManifest.java new file mode 100644 index 0000000000..35d05fd43b --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/FilterableManifest.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.offline; + +import java.util.List; + +/** + * A manifest that can generate copies of itself including only the streams specified by the given + * keys. + * + * @param The manifest type. + * @param The stream key type. + */ +public interface FilterableManifest { + + /** + * Returns a copy of the manifest including only the streams specified by the given keys. If the + * manifest is unchanged then the instance may return itself. + * + * @param streamKeys A non-empty list of stream keys. + * @return The filtered manifest. + */ + T copy(List streamKeys); +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/FilteringManifestParser.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/FilteringManifestParser.java new file mode 100644 index 0000000000..8fec07552b --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/FilteringManifestParser.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.offline; + +import android.net.Uri; +import com.google.android.exoplayer2.upstream.ParsingLoadable.Parser; +import java.io.IOException; +import java.io.InputStream; +import java.util.List; + +/** A manifest parser that includes only the tracks identified by the given track keys. */ +public final class FilteringManifestParser, K> + implements Parser { + + private final Parser parser; + private final List trackKeys; + + /** + * @param parser A parser for the manifest that will be filtered. + * @param trackKeys The track keys. If null or empty then filtering will not occur. + */ + public FilteringManifestParser(Parser parser, List trackKeys) { + this.parser = parser; + this.trackKeys = trackKeys; + } + + @Override + public T parse(Uri uri, InputStream inputStream) throws IOException { + T manifest = parser.parse(uri, inputStream); + return trackKeys == null || trackKeys.isEmpty() ? manifest : manifest.copy(trackKeys); + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloadAction.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloadAction.java index 130e9c0dc7..02ef7a7aa7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloadAction.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloadAction.java @@ -18,7 +18,6 @@ package com.google.android.exoplayer2.offline; import android.net.Uri; import android.support.annotation.Nullable; import com.google.android.exoplayer2.upstream.cache.CacheUtil; -import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; import java.io.DataInputStream; import java.io.DataOutputStream; @@ -27,46 +26,37 @@ import java.io.IOException; /** An action to download or remove downloaded progressive streams. */ public final class ProgressiveDownloadAction extends DownloadAction { - public static final Deserializer DESERIALIZER = new Deserializer() { + private static final String TYPE = "progressive"; + private static final int VERSION = 0; - @Override - public String getType() { - return TYPE; - } + public static final Deserializer DESERIALIZER = + new Deserializer(TYPE, VERSION) { + @Override + public ProgressiveDownloadAction readFromStream(int version, DataInputStream input) + throws IOException { + Uri uri = Uri.parse(input.readUTF()); + boolean isRemoveAction = input.readBoolean(); + int dataLength = input.readInt(); + byte[] data = new byte[dataLength]; + input.readFully(data); + String customCacheKey = input.readBoolean() ? input.readUTF() : null; + return new ProgressiveDownloadAction(uri, isRemoveAction, data, customCacheKey); + } + }; - @Override - public ProgressiveDownloadAction readFromStream(int version, DataInputStream input) - throws IOException { - return new ProgressiveDownloadAction(input.readUTF(), - input.readBoolean() ? input.readUTF() : null, input.readBoolean(), input.readUTF()); - } - - }; - - private static final String TYPE = "ProgressiveDownloadAction"; - - private final String uri; - private final @Nullable String customCacheKey; - private final boolean removeAction; + public final @Nullable String customCacheKey; /** * @param uri Uri of the data to be downloaded. + * @param isRemoveAction Whether this is a remove action. If false, this is a download action. + * @param data Optional custom data for this action. * @param customCacheKey A custom key that uniquely identifies the original stream. If not null it * is used for cache indexing. - * @param removeAction Whether the data should be downloaded or removed. - * @param data Optional custom data for this action. If null, an empty string is used. */ public ProgressiveDownloadAction( - String uri, @Nullable String customCacheKey, boolean removeAction, @Nullable String data) { - super(data); - this.uri = Assertions.checkNotNull(uri); + Uri uri, boolean isRemoveAction, @Nullable byte[] data, @Nullable String customCacheKey) { + super(TYPE, VERSION, uri, isRemoveAction, data); this.customCacheKey = customCacheKey; - this.removeAction = removeAction; - } - - @Override - public boolean isRemoveAction() { - return removeAction; } @Override @@ -74,29 +64,23 @@ public final class ProgressiveDownloadAction extends DownloadAction { return new ProgressiveDownloader(uri, customCacheKey, constructorHelper); } - @Override - protected String getType() { - return TYPE; - } - @Override protected void writeToStream(DataOutputStream output) throws IOException { - output.writeUTF(uri); - boolean customCacheKeyAvailable = customCacheKey != null; - output.writeBoolean(customCacheKeyAvailable); - if (customCacheKeyAvailable) { + output.writeUTF(uri.toString()); + output.writeBoolean(isRemoveAction); + output.writeInt(data.length); + output.write(data); + boolean customCacheKeySet = customCacheKey != null; + output.writeBoolean(customCacheKeySet); + if (customCacheKeySet) { output.writeUTF(customCacheKey); } - output.writeBoolean(isRemoveAction()); - output.writeUTF(getData()); } @Override - protected boolean isSameMedia(DownloadAction other) { - if (!(other instanceof ProgressiveDownloadAction)) { - return false; - } - return getCacheKey().equals(((ProgressiveDownloadAction) other).getCacheKey()); + public boolean isSameMedia(DownloadAction other) { + return ((other instanceof ProgressiveDownloadAction) + && getCacheKey().equals(((ProgressiveDownloadAction) other).getCacheKey())); } @Override @@ -108,18 +92,17 @@ public final class ProgressiveDownloadAction extends DownloadAction { return false; } ProgressiveDownloadAction that = (ProgressiveDownloadAction) o; - return uri.equals(that.uri) && Util.areEqual(customCacheKey, that.customCacheKey); + return Util.areEqual(customCacheKey, that.customCacheKey); } @Override public int hashCode() { int result = super.hashCode(); - result = 31 * result + uri.hashCode(); result = 31 * result + (customCacheKey != null ? customCacheKey.hashCode() : 0); return result; } private String getCacheKey() { - return customCacheKey != null ? customCacheKey : CacheUtil.generateKey(Uri.parse(uri)); + return customCacheKey != null ? customCacheKey : CacheUtil.generateKey(uri); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloadHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloadHelper.java new file mode 100644 index 0000000000..49b7e36ea6 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloadHelper.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.offline; + +import android.net.Uri; +import android.support.annotation.Nullable; +import com.google.android.exoplayer2.source.TrackGroupArray; +import java.util.List; + +/** A {@link DownloadHelper} for progressive streams. */ +public final class ProgressiveDownloadHelper extends DownloadHelper { + + private final Uri uri; + private final @Nullable String customCacheKey; + + public ProgressiveDownloadHelper(Uri uri) { + this(uri, null); + } + + public ProgressiveDownloadHelper(Uri uri, @Nullable String customCacheKey) { + this.uri = uri; + this.customCacheKey = customCacheKey; + } + + @Override + protected void prepareInternal() { + // Do nothing. + } + + @Override + public int getPeriodCount() { + return 1; + } + + @Override + public TrackGroupArray getTrackGroups(int periodIndex) { + return TrackGroupArray.EMPTY; + } + + @Override + public DownloadAction getDownloadAction(@Nullable byte[] data, List trackKeys) { + return new ProgressiveDownloadAction(uri, false, data, customCacheKey); + } + + @Override + public DownloadAction getRemoveAction(@Nullable byte[] data) { + return new ProgressiveDownloadAction(uri, true, data, customCacheKey); + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloader.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloader.java index e5aa429424..cf64d26bb5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloader.java @@ -16,7 +16,6 @@ package com.google.android.exoplayer2.offline; import android.net.Uri; -import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.cache.Cache; @@ -25,6 +24,7 @@ import com.google.android.exoplayer2.upstream.cache.CacheUtil; import com.google.android.exoplayer2.upstream.cache.CacheUtil.CachingCounters; import com.google.android.exoplayer2.util.PriorityTaskManager; import java.io.IOException; +import java.util.concurrent.atomic.AtomicBoolean; /** * A downloader for progressive media streams. @@ -38,49 +38,46 @@ public final class ProgressiveDownloader implements Downloader { private final CacheDataSource dataSource; private final PriorityTaskManager priorityTaskManager; private final CacheUtil.CachingCounters cachingCounters; + private final AtomicBoolean isCanceled; /** * @param uri Uri of the data to be downloaded. * @param customCacheKey A custom key that uniquely identifies the original stream. Used for cache * indexing. May be null. - * @param constructorHelper a {@link DownloaderConstructorHelper} instance. + * @param constructorHelper A {@link DownloaderConstructorHelper} instance. */ public ProgressiveDownloader( - String uri, String customCacheKey, DownloaderConstructorHelper constructorHelper) { - this.dataSpec = new DataSpec(Uri.parse(uri), 0, C.LENGTH_UNSET, customCacheKey, 0); + Uri uri, String customCacheKey, DownloaderConstructorHelper constructorHelper) { + this.dataSpec = new DataSpec(uri, 0, C.LENGTH_UNSET, customCacheKey, 0); this.cache = constructorHelper.getCache(); this.dataSource = constructorHelper.buildCacheDataSource(false); this.priorityTaskManager = constructorHelper.getPriorityTaskManager(); cachingCounters = new CachingCounters(); + isCanceled = new AtomicBoolean(); } @Override - public void init() { - CacheUtil.getCached(dataSpec, cache, cachingCounters); - } - - @Override - public void download(@Nullable ProgressListener listener) throws InterruptedException, - IOException { + public void download() throws InterruptedException, IOException { priorityTaskManager.add(C.PRIORITY_DOWNLOAD); try { - byte[] buffer = new byte[BUFFER_SIZE_BYTES]; - CacheUtil.cache(dataSpec, cache, dataSource, buffer, priorityTaskManager, C.PRIORITY_DOWNLOAD, - cachingCounters, true); - // TODO: Work out how to call onDownloadProgress periodically during the download, or else - // get rid of ProgressListener and move to a model where the manager periodically polls - // Downloaders. - if (listener != null) { - listener.onDownloadProgress(this, 100, cachingCounters.contentLength); - } + CacheUtil.cache( + dataSpec, + cache, + dataSource, + new byte[BUFFER_SIZE_BYTES], + priorityTaskManager, + C.PRIORITY_DOWNLOAD, + cachingCounters, + isCanceled, + /* enableEOFException= */ true); } finally { priorityTaskManager.remove(C.PRIORITY_DOWNLOAD); } } @Override - public void remove() { - CacheUtil.remove(cache, CacheUtil.getKey(dataSpec)); + public void cancel() { + isCanceled.set(true); } @Override @@ -91,8 +88,13 @@ public final class ProgressiveDownloader implements Downloader { @Override public float getDownloadPercentage() { long contentLength = cachingCounters.contentLength; - return contentLength == C.LENGTH_UNSET ? Float.NaN + return contentLength == C.LENGTH_UNSET + ? C.PERCENTAGE_UNSET : ((cachingCounters.totalCachedBytes() * 100f) / contentLength); } + @Override + public void remove() { + CacheUtil.remove(cache, CacheUtil.getKey(dataSpec)); + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloadAction.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloadAction.java index 6bc9a10c38..f6a32a1253 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloadAction.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloadAction.java @@ -21,95 +21,96 @@ import com.google.android.exoplayer2.util.Assertions; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; -import java.util.Arrays; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; /** * {@link DownloadAction} for {@link SegmentDownloader}s. * * @param The type of the representation key object. */ -public abstract class SegmentDownloadAction extends DownloadAction { +public abstract class SegmentDownloadAction> extends DownloadAction { /** * Base class for {@link SegmentDownloadAction} {@link Deserializer}s. * * @param The type of the representation key object. */ - protected abstract static class SegmentDownloadActionDeserializer implements Deserializer { + protected abstract static class SegmentDownloadActionDeserializer extends Deserializer { + + public SegmentDownloadActionDeserializer(String type, int version) { + super(type, version); + } @Override - public DownloadAction readFromStream(int version, DataInputStream input) throws IOException { - Uri manifestUri = Uri.parse(input.readUTF()); - String data = input.readUTF(); - boolean removeAction = input.readBoolean(); + public final DownloadAction readFromStream(int version, DataInputStream input) + throws IOException { + Uri uri = Uri.parse(input.readUTF()); + boolean isRemoveAction = input.readBoolean(); + int dataLength = input.readInt(); + byte[] data = new byte[dataLength]; + input.readFully(data); int keyCount = input.readInt(); - K[] keys = createKeyArray(keyCount); + List keys = new ArrayList<>(); for (int i = 0; i < keyCount; i++) { - keys[i] = readKey(input); + keys.add(readKey(input)); } - return createDownloadAction(manifestUri, removeAction, data, keys); + return createDownloadAction(uri, isRemoveAction, data, keys); } /** Deserializes a key from the {@code input}. */ protected abstract K readKey(DataInputStream input) throws IOException; - /** Returns a key array. */ - protected abstract K[] createKeyArray(int keyCount); - /** Returns a {@link DownloadAction}. */ - protected abstract DownloadAction createDownloadAction(Uri manifestUri, boolean removeAction, - String data, K[] keys); - + protected abstract DownloadAction createDownloadAction( + Uri manifestUri, boolean isRemoveAction, byte[] data, List keys); } - protected final Uri manifestUri; - protected final K[] keys; - private final boolean removeAction; + public final List keys; /** - * @param manifestUri The {@link Uri} of the manifest to be downloaded. - * @param removeAction Whether the data will be removed. If {@code false} it will be downloaded. - * @param data Optional custom data for this action. If null, an empty string is used. - * @param keys Keys of representations to be downloaded. If empty, all representations are - * downloaded. If {@code removeAction} is true, {@code keys} should be an empty array. + * @param type The type of the action. + * @param version The action version. + * @param uri The URI of the media being downloaded. + * @param isRemoveAction Whether the data will be removed. If {@code false} it will be downloaded. + * @param data Optional custom data for this action. If {@code null} an empty array will be used. + * @param keys Keys of tracks to be downloaded. If empty, all tracks will be downloaded. If {@code + * removeAction} is true, {@code keys} must be empty. */ protected SegmentDownloadAction( - Uri manifestUri, boolean removeAction, @Nullable String data, K[] keys) { - super(data); - this.manifestUri = manifestUri; - this.keys = Assertions.checkNotNull(keys); - this.removeAction = removeAction; - if (removeAction) { - Assertions.checkArgument(keys.length == 0); + String type, + int version, + Uri uri, + boolean isRemoveAction, + @Nullable byte[] data, + List keys) { + super(type, version, uri, isRemoveAction, data); + if (isRemoveAction) { + Assertions.checkArgument(keys.isEmpty()); + this.keys = Collections.emptyList(); + } else { + ArrayList mutableKeys = new ArrayList<>(keys); + Collections.sort(mutableKeys); + this.keys = Collections.unmodifiableList(mutableKeys); } } - @Override - public final boolean isRemoveAction() { - return removeAction; - } - @Override public final void writeToStream(DataOutputStream output) throws IOException { - output.writeUTF(manifestUri.toString()); - output.writeUTF(getData()); - output.writeBoolean(removeAction); - output.writeInt(keys.length); - for (K key : keys) { - writeKey(output, key); + output.writeUTF(uri.toString()); + output.writeBoolean(isRemoveAction); + output.writeInt(data.length); + output.write(data); + output.writeInt(keys.size()); + for (int i = 0; i < keys.size(); i++) { + writeKey(output, keys.get(i)); } } /** Serializes the {@code key} into the {@code output}. */ protected abstract void writeKey(DataOutputStream output, K key) throws IOException; - - @Override - public boolean isSameMedia(DownloadAction other) { - return other instanceof SegmentDownloadAction - && manifestUri.equals(((SegmentDownloadAction) other).manifestUri); - } - @Override public boolean equals(Object o) { if (this == o) { @@ -119,17 +120,13 @@ public abstract class SegmentDownloadAction extends DownloadAction { return false; } SegmentDownloadAction that = (SegmentDownloadAction) o; - return manifestUri.equals(that.manifestUri) - && removeAction == that.removeAction - && keys.length == that.keys.length - && Arrays.asList(keys).containsAll(Arrays.asList(that.keys)); + return keys.equals(that.keys); } @Override public int hashCode() { int result = super.hashCode(); - result = 31 * result + manifestUri.hashCode(); - result = 31 * result + Arrays.hashCode(keys); + result = 31 * result + keys.hashCode(); return result; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloader.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloader.java index 63414dc39e..6ce2121acd 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloader.java @@ -17,7 +17,6 @@ package com.google.android.exoplayer2.offline; import android.net.Uri; import android.support.annotation.NonNull; -import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; @@ -27,19 +26,19 @@ import com.google.android.exoplayer2.upstream.cache.CacheUtil; import com.google.android.exoplayer2.upstream.cache.CacheUtil.CachingCounters; import com.google.android.exoplayer2.util.PriorityTaskManager; import java.io.IOException; +import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; /** * Base class for multi segment stream downloaders. * - *

All of the methods are blocking. Also they are not thread safe, except {@link - * #getTotalSegments()}, {@link #getDownloadedSegments()} and {@link #getDownloadedBytes()}. - * * @param The type of the manifest object. - * @param The type of the representation key object. + * @param The type of the streams key object. */ -public abstract class SegmentDownloader implements Downloader { +public abstract class SegmentDownloader, K> + implements Downloader { /** Smallest unit of content to be downloaded. */ protected static class Segment implements Comparable { @@ -69,149 +68,89 @@ public abstract class SegmentDownloader implements Downloader { private final Cache cache; private final CacheDataSource dataSource; private final CacheDataSource offlineDataSource; + private final ArrayList streamKeys; + private final AtomicBoolean isCanceled; - private M manifest; - private K[] keys; private volatile int totalSegments; private volatile int downloadedSegments; private volatile long downloadedBytes; /** * @param manifestUri The {@link Uri} of the manifest to be downloaded. - * @param constructorHelper a {@link DownloaderConstructorHelper} instance. + * @param streamKeys Keys defining which streams in the manifest should be selected for download. + * If empty, all streams are downloaded. + * @param constructorHelper A {@link DownloaderConstructorHelper} instance. */ - public SegmentDownloader(Uri manifestUri, DownloaderConstructorHelper constructorHelper) { + public SegmentDownloader( + Uri manifestUri, List streamKeys, DownloaderConstructorHelper constructorHelper) { this.manifestUri = manifestUri; + this.streamKeys = new ArrayList<>(streamKeys); this.cache = constructorHelper.getCache(); this.dataSource = constructorHelper.buildCacheDataSource(false); this.offlineDataSource = constructorHelper.buildCacheDataSource(true); this.priorityTaskManager = constructorHelper.getPriorityTaskManager(); - resetCounters(); + totalSegments = C.LENGTH_UNSET; + isCanceled = new AtomicBoolean(); } /** - * Returns the manifest. Downloads and parses it if necessary. + * Downloads the selected streams in the media. If multiple streams are selected, they are + * downloaded in sync with one another. * - * @return The manifest. - * @throws IOException If an error occurs reading data. - */ - public final M getManifest() throws IOException { - return getManifestIfNeeded(false); - } - - /** - * Selects multiple representations pointed to by the keys for downloading, checking status. Any - * previous selection is cleared. If keys array is empty, all representations are downloaded. - */ - public final void selectRepresentations(K[] keys) { - this.keys = keys.length > 0 ? keys.clone() : null; - resetCounters(); - } - - /** - * Returns keys for all representations. - * - * @see #selectRepresentations(Object[]) - */ - public abstract K[] getAllRepresentationKeys() throws IOException; - - /** - * Initializes the total segments, downloaded segments and downloaded bytes counters for the - * selected representations. - * - * @throws IOException Thrown when there is an io error while reading from cache. - * @throws DownloadException Thrown if the media cannot be downloaded. - * @throws InterruptedException If the thread has been interrupted. - * @see #getTotalSegments() - * @see #getDownloadedSegments() - * @see #getDownloadedBytes() - */ - @Override - public final void init() throws InterruptedException, IOException { - try { - getManifestIfNeeded(true); - } catch (IOException e) { - // Either the manifest file isn't available offline or not parsable. - return; - } - try { - initStatus(true); - } catch (IOException | InterruptedException e) { - resetCounters(); - throw e; - } - } - - /** - * Downloads the content for the selected representations in sync or resumes a previously stopped - * download. - * - * @param listener If not null, called during download. - * @throws IOException Thrown when there is an io error while downloading. - * @throws DownloadException Thrown if the media cannot be downloaded. + * @throws IOException Thrown when there is an error downloading. * @throws InterruptedException If the thread has been interrupted. */ + // downloadedSegments and downloadedBytes are only written from this method, and this method + // should not be called from more than one thread. Hence non-atomic updates are valid. + @SuppressWarnings("NonAtomicVolatileUpdate") @Override - public final synchronized void download(@Nullable ProgressListener listener) - throws IOException, InterruptedException { + public final void download() throws IOException, InterruptedException { priorityTaskManager.add(C.PRIORITY_DOWNLOAD); + try { - getManifestIfNeeded(false); - List segments = initStatus(false); - notifyListener(listener); // Initial notification. + List segments = initDownload(); Collections.sort(segments); byte[] buffer = new byte[BUFFER_SIZE_BYTES]; CachingCounters cachingCounters = new CachingCounters(); for (int i = 0; i < segments.size(); i++) { - CacheUtil.cache(segments.get(i).dataSpec, cache, dataSource, buffer, - priorityTaskManager, C.PRIORITY_DOWNLOAD, cachingCounters, true); - downloadedBytes += cachingCounters.newlyCachedBytes; - downloadedSegments++; - notifyListener(listener); + try { + CacheUtil.cache( + segments.get(i).dataSpec, + cache, + dataSource, + buffer, + priorityTaskManager, + C.PRIORITY_DOWNLOAD, + cachingCounters, + isCanceled, + true); + downloadedSegments++; + } finally { + downloadedBytes += cachingCounters.newlyCachedBytes; + } } } finally { priorityTaskManager.remove(C.PRIORITY_DOWNLOAD); } } - /** - * Returns the total number of segments in the representations which are selected, or {@link - * C#LENGTH_UNSET} if it hasn't been calculated yet. - * - * @see #init() - */ - public final int getTotalSegments() { - return totalSegments; + @Override + public void cancel() { + isCanceled.set(true); } - /** - * Returns the total number of downloaded segments in the representations which are selected, or - * {@link C#LENGTH_UNSET} if it hasn't been calculated yet. - * - * @see #init() - */ - public final int getDownloadedSegments() { - return downloadedSegments; - } - - /** - * Returns the total number of downloaded bytes in the representations which are selected, or - * {@link C#LENGTH_UNSET} if it hasn't been calculated yet. - * - * @see #init() - */ @Override public final long getDownloadedBytes() { return downloadedBytes; } @Override - public float getDownloadPercentage() { + public final float getDownloadPercentage() { // Take local snapshot of the volatile fields int totalSegments = this.totalSegments; int downloadedSegments = this.downloadedSegments; if (totalSegments == C.LENGTH_UNSET || downloadedSegments == C.LENGTH_UNSET) { - return Float.NaN; + return C.PERCENTAGE_UNSET; } return totalSegments == 0 ? 100f : (downloadedSegments * 100f) / totalSegments; } @@ -219,29 +158,21 @@ public abstract class SegmentDownloader implements Downloader { @Override public final void remove() throws InterruptedException { try { - getManifestIfNeeded(true); + M manifest = getManifest(offlineDataSource, manifestUri); + List segments = getSegments(offlineDataSource, manifest, true); + for (int i = 0; i < segments.size(); i++) { + removeUri(segments.get(i).dataSpec.uri); + } } catch (IOException e) { - // Either the manifest file isn't available offline, or it's not parsable. Continue anyway to - // reset the counters and attempt to remove the manifest file. + // Ignore exceptions when removing. + } finally { + // Always attempt to remove the manifest. + removeUri(manifestUri); } - resetCounters(); - if (manifest != null) { - List segments = null; - try { - segments = getSegments(offlineDataSource, manifest, getAllRepresentationKeys(), true); - } catch (IOException e) { - // Ignore exceptions. We do our best with what's available offline. - } - if (segments != null) { - for (int i = 0; i < segments.size(); i++) { - remove(segments.get(i).dataSpec.uri); - } - } - manifest = null; - } - remove(manifestUri); } + // Internal methods. + /** * Loads and parses the manifest. * @@ -253,51 +184,29 @@ public abstract class SegmentDownloader implements Downloader { protected abstract M getManifest(DataSource dataSource, Uri uri) throws IOException; /** - * Returns a list of {@link Segment}s for given keys. + * Returns a list of all downloadable {@link Segment}s for a given manifest. * * @param dataSource The {@link DataSource} through which to load any required data. * @param manifest The manifest containing the segments. - * @param keys The selected representation keys. - * @param allowIncompleteIndex Whether to continue in the case that a load error prevents all + * @param allowIncompleteList Whether to continue in the case that a load error prevents all * segments from being listed. If true then a partial segment list will be returned. If false * an {@link IOException} will be thrown. * @throws InterruptedException Thrown if the thread was interrupted. * @throws IOException Thrown if {@code allowPartialIndex} is false and a load error occurs, or if * the media is not in a form that allows for its segments to be listed. - * @return A list of {@link Segment}s for given keys. + * @return The list of downloadable {@link Segment}s. */ - protected abstract List getSegments(DataSource dataSource, M manifest, K[] keys, - boolean allowIncompleteIndex) throws InterruptedException, IOException; + protected abstract List getSegments( + DataSource dataSource, M manifest, boolean allowIncompleteList) + throws InterruptedException, IOException; - private void resetCounters() { - totalSegments = C.LENGTH_UNSET; - downloadedSegments = C.LENGTH_UNSET; - downloadedBytes = C.LENGTH_UNSET; - } - - private void remove(Uri uri) { - CacheUtil.remove(cache, CacheUtil.generateKey(uri)); - } - - private void notifyListener(ProgressListener listener) { - if (listener != null) { - listener.onDownloadProgress(this, getDownloadPercentage(), downloadedBytes); + /** Initializes the download, returning a list of {@link Segment}s that need to be downloaded. */ + private List initDownload() throws IOException, InterruptedException { + M manifest = getManifest(dataSource, manifestUri); + if (!streamKeys.isEmpty()) { + manifest = manifest.copy(streamKeys); } - } - - /** - * Initializes totalSegments, downloadedSegments and downloadedBytes for selected representations. - * If not offline then downloads missing metadata. - * - * @return A list of not fully downloaded segments. - */ - private synchronized List initStatus(boolean offline) - throws IOException, InterruptedException { - DataSource dataSource = getDataSource(offline); - if (keys == null) { - keys = getAllRepresentationKeys(); - } - List segments = getSegments(dataSource, manifest, keys, offline); + List segments = getSegments(dataSource, manifest, /* allowIncompleteList= */ false); CachingCounters cachingCounters = new CachingCounters(); totalSegments = segments.size(); downloadedSegments = 0; @@ -315,15 +224,8 @@ public abstract class SegmentDownloader implements Downloader { return segments; } - private M getManifestIfNeeded(boolean offline) throws IOException { - if (manifest == null) { - manifest = getManifest(getDataSource(offline), manifestUri); - } - return manifest; - } - - private DataSource getDataSource(boolean offline) { - return offline ? offlineDataSource : dataSource; + private void removeUri(Uri uri) { + CacheUtil.remove(cache, CacheUtil.generateKey(uri)); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/TrackKey.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/TrackKey.java new file mode 100644 index 0000000000..f6a411c3a1 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/TrackKey.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.offline; + +/** + * Identifies a given track by the index of the containing period, the index of the containing group + * within the period, and the index of the track within the group. + */ +public final class TrackKey { + + /** The period index. */ + public final int periodIndex; + /** The group index. */ + public final int groupIndex; + /** The track index. */ + public final int trackIndex; + + /** + * @param periodIndex The period index. + * @param groupIndex The group index. + * @param trackIndex The track index. + */ + public TrackKey(int periodIndex, int groupIndex, int trackIndex) { + this.periodIndex = periodIndex; + this.groupIndex = groupIndex; + this.trackIndex = trackIndex; + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/PlatformScheduler.java b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/PlatformScheduler.java index cd1e12520e..b3737eb8bc 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/PlatformScheduler.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/PlatformScheduler.java @@ -16,8 +16,6 @@ package com.google.android.exoplayer2.scheduler; import android.annotation.TargetApi; -import android.app.Notification; -import android.app.Service; import android.app.job.JobInfo; import android.app.job.JobParameters; import android.app.job.JobScheduler; @@ -26,91 +24,73 @@ import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.os.PersistableBundle; +import android.support.annotation.RequiresPermission; import android.util.Log; import com.google.android.exoplayer2.util.Util; /** - * A {@link Scheduler} which uses {@link android.app.job.JobScheduler} to schedule a {@link Service} - * to be started when its requirements are met. The started service must call {@link - * Service#startForeground(int, Notification)} to make itself a foreground service upon being - * started, as documented by {@link Service#startForegroundService(Intent)}. - * - *

To use {@link PlatformScheduler} application needs to have RECEIVE_BOOT_COMPLETED permission - * and you need to define PlatformSchedulerService in your manifest: + * A {@link Scheduler} that uses {@link JobScheduler}. To use this scheduler, you must add {@link + * PlatformSchedulerService} to your manifest: * *

{@literal
  * 
  *
  * 
- * }
- * - * The service to be scheduled must be defined in the manifest with an intent-filter: - * - *
{@literal
- * 
- *  
- *    
- *    
- *  
- * 
+ *     android:permission="android.permission.BIND_JOB_SERVICE"
+ *     android:exported="true"/>
  * }
*/ @TargetApi(21) public final class PlatformScheduler implements Scheduler { private static final String TAG = "PlatformScheduler"; - private static final String SERVICE_ACTION = "SERVICE_ACTION"; - private static final String SERVICE_PACKAGE = "SERVICE_PACKAGE"; - private static final String REQUIREMENTS = "REQUIREMENTS"; + private static final String KEY_SERVICE_ACTION = "service_action"; + private static final String KEY_SERVICE_PACKAGE = "service_package"; + private static final String KEY_REQUIREMENTS = "requirements"; private final int jobId; - private final JobInfo jobInfo; + private final ComponentName jobServiceComponentName; private final JobScheduler jobScheduler; /** - * @param context Used to access to {@link JobScheduler} service. - * @param requirements The requirements to execute the job. - * @param jobId Unique identifier for the job. Using the same id as a previous job can cause that - * job to be replaced or canceled. - * @param serviceAction The action which the service will be started with. - * @param servicePackage The package of the service which contains the logic of the job. + * @param context Any context. + * @param jobId An identifier for the jobs scheduled by this instance. If the same identifier was + * used by a previous instance, anything scheduled by the previous instance will be canceled + * by this instance if {@link #schedule(Requirements, String, String)} or {@link #cancel()} + * are called. */ - public PlatformScheduler( - Context context, - Requirements requirements, - int jobId, - String serviceAction, - String servicePackage) { + @RequiresPermission(android.Manifest.permission.RECEIVE_BOOT_COMPLETED) + public PlatformScheduler(Context context, int jobId) { this.jobId = jobId; - this.jobInfo = buildJobInfo(context, requirements, jobId, serviceAction, servicePackage); - this.jobScheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE); + jobServiceComponentName = new ComponentName(context, PlatformSchedulerService.class); + jobScheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE); } @Override - public boolean schedule() { + public boolean schedule(Requirements requirements, String servicePackage, String serviceAction) { + JobInfo jobInfo = + buildJobInfo(jobId, jobServiceComponentName, requirements, serviceAction, servicePackage); int result = jobScheduler.schedule(jobInfo); - logd("Scheduling JobScheduler job: " + jobId + " result: " + result); + logd("Scheduling job: " + jobId + " result: " + result); return result == JobScheduler.RESULT_SUCCESS; } @Override public boolean cancel() { - logd("Canceling JobScheduler job: " + jobId); + logd("Canceling job: " + jobId); jobScheduler.cancel(jobId); return true; } + // @RequiresPermission constructor annotation should ensure the permission is present. + @SuppressWarnings("MissingPermission") private static JobInfo buildJobInfo( - Context context, - Requirements requirements, int jobId, + ComponentName jobServiceComponentName, + Requirements requirements, String serviceAction, String servicePackage) { - JobInfo.Builder builder = - new JobInfo.Builder(jobId, new ComponentName(context, PlatformSchedulerService.class)); + JobInfo.Builder builder = new JobInfo.Builder(jobId, jobServiceComponentName); int networkType; switch (requirements.getRequiredNetworkType()) { @@ -146,13 +126,12 @@ public final class PlatformScheduler implements Scheduler { builder.setRequiresCharging(requirements.isChargingRequired()); builder.setPersisted(true); - // Extras, work duration. PersistableBundle extras = new PersistableBundle(); - extras.putString(SERVICE_ACTION, serviceAction); - extras.putString(SERVICE_PACKAGE, servicePackage); - extras.putInt(REQUIREMENTS, requirements.getRequirementsData()); - + extras.putString(KEY_SERVICE_ACTION, serviceAction); + extras.putString(KEY_SERVICE_PACKAGE, servicePackage); + extras.putInt(KEY_REQUIREMENTS, requirements.getRequirementsData()); builder.setExtras(extras); + return builder.build(); } @@ -162,26 +141,22 @@ public final class PlatformScheduler implements Scheduler { } } - /** A {@link JobService} to start a service if the requirements are met. */ + /** A {@link JobService} that starts the target service if the requirements are met. */ public static final class PlatformSchedulerService extends JobService { @Override public boolean onStartJob(JobParameters params) { - logd("PlatformSchedulerService is started"); + logd("PlatformSchedulerService started"); PersistableBundle extras = params.getExtras(); - Requirements requirements = new Requirements(extras.getInt(REQUIREMENTS)); + Requirements requirements = new Requirements(extras.getInt(KEY_REQUIREMENTS)); if (requirements.checkRequirements(this)) { - logd("requirements are met"); - String serviceAction = extras.getString(SERVICE_ACTION); - String servicePackage = extras.getString(SERVICE_PACKAGE); + logd("Requirements are met"); + String serviceAction = extras.getString(KEY_SERVICE_ACTION); + String servicePackage = extras.getString(KEY_SERVICE_PACKAGE); Intent intent = new Intent(serviceAction).setPackage(servicePackage); - logd("starting service action: " + serviceAction + " package: " + servicePackage); - if (Util.SDK_INT >= 26) { - startForegroundService(intent); - } else { - startService(intent); - } + logd("Starting service action: " + serviceAction + " package: " + servicePackage); + Util.startForegroundService(this, intent); } else { - logd("requirements are not met"); + logd("Requirements are not met"); jobFinished(params, /* needsReschedule */ true); } return false; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Requirements.java b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Requirements.java index 8c8cf6a3b5..30b07da3eb 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Requirements.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Requirements.java @@ -32,8 +32,6 @@ import java.lang.annotation.RetentionPolicy; /** * Defines a set of device state requirements. - * - *

To use network type requirement, application needs to have ACCESS_NETWORK_STATE permission. */ public final class Requirements { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/RequirementsWatcher.java b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/RequirementsWatcher.java index 9509c7e5b8..46aa55f094 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/RequirementsWatcher.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/RequirementsWatcher.java @@ -69,14 +69,14 @@ public final class RequirementsWatcher { private CapabilityValidatedCallback networkCallback; /** - * @param context Used to register for broadcasts. + * @param context Any context. * @param listener Notified whether the {@link Requirements} are met. * @param requirements The requirements to watch. */ public RequirementsWatcher(Context context, Listener listener, Requirements requirements) { this.requirements = requirements; this.listener = listener; - this.context = context; + this.context = context.getApplicationContext(); logd(this + " created"); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Scheduler.java b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Scheduler.java index 9a9c57443f..1b225d9a4d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Scheduler.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Scheduler.java @@ -15,25 +15,36 @@ */ package com.google.android.exoplayer2.scheduler; -/** - * Implementer of this interface schedules one implementation specific job to be run when some - * requirements are met even if the app isn't running. - */ +import android.app.Notification; +import android.app.Service; +import android.content.Intent; + +/** Schedules a service to be started in the foreground when some {@link Requirements} are met. */ public interface Scheduler { - /*package*/ boolean DEBUG = false; + /* package */ boolean DEBUG = false; /** - * Schedules the job to be run when the requirements are met. + * Schedules a service to be started in the foreground when some {@link Requirements} are met. + * Anything that was previously scheduled will be canceled. * - * @return Whether the job scheduled successfully. + *

The service to be started must be declared in the manifest of {@code servicePackage} with an + * intent filter containing {@code serviceAction}. Note that when started with {@code + * serviceAction}, the service must call {@link Service#startForeground(int, Notification)} to + * make itself a foreground service, as documented by {@link + * Service#startForegroundService(Intent)}. + * + * @param requirements The requirements. + * @param servicePackage The package name. + * @param serviceAction The action with which the service will be started. + * @return Whether scheduling was successful. */ - boolean schedule(); + boolean schedule(Requirements requirements, String servicePackage, String serviceAction); /** - * Cancels any previous schedule. + * Cancels anything that was previously scheduled, or else does nothing. * - * @return Whether the job cancelled successfully. + * @return Whether cancellation was successful. */ boolean cancel(); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DeferredMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DeferredMediaPeriod.java index ab6b3a311a..229043b127 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/DeferredMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DeferredMediaPeriod.java @@ -37,7 +37,7 @@ public final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callb /** * Called the first time an error occurs while refreshing source info or preparing the period. */ - void onPrepareError(IOException exception); + void onPrepareError(MediaPeriodId mediaPeriodId, IOException exception); } public final MediaSource mediaSource; @@ -140,7 +140,7 @@ public final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callb } if (!notifiedPrepareError) { notifiedPrepareError = true; - listener.onPrepareError(e); + listener.onPrepareError(id, e); } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsLoader.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsLoader.java index 6295ca4229..d05c51a793 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsLoader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsLoader.java @@ -18,6 +18,8 @@ package com.google.android.exoplayer2.source.ads; import android.view.ViewGroup; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.source.ads.AdsMediaSource.AdLoadException; +import com.google.android.exoplayer2.upstream.DataSpec; import java.io.IOException; /** @@ -54,19 +56,12 @@ public interface AdsLoader { void onAdPlaybackState(AdPlaybackState adPlaybackState); /** - * Called when there was an error loading ads. The loader will skip the problematic ad(s). + * Called when there was an error loading ads. * * @param error The error. + * @param dataSpec The data spec associated with the load error. */ - void onAdLoadError(IOException error); - - /** - * Called when an unexpected internal error is encountered while loading ads. The loader will - * skip all remaining ads, as the error is not recoverable. - * - * @param error The error. - */ - void onInternalAdLoadError(RuntimeException error); + void onAdLoadError(AdLoadException error, DataSpec dataSpec); /** * Called when the user clicks through an ad (for example, following a 'learn more' link). diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java index 9e93f8d92d..7f9dc18eaf 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java @@ -18,8 +18,8 @@ package com.google.android.exoplayer2.source.ads; import android.net.Uri; import android.os.Handler; import android.os.Looper; +import android.support.annotation.IntDef; import android.support.annotation.Nullable; -import android.util.Log; import android.view.ViewGroup; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; @@ -30,10 +30,16 @@ import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; +import com.google.android.exoplayer2.source.MediaSourceEventListener; +import com.google.android.exoplayer2.source.MediaSourceEventListener.LoadEventInfo; +import com.google.android.exoplayer2.source.MediaSourceEventListener.MediaLoadData; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -64,7 +70,75 @@ public final class AdsMediaSource extends CompositeMediaSource { int[] getSupportedTypes(); } - /** Listener for ads media source events. */ + /** + * Wrapper for exceptions that occur while loading ads, which are notified via {@link + * MediaSourceEventListener#onLoadError(int, MediaPeriodId, LoadEventInfo, MediaLoadData, + * IOException, boolean)}. + */ + public static final class AdLoadException extends IOException { + + /** Types of ad load exceptions. */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({TYPE_AD, TYPE_AD_GROUP, TYPE_ALL_ADS, TYPE_UNEXPECTED}) + public @interface Type {} + /** Type for when an ad failed to load. The ad will be skipped. */ + public static final int TYPE_AD = 0; + /** Type for when an ad group failed to load. The ad group will be skipped. */ + public static final int TYPE_AD_GROUP = 1; + /** Type for when all ad groups failed to load. All ads will be skipped. */ + public static final int TYPE_ALL_ADS = 2; + /** Type for when an unexpected error occurred while loading ads. All ads will be skipped. */ + public static final int TYPE_UNEXPECTED = 3; + + /** Returns a new ad load exception of {@link #TYPE_AD}. */ + public static AdLoadException createForAd(Exception error) { + return new AdLoadException(TYPE_AD, error); + } + + /** Returns a new ad load exception of {@link #TYPE_AD_GROUP}. */ + public static AdLoadException createForAdGroup(Exception error, int adGroupIndex) { + return new AdLoadException( + TYPE_AD_GROUP, new IOException("Failed to load ad group " + adGroupIndex, error)); + } + + /** Returns a new ad load exception of {@link #TYPE_ALL_ADS}. */ + public static AdLoadException createForAllAds(Exception error) { + return new AdLoadException(TYPE_ALL_ADS, error); + } + + /** Returns a new ad load exception of {@link #TYPE_UNEXPECTED}. */ + public static AdLoadException createForUnexpected(RuntimeException error) { + return new AdLoadException(TYPE_UNEXPECTED, error); + } + + /** The {@link Type} of the ad load exception. */ + public final @Type int type; + + private AdLoadException(@Type int type, Exception cause) { + super(cause); + this.type = type; + } + + /** + * Returns the {@link RuntimeException} that caused the exception if its type is {@link + * #TYPE_UNEXPECTED}. + */ + public RuntimeException getRuntimeExceptionForUnexpected() { + Assertions.checkState(type == TYPE_UNEXPECTED); + return (RuntimeException) getCause(); + } + } + + /** + * Listener for ads media source events. + * + * @deprecated To listen for ad load error events, add a listener via {@link + * #addEventListener(Handler, MediaSourceEventListener)} and check for {@link + * AdLoadException}s in {@link MediaSourceEventListener#onLoadError(int, MediaPeriodId, + * LoadEventInfo, MediaLoadData, IOException, boolean)}. Individual ads loader implementations + * should expose ad interaction events, if applicable. + */ + @Deprecated public interface EventListener { /** @@ -131,7 +205,30 @@ public final class AdsMediaSource extends CompositeMediaSource { ViewGroup adUiViewGroup) { this( contentMediaSource, - dataSourceFactory, + new ExtractorMediaSource.Factory(dataSourceFactory), + adsLoader, + adUiViewGroup, + /* eventHandler= */ null, + /* eventListener= */ null); + } + + /** + * Constructs a new source that inserts ads linearly with the content specified by {@code + * contentMediaSource}. + * + * @param contentMediaSource The {@link MediaSource} providing the content to play. + * @param adMediaSourceFactory Factory for media sources used to load ad media. + * @param adsLoader The loader for ads. + * @param adUiViewGroup A {@link ViewGroup} on top of the player that will show any ad UI. + */ + public AdsMediaSource( + MediaSource contentMediaSource, + MediaSourceFactory adMediaSourceFactory, + AdsLoader adsLoader, + ViewGroup adUiViewGroup) { + this( + contentMediaSource, + adMediaSourceFactory, adsLoader, adUiViewGroup, /* eventHandler= */ null, @@ -148,7 +245,13 @@ public final class AdsMediaSource extends CompositeMediaSource { * @param adUiViewGroup A {@link ViewGroup} on top of the player that will show any ad UI. * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. + * @deprecated To listen for ad load error events, add a listener via {@link + * #addEventListener(Handler, MediaSourceEventListener)} and check for {@link + * AdLoadException}s in {@link MediaSourceEventListener#onLoadError(int, MediaPeriodId, + * LoadEventInfo, MediaLoadData, IOException, boolean)}. Individual ads loader implementations + * should expose ad interaction events, if applicable. */ + @Deprecated public AdsMediaSource( MediaSource contentMediaSource, DataSource.Factory dataSourceFactory, @@ -175,7 +278,13 @@ public final class AdsMediaSource extends CompositeMediaSource { * @param adUiViewGroup A {@link ViewGroup} on top of the player that will show any ad UI. * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. + * @deprecated To listen for ad load error events, add a listener via {@link + * #addEventListener(Handler, MediaSourceEventListener)} and check for {@link + * AdLoadException}s in {@link MediaSourceEventListener#onLoadError(int, MediaPeriodId, + * LoadEventInfo, MediaLoadData, IOException, boolean)}. Individual ads loader implementations + * should expose ad interaction events, if applicable. */ + @Deprecated public AdsMediaSource( MediaSource contentMediaSource, MediaSourceFactory adMediaSourceFactory, @@ -217,10 +326,10 @@ public final class AdsMediaSource extends CompositeMediaSource { if (adPlaybackState.adGroupCount > 0 && id.isAd()) { int adGroupIndex = id.adGroupIndex; int adIndexInAdGroup = id.adIndexInAdGroup; + Uri adUri = adPlaybackState.adGroups[adGroupIndex].uris[adIndexInAdGroup]; if (adGroupMediaSources[adGroupIndex].length <= adIndexInAdGroup) { - Uri adUri = adPlaybackState.adGroups[id.adGroupIndex].uris[id.adIndexInAdGroup]; MediaSource adMediaSource = adMediaSourceFactory.createMediaSource(adUri); - int oldAdCount = adGroupMediaSources[id.adGroupIndex].length; + int oldAdCount = adGroupMediaSources[adGroupIndex].length; if (adIndexInAdGroup >= oldAdCount) { int adCount = adIndexInAdGroup + 1; adGroupMediaSources[adGroupIndex] = @@ -239,7 +348,7 @@ public final class AdsMediaSource extends CompositeMediaSource { new MediaPeriodId(/* periodIndex= */ 0, id.windowSequenceNumber), allocator); deferredMediaPeriod.setPrepareErrorListener( - new AdPrepareErrorListener(adGroupIndex, adIndexInAdGroup)); + new AdPrepareErrorListener(adUri, adGroupIndex, adIndexInAdGroup)); List mediaPeriods = deferredMediaPeriodByAdMediaSource.get(mediaSource); if (mediaPeriods == null) { deferredMediaPeriod.createPeriod(); @@ -357,6 +466,7 @@ public final class AdsMediaSource extends CompositeMediaSource { private final class ComponentListener implements AdsLoader.EventListener { private final Handler playerHandler; + private volatile boolean released; /** @@ -424,37 +534,30 @@ public final class AdsMediaSource extends CompositeMediaSource { } @Override - public void onAdLoadError(final IOException error) { + public void onAdLoadError(final AdLoadException error, DataSpec dataSpec) { if (released) { return; } - Log.w(TAG, "Ad load error", error); + createEventDispatcher(/* mediaPeriodId= */ null) + .loadError( + dataSpec, + C.DATA_TYPE_AD, + C.TRACK_TYPE_UNKNOWN, + /* loadDurationMs= */ 0, + /* bytesLoaded= */ 0, + error, + /* wasCanceled= */ true); if (eventHandler != null && eventListener != null) { eventHandler.post( new Runnable() { @Override public void run() { if (!released) { - eventListener.onAdLoadError(error); - } - } - }); - } - } - - @Override - public void onInternalAdLoadError(final RuntimeException error) { - if (released) { - return; - } - Log.w(TAG, "Internal ad load error", error); - if (eventHandler != null && eventListener != null) { - eventHandler.post( - new Runnable() { - @Override - public void run() { - if (!released) { - eventListener.onInternalAdLoadError(error); + if (error.type == AdLoadException.TYPE_UNEXPECTED) { + eventListener.onInternalAdLoadError(error.getRuntimeExceptionForUnexpected()); + } else { + eventListener.onAdLoadError(error); + } } } }); @@ -464,16 +567,27 @@ public final class AdsMediaSource extends CompositeMediaSource { private final class AdPrepareErrorListener implements DeferredMediaPeriod.PrepareErrorListener { + private final Uri adUri; private final int adGroupIndex; private final int adIndexInAdGroup; - public AdPrepareErrorListener(int adGroupIndex, int adIndexInAdGroup) { + public AdPrepareErrorListener(Uri adUri, int adGroupIndex, int adIndexInAdGroup) { + this.adUri = adUri; this.adGroupIndex = adGroupIndex; this.adIndexInAdGroup = adIndexInAdGroup; } @Override - public void onPrepareError(final IOException exception) { + public void onPrepareError(MediaPeriodId mediaPeriodId, final IOException exception) { + createEventDispatcher(mediaPeriodId) + .loadError( + new DataSpec(adUri), + C.DATA_TYPE_AD, + C.TRACK_TYPE_UNKNOWN, + /* loadDurationMs= */ 0, + /* bytesLoaded= */ 0, + AdLoadException.createForAd(exception), + /* wasCanceled= */ true); mainHandler.post( new Runnable() { @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/Chunk.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/Chunk.java index 6c9ae690fc..0453a8fa12 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/Chunk.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/Chunk.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.source.chunk; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.upstream.DataSource; @@ -51,7 +52,7 @@ public abstract class Chunk implements Loadable { * Optional data associated with the selection of the track to which this chunk belongs. Null if * the chunk does not belong to a track. */ - public final Object trackSelectionData; + public final @Nullable Object trackSelectionData; /** * The start time of the media contained by the chunk, or {@link C#TIME_UNSET} if the data * being loaded does not contain media samples. @@ -75,8 +76,15 @@ public abstract class Chunk implements Loadable { * @param startTimeUs See {@link #startTimeUs}. * @param endTimeUs See {@link #endTimeUs}. */ - public Chunk(DataSource dataSource, DataSpec dataSpec, int type, Format trackFormat, - int trackSelectionReason, Object trackSelectionData, long startTimeUs, long endTimeUs) { + public Chunk( + DataSource dataSource, + DataSpec dataSpec, + int type, + Format trackFormat, + int trackSelectionReason, + @Nullable Object trackSelectionData, + long startTimeUs, + long endTimeUs) { this.dataSource = Assertions.checkNotNull(dataSource); this.dataSpec = Assertions.checkNotNull(dataSpec); this.type = type; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/InitializationChunk.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/InitializationChunk.java index f505beb511..6dd90b8735 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/InitializationChunk.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/InitializationChunk.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.source.chunk; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.DefaultExtractorInput; @@ -44,8 +45,12 @@ public final class InitializationChunk extends Chunk { * @param trackSelectionData See {@link #trackSelectionData}. * @param extractorWrapper A wrapped extractor to use for parsing the initialization data. */ - public InitializationChunk(DataSource dataSource, DataSpec dataSpec, Format trackFormat, - int trackSelectionReason, Object trackSelectionData, + public InitializationChunk( + DataSource dataSource, + DataSpec dataSpec, + Format trackFormat, + int trackSelectionReason, + @Nullable Object trackSelectionData, ChunkExtractorWrapper extractorWrapper) { super(dataSource, dataSpec, C.DATA_TYPE_MEDIA_INITIALIZATION, trackFormat, trackSelectionReason, trackSelectionData, C.TIME_UNSET, C.TIME_UNSET); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java index 7edcf65320..f2b4c7ed3e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java @@ -37,6 +37,7 @@ import com.google.android.exoplayer2.upstream.BandwidthMeter; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -44,24 +45,91 @@ import java.util.Map; import java.util.concurrent.atomic.AtomicReference; /** - * A default {@link TrackSelector} suitable for most use cases. + * A default {@link TrackSelector} suitable for most use cases. Track selections are made according + * to configurable {@link Parameters}, which can be set by calling {@link + * #setParameters(Parameters)}. + * + *

Modifying parameters

+ * + * To modify only some aspects of the parameters currently used by a selector, it's possible to + * obtain a {@link ParametersBuilder} initialized with the current {@link Parameters}. The desired + * modifications can be made on the builder, and the resulting {@link Parameters} can then be built + * and set on the selector. For example the following code modifies the parameters to restrict video + * track selections to SD, and to prefer German audio tracks: + * + *
{@code
+ * // Build on the current parameters.
+ * Parameters currentParameters = trackSelector.getParameters();
+ * // Build the resulting parameters.
+ * Parameters newParameters = currentParameters
+ *     .buildUpon()
+ *     .setMaxVideoSizeSd()
+ *     .setPreferredAudioLanguage("deu")
+ *     .build();
+ * // Set the new parameters.
+ * trackSelector.setParameters(newParameters);
+ * }
+ * + * Convenience methods and chaining allow this to be written more concisely as: + * + *
{@code
+ * trackSelector.setParameters(
+ *     trackSelector
+ *         .buildUponParameters()
+ *         .setMaxVideoSizeSd()
+ *         .setPreferredAudioLanguage("deu"));
+ * }
+ * + * Selection {@link Parameters} support many different options, some of which are described below. + * + *

Track selection overrides

+ * + * Track selection overrides can be used to select specific tracks. To specify an override for a + * renderer, it's first necessary to obtain the tracks that have been mapped to it: + * + *
{@code
+ * MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo();
+ * TrackGroupArray rendererTrackGroups = mappedTrackInfo == null ? null
+ *     : mappedTrackInfo.getTrackGroups(rendererIndex);
+ * }
+ * + * If {@code rendererTrackGroups} is null then there aren't any currently mapped tracks, and so + * setting an override isn't possible. Note that a {@link Player.EventListener} registered on the + * player can be used to determine when the current tracks (and therefore the mapping) changes. If + * {@code rendererTrackGroups} is non-null then an override can be set. The next step is to query + * the properties of the available tracks to determine the {@code groupIndex} and the {@code + * trackIndices} within the group it that should be selected. The override can then be specified + * using {@link ParametersBuilder#setSelectionOverride}: + * + *
{@code
+ * SelectionOverride selectionOverride = new SelectionOverride(groupIndex, trackIndices);
+ * trackSelector.setParameters(
+ *     trackSelector
+ *         .buildUponParameters()
+ *         .setSelectionOverride(rendererIndex, rendererTrackGroups, selectionOverride));
+ * }
+ * + *

Disabling renderers

+ * + * Renderers can be disabled using {@link ParametersBuilder#setRendererDisabled}. Disabling a + * renderer differs from setting a {@code null} override because the renderer is disabled + * unconditionally, whereas a {@code null} override is applied only when the track groups available + * to the renderer match the {@link TrackGroupArray} for which it was specified. * *

Constraint based track selection

* - * Whilst this selector supports setting specific track overrides, the recommended way of changing - * which tracks are selected is by setting {@link Parameters} that constrain the track selection - * process. For example an instance can specify a preferred language for the audio track, and impose - * constraints on the maximum video resolution that should be selected for adaptive playbacks. - * Modifying the parameters is simple: + * Whilst track selection overrides make it possible to select specific tracks, the recommended way + * of controlling which tracks are selected is by specifying constraints. For example consider the + * case of wanting to restrict video track selections to SD, and preferring German audio tracks. + * Track selection overrides could be used to select specific tracks meeting these criteria, however + * a simpler and more flexible approach is to specify these constraints directly: * *
{@code
- * Parameters currentParameters = trackSelector.getParameters();
- * // Generate new parameters to prefer German audio and impose a maximum video size constraint.
- * Parameters newParameters = currentParameters
- *     .withPreferredAudioLanguage("deu")
- *     .withMaxVideoSize(1024, 768);
- * // Set the new parameters on the selector.
- * trackSelector.setParameters(newParameters);
+ * trackSelector.setParameters(
+ *     trackSelector
+ *         .buildUponParameters()
+ *         .setMaxVideoSizeSd()
+ *         .setPreferredAudioLanguage("deu"));
  * }
* * There are several benefits to using constraint based track selection instead of specific track @@ -76,46 +144,11 @@ import java.util.concurrent.atomic.AtomicReference; * only applied to periods whose tracks match those for which the override was set. * * - *

Track overrides

- * - * This selector supports overriding of track selections for each renderer. To specify an override - * for a renderer it's first necessary to obtain the tracks that have been mapped to it: - * - *
{@code
- * MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo();
- * TrackGroupArray rendererTrackGroups = mappedTrackInfo == null ? null
- *     : mappedTrackInfo.getTrackGroups(rendererIndex);
- * }
- * - * If {@code rendererTrackGroups} is null then there aren't any currently mapped tracks, and so - * setting an override isn't possible. Note that a {@link Player.EventListener} registered on the - * player can be used to determine when the current tracks (and therefore the mapping) changes. If - * {@code rendererTrackGroups} is non-null then an override can be set. The next step is to query - * the properties of the available tracks to determine the {@code groupIndex} of the track group you - * want to select and the {@code trackIndices} within it. You can then create and set the override: - * - *
{@code
- * trackSelector.setSelectionOverride(rendererIndex, rendererTrackGroups,
- *     new SelectionOverride(groupIndex, trackIndices));
- * }
- * - * If the override is {@code null} then no tracks will be selected. - * - *

Note that an override applies only when the track groups available to the renderer match the - * {@link TrackGroupArray} for which the override was specified. Overrides can be cleared using the - * {@code clearSelectionOverride} methods. - * - *

Disabling renderers

- * - * Renderers can be disabled using {@link #setRendererDisabled(int, boolean)}. Disabling a renderer - * differs from setting a {@code null} override because the renderer is disabled unconditionally, - * whereas a {@code null} override is applied only when the track groups available to the renderer - * match the {@link TrackGroupArray} for which it was specified. - * *

Tunneling

* * Tunneled playback can be enabled in cases where the combination of renderers and selected tracks - * support it. See {@link #setTunnelingAudioSessionId(int)} for more details. + * support it. Tunneled playback is enabled by passing an audio session ID to {@link + * ParametersBuilder#setTunnelingAudioSessionId(int)}. */ public class DefaultTrackSelector extends MappingTrackSelector { @@ -124,6 +157,9 @@ public class DefaultTrackSelector extends MappingTrackSelector { */ public static final class ParametersBuilder { + private final SparseArray> selectionOverrides; + private final SparseBooleanArray rendererDisabledFlags; + private String preferredAudioLanguage; private String preferredTextLanguage; private boolean selectUndeterminedTextLanguage; @@ -139,6 +175,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { private int viewportWidth; private int viewportHeight; private boolean viewportOrientationMayChange; + private int tunnelingAudioSessionId; /** * Creates a builder obtaining the initial values from {@link Parameters#DEFAULT}. @@ -152,6 +189,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { * obtained. */ private ParametersBuilder(Parameters initialValues) { + selectionOverrides = cloneSelectionOverrides(initialValues.selectionOverrides); + rendererDisabledFlags = initialValues.rendererDisabledFlags.clone(); preferredAudioLanguage = initialValues.preferredAudioLanguage; preferredTextLanguage = initialValues.preferredTextLanguage; selectUndeterminedTextLanguage = initialValues.selectUndeterminedTextLanguage; @@ -167,6 +206,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { viewportWidth = initialValues.viewportWidth; viewportHeight = initialValues.viewportHeight; viewportOrientationMayChange = initialValues.viewportOrientationMayChange; + tunnelingAudioSessionId = initialValues.tunnelingAudioSessionId; } /** @@ -341,11 +381,134 @@ public class DefaultTrackSelector extends MappingTrackSelector { return this; } + /** + * Sets whether the renderer at the specified index is disabled. Disabling a renderer prevents + * the selector from selecting any tracks for it. + * + * @param rendererIndex The renderer index. + * @param disabled Whether the renderer is disabled. + */ + public final ParametersBuilder setRendererDisabled(int rendererIndex, boolean disabled) { + if (rendererDisabledFlags.get(rendererIndex) == disabled) { + // The disabled flag is unchanged. + return this; + } + // Only true values are placed in the array to make it easier to check for equality. + if (disabled) { + rendererDisabledFlags.put(rendererIndex, true); + } else { + rendererDisabledFlags.delete(rendererIndex); + } + return this; + } + + /** + * Overrides the track selection for the renderer at the specified index. + * + *

When the {@link TrackGroupArray} mapped to the renderer matches the one provided, the + * override is applied. When the {@link TrackGroupArray} does not match, the override has no + * effect. The override replaces any previous override for the specified {@link TrackGroupArray} + * for the specified {@link Renderer}. + * + *

Passing a {@code null} override will cause the renderer to be disabled when the {@link + * TrackGroupArray} mapped to it matches the one provided. When the {@link TrackGroupArray} does + * not match a {@code null} override has no effect. Hence a {@code null} override differs from + * disabling the renderer using {@link #setRendererDisabled(int, boolean)} because the renderer + * is disabled conditionally on the {@link TrackGroupArray} mapped to it, where-as {@link + * #setRendererDisabled(int, boolean)} disables the renderer unconditionally. + * + *

To remove overrides use {@link #clearSelectionOverride(int, TrackGroupArray)}, {@link + * #clearSelectionOverrides(int)} or {@link #clearSelectionOverrides()}. + * + * @param rendererIndex The renderer index. + * @param groups The {@link TrackGroupArray} for which the override should be applied. + * @param override The override. + */ + public final ParametersBuilder setSelectionOverride( + int rendererIndex, TrackGroupArray groups, SelectionOverride override) { + Map overrides = selectionOverrides.get(rendererIndex); + if (overrides == null) { + overrides = new HashMap<>(); + selectionOverrides.put(rendererIndex, overrides); + } + if (overrides.containsKey(groups) && Util.areEqual(overrides.get(groups), override)) { + // The override is unchanged. + return this; + } + overrides.put(groups, override); + return this; + } + + /** + * Clears a track selection override for the specified renderer and {@link TrackGroupArray}. + * + * @param rendererIndex The renderer index. + * @param groups The {@link TrackGroupArray} for which the override should be cleared. + */ + public final ParametersBuilder clearSelectionOverride( + int rendererIndex, TrackGroupArray groups) { + Map overrides = selectionOverrides.get(rendererIndex); + if (overrides == null || !overrides.containsKey(groups)) { + // Nothing to clear. + return this; + } + overrides.remove(groups); + if (overrides.isEmpty()) { + selectionOverrides.remove(rendererIndex); + } + return this; + } + + /** + * Clears all track selection overrides for the specified renderer. + * + * @param rendererIndex The renderer index. + */ + public final ParametersBuilder clearSelectionOverrides(int rendererIndex) { + Map overrides = selectionOverrides.get(rendererIndex); + if (overrides == null || overrides.isEmpty()) { + // Nothing to clear. + return this; + } + selectionOverrides.remove(rendererIndex); + return this; + } + + /** Clears all track selection overrides for all renderers. */ + public final ParametersBuilder clearSelectionOverrides() { + if (selectionOverrides.size() == 0) { + // Nothing to clear. + return this; + } + selectionOverrides.clear(); + return this; + } + + /** + * Enables or disables tunneling. To enable tunneling, pass an audio session id to use when in + * tunneling mode. Session ids can be generated using {@link + * C#generateAudioSessionIdV21(Context)}. To disable tunneling pass {@link + * C#AUDIO_SESSION_ID_UNSET}. Tunneling will only be activated if it's both enabled and + * supported by the audio and video renderers for the selected tracks. + * + * @param tunnelingAudioSessionId The audio session id to use when tunneling, or {@link + * C#AUDIO_SESSION_ID_UNSET} to disable tunneling. + */ + public ParametersBuilder setTunnelingAudioSessionId(int tunnelingAudioSessionId) { + if (this.tunnelingAudioSessionId != tunnelingAudioSessionId) { + this.tunnelingAudioSessionId = tunnelingAudioSessionId; + return this; + } + return this; + } + /** * Builds a {@link Parameters} instance with the selected values. */ public Parameters build() { return new Parameters( + selectionOverrides, + rendererDisabledFlags, preferredAudioLanguage, preferredTextLanguage, selectUndeterminedTextLanguage, @@ -360,9 +523,18 @@ public class DefaultTrackSelector extends MappingTrackSelector { exceedRendererCapabilitiesIfNecessary, viewportWidth, viewportHeight, - viewportOrientationMayChange); + viewportOrientationMayChange, + tunnelingAudioSessionId); } + private static SparseArray> cloneSelectionOverrides( + SparseArray> selectionOverrides) { + SparseArray> clone = new SparseArray<>(); + for (int i = 0; i < selectionOverrides.size(); i++) { + clone.put(selectionOverrides.keyAt(i), new HashMap<>(selectionOverrides.valueAt(i))); + } + return clone; + } } /** Constraint parameters for {@link DefaultTrackSelector}. */ @@ -389,6 +561,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { */ public static final Parameters DEFAULT = new Parameters(); + // Per renderer overrides. + + private final SparseArray> selectionOverrides; + private final SparseBooleanArray rendererDisabledFlags; + // Audio /** * The preferred language for audio, as well as for forced text tracks, as an ISO 639-2/T tag. @@ -464,9 +641,16 @@ public class DefaultTrackSelector extends MappingTrackSelector { * Whether to exceed renderer capabilities when no selection can be made otherwise. */ public final boolean exceedRendererCapabilitiesIfNecessary; + /** + * The audio session id to use when tunneling, or {@link C#AUDIO_SESSION_ID_UNSET} if tunneling + * is not to be enabled. + */ + public final int tunnelingAudioSessionId; private Parameters() { this( + new SparseArray>(), + new SparseBooleanArray(), null, null, false, @@ -481,10 +665,13 @@ public class DefaultTrackSelector extends MappingTrackSelector { true, Integer.MAX_VALUE, Integer.MAX_VALUE, - true); + true, + C.AUDIO_SESSION_ID_UNSET); } /* package */ Parameters( + SparseArray> selectionOverrides, + SparseBooleanArray rendererDisabledFlags, String preferredAudioLanguage, String preferredTextLanguage, boolean selectUndeterminedTextLanguage, @@ -499,7 +686,10 @@ public class DefaultTrackSelector extends MappingTrackSelector { boolean exceedRendererCapabilitiesIfNecessary, int viewportWidth, int viewportHeight, - boolean viewportOrientationMayChange) { + boolean viewportOrientationMayChange, + int tunnelingAudioSessionId) { + this.selectionOverrides = selectionOverrides; + this.rendererDisabledFlags = rendererDisabledFlags; this.preferredAudioLanguage = Util.normalizeLanguageCode(preferredAudioLanguage); this.preferredTextLanguage = Util.normalizeLanguageCode(preferredTextLanguage); this.selectUndeterminedTextLanguage = selectUndeterminedTextLanguage; @@ -515,9 +705,12 @@ public class DefaultTrackSelector extends MappingTrackSelector { this.viewportWidth = viewportWidth; this.viewportHeight = viewportHeight; this.viewportOrientationMayChange = viewportOrientationMayChange; + this.tunnelingAudioSessionId = tunnelingAudioSessionId; } /* package */ Parameters(Parcel in) { + this.selectionOverrides = readSelectionOverrides(in); + this.rendererDisabledFlags = in.readSparseBooleanArray(); this.preferredAudioLanguage = in.readString(); this.preferredTextLanguage = in.readString(); this.selectUndeterminedTextLanguage = Util.readBoolean(in); @@ -533,6 +726,41 @@ public class DefaultTrackSelector extends MappingTrackSelector { this.viewportWidth = in.readInt(); this.viewportHeight = in.readInt(); this.viewportOrientationMayChange = Util.readBoolean(in); + this.tunnelingAudioSessionId = in.readInt(); + } + + /** + * Returns whether the renderer is disabled. + * + * @param rendererIndex The renderer index. + * @return Whether the renderer is disabled. + */ + public final boolean getRendererDisabled(int rendererIndex) { + return rendererDisabledFlags.get(rendererIndex); + } + + /** + * Returns whether there is an override for the specified renderer and {@link TrackGroupArray}. + * + * @param rendererIndex The renderer index. + * @param groups The {@link TrackGroupArray}. + * @return Whether there is an override. + */ + public final boolean hasSelectionOverride(int rendererIndex, TrackGroupArray groups) { + Map overrides = selectionOverrides.get(rendererIndex); + return overrides != null && overrides.containsKey(groups); + } + + /** + * Returns the override for the specified renderer and {@link TrackGroupArray}. + * + * @param rendererIndex The renderer index. + * @param groups The {@link TrackGroupArray}. + * @return The override, or null if no override exists. + */ + public final SelectionOverride getSelectionOverride(int rendererIndex, TrackGroupArray groups) { + Map overrides = selectionOverrides.get(rendererIndex); + return overrides != null ? overrides.get(groups) : null; } /** @@ -564,8 +792,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { && viewportWidth == other.viewportWidth && viewportHeight == other.viewportHeight && maxVideoBitrate == other.maxVideoBitrate + && tunnelingAudioSessionId == other.tunnelingAudioSessionId && TextUtils.equals(preferredAudioLanguage, other.preferredAudioLanguage) - && TextUtils.equals(preferredTextLanguage, other.preferredTextLanguage); + && TextUtils.equals(preferredTextLanguage, other.preferredTextLanguage) + && areRendererDisabledFlagsEqual(rendererDisabledFlags, other.rendererDisabledFlags) + && areSelectionOverridesEqual(selectionOverrides, other.selectionOverrides); } @Override @@ -583,6 +814,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { result = 31 * result + viewportWidth; result = 31 * result + viewportHeight; result = 31 * result + maxVideoBitrate; + result = 31 * result + tunnelingAudioSessionId; result = 31 * result + preferredAudioLanguage.hashCode(); result = 31 * result + preferredTextLanguage.hashCode(); return result; @@ -597,6 +829,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { @Override public void writeToParcel(Parcel dest, int flags) { + writeSelectionOverridesToParcel(dest, selectionOverrides); + dest.writeSparseBooleanArray(rendererDisabledFlags); dest.writeString(preferredAudioLanguage); dest.writeString(preferredTextLanguage); Util.writeBoolean(dest, selectUndeterminedTextLanguage); @@ -612,6 +846,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { dest.writeInt(viewportWidth); dest.writeInt(viewportHeight); Util.writeBoolean(dest, viewportOrientationMayChange); + dest.writeInt(tunnelingAudioSessionId); } public static final Parcelable.Creator CREATOR = @@ -627,36 +862,118 @@ public class DefaultTrackSelector extends MappingTrackSelector { return new Parameters[size]; } }; + + // Static utility methods. + + private static SparseArray> readSelectionOverrides( + Parcel in) { + int renderersWithOverridesCount = in.readInt(); + SparseArray> selectionOverrides = + new SparseArray<>(renderersWithOverridesCount); + for (int i = 0; i < renderersWithOverridesCount; i++) { + int rendererIndex = in.readInt(); + int overrideCount = in.readInt(); + Map overrides = new HashMap<>(overrideCount); + for (int j = 0; j < overrideCount; j++) { + TrackGroupArray trackGroups = in.readParcelable(TrackGroupArray.class.getClassLoader()); + SelectionOverride override = in.readParcelable(SelectionOverride.class.getClassLoader()); + overrides.put(trackGroups, override); + } + selectionOverrides.put(rendererIndex, overrides); + } + return selectionOverrides; + } + + private static void writeSelectionOverridesToParcel( + Parcel dest, SparseArray> selectionOverrides) { + int renderersWithOverridesCount = selectionOverrides.size(); + dest.writeInt(renderersWithOverridesCount); + for (int i = 0; i < renderersWithOverridesCount; i++) { + int rendererIndex = selectionOverrides.keyAt(i); + Map overrides = selectionOverrides.valueAt(i); + int overrideCount = overrides.size(); + dest.writeInt(rendererIndex); + dest.writeInt(overrideCount); + for (Map.Entry override : overrides.entrySet()) { + dest.writeParcelable(override.getKey(), /* parcelableFlags= */ 0); + dest.writeParcelable(override.getValue(), /* parcelableFlags= */ 0); + } + } + } + + private static boolean areRendererDisabledFlagsEqual( + SparseBooleanArray first, SparseBooleanArray second) { + int firstSize = first.size(); + if (second.size() != firstSize) { + return false; + } + // Only true values are put into rendererDisabledFlags, so we don't need to compare values. + for (int indexInFirst = 0; indexInFirst < firstSize; indexInFirst++) { + if (second.indexOfKey(first.keyAt(indexInFirst)) < 0) { + return false; + } + } + return true; + } + + private static boolean areSelectionOverridesEqual( + SparseArray> first, + SparseArray> second) { + int firstSize = first.size(); + if (second.size() != firstSize) { + return false; + } + for (int indexInFirst = 0; indexInFirst < firstSize; indexInFirst++) { + int indexInSecond = second.indexOfKey(first.keyAt(indexInFirst)); + if (indexInSecond < 0 + || !areSelectionOverridesEqual( + first.valueAt(indexInFirst), second.valueAt(indexInSecond))) { + return false; + } + } + return true; + } + + private static boolean areSelectionOverridesEqual( + Map first, + Map second) { + int firstSize = first.size(); + if (second.size() != firstSize) { + return false; + } + for (Map.Entry firstEntry : first.entrySet()) { + TrackGroupArray key = firstEntry.getKey(); + if (!second.containsKey(key) || !Util.areEqual(firstEntry.getValue(), second.get(key))) { + return false; + } + } + return true; + } } /** A track selection override. */ - public static class SelectionOverride { + public static final class SelectionOverride implements Parcelable { - public final TrackSelection.Factory factory; public final int groupIndex; public final int[] tracks; public final int length; /** - * @param factory A factory for creating selections from this override. * @param groupIndex The overriding track group index. * @param tracks The overriding track indices within the track group. */ - public SelectionOverride(TrackSelection.Factory factory, int groupIndex, int... tracks) { - this.factory = factory; + public SelectionOverride(int groupIndex, int... tracks) { this.groupIndex = groupIndex; - this.tracks = tracks; + this.tracks = Arrays.copyOf(tracks, tracks.length); this.length = tracks.length; + Arrays.sort(this.tracks); } - /** - * Creates an selection from this override. - * - * @param groups The track groups whose selection is being overridden. - * @return The selection. - */ - public TrackSelection createTrackSelection(TrackGroupArray groups) { - return factory.createTrackSelection(groups.get(groupIndex), tracks); + /* package */ SelectionOverride(Parcel in) { + groupIndex = in.readInt(); + length = in.readByte(); + tracks = new int[length]; + in.readIntArray(tracks); } /** Returns whether this override contains the specified track index. */ @@ -668,6 +985,51 @@ public class DefaultTrackSelector extends MappingTrackSelector { } return false; } + + @Override + public int hashCode() { + return 31 * groupIndex + Arrays.hashCode(tracks); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + SelectionOverride other = (SelectionOverride) obj; + return groupIndex == other.groupIndex && Arrays.equals(tracks, other.tracks); + } + + // Parcelable implementation. + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(groupIndex); + dest.writeInt(tracks.length); + dest.writeIntArray(tracks); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + + @Override + public SelectionOverride createFromParcel(Parcel in) { + return new SelectionOverride(in); + } + + @Override + public SelectionOverride[] newArray(int size) { + return new SelectionOverride[size]; + } + }; } /** @@ -680,11 +1042,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { private static final int WITHIN_RENDERER_CAPABILITIES_BONUS = 1000; private final TrackSelection.Factory adaptiveTrackSelectionFactory; - private final AtomicReference paramsReference; - private final SparseArray> selectionOverrides; - private final SparseBooleanArray rendererDisabledFlags; - - private int tunnelingAudioSessionId; + private final AtomicReference parametersReference; /** * Constructs an instance that does not support adaptive track selection. @@ -712,214 +1070,150 @@ public class DefaultTrackSelector extends MappingTrackSelector { */ public DefaultTrackSelector(TrackSelection.Factory adaptiveTrackSelectionFactory) { this.adaptiveTrackSelectionFactory = adaptiveTrackSelectionFactory; - paramsReference = new AtomicReference<>(Parameters.DEFAULT); - selectionOverrides = new SparseArray<>(); - rendererDisabledFlags = new SparseBooleanArray(); - tunnelingAudioSessionId = C.AUDIO_SESSION_ID_UNSET; + parametersReference = new AtomicReference<>(Parameters.DEFAULT); } /** * Atomically sets the provided parameters for track selection. * - * @param params The parameters for track selection. + * @param parameters The parameters for track selection. */ - public void setParameters(Parameters params) { - Assertions.checkNotNull(params); - if (!paramsReference.getAndSet(params).equals(params)) { + public void setParameters(Parameters parameters) { + Assertions.checkNotNull(parameters); + if (!parametersReference.getAndSet(parameters).equals(parameters)) { invalidate(); } } + /** + * Atomically sets the provided parameters for track selection. + * + * @param parametersBuilder A builder from which to obtain the parameters for track selection. + */ + public void setParameters(ParametersBuilder parametersBuilder) { + setParameters(parametersBuilder.build()); + } + /** * Gets the current selection parameters. * * @return The current selection parameters. */ public Parameters getParameters() { - return paramsReference.get(); + return parametersReference.get(); } - /** - * Sets whether the renderer at the specified index is disabled. Disabling a renderer prevents the - * selector from selecting any tracks for it. - * - * @param rendererIndex The renderer index. - * @param disabled Whether the renderer is disabled. - */ + /** Returns a new {@link ParametersBuilder} initialized with the current selection parameters. */ + public ParametersBuilder buildUponParameters() { + return getParameters().buildUpon(); + } + + /** @deprecated Use {@link ParametersBuilder#setRendererDisabled(int, boolean)}. */ + @Deprecated public final void setRendererDisabled(int rendererIndex, boolean disabled) { - if (rendererDisabledFlags.get(rendererIndex) == disabled) { - // The disabled flag is unchanged. - return; - } - rendererDisabledFlags.put(rendererIndex, disabled); - invalidate(); + setParameters(buildUponParameters().setRendererDisabled(rendererIndex, disabled)); } - /** - * Returns whether the renderer is disabled. - * - * @param rendererIndex The renderer index. - * @return Whether the renderer is disabled. - */ + /** @deprecated Use {@link Parameters#getRendererDisabled(int)}. * */ + @Deprecated public final boolean getRendererDisabled(int rendererIndex) { - return rendererDisabledFlags.get(rendererIndex); + return getParameters().getRendererDisabled(rendererIndex); } /** - * Overrides the track selection for the renderer at the specified index. - * - *

When the {@link TrackGroupArray} mapped to the renderer matches the one provided, the - * override is applied. When the {@link TrackGroupArray} does not match, the override has no - * effect. The override replaces any previous override for the specified {@link TrackGroupArray} - * for the specified {@link Renderer}. - * - *

Passing a {@code null} override will cause the renderer to be disabled when the {@link - * TrackGroupArray} mapped to it matches the one provided. When the {@link TrackGroupArray} does - * not match a {@code null} override has no effect. Hence a {@code null} override differs from - * disabling the renderer using {@link #setRendererDisabled(int, boolean)} because the renderer is - * disabled conditionally on the {@link TrackGroupArray} mapped to it, where-as {@link - * #setRendererDisabled(int, boolean)} disables the renderer unconditionally. - * - *

To remove overrides use {@link #clearSelectionOverride(int, TrackGroupArray)}, {@link - * #clearSelectionOverrides(int)} or {@link #clearSelectionOverrides()}. - * - * @param rendererIndex The renderer index. - * @param groups The {@link TrackGroupArray} for which the override should be applied. - * @param override The override. + * @deprecated Use {@link ParametersBuilder#setSelectionOverride(int, TrackGroupArray, + * SelectionOverride)}. */ + @Deprecated public final void setSelectionOverride( int rendererIndex, TrackGroupArray groups, SelectionOverride override) { - Map overrides = selectionOverrides.get(rendererIndex); - if (overrides == null) { - overrides = new HashMap<>(); - selectionOverrides.put(rendererIndex, overrides); - } - if (overrides.containsKey(groups) && Util.areEqual(overrides.get(groups), override)) { - // The override is unchanged. - return; - } - overrides.put(groups, override); - invalidate(); + setParameters(buildUponParameters().setSelectionOverride(rendererIndex, groups, override)); } - /** - * Returns whether there is an override for the specified renderer and {@link TrackGroupArray}. - * - * @param rendererIndex The renderer index. - * @param groups The {@link TrackGroupArray}. - * @return Whether there is an override. - */ + /** @deprecated Use {@link Parameters#hasSelectionOverride(int, TrackGroupArray)}. * */ + @Deprecated public final boolean hasSelectionOverride(int rendererIndex, TrackGroupArray groups) { - Map overrides = selectionOverrides.get(rendererIndex); - return overrides != null && overrides.containsKey(groups); + return getParameters().hasSelectionOverride(rendererIndex, groups); } - /** - * Returns the override for the specified renderer and {@link TrackGroupArray}. - * - * @param rendererIndex The renderer index. - * @param groups The {@link TrackGroupArray}. - * @return The override, or null if no override exists. - */ + /** @deprecated Use {@link Parameters#getSelectionOverride(int, TrackGroupArray)}. */ + @Deprecated public final SelectionOverride getSelectionOverride(int rendererIndex, TrackGroupArray groups) { - Map overrides = selectionOverrides.get(rendererIndex); - return overrides != null ? overrides.get(groups) : null; + return getParameters().getSelectionOverride(rendererIndex, groups); } - /** - * Clears a track selection override for the specified renderer and {@link TrackGroupArray}. - * - * @param rendererIndex The renderer index. - * @param groups The {@link TrackGroupArray} for which the override should be cleared. - */ + /** @deprecated Use {@link ParametersBuilder#clearSelectionOverride(int, TrackGroupArray)}. */ + @Deprecated public final void clearSelectionOverride(int rendererIndex, TrackGroupArray groups) { - Map overrides = selectionOverrides.get(rendererIndex); - if (overrides == null || !overrides.containsKey(groups)) { - // Nothing to clear. - return; - } - overrides.remove(groups); - if (overrides.isEmpty()) { - selectionOverrides.remove(rendererIndex); - } - invalidate(); + setParameters(buildUponParameters().clearSelectionOverride(rendererIndex, groups)); } - /** - * Clears all track selection overrides for the specified renderer. - * - * @param rendererIndex The renderer index. - */ + /** @deprecated Use {@link ParametersBuilder#clearSelectionOverrides(int)}. */ + @Deprecated public final void clearSelectionOverrides(int rendererIndex) { - Map overrides = selectionOverrides.get(rendererIndex); - if (overrides == null || overrides.isEmpty()) { - // Nothing to clear. - return; - } - selectionOverrides.remove(rendererIndex); - invalidate(); + setParameters(buildUponParameters().clearSelectionOverrides(rendererIndex)); } - /** Clears all track selection overrides for all renderers. */ + /** @deprecated Use {@link ParametersBuilder#clearSelectionOverrides()}. */ + @Deprecated public final void clearSelectionOverrides() { - if (selectionOverrides.size() == 0) { - // Nothing to clear. - return; - } - selectionOverrides.clear(); - invalidate(); + setParameters(buildUponParameters().clearSelectionOverrides()); } - /** - * Enables or disables tunneling. To enable tunneling, pass an audio session id to use when in - * tunneling mode. Session ids can be generated using {@link - * C#generateAudioSessionIdV21(Context)}. To disable tunneling pass {@link - * C#AUDIO_SESSION_ID_UNSET}. Tunneling will only be activated if it's both enabled and supported - * by the audio and video renderers for the selected tracks. - * - * @param tunnelingAudioSessionId The audio session id to use when tunneling, or {@link - * C#AUDIO_SESSION_ID_UNSET} to disable tunneling. - */ + /** @deprecated Use {@link ParametersBuilder#setTunnelingAudioSessionId(int)}. */ + @Deprecated public void setTunnelingAudioSessionId(int tunnelingAudioSessionId) { - if (this.tunnelingAudioSessionId != tunnelingAudioSessionId) { - this.tunnelingAudioSessionId = tunnelingAudioSessionId; - invalidate(); - } + setParameters(buildUponParameters().setTunnelingAudioSessionId(tunnelingAudioSessionId)); } // MappingTrackSelector implementation. @Override protected final Pair selectTracks( - RendererCapabilities[] rendererCapabilities, MappedTrackInfo mappedTrackInfo) + MappedTrackInfo mappedTrackInfo, + int[][][] rendererFormatSupports, + int[] rendererMixedMimeTypeAdaptationSupports) throws ExoPlaybackException { - int rendererCount = rendererCapabilities.length; + Parameters params = parametersReference.get(); + int rendererCount = mappedTrackInfo.getRendererCount(); TrackSelection[] rendererTrackSelections = - selectAllTracks(rendererCapabilities, mappedTrackInfo); + selectAllTracks( + mappedTrackInfo, + rendererFormatSupports, + rendererMixedMimeTypeAdaptationSupports, + params); // Apply track disabling and overriding. for (int i = 0; i < rendererCount; i++) { - if (rendererDisabledFlags.get(i)) { + if (params.getRendererDisabled(i)) { rendererTrackSelections[i] = null; } else { - TrackGroupArray rendererTrackGroup = mappedTrackInfo.getTrackGroups(i); - if (hasSelectionOverride(i, rendererTrackGroup)) { - SelectionOverride override = selectionOverrides.get(i).get(rendererTrackGroup); - rendererTrackSelections[i] = - override == null ? null : override.createTrackSelection(rendererTrackGroup); + TrackGroupArray rendererTrackGroups = mappedTrackInfo.getTrackGroups(i); + if (params.hasSelectionOverride(i, rendererTrackGroups)) { + SelectionOverride override = params.getSelectionOverride(i, rendererTrackGroups); + if (override == null) { + rendererTrackSelections[i] = null; + } else if (override.length == 1) { + rendererTrackSelections[i] = + new FixedTrackSelection( + rendererTrackGroups.get(override.groupIndex), override.tracks[0]); + } else { + rendererTrackSelections[i] = + adaptiveTrackSelectionFactory.createTrackSelection( + rendererTrackGroups.get(override.groupIndex), override.tracks); + } } } } // Initialize the renderer configurations to the default configuration for all renderers with // selections, and null otherwise. - RendererConfiguration[] rendererConfigurations = - new RendererConfiguration[rendererCapabilities.length]; + RendererConfiguration[] rendererConfigurations = new RendererConfiguration[rendererCount]; for (int i = 0; i < rendererCount; i++) { - boolean forceRendererDisabled = rendererDisabledFlags.get(i); + boolean forceRendererDisabled = params.getRendererDisabled(i); boolean rendererEnabled = !forceRendererDisabled - && (rendererCapabilities[i].getTrackType() == C.TRACK_TYPE_NONE + && (mappedTrackInfo.getRendererType(i) == C.TRACK_TYPE_NONE || rendererTrackSelections[i] != null); rendererConfigurations[i] = rendererEnabled ? RendererConfiguration.DEFAULT : null; } @@ -927,10 +1221,10 @@ public class DefaultTrackSelector extends MappingTrackSelector { // Configure audio and video renderers to use tunneling if appropriate. maybeConfigureRenderersForTunneling( mappedTrackInfo, - rendererCapabilities, + rendererFormatSupports, rendererConfigurations, rendererTrackSelections, - tunnelingAudioSessionId); + params.tunnelingAudioSessionId); return Pair.create(rendererConfigurations, rendererTrackSelections); } @@ -938,35 +1232,40 @@ public class DefaultTrackSelector extends MappingTrackSelector { // Track selection prior to overrides and disabled flags being applied. /** - * Called from {@link #selectTracks(RendererCapabilities[], MappedTrackInfo)} to make a track - * selection for each renderer, prior to overrides and disabled flags being applied. + * Called from {@link #selectTracks(MappedTrackInfo, int[][][], int[])} to make a track selection + * for each renderer, prior to overrides and disabled flags being applied. * *

The implementation should not account for overrides and disabled flags. Track selections * generated by this method will be overridden to account for these properties. * - * @param rendererCapabilities The {@link RendererCapabilities} of each renderer. * @param mappedTrackInfo Mapped track information. + * @param rendererFormatSupports The result of {@link RendererCapabilities#supportsFormat} for + * each mapped track, indexed by renderer, track group and track (in that order). + * @param rendererMixedMimeTypeAdaptationSupports The result of {@link + * RendererCapabilities#supportsMixedMimeTypeAdaptation()} for each renderer. * @return Track selections for each renderer. A null selection indicates the renderer should be * disabled, unless RendererCapabilities#getTrackType()} is {@link C#TRACK_TYPE_NONE}. * @throws ExoPlaybackException If an error occurs while selecting the tracks. */ protected TrackSelection[] selectAllTracks( - RendererCapabilities[] rendererCapabilities, MappedTrackInfo mappedTrackInfo) + MappedTrackInfo mappedTrackInfo, + int[][][] rendererFormatSupports, + int[] rendererMixedMimeTypeAdaptationSupports, + Parameters params) throws ExoPlaybackException { - int rendererCount = rendererCapabilities.length; + int rendererCount = mappedTrackInfo.getRendererCount(); TrackSelection[] rendererTrackSelections = new TrackSelection[rendererCount]; - Parameters params = paramsReference.get(); boolean seenVideoRendererWithMappedTracks = false; boolean selectedVideoTracks = false; for (int i = 0; i < rendererCount; i++) { - if (C.TRACK_TYPE_VIDEO == rendererCapabilities[i].getTrackType()) { + if (C.TRACK_TYPE_VIDEO == mappedTrackInfo.getRendererType(i)) { if (!selectedVideoTracks) { rendererTrackSelections[i] = selectVideoTrack( - rendererCapabilities[i], mappedTrackInfo.getTrackGroups(i), - mappedTrackInfo.getRendererTrackSupport(i), + rendererFormatSupports[i], + rendererMixedMimeTypeAdaptationSupports[i], params, adaptiveTrackSelectionFactory); selectedVideoTracks = rendererTrackSelections[i] != null; @@ -978,7 +1277,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { boolean selectedAudioTracks = false; boolean selectedTextTracks = false; for (int i = 0; i < rendererCount; i++) { - switch (rendererCapabilities[i].getTrackType()) { + int trackType = mappedTrackInfo.getRendererType(i); + switch (trackType) { case C.TRACK_TYPE_VIDEO: // Already done. Do nothing. break; @@ -987,7 +1287,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { rendererTrackSelections[i] = selectAudioTrack( mappedTrackInfo.getTrackGroups(i), - mappedTrackInfo.getRendererTrackSupport(i), + rendererFormatSupports[i], + rendererMixedMimeTypeAdaptationSupports[i], params, seenVideoRendererWithMappedTracks ? null : adaptiveTrackSelectionFactory); selectedAudioTracks = rendererTrackSelections[i] != null; @@ -997,19 +1298,14 @@ public class DefaultTrackSelector extends MappingTrackSelector { if (!selectedTextTracks) { rendererTrackSelections[i] = selectTextTrack( - mappedTrackInfo.getTrackGroups(i), - mappedTrackInfo.getRendererTrackSupport(i), - params); + mappedTrackInfo.getTrackGroups(i), rendererFormatSupports[i], params); selectedTextTracks = rendererTrackSelections[i] != null; } break; default: rendererTrackSelections[i] = selectOtherTrack( - rendererCapabilities[i].getTrackType(), - mappedTrackInfo.getTrackGroups(i), - mappedTrackInfo.getRendererTrackSupport(i), - params); + trackType, mappedTrackInfo.getTrackGroups(i), rendererFormatSupports[i], params); break; } } @@ -1020,13 +1316,14 @@ public class DefaultTrackSelector extends MappingTrackSelector { // Video track selection implementation. /** - * Called by {@link #selectTracks(RendererCapabilities[], MappedTrackInfo)} to create a {@link - * TrackSelection} for a video renderer. + * Called by {@link #selectAllTracks(MappedTrackInfo, int[][][], int[], Parameters)} to create a + * {@link TrackSelection} for a video renderer. * - * @param rendererCapabilities The {@link RendererCapabilities} for the renderer. * @param groups The {@link TrackGroupArray} mapped to the renderer. - * @param formatSupport The result of {@link RendererCapabilities#supportsFormat} for each mapped + * @param formatSupports The result of {@link RendererCapabilities#supportsFormat} for each mapped * track, indexed by track group index and track index (in that order). + * @param mixedMimeTypeAdaptationSupports The result of {@link + * RendererCapabilities#supportsMixedMimeTypeAdaptation()} for the renderer. * @param params The selector's current constraint parameters. * @param adaptiveTrackSelectionFactory A factory for generating adaptive track selections, or * null if a fixed track selection is required. @@ -1034,31 +1331,41 @@ public class DefaultTrackSelector extends MappingTrackSelector { * @throws ExoPlaybackException If an error occurs while selecting the tracks. */ protected TrackSelection selectVideoTrack( - RendererCapabilities rendererCapabilities, TrackGroupArray groups, - int[][] formatSupport, + int[][] formatSupports, + int mixedMimeTypeAdaptationSupports, Parameters params, TrackSelection.Factory adaptiveTrackSelectionFactory) throws ExoPlaybackException { TrackSelection selection = null; if (!params.forceLowestBitrate && adaptiveTrackSelectionFactory != null) { - selection = selectAdaptiveVideoTrack(rendererCapabilities, groups, formatSupport, - params, adaptiveTrackSelectionFactory); + selection = + selectAdaptiveVideoTrack( + groups, + formatSupports, + mixedMimeTypeAdaptationSupports, + params, + adaptiveTrackSelectionFactory); } if (selection == null) { - selection = selectFixedVideoTrack(groups, formatSupport, params); + selection = selectFixedVideoTrack(groups, formatSupports, params); } return selection; } - private static TrackSelection selectAdaptiveVideoTrack(RendererCapabilities rendererCapabilities, - TrackGroupArray groups, int[][] formatSupport, Parameters params, - TrackSelection.Factory adaptiveTrackSelectionFactory) throws ExoPlaybackException { + private static TrackSelection selectAdaptiveVideoTrack( + TrackGroupArray groups, + int[][] formatSupport, + int mixedMimeTypeAdaptationSupports, + Parameters params, + TrackSelection.Factory adaptiveTrackSelectionFactory) + throws ExoPlaybackException { int requiredAdaptiveSupport = params.allowNonSeamlessAdaptiveness ? (RendererCapabilities.ADAPTIVE_NOT_SEAMLESS | RendererCapabilities.ADAPTIVE_SEAMLESS) : RendererCapabilities.ADAPTIVE_SEAMLESS; - boolean allowMixedMimeTypes = params.allowMixedMimeAdaptiveness - && (rendererCapabilities.supportsMixedMimeTypeAdaptation() & requiredAdaptiveSupport) != 0; + boolean allowMixedMimeTypes = + params.allowMixedMimeAdaptiveness + && (mixedMimeTypeAdaptationSupports & requiredAdaptiveSupport) != 0; for (int i = 0; i < groups.length; i++) { TrackGroup group = groups.get(i); int[] adaptiveTracks = getAdaptiveVideoTracksForGroup(group, formatSupport[i], @@ -1151,8 +1458,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { && (format.bitrate == Format.NO_VALUE || format.bitrate <= maxVideoBitrate); } - private static TrackSelection selectFixedVideoTrack(TrackGroupArray groups, - int[][] formatSupport, Parameters params) { + private static TrackSelection selectFixedVideoTrack( + TrackGroupArray groups, int[][] formatSupports, Parameters params) { TrackGroup selectedGroup = null; int selectedTrackIndex = 0; int selectedTrackScore = 0; @@ -1162,7 +1469,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { TrackGroup trackGroup = groups.get(groupIndex); List selectedTrackIndices = getViewportFilteredTrackIndices(trackGroup, params.viewportWidth, params.viewportHeight, params.viewportOrientationMayChange); - int[] trackFormatSupport = formatSupport[groupIndex]; + int[] trackFormatSupport = formatSupports[groupIndex]; for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { if (isSupported(trackFormatSupport[trackIndex], params.exceedRendererCapabilitiesIfNecessary)) { @@ -1215,12 +1522,14 @@ public class DefaultTrackSelector extends MappingTrackSelector { // Audio track selection implementation. /** - * Called by {@link #selectTracks(RendererCapabilities[], MappedTrackInfo)} to create a {@link - * TrackSelection} for an audio renderer. + * Called by {@link #selectAllTracks(MappedTrackInfo, int[][][], int[], Parameters)} to create a + * {@link TrackSelection} for an audio renderer. * * @param groups The {@link TrackGroupArray} mapped to the renderer. - * @param formatSupport The result of {@link RendererCapabilities#supportsFormat} for each mapped + * @param formatSupports The result of {@link RendererCapabilities#supportsFormat} for each mapped * track, indexed by track group index and track index (in that order). + * @param mixedMimeTypeAdaptationSupports The result of {@link + * RendererCapabilities#supportsMixedMimeTypeAdaptation()} for the renderer. * @param params The selector's current constraint parameters. * @param adaptiveTrackSelectionFactory A factory for generating adaptive track selections, or * null if a fixed track selection is required. @@ -1229,7 +1538,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { */ protected TrackSelection selectAudioTrack( TrackGroupArray groups, - int[][] formatSupport, + int[][] formatSupports, + int mixedMimeTypeAdaptationSupports, Parameters params, TrackSelection.Factory adaptiveTrackSelectionFactory) throws ExoPlaybackException { @@ -1238,7 +1548,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { AudioTrackScore selectedTrackScore = null; for (int groupIndex = 0; groupIndex < groups.length; groupIndex++) { TrackGroup trackGroup = groups.get(groupIndex); - int[] trackFormatSupport = formatSupport[groupIndex]; + int[] trackFormatSupport = formatSupports[groupIndex]; for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { if (isSupported(trackFormatSupport[trackIndex], params.exceedRendererCapabilitiesIfNecessary)) { @@ -1261,8 +1571,9 @@ public class DefaultTrackSelector extends MappingTrackSelector { TrackGroup selectedGroup = groups.get(selectedGroupIndex); if (!params.forceLowestBitrate && adaptiveTrackSelectionFactory != null) { // If the group of the track with the highest score allows it, try to enable adaptation. - int[] adaptiveTracks = getAdaptiveAudioTracks(selectedGroup, - formatSupport[selectedGroupIndex], params.allowMixedMimeAdaptiveness); + int[] adaptiveTracks = + getAdaptiveAudioTracks( + selectedGroup, formatSupports[selectedGroupIndex], params.allowMixedMimeAdaptiveness); if (adaptiveTracks.length > 0) { return adaptiveTrackSelectionFactory.createTrackSelection(selectedGroup, adaptiveTracks); @@ -1326,8 +1637,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { // Text track selection implementation. /** - * Called by {@link #selectTracks(RendererCapabilities[], MappedTrackInfo)} to create a {@link - * TrackSelection} for a text renderer. + * Called by {@link #selectAllTracks(MappedTrackInfo, int[][][], int[], Parameters)} to create a + * {@link TrackSelection} for a text renderer. * * @param groups The {@link TrackGroupArray} mapped to the renderer. * @param formatSupport The result of {@link RendererCapabilities#supportsFormat} for each mapped @@ -1398,8 +1709,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { // General track selection methods. /** - * Called by {@link #selectTracks(RendererCapabilities[], MappedTrackInfo)} to create a {@link - * TrackSelection} for a renderer whose type is neither video, audio or text. + * Called by {@link #selectAllTracks(MappedTrackInfo, int[][][], int[], Parameters)} to create a + * {@link TrackSelection} for a renderer whose type is neither video, audio or text. * * @param trackType The type of the renderer. * @param groups The {@link TrackGroupArray} mapped to the renderer. @@ -1446,8 +1757,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { * {@code rendererConfigurations} with configurations that enable tunneling on the appropriate * renderers if so. * - * @param rendererCapabilities The {@link RendererCapabilities} of the renderers for which {@link - * TrackSelection}s are to be generated. + * @param mappedTrackInfo Mapped track information. * @param rendererConfigurations The renderer configurations. Configurations may be replaced with * ones that enable tunneling as a result of this call. * @param trackSelections The renderer track selections. @@ -1456,7 +1766,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { */ private static void maybeConfigureRenderersForTunneling( MappedTrackInfo mappedTrackInfo, - RendererCapabilities[] rendererCapabilities, + int[][][] renderererFormatSupports, RendererConfiguration[] rendererConfigurations, TrackSelection[] trackSelections, int tunnelingAudioSessionId) { @@ -1468,15 +1778,13 @@ public class DefaultTrackSelector extends MappingTrackSelector { int tunnelingAudioRendererIndex = -1; int tunnelingVideoRendererIndex = -1; boolean enableTunneling = true; - for (int i = 0; i < rendererCapabilities.length; i++) { - int rendererType = rendererCapabilities[i].getTrackType(); + for (int i = 0; i < mappedTrackInfo.getRendererCount(); i++) { + int rendererType = mappedTrackInfo.getRendererType(i); TrackSelection trackSelection = trackSelections[i]; if ((rendererType == C.TRACK_TYPE_AUDIO || rendererType == C.TRACK_TYPE_VIDEO) && trackSelection != null) { if (rendererSupportsTunneling( - mappedTrackInfo.getRendererTrackSupport(i), - mappedTrackInfo.getTrackGroups(i), - trackSelection)) { + renderererFormatSupports[i], mappedTrackInfo.getTrackGroups(i), trackSelection)) { if (rendererType == C.TRACK_TYPE_AUDIO) { if (tunnelingAudioRendererIndex != -1) { enableTunneling = false; @@ -1507,20 +1815,20 @@ public class DefaultTrackSelector extends MappingTrackSelector { /** * Returns whether a renderer supports tunneling for a {@link TrackSelection}. * - * @param formatSupport The result of {@link RendererCapabilities#supportsFormat} for each track, + * @param formatSupports The result of {@link RendererCapabilities#supportsFormat} for each track, * indexed by group index and track index (in that order). * @param trackGroups The {@link TrackGroupArray}s for the renderer. * @param selection The track selection. * @return Whether the renderer supports tunneling for the {@link TrackSelection}. */ private static boolean rendererSupportsTunneling( - int[][] formatSupport, TrackGroupArray trackGroups, TrackSelection selection) { + int[][] formatSupports, TrackGroupArray trackGroups, TrackSelection selection) { if (selection == null) { return false; } int trackGroupIndex = trackGroups.indexOf(selection.getTrackGroup()); for (int i = 0; i < selection.length(); i++) { - int trackFormatSupport = formatSupport[trackGroupIndex][selection.getIndexInTrackGroup(i)]; + int trackFormatSupport = formatSupports[trackGroupIndex][selection.getIndexInTrackGroup(i)]; if ((trackFormatSupport & RendererCapabilities.TUNNELING_SUPPORT_MASK) != RendererCapabilities.TUNNELING_SUPPORTED) { return false; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java index 75a7565b98..4af969369e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java @@ -52,46 +52,76 @@ public abstract class MappingTrackSelector extends TrackSelector { @interface RendererSupport {} /** The renderer does not have any associated tracks. */ public static final int RENDERER_SUPPORT_NO_TRACKS = 0; - /** The renderer has associated tracks, but all are of unsupported types. */ + /** + * The renderer has tracks mapped to it, but all are unsupported. In other words, {@link + * #getTrackSupport(int, int, int)} returns {@link RendererCapabilities#FORMAT_UNSUPPORTED_DRM}, + * {@link RendererCapabilities#FORMAT_UNSUPPORTED_SUBTYPE} or {@link + * RendererCapabilities#FORMAT_UNSUPPORTED_TYPE} for all tracks mapped to the renderer. + */ public static final int RENDERER_SUPPORT_UNSUPPORTED_TRACKS = 1; /** - * The renderer has associated tracks and at least one is of a supported type, but all of the - * tracks whose types are supported exceed the renderer's capabilities. + * The renderer has tracks mapped to it and at least one is of a supported type, but all such + * tracks exceed the renderer's capabilities. In other words, {@link #getTrackSupport(int, int, + * int)} returns {@link RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES} for at least one + * track mapped to the renderer, but does not return {@link + * RendererCapabilities#FORMAT_HANDLED} for any tracks mapped to the renderer. */ public static final int RENDERER_SUPPORT_EXCEEDS_CAPABILITIES_TRACKS = 2; - /** The renderer has associated tracks and can play at least one of them. */ + /** + * The renderer has tracks mapped to it, and at least one such track is playable. In other + * words, {@link #getTrackSupport(int, int, int)} returns {@link + * RendererCapabilities#FORMAT_HANDLED} for at least one track mapped to the renderer. + */ public static final int RENDERER_SUPPORT_PLAYABLE_TRACKS = 3; - /** - * The number of renderers to which tracks are mapped. - */ - public final int length; + /** @deprecated Use {@link #getRendererCount()}. */ + @Deprecated public final int length; + private final int rendererCount; private final int[] rendererTrackTypes; - private final TrackGroupArray[] trackGroups; - private final int[] mixedMimeTypeAdaptiveSupport; - private final int[][][] formatSupport; - private final TrackGroupArray unassociatedTrackGroups; + private final TrackGroupArray[] rendererTrackGroups; + private final int[] rendererMixedMimeTypeAdaptiveSupports; + private final int[][][] rendererFormatSupports; + private final TrackGroupArray unmappedTrackGroups; /** - * @param rendererTrackTypes The track type supported by each renderer. - * @param trackGroups The {@link TrackGroup}s mapped to each renderer. - * @param mixedMimeTypeAdaptiveSupport The result of - * {@link RendererCapabilities#supportsMixedMimeTypeAdaptation()} for each renderer. - * @param formatSupport The result of {@link RendererCapabilities#supportsFormat} for each - * mapped track, indexed by renderer index, track group index and track index (in that - * order). - * @param unassociatedTrackGroups Any {@link TrackGroup}s not mapped to any renderer. + * @param rendererTrackTypes The track type handled by each renderer. + * @param rendererTrackGroups The {@link TrackGroup}s mapped to each renderer. + * @param rendererMixedMimeTypeAdaptiveSupports The result of {@link + * RendererCapabilities#supportsMixedMimeTypeAdaptation()} for each renderer. + * @param rendererFormatSupports The result of {@link RendererCapabilities#supportsFormat} for + * each mapped track, indexed by renderer, track group and track (in that order). + * @param unmappedTrackGroups {@link TrackGroup}s not mapped to any renderer. */ - /* package */ MappedTrackInfo(int[] rendererTrackTypes, - TrackGroupArray[] trackGroups, int[] mixedMimeTypeAdaptiveSupport, - int[][][] formatSupport, TrackGroupArray unassociatedTrackGroups) { + /* package */ MappedTrackInfo( + int[] rendererTrackTypes, + TrackGroupArray[] rendererTrackGroups, + int[] rendererMixedMimeTypeAdaptiveSupports, + int[][][] rendererFormatSupports, + TrackGroupArray unmappedTrackGroups) { this.rendererTrackTypes = rendererTrackTypes; - this.trackGroups = trackGroups; - this.formatSupport = formatSupport; - this.mixedMimeTypeAdaptiveSupport = mixedMimeTypeAdaptiveSupport; - this.unassociatedTrackGroups = unassociatedTrackGroups; - this.length = trackGroups.length; + this.rendererTrackGroups = rendererTrackGroups; + this.rendererFormatSupports = rendererFormatSupports; + this.rendererMixedMimeTypeAdaptiveSupports = rendererMixedMimeTypeAdaptiveSupports; + this.unmappedTrackGroups = unmappedTrackGroups; + this.rendererCount = rendererTrackTypes.length; + this.length = rendererCount; + } + + /** Returns the number of renderers. */ + public int getRendererCount() { + return rendererCount; + } + + /** + * Returns the track type that the renderer at a given index handles. + * + * @see Renderer#getTrackType() + * @param rendererIndex The renderer index. + * @return One of the {@code TRACK_TYPE_*} constants defined in {@link C}. + */ + public int getRendererType(int rendererIndex) { + return rendererTrackTypes[rendererIndex]; } /** @@ -101,23 +131,11 @@ public abstract class MappingTrackSelector extends TrackSelector { * @return The corresponding {@link TrackGroup}s. */ public TrackGroupArray getTrackGroups(int rendererIndex) { - return trackGroups[rendererIndex]; + return rendererTrackGroups[rendererIndex]; } /** - * Returns the extent to which a renderer can play each of the tracks in the track groups mapped - * to it. - * - * @param rendererIndex The renderer index. - * @return The result of {@link RendererCapabilities#supportsFormat} for each track mapped to - * the renderer, indexed by track group and track index (in that order). - */ - public int[][] getRendererTrackSupport(int rendererIndex) { - return formatSupport[rendererIndex]; - } - - /** - * Returns the extent to which a renderer can play the tracks in the track groups mapped to it. + * Returns the extent to which a renderer can play the tracks that are mapped to it. * * @param rendererIndex The renderer index. * @return One of {@link #RENDERER_SUPPORT_PLAYABLE_TRACKS}, {@link @@ -126,7 +144,7 @@ public abstract class MappingTrackSelector extends TrackSelector { */ public @RendererSupport int getRendererSupport(int rendererIndex) { int bestRendererSupport = RENDERER_SUPPORT_NO_TRACKS; - int[][] rendererFormatSupport = formatSupport[rendererIndex]; + int[][] rendererFormatSupport = rendererFormatSupports[rendererIndex]; for (int i = 0; i < rendererFormatSupport.length; i++) { for (int j = 0; j < rendererFormatSupport[i].length; j++) { int trackRendererSupport; @@ -146,19 +164,26 @@ public abstract class MappingTrackSelector extends TrackSelector { return bestRendererSupport; } + /** @deprecated Use {@link #getTypeSupport(int)}. */ + @Deprecated + public @RendererSupport int getTrackTypeRendererSupport(int trackType) { + return getTypeSupport(trackType); + } + /** - * Returns the best level of support obtained from {@link #getRendererSupport(int)} for all - * renderers of the specified track type. If no renderers exist for the specified type then - * {@link #RENDERER_SUPPORT_NO_TRACKS} is returned. + * Returns the extent to which tracks of a specified type are supported. This is the best level + * of support obtained from {@link #getRendererSupport(int)} for all renderers that handle the + * specified type. If no such renderers exist then {@link #RENDERER_SUPPORT_NO_TRACKS} is + * returned. * * @param trackType The track type. One of the {@link C} {@code TRACK_TYPE_*} constants. - * @return One of {@link #RENDERER_SUPPORT_PLAYABLE_TRACKS}, - * {@link #RENDERER_SUPPORT_EXCEEDS_CAPABILITIES_TRACKS}, - * {@link #RENDERER_SUPPORT_UNSUPPORTED_TRACKS} and {@link #RENDERER_SUPPORT_NO_TRACKS}. + * @return One of {@link #RENDERER_SUPPORT_PLAYABLE_TRACKS}, {@link + * #RENDERER_SUPPORT_EXCEEDS_CAPABILITIES_TRACKS}, {@link + * #RENDERER_SUPPORT_UNSUPPORTED_TRACKS} and {@link #RENDERER_SUPPORT_NO_TRACKS}. */ - public int getTrackTypeRendererSupport(int trackType) { + public @RendererSupport int getTypeSupport(int trackType) { int bestRendererSupport = RENDERER_SUPPORT_NO_TRACKS; - for (int i = 0; i < length; i++) { + for (int i = 0; i < rendererCount; i++) { if (rendererTrackTypes[i] == trackType) { bestRendererSupport = Math.max(bestRendererSupport, getRendererSupport(i)); } @@ -166,53 +191,58 @@ public abstract class MappingTrackSelector extends TrackSelector { return bestRendererSupport; } + /** @deprecated Use {@link #getTrackSupport(int, int, int)}. */ + @Deprecated + public int getTrackFormatSupport(int rendererIndex, int groupIndex, int trackIndex) { + return getTrackSupport(rendererIndex, groupIndex, trackIndex); + } + /** * Returns the extent to which an individual track is supported by the renderer. * * @param rendererIndex The renderer index. * @param groupIndex The index of the track group to which the track belongs. * @param trackIndex The index of the track within the track group. - * @return One of {@link RendererCapabilities#FORMAT_HANDLED}, - * {@link RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES}, - * {@link RendererCapabilities#FORMAT_UNSUPPORTED_DRM}, - * {@link RendererCapabilities#FORMAT_UNSUPPORTED_SUBTYPE} and - * {@link RendererCapabilities#FORMAT_UNSUPPORTED_TYPE}. + * @return One of {@link RendererCapabilities#FORMAT_HANDLED}, {@link + * RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES}, {@link + * RendererCapabilities#FORMAT_UNSUPPORTED_DRM}, {@link + * RendererCapabilities#FORMAT_UNSUPPORTED_SUBTYPE} and {@link + * RendererCapabilities#FORMAT_UNSUPPORTED_TYPE}. */ - public int getTrackFormatSupport(int rendererIndex, int groupIndex, int trackIndex) { - return formatSupport[rendererIndex][groupIndex][trackIndex] + public int getTrackSupport(int rendererIndex, int groupIndex, int trackIndex) { + return rendererFormatSupports[rendererIndex][groupIndex][trackIndex] & RendererCapabilities.FORMAT_SUPPORT_MASK; } /** * Returns the extent to which a renderer supports adaptation between supported tracks in a * specified {@link TrackGroup}. - *

- * Tracks for which {@link #getTrackFormatSupport(int, int, int)} returns - * {@link RendererCapabilities#FORMAT_HANDLED} are always considered. - * Tracks for which {@link #getTrackFormatSupport(int, int, int)} returns - * {@link RendererCapabilities#FORMAT_UNSUPPORTED_DRM}, - * {@link RendererCapabilities#FORMAT_UNSUPPORTED_TYPE} or - * {@link RendererCapabilities#FORMAT_UNSUPPORTED_SUBTYPE} are never considered. - * Tracks for which {@link #getTrackFormatSupport(int, int, int)} returns - * {@link RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES} are considered only if - * {@code includeCapabilitiesExceededTracks} is set to {@code true}. + * + *

Tracks for which {@link #getTrackSupport(int, int, int)} returns {@link + * RendererCapabilities#FORMAT_HANDLED} are always considered. Tracks for which {@link + * #getTrackSupport(int, int, int)} returns {@link + * RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES} are also considered if {@code + * includeCapabilitiesExceededTracks} is set to {@code true}. Tracks for which {@link + * #getTrackSupport(int, int, int)} returns {@link RendererCapabilities#FORMAT_UNSUPPORTED_DRM}, + * {@link RendererCapabilities#FORMAT_UNSUPPORTED_TYPE} or {@link + * RendererCapabilities#FORMAT_UNSUPPORTED_SUBTYPE} are never considered. * * @param rendererIndex The renderer index. * @param groupIndex The index of the track group. - * @param includeCapabilitiesExceededTracks True if formats that exceed the capabilities of the - * renderer should be included when determining support. False otherwise. - * @return One of {@link RendererCapabilities#ADAPTIVE_SEAMLESS}, - * {@link RendererCapabilities#ADAPTIVE_NOT_SEAMLESS} and - * {@link RendererCapabilities#ADAPTIVE_NOT_SUPPORTED}. + * @param includeCapabilitiesExceededTracks Whether tracks that exceed the capabilities of the + * renderer are included when determining support. + * @return One of {@link RendererCapabilities#ADAPTIVE_SEAMLESS}, {@link + * RendererCapabilities#ADAPTIVE_NOT_SEAMLESS} and {@link + * RendererCapabilities#ADAPTIVE_NOT_SUPPORTED}. */ - public int getAdaptiveSupport(int rendererIndex, int groupIndex, - boolean includeCapabilitiesExceededTracks) { - int trackCount = trackGroups[rendererIndex].get(groupIndex).length; + public int getAdaptiveSupport( + int rendererIndex, int groupIndex, boolean includeCapabilitiesExceededTracks) { + int trackCount = rendererTrackGroups[rendererIndex].get(groupIndex).length; // Iterate over the tracks in the group, recording the indices of those to consider. int[] trackIndices = new int[trackCount]; int trackIndexCount = 0; for (int i = 0; i < trackCount; i++) { - int fixedSupport = getTrackFormatSupport(rendererIndex, groupIndex, i); + int fixedSupport = getTrackSupport(rendererIndex, groupIndex, i); if (fixedSupport == RendererCapabilities.FORMAT_HANDLED || (includeCapabilitiesExceededTracks && fixedSupport == RendererCapabilities.FORMAT_EXCEEDS_CAPABILITIES)) { @@ -224,14 +254,14 @@ public abstract class MappingTrackSelector extends TrackSelector { } /** - * Returns the extent to which a renderer supports adaptation between specified tracks within - * a {@link TrackGroup}. + * Returns the extent to which a renderer supports adaptation between specified tracks within a + * {@link TrackGroup}. * * @param rendererIndex The renderer index. * @param groupIndex The index of the track group. - * @return One of {@link RendererCapabilities#ADAPTIVE_SEAMLESS}, - * {@link RendererCapabilities#ADAPTIVE_NOT_SEAMLESS} and - * {@link RendererCapabilities#ADAPTIVE_NOT_SUPPORTED}. + * @return One of {@link RendererCapabilities#ADAPTIVE_SEAMLESS}, {@link + * RendererCapabilities#ADAPTIVE_NOT_SEAMLESS} and {@link + * RendererCapabilities#ADAPTIVE_NOT_SUPPORTED}. */ public int getAdaptiveSupport(int rendererIndex, int groupIndex, int[] trackIndices) { int handledTrackCount = 0; @@ -240,37 +270,33 @@ public abstract class MappingTrackSelector extends TrackSelector { String firstSampleMimeType = null; for (int i = 0; i < trackIndices.length; i++) { int trackIndex = trackIndices[i]; - String sampleMimeType = trackGroups[rendererIndex].get(groupIndex).getFormat(trackIndex) - .sampleMimeType; + String sampleMimeType = + rendererTrackGroups[rendererIndex].get(groupIndex).getFormat(trackIndex).sampleMimeType; if (handledTrackCount++ == 0) { firstSampleMimeType = sampleMimeType; } else { multipleMimeTypes |= !Util.areEqual(firstSampleMimeType, sampleMimeType); } - adaptiveSupport = Math.min(adaptiveSupport, formatSupport[rendererIndex][groupIndex][i] - & RendererCapabilities.ADAPTIVE_SUPPORT_MASK); + adaptiveSupport = + Math.min( + adaptiveSupport, + rendererFormatSupports[rendererIndex][groupIndex][i] + & RendererCapabilities.ADAPTIVE_SUPPORT_MASK); } return multipleMimeTypes - ? Math.min(adaptiveSupport, mixedMimeTypeAdaptiveSupport[rendererIndex]) + ? Math.min(adaptiveSupport, rendererMixedMimeTypeAdaptiveSupports[rendererIndex]) : adaptiveSupport; } - /** - * Returns {@link TrackGroup}s not mapped to any renderer. - */ + /** @deprecated Use {@link #getUnmappedTrackGroups()}. */ + @Deprecated public TrackGroupArray getUnassociatedTrackGroups() { - return unassociatedTrackGroups; + return getUnmappedTrackGroups(); } - } - - // TODO: Make DefaultTrackSelector.SelectionOverride final when this is removed. - /** @deprecated Use {@link DefaultTrackSelector.SelectionOverride} */ - @Deprecated - public static final class SelectionOverride extends DefaultTrackSelector.SelectionOverride { - - public SelectionOverride(TrackSelection.Factory factory, int groupIndex, int... tracks) { - super(factory, groupIndex, tracks); + /** Returns {@link TrackGroup}s not mapped to any renderer. */ + public TrackGroupArray getUnmappedTrackGroups() { + return unmappedTrackGroups; } } @@ -307,7 +333,8 @@ public abstract class MappingTrackSelector extends TrackSelector { } // Determine the extent to which each renderer supports mixed mimeType adaptation. - int[] mixedMimeTypeAdaptationSupport = getMixedMimeTypeAdaptationSupport(rendererCapabilities); + int[] rendererMixedMimeTypeAdaptationSupports = + getMixedMimeTypeAdaptationSupports(rendererCapabilities); // Associate each track group to a preferred renderer, and evaluate the support that the // renderer provides for each track in the group. @@ -336,30 +363,36 @@ public abstract class MappingTrackSelector extends TrackSelector { rendererTrackTypes[i] = rendererCapabilities[i].getTrackType(); } - // Create a track group array for track groups not associated with a renderer. - int unassociatedTrackGroupCount = rendererTrackGroupCounts[rendererCapabilities.length]; - TrackGroupArray unassociatedTrackGroupArray = new TrackGroupArray(Arrays.copyOf( - rendererTrackGroups[rendererCapabilities.length], unassociatedTrackGroupCount)); + // Create a track group array for track groups not mapped to a renderer. + int unmappedTrackGroupCount = rendererTrackGroupCounts[rendererCapabilities.length]; + TrackGroupArray unmappedTrackGroupArray = + new TrackGroupArray( + Arrays.copyOf( + rendererTrackGroups[rendererCapabilities.length], unmappedTrackGroupCount)); // Package up the track information and selections. MappedTrackInfo mappedTrackInfo = new MappedTrackInfo( rendererTrackTypes, rendererTrackGroupArrays, - mixedMimeTypeAdaptationSupport, + rendererMixedMimeTypeAdaptationSupports, rendererFormatSupports, - unassociatedTrackGroupArray); + unmappedTrackGroupArray); Pair result = - selectTracks(rendererCapabilities, mappedTrackInfo); + selectTracks( + mappedTrackInfo, rendererFormatSupports, rendererMixedMimeTypeAdaptationSupports); return new TrackSelectorResult(result.first, result.second, mappedTrackInfo); } /** * Given mapped track information, returns a track selection and configuration for each renderer. * - * @param rendererCapabilities The {@link RendererCapabilities} of each renderer. * @param mappedTrackInfo Mapped track information. + * @param rendererFormatSupports The result of {@link RendererCapabilities#supportsFormat} for + * each mapped track, indexed by renderer, track group and track (in that order). + * @param rendererMixedMimeTypeAdaptationSupport The result of {@link + * RendererCapabilities#supportsMixedMimeTypeAdaptation()} for each renderer. * @return A pair consisting of the track selections and configurations for each renderer. A null * configuration indicates the renderer should be disabled, in which case the track selection * will also be null. A track selection may also be null for a non-disabled renderer if {@link @@ -367,7 +400,9 @@ public abstract class MappingTrackSelector extends TrackSelector { * @throws ExoPlaybackException If an error occurs while selecting the tracks. */ protected abstract Pair selectTracks( - RendererCapabilities[] rendererCapabilities, MappedTrackInfo mappedTrackInfo) + MappedTrackInfo mappedTrackInfo, + int[][][] rendererFormatSupports, + int[] rendererMixedMimeTypeAdaptationSupport) throws ExoPlaybackException; /** @@ -436,11 +471,11 @@ public abstract class MappingTrackSelector extends TrackSelector { * returning the results in an array. * * @param rendererCapabilities The {@link RendererCapabilities} of the renderers. - * @return An array containing the result of calling - * {@link RendererCapabilities#supportsMixedMimeTypeAdaptation()} for each renderer. + * @return An array containing the result of calling {@link + * RendererCapabilities#supportsMixedMimeTypeAdaptation()} for each renderer. * @throws ExoPlaybackException If an error occurs determining the adaptation support. */ - private static int[] getMixedMimeTypeAdaptationSupport( + private static int[] getMixedMimeTypeAdaptationSupports( RendererCapabilities[] rendererCapabilities) throws ExoPlaybackException { int[] mixedMimeTypeAdaptationSupport = new int[rendererCapabilities.length]; for (int i = 0; i < mixedMimeTypeAdaptationSupport.length; i++) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSink.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSink.java index 2f49fed5a8..4973bb71e8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSink.java @@ -37,6 +37,9 @@ public interface DataSink { /** * Opens the sink to consume the specified data. * + *

Note: If an {@link IOException} is thrown, callers must still call {@link #close()} to + * ensure that any partial effects of the invocation are cleaned up. + * * @param dataSpec Defines the data to be consumed. * @throws IOException If an error occurs opening the sink. */ @@ -55,8 +58,10 @@ public interface DataSink { /** * Closes the sink. * + *

Note: This method must be called even if the corresponding call to {@link #open(DataSpec)} + * threw an {@link IOException}. See {@link #open(DataSpec)} for more details. + * * @throws IOException If an error occurs closing the sink. */ void close() throws IOException; - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java index d45b7182c4..a47a5b5348 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java @@ -526,7 +526,7 @@ public class DefaultHttpDataSource implements HttpDataSource { while (bytesSkipped != bytesToSkip) { int readLength = (int) Math.min(bytesToSkip - bytesSkipped, skipBuffer.length); int read = inputStream.read(skipBuffer, 0, readLength); - if (Thread.interrupted()) { + if (Thread.currentThread().isInterrupted()) { throw new InterruptedIOException(); } if (read == -1) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/ParsingLoadable.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/ParsingLoadable.java index a9927f6e86..7ef79b8963 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/ParsingLoadable.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/ParsingLoadable.java @@ -48,6 +48,22 @@ public final class ParsingLoadable implements Loadable { } + /** + * Loads a single parsable object. + * + * @param dataSource The {@link DataSource} through which the object should be read. + * @param uri The {@link Uri} of the object to read. + * @return The parsed object + * @throws IOException Thrown if there is an error while loading or parsing. + */ + public static T load(DataSource dataSource, Parser parser, Uri uri) + throws IOException { + ParsingLoadable loadable = + new ParsingLoadable<>(dataSource, uri, C.DATA_TYPE_UNKNOWN, parser); + loadable.load(); + return loadable.getResult(); + } + /** * The {@link DataSpec} that defines the data to be loaded. */ diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/TeeDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/TeeDataSource.java index 3f76ee59d5..6fcb08e6b5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/TeeDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/TeeDataSource.java @@ -28,6 +28,9 @@ public final class TeeDataSource implements DataSource { private final DataSource upstream; private final DataSink dataSink; + private boolean dataSinkNeedsClosing; + private long bytesRemaining; + /** * @param upstream The upstream {@link DataSource}. * @param dataSink The {@link DataSink} into which data is written. @@ -39,24 +42,40 @@ public final class TeeDataSource implements DataSource { @Override public long open(DataSpec dataSpec) throws IOException { - long dataLength = upstream.open(dataSpec); - if (dataSpec.length == C.LENGTH_UNSET && dataLength != C.LENGTH_UNSET) { - // Reconstruct dataSpec in order to provide the resolved length to the sink. - dataSpec = new DataSpec(dataSpec.uri, dataSpec.absoluteStreamPosition, dataSpec.position, - dataLength, dataSpec.key, dataSpec.flags); + bytesRemaining = upstream.open(dataSpec); + if (bytesRemaining == 0) { + return 0; } + if (dataSpec.length == C.LENGTH_UNSET && bytesRemaining != C.LENGTH_UNSET) { + // Reconstruct dataSpec in order to provide the resolved length to the sink. + dataSpec = + new DataSpec( + dataSpec.uri, + dataSpec.absoluteStreamPosition, + dataSpec.position, + bytesRemaining, + dataSpec.key, + dataSpec.flags); + } + dataSinkNeedsClosing = true; dataSink.open(dataSpec); - return dataLength; + return bytesRemaining; } @Override public int read(byte[] buffer, int offset, int max) throws IOException { - int num = upstream.read(buffer, offset, max); - if (num > 0) { - // TODO: Consider continuing even if disk writes fail. - dataSink.write(buffer, offset, num); + if (bytesRemaining == 0) { + return C.RESULT_END_OF_INPUT; } - return num; + int bytesRead = upstream.read(buffer, offset, max); + if (bytesRead > 0) { + // TODO: Consider continuing even if writes to the sink fail. + dataSink.write(buffer, offset, bytesRead); + if (bytesRemaining != C.LENGTH_UNSET) { + bytesRemaining -= bytesRead; + } + } + return bytesRead; } @Override @@ -69,7 +88,10 @@ public final class TeeDataSource implements DataSource { try { upstream.close(); } finally { - dataSink.close(); + if (dataSinkNeedsClosing) { + dataSinkNeedsClosing = false; + dataSink.close(); + } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java index 6de48cb9d2..045fc25338 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java @@ -18,7 +18,6 @@ package com.google.android.exoplayer2.upstream.cache; import android.net.Uri; import android.support.annotation.IntDef; import android.support.annotation.Nullable; -import android.util.Log; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.upstream.DataSink; import com.google.android.exoplayer2.upstream.DataSource; @@ -52,8 +51,6 @@ public final class CacheDataSource implements DataSource { */ public static final long DEFAULT_MAX_CACHE_FILE_SIZE = 2 * 1024 * 1024; - private static final String TAG = "CacheDataSource"; - /** * Flags controlling the cache's behavior. */ @@ -85,10 +82,13 @@ public final class CacheDataSource implements DataSource { @IntDef({CACHE_IGNORED_REASON_ERROR, CACHE_IGNORED_REASON_UNSET_LENGTH}) public @interface CacheIgnoredReason {} - /** Cache ignored due to a cache related error */ + /** Cache not ignored. */ + private static final int CACHE_NOT_IGNORED = -1; + + /** Cache ignored due to a cache related error. */ public static final int CACHE_IGNORED_REASON_ERROR = 0; - /** Cache ignored due to a request with an unset length */ + /** Cache ignored due to a request with an unset length. */ public static final int CACHE_IGNORED_REASON_UNSET_LENGTH = 1; /** @@ -218,11 +218,16 @@ public final class CacheDataSource implements DataSource { try { key = CacheUtil.getKey(dataSpec); uri = dataSpec.uri; - actualUri = loadRedirectedUriOrReturnGivenUri(cache, key, uri); + actualUri = getRedirectedUriOrDefault(cache, key, /* defaultUri= */ uri); flags = dataSpec.flags; readPosition = dataSpec.position; - currentRequestIgnoresCache = (ignoreCacheOnError && seenCacheError) - || (dataSpec.length == C.LENGTH_UNSET && ignoreCacheForUnsetLengthRequests); + + int reason = shouldIgnoreCacheForRequest(dataSpec); + currentRequestIgnoresCache = reason != CACHE_NOT_IGNORED; + if (currentRequestIgnoresCache) { + notifyCacheIgnored(reason); + } + if (dataSpec.length != C.LENGTH_UNSET || currentRequestIgnoresCache) { bytesRemaining = dataSpec.length; } else { @@ -264,7 +269,7 @@ public final class CacheDataSource implements DataSource { bytesRemaining -= bytesRead; } } else if (currentDataSpecLengthUnset) { - setBytesRemainingAndMaybeStoreLength(0); + setNoBytesRemainingAndMaybeStoreLength(); } else if (bytesRemaining > 0 || bytesRemaining == C.LENGTH_UNSET) { closeCurrentSource(); openNextSource(false); @@ -273,7 +278,7 @@ public final class CacheDataSource implements DataSource { return bytesRead; } catch (IOException e) { if (currentDataSpecLengthUnset && isCausedByPositionOutOfRange(e)) { - setBytesRemainingAndMaybeStoreLength(0); + setNoBytesRemainingAndMaybeStoreLength(); return C.RESULT_END_OF_INPUT; } handleBeforeThrow(e); @@ -317,17 +322,11 @@ public final class CacheDataSource implements DataSource { CacheSpan nextSpan; if (currentRequestIgnoresCache) { nextSpan = null; - if (eventListener != null) { - int reason = - ignoreCacheOnError && seenCacheError - ? CACHE_IGNORED_REASON_ERROR - : CACHE_IGNORED_REASON_UNSET_LENGTH; - eventListener.onCacheIgnored(reason); - } } else if (blockOnCache) { try { nextSpan = cache.startReadWrite(key, readPosition); } catch (InterruptedException e) { + Thread.currentThread().interrupt(); throw new InterruptedIOException(); } } else { @@ -400,46 +399,38 @@ public final class CacheDataSource implements DataSource { currentDataSource = nextDataSource; currentDataSpecLengthUnset = nextDataSpec.length == C.LENGTH_UNSET; long resolvedLength = nextDataSource.open(nextDataSpec); - if (currentDataSpecLengthUnset && resolvedLength != C.LENGTH_UNSET) { - setBytesRemainingAndMaybeStoreLength(resolvedLength); - } - // TODO find a way to store length and redirected uri in one metadata mutation. - maybeUpdateActualUriFieldAndRedirectedUriMetadata(); - } - private void maybeUpdateActualUriFieldAndRedirectedUriMetadata() { - if (!isReadingFromUpstream()) { - return; - } - actualUri = currentDataSource.getUri(); - maybeUpdateRedirectedUriMetadata(); - } - - private void maybeUpdateRedirectedUriMetadata() { - if (!isWritingToCache()) { - return; - } + // Update bytesRemaining, actualUri and (if writing to cache) the cache metadata. ContentMetadataMutations mutations = new ContentMetadataMutations(); - boolean isRedirected = !uri.equals(actualUri); - if (isRedirected) { - ContentMetadataInternal.setRedirectedUri(mutations, actualUri); - } else { - ContentMetadataInternal.removeRedirectedUri(mutations); + if (currentDataSpecLengthUnset && resolvedLength != C.LENGTH_UNSET) { + bytesRemaining = resolvedLength; + ContentMetadataInternal.setContentLength(mutations, readPosition + bytesRemaining); } - try { + if (isReadingFromUpstream()) { + actualUri = currentDataSource.getUri(); + boolean isRedirected = !uri.equals(actualUri); + if (isRedirected) { + ContentMetadataInternal.setRedirectedUri(mutations, actualUri); + } else { + ContentMetadataInternal.removeRedirectedUri(mutations); + } + } + if (isWritingToCache()) { cache.applyContentMetadataMutations(key, mutations); - } catch (CacheException e) { - String message = - "Couldn't update redirected URI. " - + "This might cause relative URIs get resolved incorrectly."; - Log.w(TAG, message, e); } } - private static Uri loadRedirectedUriOrReturnGivenUri(Cache cache, String key, Uri uri) { + private void setNoBytesRemainingAndMaybeStoreLength() throws IOException { + bytesRemaining = 0; + if (isWritingToCache()) { + cache.setContentLength(key, readPosition); + } + } + + private static Uri getRedirectedUriOrDefault(Cache cache, String key, Uri defaultUri) { ContentMetadata contentMetadata = cache.getContentMetadata(key); Uri redirectedUri = ContentMetadataInternal.getRedirectedUri(contentMetadata); - return redirectedUri == null ? uri : redirectedUri; + return redirectedUri == null ? defaultUri : redirectedUri; } private static boolean isCausedByPositionOutOfRange(IOException e) { @@ -456,13 +447,6 @@ public final class CacheDataSource implements DataSource { return false; } - private void setBytesRemainingAndMaybeStoreLength(long bytesRemaining) throws IOException { - this.bytesRemaining = bytesRemaining; - if (isWritingToCache()) { - cache.setContentLength(key, readPosition + bytesRemaining); - } - } - private boolean isReadingFromUpstream() { return !isReadingFromCache(); } @@ -501,6 +485,22 @@ public final class CacheDataSource implements DataSource { } } + private int shouldIgnoreCacheForRequest(DataSpec dataSpec) { + if (ignoreCacheOnError && seenCacheError) { + return CACHE_IGNORED_REASON_ERROR; + } else if (ignoreCacheForUnsetLengthRequests && dataSpec.length == C.LENGTH_UNSET) { + return CACHE_IGNORED_REASON_UNSET_LENGTH; + } else { + return CACHE_NOT_IGNORED; + } + } + + private void notifyCacheIgnored(@CacheIgnoredReason int reason) { + if (eventListener != null) { + eventListener.onCacheIgnored(reason); + } + } + private void notifyBytesRead() { if (eventListener != null && totalCachedBytesRead > 0) { eventListener.onCachedBytesRead(cache.getCacheSpace(), totalCachedBytesRead); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheUtil.java index 22150f8e78..a1f7aa3097 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheUtil.java @@ -26,6 +26,7 @@ import com.google.android.exoplayer2.util.Util; import java.io.EOFException; import java.io.IOException; import java.util.NavigableSet; +import java.util.concurrent.atomic.AtomicBoolean; /** * Caching related utility methods. @@ -112,14 +113,27 @@ public final class CacheUtil { * @param cache A {@link Cache} to store the data. * @param upstream A {@link DataSource} for reading data not in the cache. * @param counters If not null, updated during caching. + * @param isCanceled An optional flag that will interrupt caching if set to true. * @throws IOException If an error occurs reading from the source. - * @throws InterruptedException If the thread was interrupted. + * @throws InterruptedException If the thread was interrupted directly or via {@code isCanceled}. */ public static void cache( - DataSpec dataSpec, Cache cache, DataSource upstream, @Nullable CachingCounters counters) + DataSpec dataSpec, + Cache cache, + DataSource upstream, + @Nullable CachingCounters counters, + @Nullable AtomicBoolean isCanceled) throws IOException, InterruptedException { - cache(dataSpec, cache, new CacheDataSource(cache, upstream), - new byte[DEFAULT_BUFFER_SIZE_BYTES], null, 0, counters, false); + cache( + dataSpec, + cache, + new CacheDataSource(cache, upstream), + new byte[DEFAULT_BUFFER_SIZE_BYTES], + null, + 0, + counters, + null, + false); } /** @@ -140,10 +154,11 @@ public final class CacheUtil { * caching. * @param priority The priority of this task. Used with {@code priorityTaskManager}. * @param counters If not null, updated during caching. + * @param isCanceled An optional flag that will interrupt caching if set to true. * @param enableEOFException Whether to throw an {@link EOFException} if end of input has been * reached unexpectedly. * @throws IOException If an error occurs reading from the source. - * @throws InterruptedException If the thread was interrupted. + * @throws InterruptedException If the thread was interrupted directly or via {@code isCanceled}. */ public static void cache( DataSpec dataSpec, @@ -153,6 +168,7 @@ public final class CacheUtil { PriorityTaskManager priorityTaskManager, int priority, @Nullable CachingCounters counters, + @Nullable AtomicBoolean isCanceled, boolean enableEOFException) throws IOException, InterruptedException { Assertions.checkNotNull(dataSource); @@ -170,6 +186,9 @@ public final class CacheUtil { long start = dataSpec.absoluteStreamPosition; long left = dataSpec.length != C.LENGTH_UNSET ? dataSpec.length : cache.getContentLength(key); while (left != 0) { + if (isCanceled != null && isCanceled.get()) { + throw new InterruptedException(); + } long blockLength = cache.getCachedLength(key, start, left != C.LENGTH_UNSET ? left : Long.MAX_VALUE); if (blockLength > 0) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java index ca0063bf86..7d2d5b79a9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java @@ -37,6 +37,8 @@ public final class SimpleCache implements Cache { private static final String TAG = "SimpleCache"; private static final HashSet lockedCacheDirs = new HashSet<>(); + private static boolean cacheFolderLockingDisabled; + private final File cacheDir; private final CacheEvictor evictor; private final CachedContentIndex index; @@ -45,6 +47,31 @@ public final class SimpleCache implements Cache { private long totalSpace; private boolean released; + /** + * Returns whether {@code cacheFolder} is locked by a {@link SimpleCache} instance. To unlock the + * folder the {@link SimpleCache} instance should be released. + */ + public static synchronized boolean isCacheFolderLocked(File cacheFolder) { + return lockedCacheDirs.contains(cacheFolder.getAbsoluteFile()); + } + + /** + * Disables locking the cache folders which {@link SimpleCache} instances are using and releases + * any previous lock. + * + *

The locking prevents multiple {@link SimpleCache} instances from being created for the same + * folder. Disabling it may cause the cache data to be corrupted. Use at your own risk. + * + * @deprecated Don't create multiple {@link SimpleCache} instances for the same cache folder. If + * you need to create another instance, make sure you call {@link #release()} on the previous + * instance. + */ + @Deprecated + public static synchronized void disableCacheFolderLocking() { + cacheFolderLockingDisabled = true; + lockedCacheDirs.clear(); + } + /** * Constructs the cache. The cache will delete any unrecognized files from the directory. Hence * the directory cannot be used to store other files. @@ -93,6 +120,10 @@ public final class SimpleCache implements Cache { * @param index The CachedContentIndex to be used. */ /*package*/ SimpleCache(File cacheDir, CacheEvictor evictor, CachedContentIndex index) { + if (!lockFolder(cacheDir)) { + throw new IllegalStateException("Another SimpleCache instance uses the folder: " + cacheDir); + } + this.cacheDir = cacheDir; this.evictor = evictor; this.index = index; @@ -122,7 +153,7 @@ public final class SimpleCache implements Cache { try { removeStaleSpansAndCachedContents(); } finally { - releaseFolder(cacheDir); + unlockFolder(cacheDir); released = true; } } @@ -461,10 +492,15 @@ public final class SimpleCache implements Cache { } private static synchronized boolean lockFolder(File cacheDir) { + if (cacheFolderLockingDisabled) { + return true; + } return lockedCacheDirs.add(cacheDir.getAbsoluteFile()); } - private static synchronized void releaseFolder(File cacheDir) { - lockedCacheDirs.remove(cacheDir.getAbsoluteFile()); + private static synchronized void unlockFolder(File cacheDir) { + if (!cacheFolderLockingDisabled) { + lockedCacheDirs.remove(cacheDir.getAbsoluteFile()); + } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/Assertions.java b/library/core/src/main/java/com/google/android/exoplayer2/util/Assertions.java index aee46eea0e..53c196a14f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/Assertions.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/Assertions.java @@ -16,8 +16,10 @@ package com.google.android.exoplayer2.util; import android.os.Looper; +import android.support.annotation.Nullable; import android.text.TextUtils; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; +import org.checkerframework.checker.nullness.qual.EnsuresNonNull; /** * Provides methods for asserting the truth of expressions and properties. @@ -102,7 +104,8 @@ public final class Assertions { * @return The non-null reference that was validated. * @throws NullPointerException If {@code reference} is null. */ - public static T checkNotNull(T reference) { + @EnsuresNonNull({"#1"}) + public static T checkNotNull(@Nullable T reference) { if (ExoPlayerLibraryInfo.ASSERTIONS_ENABLED && reference == null) { throw new NullPointerException(); } @@ -119,7 +122,8 @@ public final class Assertions { * @return The non-null reference that was validated. * @throws NullPointerException If {@code reference} is null. */ - public static T checkNotNull(T reference, Object errorMessage) { + @EnsuresNonNull({"#1"}) + public static T checkNotNull(@Nullable T reference, Object errorMessage) { if (ExoPlayerLibraryInfo.ASSERTIONS_ENABLED && reference == null) { throw new NullPointerException(String.valueOf(errorMessage)); } @@ -133,7 +137,8 @@ public final class Assertions { * @return The non-null, non-empty string that was validated. * @throws IllegalArgumentException If {@code string} is null or 0-length. */ - public static String checkNotEmpty(String string) { + @EnsuresNonNull({"#1"}) + public static String checkNotEmpty(@Nullable String string) { if (ExoPlayerLibraryInfo.ASSERTIONS_ENABLED && TextUtils.isEmpty(string)) { throw new IllegalArgumentException(); } @@ -149,7 +154,8 @@ public final class Assertions { * @return The non-null, non-empty string that was validated. * @throws IllegalArgumentException If {@code string} is null or 0-length. */ - public static String checkNotEmpty(String string, Object errorMessage) { + @EnsuresNonNull({"#1"}) + public static String checkNotEmpty(@Nullable String string, Object errorMessage) { if (ExoPlayerLibraryInfo.ASSERTIONS_ENABLED && TextUtils.isEmpty(string)) { throw new IllegalArgumentException(String.valueOf(errorMessage)); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/EventLogger.java b/library/core/src/main/java/com/google/android/exoplayer2/util/EventLogger.java index 010e60830a..deb09f8074 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/EventLogger.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/EventLogger.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.util; +import android.net.NetworkInfo; import android.os.SystemClock; import android.support.annotation.Nullable; import android.util.Log; @@ -26,34 +27,23 @@ import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.audio.AudioRendererEventListener; +import com.google.android.exoplayer2.analytics.AnalyticsListener; import com.google.android.exoplayer2.decoder.DecoderCounters; -import com.google.android.exoplayer2.drm.DefaultDrmSessionEventListener; import com.google.android.exoplayer2.metadata.Metadata; -import com.google.android.exoplayer2.metadata.MetadataOutput; -import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; -import com.google.android.exoplayer2.source.MediaSourceEventListener; +import com.google.android.exoplayer2.source.MediaSourceEventListener.LoadEventInfo; +import com.google.android.exoplayer2.source.MediaSourceEventListener.MediaLoadData; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; -import com.google.android.exoplayer2.source.ads.AdsMediaSource; import com.google.android.exoplayer2.trackselection.MappingTrackSelector; import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; -import com.google.android.exoplayer2.video.VideoRendererEventListener; import java.io.IOException; import java.text.NumberFormat; import java.util.Locale; /** Logs events from {@link Player} and other core components using {@link Log}. */ -public class EventLogger - implements Player.EventListener, - MetadataOutput, - AudioRendererEventListener, - VideoRendererEventListener, - MediaSourceEventListener, - AdsMediaSource.EventListener, - DefaultDrmSessionEventListener { +public class EventLogger implements AnalyticsListener { private static final String TAG = "EventLogger"; private static final int MAX_TIMELINE_ITEM_LINES = 3; @@ -65,81 +55,89 @@ public class EventLogger TIME_FORMAT.setGroupingUsed(false); } - private final MappingTrackSelector trackSelector; + private final @Nullable MappingTrackSelector trackSelector; private final Timeline.Window window; private final Timeline.Period period; private final long startTimeMs; - public EventLogger(MappingTrackSelector trackSelector) { + /** + * Creates event logger. + * + * @param trackSelector The mapping track selector used by the player. May be null if detailed + * logging of track mapping is not required. + */ + public EventLogger(@Nullable MappingTrackSelector trackSelector) { this.trackSelector = trackSelector; window = new Timeline.Window(); period = new Timeline.Period(); startTimeMs = SystemClock.elapsedRealtime(); } - // Player.EventListener + // AnalyticsListener @Override - public void onLoadingChanged(boolean isLoading) { - logd("loading [" + isLoading + "]"); + public void onLoadingChanged(EventTime eventTime, boolean isLoading) { + logd(eventTime, "loading", Boolean.toString(isLoading)); } @Override - public void onPlayerStateChanged(boolean playWhenReady, int state) { - logd( - "state [" - + getSessionTimeString() - + ", " - + playWhenReady - + ", " - + getStateString(state) - + "]"); - } - - @Override - public void onRepeatModeChanged(@Player.RepeatMode int repeatMode) { - logd("repeatMode [" + getRepeatModeString(repeatMode) + "]"); - } - - @Override - public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { - logd("shuffleModeEnabled [" + shuffleModeEnabled + "]"); - } - - @Override - public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { - logd("positionDiscontinuity [" + getDiscontinuityReasonString(reason) + "]"); - } - - @Override - public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { + public void onPlayerStateChanged(EventTime eventTime, boolean playWhenReady, int state) { + logd(eventTime, "state", playWhenReady + ", " + getStateString(state)); + } + + @Override + public void onRepeatModeChanged(EventTime eventTime, @Player.RepeatMode int repeatMode) { + logd(eventTime, "repeatMode", getRepeatModeString(repeatMode)); + } + + @Override + public void onShuffleModeChanged(EventTime eventTime, boolean shuffleModeEnabled) { + logd(eventTime, "shuffleModeEnabled", Boolean.toString(shuffleModeEnabled)); + } + + @Override + public void onPositionDiscontinuity(EventTime eventTime, @Player.DiscontinuityReason int reason) { + logd(eventTime, "positionDiscontinuity", getDiscontinuityReasonString(reason)); + } + + @Override + public void onSeekStarted(EventTime eventTime) { + logd(eventTime, "seekStarted"); + } + + @Override + public void onPlaybackParametersChanged( + EventTime eventTime, PlaybackParameters playbackParameters) { logd( + eventTime, + "playbackParameters", Util.formatInvariant( - "playbackParameters [speed=%.2f, pitch=%.2f, skipSilence=%s]", + "speed=%.2f, pitch=%.2f, skipSilence=%s", playbackParameters.speed, playbackParameters.pitch, playbackParameters.skipSilence)); } @Override - public void onTimelineChanged( - Timeline timeline, Object manifest, @Player.TimelineChangeReason int reason) { - int periodCount = timeline.getPeriodCount(); - int windowCount = timeline.getWindowCount(); + public void onTimelineChanged(EventTime eventTime, @Player.TimelineChangeReason int reason) { + int periodCount = eventTime.timeline.getPeriodCount(); + int windowCount = eventTime.timeline.getWindowCount(); logd( - "timelineChanged [periodCount=" + "timelineChanged [" + + getEventTimeString(eventTime) + + ", periodCount=" + periodCount + ", windowCount=" + windowCount + ", reason=" + getTimelineChangeReasonString(reason)); for (int i = 0; i < Math.min(periodCount, MAX_TIMELINE_ITEM_LINES); i++) { - timeline.getPeriod(i, period); + eventTime.timeline.getPeriod(i, period); logd(" " + "period [" + getTimeString(period.getDurationMs()) + "]"); } if (periodCount > MAX_TIMELINE_ITEM_LINES) { logd(" ..."); } for (int i = 0; i < Math.min(windowCount, MAX_TIMELINE_ITEM_LINES); i++) { - timeline.getWindow(i, window); + eventTime.timeline.getWindow(i, window); logd( " " + "window [" @@ -157,20 +155,23 @@ public class EventLogger } @Override - public void onPlayerError(ExoPlaybackException e) { - loge("playerFailed [" + getSessionTimeString() + "]", e); + public void onPlayerError(EventTime eventTime, ExoPlaybackException e) { + loge(eventTime, "playerFailed", e); } @Override - public void onTracksChanged(TrackGroupArray ignored, TrackSelectionArray trackSelections) { - MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo(); + public void onTracksChanged( + EventTime eventTime, TrackGroupArray ignored, TrackSelectionArray trackSelections) { + MappedTrackInfo mappedTrackInfo = + trackSelector != null ? trackSelector.getCurrentMappedTrackInfo() : null; if (mappedTrackInfo == null) { - logd("Tracks []"); + logd(eventTime, "tracksChanged", "[]"); return; } - logd("Tracks ["); + logd("tracksChanged [" + getEventTimeString(eventTime) + ", "); // Log tracks associated to renderers. - for (int rendererIndex = 0; rendererIndex < mappedTrackInfo.length; rendererIndex++) { + int rendererCount = mappedTrackInfo.getRendererCount(); + for (int rendererIndex = 0; rendererIndex < rendererCount; rendererIndex++) { TrackGroupArray rendererTrackGroups = mappedTrackInfo.getTrackGroups(rendererIndex); TrackSelection trackSelection = trackSelections.get(rendererIndex); if (rendererTrackGroups.length > 0) { @@ -186,7 +187,7 @@ public class EventLogger String status = getTrackStatusString(trackSelection, trackGroup, trackIndex); String formatSupport = getFormatSupportString( - mappedTrackInfo.getTrackFormatSupport(rendererIndex, groupIndex, trackIndex)); + mappedTrackInfo.getTrackSupport(rendererIndex, groupIndex, trackIndex)); logd( " " + status @@ -215,7 +216,7 @@ public class EventLogger } } // Log tracks not associated with a renderer. - TrackGroupArray unassociatedTrackGroups = mappedTrackInfo.getUnassociatedTrackGroups(); + TrackGroupArray unassociatedTrackGroups = mappedTrackInfo.getUnmappedTrackGroups(); if (unassociatedTrackGroups.length > 0) { logd(" Renderer:None ["); for (int groupIndex = 0; groupIndex < unassociatedTrackGroups.length; groupIndex++) { @@ -243,201 +244,163 @@ public class EventLogger } @Override - public void onSeekProcessed() { - logd("seekProcessed"); + public void onSeekProcessed(EventTime eventTime) { + logd(eventTime, "seekProcessed"); } - // MetadataOutput - @Override - public void onMetadata(Metadata metadata) { - logd("onMetadata ["); + public void onMetadata(EventTime eventTime, Metadata metadata) { + logd("metadata [" + getEventTimeString(eventTime) + ", "); printMetadata(metadata, " "); logd("]"); } - // AudioRendererEventListener - @Override - public void onAudioEnabled(DecoderCounters counters) { - logd("audioEnabled [" + getSessionTimeString() + "]"); + public void onDecoderEnabled(EventTime eventTime, int trackType, DecoderCounters counters) { + logd(eventTime, "decoderEnabled", getTrackTypeString(trackType)); } @Override - public void onAudioSessionId(int audioSessionId) { - logd("audioSessionId [" + audioSessionId + "]"); + public void onAudioSessionId(EventTime eventTime, int audioSessionId) { + logd(eventTime, "audioSessionId", Integer.toString(audioSessionId)); } @Override - public void onAudioDecoderInitialized( - String decoderName, long elapsedRealtimeMs, long initializationDurationMs) { - logd("audioDecoderInitialized [" + getSessionTimeString() + ", " + decoderName + "]"); + public void onDecoderInitialized( + EventTime eventTime, int trackType, String decoderName, long initializationDurationMs) { + logd(eventTime, "decoderInitialized", getTrackTypeString(trackType) + ", " + decoderName); } @Override - public void onAudioInputFormatChanged(Format format) { - logd("audioFormatChanged [" + getSessionTimeString() + ", " + Format.toLogString(format) + "]"); + public void onDecoderInputFormatChanged(EventTime eventTime, int trackType, Format format) { + logd( + eventTime, + "decoderInputFormatChanged", + getTrackTypeString(trackType) + ", " + Format.toLogString(format)); } @Override - public void onAudioDisabled(DecoderCounters counters) { - logd("audioDisabled [" + getSessionTimeString() + "]"); + public void onDecoderDisabled(EventTime eventTime, int trackType, DecoderCounters counters) { + logd(eventTime, "decoderDisabled", getTrackTypeString(trackType)); } @Override - public void onAudioSinkUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) { - printInternalError("audioTrackUnderrun [" + bufferSize + ", " + bufferSizeMs + ", " - + elapsedSinceLastFeedMs + "]", null); - } - - // VideoRendererEventListener - - @Override - public void onVideoEnabled(DecoderCounters counters) { - logd("videoEnabled [" + getSessionTimeString() + "]"); + public void onAudioUnderrun( + EventTime eventTime, int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) { + loge( + eventTime, + "audioTrackUnderrun", + bufferSize + ", " + bufferSizeMs + ", " + elapsedSinceLastFeedMs + "]", + null); } @Override - public void onVideoDecoderInitialized( - String decoderName, long elapsedRealtimeMs, long initializationDurationMs) { - logd("videoDecoderInitialized [" + getSessionTimeString() + ", " + decoderName + "]"); - } - - @Override - public void onVideoInputFormatChanged(Format format) { - logd("videoFormatChanged [" + getSessionTimeString() + ", " + Format.toLogString(format) + "]"); - } - - @Override - public void onVideoDisabled(DecoderCounters counters) { - logd("videoDisabled [" + getSessionTimeString() + "]"); - } - - @Override - public void onDroppedFrames(int count, long elapsed) { - logd("droppedFrames [" + getSessionTimeString() + ", " + count + "]"); + public void onDroppedVideoFrames(EventTime eventTime, int count, long elapsedMs) { + logd(eventTime, "droppedFrames", Integer.toString(count)); } @Override public void onVideoSizeChanged( - int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { - logd("videoSizeChanged [" + width + ", " + height + "]"); + EventTime eventTime, + int width, + int height, + int unappliedRotationDegrees, + float pixelWidthHeightRatio) { + logd(eventTime, "videoSizeChanged", width + ", " + height); } @Override - public void onRenderedFirstFrame(Surface surface) { - logd("renderedFirstFrame [" + surface + "]"); - } - - // DefaultDrmSessionManager.EventListener - - @Override - public void onDrmSessionManagerError(Exception e) { - printInternalError("drmSessionManagerError", e); + public void onRenderedFirstFrame(EventTime eventTime, Surface surface) { + logd(eventTime, "renderedFirstFrame", surface.toString()); } @Override - public void onDrmKeysRestored() { - logd("drmKeysRestored [" + getSessionTimeString() + "]"); + public void onMediaPeriodCreated(EventTime eventTime) { + logd(eventTime, "mediaPeriodCreated"); } @Override - public void onDrmKeysRemoved() { - logd("drmKeysRemoved [" + getSessionTimeString() + "]"); - } - - @Override - public void onDrmKeysLoaded() { - logd("drmKeysLoaded [" + getSessionTimeString() + "]"); - } - - // MediaSourceEventListener - - @Override - public void onMediaPeriodCreated(int windowIndex, MediaPeriodId mediaPeriodId) { - // Do nothing. - } - - @Override - public void onMediaPeriodReleased(int windowIndex, MediaPeriodId mediaPeriodId) { - // Do nothing. + public void onMediaPeriodReleased(EventTime eventTime) { + logd(eventTime, "mediaPeriodReleased"); } @Override public void onLoadStarted( - int windowIndex, - @Nullable MediaPeriodId mediaPeriodId, - LoadEventInfo loadEventInfo, - MediaLoadData mediaLoadData) { + EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) { // Do nothing. } @Override public void onLoadError( - int windowIndex, - @Nullable MediaPeriodId mediaPeriodId, + EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData, IOException error, boolean wasCanceled) { - printInternalError("loadError", error); + printInternalError(eventTime, "loadError", error); } @Override public void onLoadCanceled( - int windowIndex, - @Nullable MediaPeriodId mediaPeriodId, - LoadEventInfo loadEventInfo, - MediaLoadData mediaLoadData) { + EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) { // Do nothing. } @Override public void onLoadCompleted( - int windowIndex, - @Nullable MediaPeriodId mediaPeriodId, - LoadEventInfo loadEventInfo, - MediaLoadData mediaLoadData) { + EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) { // Do nothing. } @Override - public void onReadingStarted(int windowIndex, MediaPeriodId mediaPeriodId) { + public void onReadingStarted(EventTime eventTime) { + logd(eventTime, "mediaPeriodReadingStarted"); + } + + @Override + public void onBandwidthEstimate( + EventTime eventTime, int totalLoadTimeMs, long totalBytesLoaded, long bitrateEstimate) { // Do nothing. } @Override - public void onUpstreamDiscarded( - int windowIndex, @Nullable MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData) { - // Do nothing. + public void onViewportSizeChange(EventTime eventTime, int width, int height) { + logd(eventTime, "viewportSizeChanged", width + ", " + height); } @Override - public void onDownstreamFormatChanged( - int windowIndex, @Nullable MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData) { - // Do nothing. - } - - // AdsMediaSource.EventListener - - @Override - public void onAdLoadError(IOException error) { - printInternalError("adLoadError", error); + public void onNetworkTypeChanged(EventTime eventTime, @Nullable NetworkInfo networkInfo) { + logd(eventTime, "networkTypeChanged", networkInfo == null ? "none" : networkInfo.toString()); } @Override - public void onInternalAdLoadError(RuntimeException error) { - printInternalError("internalAdLoadError", error); + public void onUpstreamDiscarded(EventTime eventTime, MediaLoadData mediaLoadData) { + logd(eventTime, "upstreamDiscarded", Format.toLogString(mediaLoadData.trackFormat)); } @Override - public void onAdClicked() { - // Do nothing. + public void onDownstreamFormatChanged(EventTime eventTime, MediaLoadData mediaLoadData) { + logd(eventTime, "downstreamFormatChanged", Format.toLogString(mediaLoadData.trackFormat)); } @Override - public void onAdTapped() { - // Do nothing. + public void onDrmSessionManagerError(EventTime eventTime, Exception e) { + printInternalError(eventTime, "drmSessionManagerError", e); + } + + @Override + public void onDrmKeysRestored(EventTime eventTime) { + logd(eventTime, "drmKeysRestored"); + } + + @Override + public void onDrmKeysRemoved(EventTime eventTime) { + logd(eventTime, "drmKeysRemoved"); + } + + @Override + public void onDrmKeysLoaded(EventTime eventTime) { + logd(eventTime, "drmKeysLoaded"); } /** @@ -461,8 +424,25 @@ public class EventLogger // Internal methods - private void printInternalError(String type, Exception e) { - loge("internalError [" + getSessionTimeString() + ", " + type + "]", e); + private void logd(EventTime eventTime, String eventName) { + logd(getEventString(eventTime, eventName)); + } + + private void logd(EventTime eventTime, String eventName, String eventDescription) { + logd(getEventString(eventTime, eventName, eventDescription)); + } + + private void loge(EventTime eventTime, String eventName, Throwable throwable) { + loge(getEventString(eventTime, eventName), throwable); + } + + private void loge( + EventTime eventTime, String eventName, String eventDescription, Throwable throwable) { + loge(getEventString(eventTime, eventName, eventDescription), throwable); + } + + private void printInternalError(EventTime eventTime, String type, Exception e) { + loge(eventTime, "internalError", type, e); } private void printMetadata(Metadata metadata, String prefix) { @@ -471,8 +451,28 @@ public class EventLogger } } - private String getSessionTimeString() { - return getTimeString(SystemClock.elapsedRealtime() - startTimeMs); + private String getEventString(EventTime eventTime, String eventName) { + return eventName + " [" + getEventTimeString(eventTime) + "]"; + } + + private String getEventString(EventTime eventTime, String eventName, String eventDescription) { + return eventName + " [" + getEventTimeString(eventTime) + ", " + eventDescription + "]"; + } + + private String getEventTimeString(EventTime eventTime) { + String windowPeriodString = "window=" + eventTime.windowIndex; + if (eventTime.mediaPeriodId != null) { + windowPeriodString += ", period=" + eventTime.mediaPeriodId.periodIndex; + if (eventTime.mediaPeriodId.isAd()) { + windowPeriodString += ", adGroup=" + eventTime.mediaPeriodId.adGroupIndex; + windowPeriodString += ", ad=" + eventTime.mediaPeriodId.adIndexInAdGroup; + } + } + return getTimeString(eventTime.realtimeMs - startTimeMs) + + ", " + + getTimeString(eventTime.currentPlaybackPositionMs) + + ", " + + windowPeriodString; } private static String getTimeString(long timeMs) { @@ -482,13 +482,13 @@ public class EventLogger private static String getStateString(int state) { switch (state) { case Player.STATE_BUFFERING: - return "B"; + return "BUFFERING"; case Player.STATE_ENDED: - return "E"; + return "ENDED"; case Player.STATE_IDLE: - return "I"; + return "IDLE"; case Player.STATE_READY: - return "R"; + return "READY"; default: return "?"; } @@ -583,4 +583,22 @@ public class EventLogger } } + private static String getTrackTypeString(int trackType) { + switch (trackType) { + case C.TRACK_TYPE_AUDIO: + return "audio"; + case C.TRACK_TYPE_DEFAULT: + return "default"; + case C.TRACK_TYPE_METADATA: + return "metadata"; + case C.TRACK_TYPE_NONE: + return "none"; + case C.TRACK_TYPE_TEXT: + return "text"; + case C.TRACK_TYPE_VIDEO: + return "video"; + default: + return trackType >= C.TRACK_TYPE_CUSTOM_BASE ? "custom (" + trackType + ")" : "?"; + } + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java b/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java index 041ee55cf1..d13aa877e0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java @@ -134,14 +134,13 @@ public final class MimeTypes { return BASE_TYPE_APPLICATION.equals(getTopLevelType(mimeType)); } - /** * Derives a video sample mimeType from a codecs attribute. * * @param codecs The codecs attribute. * @return The derived video mimeType, or null if it could not be derived. */ - public static String getVideoMediaMimeType(String codecs) { + public static @Nullable String getVideoMediaMimeType(@Nullable String codecs) { if (codecs == null) { return null; } @@ -161,7 +160,7 @@ public final class MimeTypes { * @param codecs The codecs attribute. * @return The derived audio mimeType, or null if it could not be derived. */ - public static String getAudioMediaMimeType(String codecs) { + public static @Nullable String getAudioMediaMimeType(@Nullable String codecs) { if (codecs == null) { return null; } @@ -181,7 +180,7 @@ public final class MimeTypes { * @param codec The codec identifier to derive. * @return The mimeType, or null if it could not be derived. */ - public static String getMediaMimeType(String codec) { + public static @Nullable String getMediaMimeType(@Nullable String codec) { if (codec == null) { return null; } @@ -345,7 +344,7 @@ public final class MimeTypes { * @param mimeType The mimeType whose top-level type is required. * @return The top-level type, or null if the mimeType is null. */ - private static String getTopLevelType(String mimeType) { + private static @Nullable String getTopLevelType(@Nullable String mimeType) { if (mimeType == null) { return null; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/NotificationUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/util/NotificationUtil.java new file mode 100644 index 0000000000..c93d7cd72e --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/NotificationUtil.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.util; + +import android.annotation.SuppressLint; +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.content.Context; +import android.content.Intent; +import android.support.annotation.IntDef; +import android.support.annotation.Nullable; +import android.support.annotation.StringRes; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** Utility methods for displaying {@link android.app.Notification}s. */ +@SuppressLint("InlinedApi") +public final class NotificationUtil { + + /** Notification channel importance levels. */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + IMPORTANCE_UNSPECIFIED, + IMPORTANCE_NONE, + IMPORTANCE_MIN, + IMPORTANCE_LOW, + IMPORTANCE_DEFAULT, + IMPORTANCE_HIGH + }) + public @interface Importance {} + /** @see NotificationManager#IMPORTANCE_UNSPECIFIED */ + public static final int IMPORTANCE_UNSPECIFIED = NotificationManager.IMPORTANCE_UNSPECIFIED; + /** @see NotificationManager#IMPORTANCE_NONE */ + public static final int IMPORTANCE_NONE = NotificationManager.IMPORTANCE_NONE; + /** @see NotificationManager#IMPORTANCE_MIN */ + public static final int IMPORTANCE_MIN = NotificationManager.IMPORTANCE_MIN; + /** @see NotificationManager#IMPORTANCE_LOW */ + public static final int IMPORTANCE_LOW = NotificationManager.IMPORTANCE_LOW; + /** @see NotificationManager#IMPORTANCE_DEFAULT */ + public static final int IMPORTANCE_DEFAULT = NotificationManager.IMPORTANCE_DEFAULT; + /** @see NotificationManager#IMPORTANCE_HIGH */ + public static final int IMPORTANCE_HIGH = NotificationManager.IMPORTANCE_HIGH; + + /** + * Creates a notification channel that notifications can be posted to. See {@link + * NotificationChannel} and {@link + * NotificationManager#createNotificationChannel(NotificationChannel)} for details. + * + * @param context A {@link Context} to retrieve {@link NotificationManager}. + * @param id The id of the channel. Must be unique per package. The value may be truncated if it + * is too long. + * @param name A string resource identifier for the user visible name of the channel. You can + * rename this channel when the system locale changes by listening for the {@link + * Intent#ACTION_LOCALE_CHANGED} broadcast. The recommended maximum length is 40 characters; + * the value may be truncated if it is too long. + * @param importance The importance of the channel. This controls how interruptive notifications + * posted to this channel are. One of {@link #IMPORTANCE_UNSPECIFIED}, {@link + * #IMPORTANCE_NONE}, {@link #IMPORTANCE_MIN}, {@link #IMPORTANCE_LOW}, {@link + * #IMPORTANCE_DEFAULT} and {@link #IMPORTANCE_HIGH}. + */ + public static void createNotificationChannel( + Context context, String id, @StringRes int name, @Importance int importance) { + if (Util.SDK_INT >= 26) { + NotificationManager notificationManager = + (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + NotificationChannel channel = + new NotificationChannel(id, context.getString(name), importance); + notificationManager.createNotificationChannel(channel); + } + } + + /** + * Post a notification to be shown in the status bar. If a notification with the same id has + * already been posted by your application and has not yet been canceled, it will be replaced by + * the updated information. If {@code notification} is null, then cancels a previously shown + * notification. + * + * @param context A {@link Context} to retrieve {@link NotificationManager}. + * @param id An identifier for this notification unique within your application. + * @param notification A {@link Notification} object describing what to show the user. If null, + * then cancels a previously shown notification. + */ + public static void setNotification(Context context, int id, @Nullable Notification notification) { + NotificationManager notificationManager = + (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + if (notification != null) { + notificationManager.notify(id, notification); + } else { + notificationManager.cancel(id); + } + } + + private NotificationUtil() {} +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/ParcelableArray.java b/library/core/src/main/java/com/google/android/exoplayer2/util/ParcelableArray.java deleted file mode 100644 index 3463f8f813..0000000000 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/ParcelableArray.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright (C) 2018 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.util; - -import android.os.Parcel; -import android.os.Parcelable; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - -/** A {@link android.os.Parcelable} wrapper around an array. */ -public final class ParcelableArray implements Parcelable { - - @SuppressWarnings("rawtypes") // V cannot be obtained from static context - public static final Creator CREATOR = - new Creator() { - @SuppressWarnings("unchecked") - @Override - public ParcelableArray createFromParcel(Parcel in) { - ClassLoader classLoader = ParcelableArray.class.getClassLoader(); - Parcelable[] elements = in.readParcelableArray(classLoader); - return new ParcelableArray(elements); - } - - @Override - public ParcelableArray[] newArray(int size) { - return new ParcelableArray[size]; - } - }; - - private final V[] elements; - - public ParcelableArray(V[] elements) { - this.elements = elements; - } - - /** Returns an unmodifiable list containing all elements. */ - public List asList() { - return Collections.unmodifiableList(Arrays.asList(elements)); - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeParcelableArray(elements, flags); - } -} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java index 0a4a38697e..fe83ce13e6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java @@ -18,7 +18,9 @@ package com.google.android.exoplayer2.util; import android.Manifest.permission; import android.annotation.TargetApi; import android.app.Activity; +import android.content.ComponentName; import android.content.Context; +import android.content.Intent; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; @@ -27,6 +29,7 @@ import android.net.Uri; import android.os.Build; import android.os.Parcel; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.text.TextUtils; import android.util.Log; import android.view.Display; @@ -128,6 +131,22 @@ public final class Util { return outputStream.toByteArray(); } + /** + * Calls {@link Context#startForegroundService(Intent)} if {@link #SDK_INT} is 26 or higher, or + * {@link Context#startService(Intent)} otherwise. + * + * @param context The context to call. + * @param intent The intent to pass to the called method. + * @return The result of the called method. + */ + public static ComponentName startForegroundService(Context context, Intent intent) { + if (Util.SDK_INT >= 26) { + return context.startForegroundService(intent); + } else { + return context.startService(intent); + } + } + /** * Checks whether it's necessary to request the {@link permission#READ_EXTERNAL_STORAGE} * permission read the specified {@link Uri}s, requesting the permission if necessary. @@ -172,7 +191,7 @@ public final class Util { * @param o2 The second object. * @return {@code o1 == null ? o2 == null : o1.equals(o2)}. */ - public static boolean areEqual(Object o1, Object o2) { + public static boolean areEqual(@Nullable Object o1, @Nullable Object o2) { return o1 == null ? o2 == null : o1.equals(o2); } @@ -206,6 +225,20 @@ public final class Util { list.subList(fromIndex, toIndex).clear(); } + /** + * Copies and optionally truncates an array. Prevents null array elements created by {@link + * Arrays#copyOf(Object[], int)} by ensuring the new length does not exceed the current length. + * + * @param input The input array. + * @param length The output array length. Must be less or equal to the length of the input array. + * @return The copied array. + */ + @SuppressWarnings("nullness:assignment.type.incompatible") + public static T[] nullSafeArrayCopy(T[] input, int length) { + Assertions.checkArgument(length <= input.length); + return Arrays.copyOf(input, length); + } + /** * Instantiates a new single threaded executor whose thread has the specified name. * diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java index 3cfc286881..34a3eb7284 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java @@ -144,7 +144,13 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { */ public MediaCodecVideoRenderer(Context context, MediaCodecSelector mediaCodecSelector, long allowedJoiningTimeMs) { - this(context, mediaCodecSelector, allowedJoiningTimeMs, null, null, -1); + this( + context, + mediaCodecSelector, + allowedJoiningTimeMs, + /* eventHandler= */ null, + /* eventListener= */ null, + -1); } /** @@ -161,8 +167,15 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { public MediaCodecVideoRenderer(Context context, MediaCodecSelector mediaCodecSelector, long allowedJoiningTimeMs, @Nullable Handler eventHandler, @Nullable VideoRendererEventListener eventListener, int maxDroppedFrameCountToNotify) { - this(context, mediaCodecSelector, allowedJoiningTimeMs, null, false, eventHandler, - eventListener, maxDroppedFrameCountToNotify); + this( + context, + mediaCodecSelector, + allowedJoiningTimeMs, + /* drmSessionManager= */ null, + /* playClearSamplesWithoutKeys= */ false, + eventHandler, + eventListener, + maxDroppedFrameCountToNotify); } /** @@ -442,8 +455,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { @Override protected @KeepCodecResult int canKeepCodec( - MediaCodec codec, boolean codecIsAdaptive, Format oldFormat, Format newFormat) { - if (areAdaptationCompatible(codecIsAdaptive, oldFormat, newFormat) + MediaCodec codec, MediaCodecInfo codecInfo, Format oldFormat, Format newFormat) { + if (areAdaptationCompatible(codecInfo.adaptive, oldFormat, newFormat) && newFormat.width <= codecMaxValues.width && newFormat.height <= codecMaxValues.height && getMaxInputSize(newFormat) <= codecMaxValues.inputSize) { @@ -908,50 +921,6 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { mediaFormat.setInteger(MediaFormat.KEY_AUDIO_SESSION_ID, tunnelingAudioSessionId); } - /** - * Returns {@link CodecMaxValues} suitable for configuring a codec for {@code format} in a way - * that will allow possible adaptation to other compatible formats in {@code streamFormats}. - * - * @param codecInfo Information about the {@link MediaCodec} being configured. - * @param format The format for which the codec is being configured. - * @param streamFormats The possible stream formats. - * @return Suitable {@link CodecMaxValues}. - * @throws DecoderQueryException If an error occurs querying {@code codecInfo}. - */ - protected CodecMaxValues getCodecMaxValues(MediaCodecInfo codecInfo, Format format, - Format[] streamFormats) throws DecoderQueryException { - int maxWidth = format.width; - int maxHeight = format.height; - int maxInputSize = getMaxInputSize(format); - if (streamFormats.length == 1) { - // The single entry in streamFormats must correspond to the format for which the codec is - // being configured. - return new CodecMaxValues(maxWidth, maxHeight, maxInputSize); - } - boolean haveUnknownDimensions = false; - for (Format streamFormat : streamFormats) { - if (areAdaptationCompatible(codecInfo.adaptive, format, streamFormat)) { - haveUnknownDimensions |= (streamFormat.width == Format.NO_VALUE - || streamFormat.height == Format.NO_VALUE); - maxWidth = Math.max(maxWidth, streamFormat.width); - maxHeight = Math.max(maxHeight, streamFormat.height); - maxInputSize = Math.max(maxInputSize, getMaxInputSize(streamFormat)); - } - } - if (haveUnknownDimensions) { - Log.w(TAG, "Resolutions unknown. Codec max resolution: " + maxWidth + "x" + maxHeight); - Point codecMaxSize = getCodecMaxSize(codecInfo, format); - if (codecMaxSize != null) { - maxWidth = Math.max(maxWidth, codecMaxSize.x); - maxHeight = Math.max(maxHeight, codecMaxSize.y); - maxInputSize = Math.max(maxInputSize, - getMaxInputSize(format.sampleMimeType, maxWidth, maxHeight)); - Log.w(TAG, "Codec max resolution adjusted to: " + maxWidth + "x" + maxHeight); - } - } - return new CodecMaxValues(maxWidth, maxHeight, maxInputSize); - } - /** * Returns the framework {@link MediaFormat} that should be used to configure the decoder. * @@ -997,6 +966,51 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { return mediaFormat; } + /** + * Returns {@link CodecMaxValues} suitable for configuring a codec for {@code format} in a way + * that will allow possible adaptation to other compatible formats in {@code streamFormats}. + * + * @param codecInfo Information about the {@link MediaCodec} being configured. + * @param format The format for which the codec is being configured. + * @param streamFormats The possible stream formats. + * @return Suitable {@link CodecMaxValues}. + * @throws DecoderQueryException If an error occurs querying {@code codecInfo}. + */ + protected CodecMaxValues getCodecMaxValues( + MediaCodecInfo codecInfo, Format format, Format[] streamFormats) + throws DecoderQueryException { + int maxWidth = format.width; + int maxHeight = format.height; + int maxInputSize = getMaxInputSize(format); + if (streamFormats.length == 1) { + // The single entry in streamFormats must correspond to the format for which the codec is + // being configured. + return new CodecMaxValues(maxWidth, maxHeight, maxInputSize); + } + boolean haveUnknownDimensions = false; + for (Format streamFormat : streamFormats) { + if (areAdaptationCompatible(codecInfo.adaptive, format, streamFormat)) { + haveUnknownDimensions |= + (streamFormat.width == Format.NO_VALUE || streamFormat.height == Format.NO_VALUE); + maxWidth = Math.max(maxWidth, streamFormat.width); + maxHeight = Math.max(maxHeight, streamFormat.height); + maxInputSize = Math.max(maxInputSize, getMaxInputSize(streamFormat)); + } + } + if (haveUnknownDimensions) { + Log.w(TAG, "Resolutions unknown. Codec max resolution: " + maxWidth + "x" + maxHeight); + Point codecMaxSize = getCodecMaxSize(codecInfo, format); + if (codecMaxSize != null) { + maxWidth = Math.max(maxWidth, codecMaxSize.x); + maxHeight = Math.max(maxHeight, codecMaxSize.y); + maxInputSize = + Math.max(maxInputSize, getMaxInputSize(format.sampleMimeType, maxWidth, maxHeight)); + Log.w(TAG, "Codec max resolution adjusted to: " + maxWidth + "x" + maxHeight); + } + } + return new CodecMaxValues(maxWidth, maxHeight, maxInputSize); + } + /** * Returns a maximum video size to use when configuring a codec for {@code format} in a way * that will allow possible adaptation to other compatible formats that are expected to have the diff --git a/library/core/src/test/assets/amr/sample_nb.amr b/library/core/src/test/assets/amr/sample_nb.amr new file mode 100644 index 0000000000..2e21cc843c Binary files /dev/null and b/library/core/src/test/assets/amr/sample_nb.amr differ diff --git a/library/core/src/test/assets/amr/sample_nb.amr.0.dump b/library/core/src/test/assets/amr/sample_nb.amr.0.dump new file mode 100644 index 0000000000..e0dec9c62c --- /dev/null +++ b/library/core/src/test/assets/amr/sample_nb.amr.0.dump @@ -0,0 +1,902 @@ +seekMap: + isSeekable = false + duration = UNSET TIME + getPosition(0) = [[timeUs=0, position=0]] +numberOfTracks = 1 +track 0: + format: + bitrate = -1 + id = null + containerMimeType = null + sampleMimeType = audio/3gpp + maxInputSize = 61 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 1 + sampleRate = 8000 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = null + drmInitData = - + initializationData: + total output bytes = 2834 + sample count = 218 + sample 0: + time = 0 + flags = 1 + data = length 13, hash 371B046C + sample 1: + time = 20000 + flags = 1 + data = length 13, hash CE30BF5B + sample 2: + time = 40000 + flags = 1 + data = length 13, hash 19A59975 + sample 3: + time = 60000 + flags = 1 + data = length 13, hash 4879773C + sample 4: + time = 80000 + flags = 1 + data = length 13, hash E8F83019 + sample 5: + time = 100000 + flags = 1 + data = length 13, hash D265CDC9 + sample 6: + time = 120000 + flags = 1 + data = length 13, hash 91653DAA + sample 7: + time = 140000 + flags = 1 + data = length 13, hash C79456F6 + sample 8: + time = 160000 + flags = 1 + data = length 13, hash CDDC4422 + sample 9: + time = 180000 + flags = 1 + data = length 13, hash D9ED3AF1 + sample 10: + time = 200000 + flags = 1 + data = length 13, hash BAB75A33 + sample 11: + time = 220000 + flags = 1 + data = length 13, hash 2221B4FF + sample 12: + time = 240000 + flags = 1 + data = length 13, hash 96400A0B + sample 13: + time = 260000 + flags = 1 + data = length 13, hash 582E6FB + sample 14: + time = 280000 + flags = 1 + data = length 13, hash C4E878E5 + sample 15: + time = 300000 + flags = 1 + data = length 13, hash C849A1BD + sample 16: + time = 320000 + flags = 1 + data = length 13, hash CFA8A9ED + sample 17: + time = 340000 + flags = 1 + data = length 13, hash 70CA4907 + sample 18: + time = 360000 + flags = 1 + data = length 13, hash B47D4454 + sample 19: + time = 380000 + flags = 1 + data = length 13, hash 282998C1 + sample 20: + time = 400000 + flags = 1 + data = length 13, hash 3F3F7A65 + sample 21: + time = 420000 + flags = 1 + data = length 13, hash CC2EAB58 + sample 22: + time = 440000 + flags = 1 + data = length 13, hash 279EF712 + sample 23: + time = 460000 + flags = 1 + data = length 13, hash AA2F4B29 + sample 24: + time = 480000 + flags = 1 + data = length 13, hash F6F658C4 + sample 25: + time = 500000 + flags = 1 + data = length 13, hash D7DEBD17 + sample 26: + time = 520000 + flags = 1 + data = length 13, hash 6DAB9A17 + sample 27: + time = 540000 + flags = 1 + data = length 13, hash 6ECE1571 + sample 28: + time = 560000 + flags = 1 + data = length 13, hash B3D0507F + sample 29: + time = 580000 + flags = 1 + data = length 13, hash 21E356B9 + sample 30: + time = 600000 + flags = 1 + data = length 13, hash 410EA12 + sample 31: + time = 620000 + flags = 1 + data = length 13, hash 533895A8 + sample 32: + time = 640000 + flags = 1 + data = length 13, hash C61B3E5A + sample 33: + time = 660000 + flags = 1 + data = length 13, hash 982170E6 + sample 34: + time = 680000 + flags = 1 + data = length 13, hash 7A0468C5 + sample 35: + time = 700000 + flags = 1 + data = length 13, hash 9C85EAA7 + sample 36: + time = 720000 + flags = 1 + data = length 13, hash B6B341B6 + sample 37: + time = 740000 + flags = 1 + data = length 13, hash 6937532E + sample 38: + time = 760000 + flags = 1 + data = length 13, hash 8CF2A3A0 + sample 39: + time = 780000 + flags = 1 + data = length 13, hash D2682AC6 + sample 40: + time = 800000 + flags = 1 + data = length 13, hash BBC5710F + sample 41: + time = 820000 + flags = 1 + data = length 13, hash 59080B6C + sample 42: + time = 840000 + flags = 1 + data = length 13, hash E4118291 + sample 43: + time = 860000 + flags = 1 + data = length 13, hash A1E5B296 + sample 44: + time = 880000 + flags = 1 + data = length 13, hash D7B8F95B + sample 45: + time = 900000 + flags = 1 + data = length 13, hash CC839BE1 + sample 46: + time = 920000 + flags = 1 + data = length 13, hash D459DFCE + sample 47: + time = 940000 + flags = 1 + data = length 13, hash D6AD19EC + sample 48: + time = 960000 + flags = 1 + data = length 13, hash D05E373D + sample 49: + time = 980000 + flags = 1 + data = length 13, hash 6A4460C7 + sample 50: + time = 1000000 + flags = 1 + data = length 13, hash C9A0D93F + sample 51: + time = 1020000 + flags = 1 + data = length 13, hash 3FA819E7 + sample 52: + time = 1040000 + flags = 1 + data = length 13, hash 1D3CBDFC + sample 53: + time = 1060000 + flags = 1 + data = length 13, hash 8BBBB403 + sample 54: + time = 1080000 + flags = 1 + data = length 13, hash 21B4A0F9 + sample 55: + time = 1100000 + flags = 1 + data = length 13, hash C0F921D1 + sample 56: + time = 1120000 + flags = 1 + data = length 13, hash 5D812AAB + sample 57: + time = 1140000 + flags = 1 + data = length 13, hash 50C9F3F8 + sample 58: + time = 1160000 + flags = 1 + data = length 13, hash 5C2BB5D1 + sample 59: + time = 1180000 + flags = 1 + data = length 13, hash 6BF9BEA5 + sample 60: + time = 1200000 + flags = 1 + data = length 13, hash 2738C1E6 + sample 61: + time = 1220000 + flags = 1 + data = length 13, hash 5FC288A6 + sample 62: + time = 1240000 + flags = 1 + data = length 13, hash 7E8E442A + sample 63: + time = 1260000 + flags = 1 + data = length 13, hash AEAA2BBA + sample 64: + time = 1280000 + flags = 1 + data = length 13, hash 4E2ACD2F + sample 65: + time = 1300000 + flags = 1 + data = length 13, hash D6C90ACF + sample 66: + time = 1320000 + flags = 1 + data = length 13, hash 6FD8A944 + sample 67: + time = 1340000 + flags = 1 + data = length 13, hash A835BBF9 + sample 68: + time = 1360000 + flags = 1 + data = length 13, hash F7713830 + sample 69: + time = 1380000 + flags = 1 + data = length 13, hash 3AA966E5 + sample 70: + time = 1400000 + flags = 1 + data = length 13, hash F939E829 + sample 71: + time = 1420000 + flags = 1 + data = length 13, hash 7676DE49 + sample 72: + time = 1440000 + flags = 1 + data = length 13, hash 93BB890A + sample 73: + time = 1460000 + flags = 1 + data = length 13, hash B57DBEC8 + sample 74: + time = 1480000 + flags = 1 + data = length 13, hash 66B0A5B6 + sample 75: + time = 1500000 + flags = 1 + data = length 13, hash D733E0D + sample 76: + time = 1520000 + flags = 1 + data = length 13, hash 80941726 + sample 77: + time = 1540000 + flags = 1 + data = length 13, hash 556ED633 + sample 78: + time = 1560000 + flags = 1 + data = length 13, hash C5EDF4E1 + sample 79: + time = 1580000 + flags = 1 + data = length 13, hash 6B287445 + sample 80: + time = 1600000 + flags = 1 + data = length 13, hash DC97C4A7 + sample 81: + time = 1620000 + flags = 1 + data = length 13, hash DA8CBDF4 + sample 82: + time = 1640000 + flags = 1 + data = length 13, hash 6F60FF77 + sample 83: + time = 1660000 + flags = 1 + data = length 13, hash 3EB22B96 + sample 84: + time = 1680000 + flags = 1 + data = length 13, hash B3C31AF5 + sample 85: + time = 1700000 + flags = 1 + data = length 13, hash 1854AA92 + sample 86: + time = 1720000 + flags = 1 + data = length 13, hash 6488264B + sample 87: + time = 1740000 + flags = 1 + data = length 13, hash 4CC8C5C1 + sample 88: + time = 1760000 + flags = 1 + data = length 13, hash 19CC7523 + sample 89: + time = 1780000 + flags = 1 + data = length 13, hash 9BE7B928 + sample 90: + time = 1800000 + flags = 1 + data = length 13, hash 47EC7CFD + sample 91: + time = 1820000 + flags = 1 + data = length 13, hash EC940120 + sample 92: + time = 1840000 + flags = 1 + data = length 13, hash 73BDA6D0 + sample 93: + time = 1860000 + flags = 1 + data = length 13, hash FACB3314 + sample 94: + time = 1880000 + flags = 1 + data = length 13, hash EC61D13B + sample 95: + time = 1900000 + flags = 1 + data = length 13, hash B28C7B6C + sample 96: + time = 1920000 + flags = 1 + data = length 13, hash B1A4CECD + sample 97: + time = 1940000 + flags = 1 + data = length 13, hash 56D41BA6 + sample 98: + time = 1960000 + flags = 1 + data = length 13, hash 90499F4 + sample 99: + time = 1980000 + flags = 1 + data = length 13, hash 65D9A9D3 + sample 100: + time = 2000000 + flags = 1 + data = length 13, hash D9004CC + sample 101: + time = 2020000 + flags = 1 + data = length 13, hash 4139C6ED + sample 102: + time = 2040000 + flags = 1 + data = length 13, hash C4F8097C + sample 103: + time = 2060000 + flags = 1 + data = length 13, hash 94D424FA + sample 104: + time = 2080000 + flags = 1 + data = length 13, hash C2C6F5FD + sample 105: + time = 2100000 + flags = 1 + data = length 13, hash 15719008 + sample 106: + time = 2120000 + flags = 1 + data = length 13, hash 4F64F524 + sample 107: + time = 2140000 + flags = 1 + data = length 13, hash F9E01C1E + sample 108: + time = 2160000 + flags = 1 + data = length 13, hash 74C4EE74 + sample 109: + time = 2180000 + flags = 1 + data = length 13, hash 7EE7553D + sample 110: + time = 2200000 + flags = 1 + data = length 13, hash 62DE6539 + sample 111: + time = 2220000 + flags = 1 + data = length 13, hash 7F5EC222 + sample 112: + time = 2240000 + flags = 1 + data = length 13, hash 644067F + sample 113: + time = 2260000 + flags = 1 + data = length 13, hash CDF6C9DC + sample 114: + time = 2280000 + flags = 1 + data = length 13, hash 8B5DBC80 + sample 115: + time = 2300000 + flags = 1 + data = length 13, hash AD4BBA03 + sample 116: + time = 2320000 + flags = 1 + data = length 13, hash 7A76340 + sample 117: + time = 2340000 + flags = 1 + data = length 13, hash 3610F5B0 + sample 118: + time = 2360000 + flags = 1 + data = length 13, hash 430BC60B + sample 119: + time = 2380000 + flags = 1 + data = length 13, hash 99CF1CA6 + sample 120: + time = 2400000 + flags = 1 + data = length 13, hash 1331C70B + sample 121: + time = 2420000 + flags = 1 + data = length 13, hash BD76E69D + sample 122: + time = 2440000 + flags = 1 + data = length 13, hash 5DA652AC + sample 123: + time = 2460000 + flags = 1 + data = length 13, hash 3B7BF6CE + sample 124: + time = 2480000 + flags = 1 + data = length 13, hash ABBFD143 + sample 125: + time = 2500000 + flags = 1 + data = length 13, hash E9447166 + sample 126: + time = 2520000 + flags = 1 + data = length 13, hash EC40068C + sample 127: + time = 2540000 + flags = 1 + data = length 13, hash A2869400 + sample 128: + time = 2560000 + flags = 1 + data = length 13, hash C7E0746B + sample 129: + time = 2580000 + flags = 1 + data = length 13, hash 60601BB1 + sample 130: + time = 2600000 + flags = 1 + data = length 13, hash 975AAE9B + sample 131: + time = 2620000 + flags = 1 + data = length 13, hash 8BBC0EB2 + sample 132: + time = 2640000 + flags = 1 + data = length 13, hash 57FB39E5 + sample 133: + time = 2660000 + flags = 1 + data = length 13, hash 4CDCEEDB + sample 134: + time = 2680000 + flags = 1 + data = length 13, hash EA16E256 + sample 135: + time = 2700000 + flags = 1 + data = length 13, hash 287E7D9E + sample 136: + time = 2720000 + flags = 1 + data = length 13, hash 55AB8FB9 + sample 137: + time = 2740000 + flags = 1 + data = length 13, hash 129890EF + sample 138: + time = 2760000 + flags = 1 + data = length 13, hash 90834F57 + sample 139: + time = 2780000 + flags = 1 + data = length 13, hash 5B3228E0 + sample 140: + time = 2800000 + flags = 1 + data = length 13, hash DD19E175 + sample 141: + time = 2820000 + flags = 1 + data = length 13, hash EE7EA342 + sample 142: + time = 2840000 + flags = 1 + data = length 13, hash DB3AF473 + sample 143: + time = 2860000 + flags = 1 + data = length 13, hash 25AEC43F + sample 144: + time = 2880000 + flags = 1 + data = length 13, hash EE9BF97F + sample 145: + time = 2900000 + flags = 1 + data = length 13, hash FFFBE047 + sample 146: + time = 2920000 + flags = 1 + data = length 13, hash BEACFCB0 + sample 147: + time = 2940000 + flags = 1 + data = length 13, hash AEB5096C + sample 148: + time = 2960000 + flags = 1 + data = length 13, hash B0D381B + sample 149: + time = 2980000 + flags = 1 + data = length 13, hash 3D9D5122 + sample 150: + time = 3000000 + flags = 1 + data = length 13, hash 6C1DDB95 + sample 151: + time = 3020000 + flags = 1 + data = length 13, hash ADACADCF + sample 152: + time = 3040000 + flags = 1 + data = length 13, hash 159E321E + sample 153: + time = 3060000 + flags = 1 + data = length 13, hash B1466264 + sample 154: + time = 3080000 + flags = 1 + data = length 13, hash 4DDF7223 + sample 155: + time = 3100000 + flags = 1 + data = length 13, hash C9BDB82A + sample 156: + time = 3120000 + flags = 1 + data = length 13, hash A49B2D9D + sample 157: + time = 3140000 + flags = 1 + data = length 13, hash D645E7E5 + sample 158: + time = 3160000 + flags = 1 + data = length 13, hash 1C4232DC + sample 159: + time = 3180000 + flags = 1 + data = length 13, hash 83078219 + sample 160: + time = 3200000 + flags = 1 + data = length 13, hash D6D8B072 + sample 161: + time = 3220000 + flags = 1 + data = length 13, hash 975DB40 + sample 162: + time = 3240000 + flags = 1 + data = length 13, hash A15FDD05 + sample 163: + time = 3260000 + flags = 1 + data = length 13, hash 4B839E41 + sample 164: + time = 3280000 + flags = 1 + data = length 13, hash 7418F499 + sample 165: + time = 3300000 + flags = 1 + data = length 13, hash 7A4945E4 + sample 166: + time = 3320000 + flags = 1 + data = length 13, hash 6249558C + sample 167: + time = 3340000 + flags = 1 + data = length 13, hash BD4C5BE3 + sample 168: + time = 3360000 + flags = 1 + data = length 13, hash BAB30F1D + sample 169: + time = 3380000 + flags = 1 + data = length 13, hash 1E1C7012 + sample 170: + time = 3400000 + flags = 1 + data = length 13, hash 9A3F8A89 + sample 171: + time = 3420000 + flags = 1 + data = length 13, hash 20BE6D7B + sample 172: + time = 3440000 + flags = 1 + data = length 13, hash CAA0591D + sample 173: + time = 3460000 + flags = 1 + data = length 13, hash 6D554D17 + sample 174: + time = 3480000 + flags = 1 + data = length 13, hash D97C3B31 + sample 175: + time = 3500000 + flags = 1 + data = length 13, hash 75BC5C3 + sample 176: + time = 3520000 + flags = 1 + data = length 13, hash 7BA1784B + sample 177: + time = 3540000 + flags = 1 + data = length 13, hash 1D175D92 + sample 178: + time = 3560000 + flags = 1 + data = length 13, hash ADCA60FD + sample 179: + time = 3580000 + flags = 1 + data = length 13, hash 37018693 + sample 180: + time = 3600000 + flags = 1 + data = length 13, hash 4553606F + sample 181: + time = 3620000 + flags = 1 + data = length 13, hash CF434565 + sample 182: + time = 3640000 + flags = 1 + data = length 13, hash D264D757 + sample 183: + time = 3660000 + flags = 1 + data = length 13, hash 4FB493EF + sample 184: + time = 3680000 + flags = 1 + data = length 13, hash 919F53A + sample 185: + time = 3700000 + flags = 1 + data = length 13, hash C22B009B + sample 186: + time = 3720000 + flags = 1 + data = length 13, hash 5981470 + sample 187: + time = 3740000 + flags = 1 + data = length 13, hash A5D3937C + sample 188: + time = 3760000 + flags = 1 + data = length 13, hash A2504429 + sample 189: + time = 3780000 + flags = 1 + data = length 13, hash AD1B70BE + sample 190: + time = 3800000 + flags = 1 + data = length 13, hash 2E39ED5E + sample 191: + time = 3820000 + flags = 1 + data = length 13, hash 13A8BE8E + sample 192: + time = 3840000 + flags = 1 + data = length 13, hash 1ACD740B + sample 193: + time = 3860000 + flags = 1 + data = length 13, hash 80F38B3 + sample 194: + time = 3880000 + flags = 1 + data = length 13, hash DA9DA79F + sample 195: + time = 3900000 + flags = 1 + data = length 13, hash 21B95B7E + sample 196: + time = 3920000 + flags = 1 + data = length 13, hash CD22497B + sample 197: + time = 3940000 + flags = 1 + data = length 13, hash 718BB35D + sample 198: + time = 3960000 + flags = 1 + data = length 13, hash 69ABA6AD + sample 199: + time = 3980000 + flags = 1 + data = length 13, hash BAE19549 + sample 200: + time = 4000000 + flags = 1 + data = length 13, hash 2A792FB3 + sample 201: + time = 4020000 + flags = 1 + data = length 13, hash 71FCD8 + sample 202: + time = 4040000 + flags = 1 + data = length 13, hash 44D2B5B3 + sample 203: + time = 4060000 + flags = 1 + data = length 13, hash 1E87B11B + sample 204: + time = 4080000 + flags = 1 + data = length 13, hash 78CD2C11 + sample 205: + time = 4100000 + flags = 1 + data = length 13, hash 9F198DF0 + sample 206: + time = 4120000 + flags = 1 + data = length 13, hash B291F16A + sample 207: + time = 4140000 + flags = 1 + data = length 13, hash CF820EE0 + sample 208: + time = 4160000 + flags = 1 + data = length 13, hash 4E24F683 + sample 209: + time = 4180000 + flags = 1 + data = length 13, hash 52BCD68F + sample 210: + time = 4200000 + flags = 1 + data = length 13, hash 42588CB0 + sample 211: + time = 4220000 + flags = 1 + data = length 13, hash EBBFECA2 + sample 212: + time = 4240000 + flags = 1 + data = length 13, hash C11050CF + sample 213: + time = 4260000 + flags = 1 + data = length 13, hash 6F738603 + sample 214: + time = 4280000 + flags = 1 + data = length 13, hash DAD06E5 + sample 215: + time = 4300000 + flags = 1 + data = length 13, hash 5B036C64 + sample 216: + time = 4320000 + flags = 1 + data = length 13, hash A58DC12E + sample 217: + time = 4340000 + flags = 1 + data = length 13, hash AC59BA7C +tracksEnded = true diff --git a/library/core/src/test/assets/amr/sample_wb.amr b/library/core/src/test/assets/amr/sample_wb.amr new file mode 100644 index 0000000000..14b85b553c Binary files /dev/null and b/library/core/src/test/assets/amr/sample_wb.amr differ diff --git a/library/core/src/test/assets/amr/sample_wb.amr.0.dump b/library/core/src/test/assets/amr/sample_wb.amr.0.dump new file mode 100644 index 0000000000..1b3b8bd0dd --- /dev/null +++ b/library/core/src/test/assets/amr/sample_wb.amr.0.dump @@ -0,0 +1,706 @@ +seekMap: + isSeekable = false + duration = UNSET TIME + getPosition(0) = [[timeUs=0, position=0]] +numberOfTracks = 1 +track 0: + format: + bitrate = -1 + id = null + containerMimeType = null + sampleMimeType = audio/amr-wb + maxInputSize = 61 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 1 + sampleRate = 16000 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = null + drmInitData = - + initializationData: + total output bytes = 4056 + sample count = 169 + sample 0: + time = 0 + flags = 1 + data = length 24, hash C3025798 + sample 1: + time = 20000 + flags = 1 + data = length 24, hash 39CABAE9 + sample 2: + time = 40000 + flags = 1 + data = length 24, hash 2752F470 + sample 3: + time = 60000 + flags = 1 + data = length 24, hash 394F76F6 + sample 4: + time = 80000 + flags = 1 + data = length 24, hash FF9EEF + sample 5: + time = 100000 + flags = 1 + data = length 24, hash 54ECB1B4 + sample 6: + time = 120000 + flags = 1 + data = length 24, hash 6D7A3A5F + sample 7: + time = 140000 + flags = 1 + data = length 24, hash 684CD144 + sample 8: + time = 160000 + flags = 1 + data = length 24, hash 87B7D176 + sample 9: + time = 180000 + flags = 1 + data = length 24, hash 4C02F9A5 + sample 10: + time = 200000 + flags = 1 + data = length 24, hash B4154108 + sample 11: + time = 220000 + flags = 1 + data = length 24, hash 4448F477 + sample 12: + time = 240000 + flags = 1 + data = length 24, hash 755A4939 + sample 13: + time = 260000 + flags = 1 + data = length 24, hash 8C8BC6C3 + sample 14: + time = 280000 + flags = 1 + data = length 24, hash BC37F63F + sample 15: + time = 300000 + flags = 1 + data = length 24, hash 3352C43C + sample 16: + time = 320000 + flags = 1 + data = length 24, hash 7998E1F2 + sample 17: + time = 340000 + flags = 1 + data = length 24, hash A8ECBEFC + sample 18: + time = 360000 + flags = 1 + data = length 24, hash 944AC118 + sample 19: + time = 380000 + flags = 1 + data = length 24, hash FD2C8E1F + sample 20: + time = 400000 + flags = 1 + data = length 24, hash B3D867AF + sample 21: + time = 420000 + flags = 1 + data = length 24, hash 3DC6E592 + sample 22: + time = 440000 + flags = 1 + data = length 24, hash 32B276CD + sample 23: + time = 460000 + flags = 1 + data = length 24, hash 5488AEF3 + sample 24: + time = 480000 + flags = 1 + data = length 24, hash 7A4D516 + sample 25: + time = 500000 + flags = 1 + data = length 24, hash 570AE83F + sample 26: + time = 520000 + flags = 1 + data = length 24, hash E5CB3477 + sample 27: + time = 540000 + flags = 1 + data = length 24, hash E04C00E4 + sample 28: + time = 560000 + flags = 1 + data = length 24, hash 21B7C97 + sample 29: + time = 580000 + flags = 1 + data = length 24, hash 1633F470 + sample 30: + time = 600000 + flags = 1 + data = length 24, hash 28D65CA6 + sample 31: + time = 620000 + flags = 1 + data = length 24, hash CC6A675C + sample 32: + time = 640000 + flags = 1 + data = length 24, hash 4C91080A + sample 33: + time = 660000 + flags = 1 + data = length 24, hash F6482FB5 + sample 34: + time = 680000 + flags = 1 + data = length 24, hash 2C76F48C + sample 35: + time = 700000 + flags = 1 + data = length 24, hash 6E3B0D72 + sample 36: + time = 720000 + flags = 1 + data = length 24, hash 799AA003 + sample 37: + time = 740000 + flags = 1 + data = length 24, hash DFC0BA81 + sample 38: + time = 760000 + flags = 1 + data = length 24, hash CBDF3826 + sample 39: + time = 780000 + flags = 1 + data = length 24, hash 16862B75 + sample 40: + time = 800000 + flags = 1 + data = length 24, hash 865A828E + sample 41: + time = 820000 + flags = 1 + data = length 24, hash 336BBDC9 + sample 42: + time = 840000 + flags = 1 + data = length 24, hash 6CFC6C34 + sample 43: + time = 860000 + flags = 1 + data = length 24, hash 32C8CD46 + sample 44: + time = 880000 + flags = 1 + data = length 24, hash 9FE11C4C + sample 45: + time = 900000 + flags = 1 + data = length 24, hash AA5A12B7 + sample 46: + time = 920000 + flags = 1 + data = length 24, hash AA0F4A4D + sample 47: + time = 940000 + flags = 1 + data = length 24, hash 34415484 + sample 48: + time = 960000 + flags = 1 + data = length 24, hash 5018928E + sample 49: + time = 980000 + flags = 1 + data = length 24, hash 4A04D162 + sample 50: + time = 1000000 + flags = 1 + data = length 24, hash 4C70F9F0 + sample 51: + time = 1020000 + flags = 1 + data = length 24, hash 99EF3168 + sample 52: + time = 1040000 + flags = 1 + data = length 24, hash C600DAF + sample 53: + time = 1060000 + flags = 1 + data = length 24, hash FDBB192E + sample 54: + time = 1080000 + flags = 1 + data = length 24, hash 99096A48 + sample 55: + time = 1100000 + flags = 1 + data = length 24, hash D793F88B + sample 56: + time = 1120000 + flags = 1 + data = length 24, hash EEB921BD + sample 57: + time = 1140000 + flags = 1 + data = length 24, hash 8B941A4C + sample 58: + time = 1160000 + flags = 1 + data = length 24, hash ED5F5FEE + sample 59: + time = 1180000 + flags = 1 + data = length 24, hash A588E0BB + sample 60: + time = 1200000 + flags = 1 + data = length 24, hash 588CBC01 + sample 61: + time = 1220000 + flags = 1 + data = length 24, hash DE22266C + sample 62: + time = 1240000 + flags = 1 + data = length 24, hash 921B6E5C + sample 63: + time = 1260000 + flags = 1 + data = length 24, hash EC11F041 + sample 64: + time = 1280000 + flags = 1 + data = length 24, hash 5BA9E0A3 + sample 65: + time = 1300000 + flags = 1 + data = length 24, hash DB6D52F3 + sample 66: + time = 1320000 + flags = 1 + data = length 24, hash 8EEBE525 + sample 67: + time = 1340000 + flags = 1 + data = length 24, hash 47A742AE + sample 68: + time = 1360000 + flags = 1 + data = length 24, hash E93F1E03 + sample 69: + time = 1380000 + flags = 1 + data = length 24, hash 3251F57C + sample 70: + time = 1400000 + flags = 1 + data = length 24, hash 3EDBBBDD + sample 71: + time = 1420000 + flags = 1 + data = length 24, hash 2E98465A + sample 72: + time = 1440000 + flags = 1 + data = length 24, hash A09EA52E + sample 73: + time = 1460000 + flags = 1 + data = length 24, hash A2A86FA6 + sample 74: + time = 1480000 + flags = 1 + data = length 24, hash 71DCD51C + sample 75: + time = 1500000 + flags = 1 + data = length 24, hash 2B02DEE1 + sample 76: + time = 1520000 + flags = 1 + data = length 24, hash 7A725192 + sample 77: + time = 1540000 + flags = 1 + data = length 24, hash 929AD483 + sample 78: + time = 1560000 + flags = 1 + data = length 24, hash 68440BF5 + sample 79: + time = 1580000 + flags = 1 + data = length 24, hash 5BD41AD6 + sample 80: + time = 1600000 + flags = 1 + data = length 24, hash 91A381 + sample 81: + time = 1620000 + flags = 1 + data = length 24, hash 8010C408 + sample 82: + time = 1640000 + flags = 1 + data = length 24, hash 482274BE + sample 83: + time = 1660000 + flags = 1 + data = length 24, hash D7DB8BCC + sample 84: + time = 1680000 + flags = 1 + data = length 24, hash 680BD9DD + sample 85: + time = 1700000 + flags = 1 + data = length 24, hash E313577C + sample 86: + time = 1720000 + flags = 1 + data = length 24, hash 9C10B0CD + sample 87: + time = 1740000 + flags = 1 + data = length 24, hash 2D90AC02 + sample 88: + time = 1760000 + flags = 1 + data = length 24, hash 64E8C245 + sample 89: + time = 1780000 + flags = 1 + data = length 24, hash 3954AC1B + sample 90: + time = 1800000 + flags = 1 + data = length 24, hash ACB8999F + sample 91: + time = 1820000 + flags = 1 + data = length 24, hash 43AE3957 + sample 92: + time = 1840000 + flags = 1 + data = length 24, hash 3C664DB7 + sample 93: + time = 1860000 + flags = 1 + data = length 24, hash 9354B576 + sample 94: + time = 1880000 + flags = 1 + data = length 24, hash B5B9C14E + sample 95: + time = 1900000 + flags = 1 + data = length 24, hash 7DA9C98F + sample 96: + time = 1920000 + flags = 1 + data = length 24, hash EFEE54C6 + sample 97: + time = 1940000 + flags = 1 + data = length 24, hash 79DC8CBD + sample 98: + time = 1960000 + flags = 1 + data = length 24, hash A71A475C + sample 99: + time = 1980000 + flags = 1 + data = length 24, hash CA1CBB94 + sample 100: + time = 2000000 + flags = 1 + data = length 24, hash 91922226 + sample 101: + time = 2020000 + flags = 1 + data = length 24, hash C90278BC + sample 102: + time = 2040000 + flags = 1 + data = length 24, hash BD51986F + sample 103: + time = 2060000 + flags = 1 + data = length 24, hash 90AEF368 + sample 104: + time = 2080000 + flags = 1 + data = length 24, hash 1D83C955 + sample 105: + time = 2100000 + flags = 1 + data = length 24, hash 8FA9A915 + sample 106: + time = 2120000 + flags = 1 + data = length 24, hash C6C753E0 + sample 107: + time = 2140000 + flags = 1 + data = length 24, hash 85FA27A7 + sample 108: + time = 2160000 + flags = 1 + data = length 24, hash A0277324 + sample 109: + time = 2180000 + flags = 1 + data = length 24, hash B7696535 + sample 110: + time = 2200000 + flags = 1 + data = length 24, hash D69D668C + sample 111: + time = 2220000 + flags = 1 + data = length 24, hash 34C057CD + sample 112: + time = 2240000 + flags = 1 + data = length 24, hash 4EC5E974 + sample 113: + time = 2260000 + flags = 1 + data = length 24, hash 1C1CD40D + sample 114: + time = 2280000 + flags = 1 + data = length 24, hash 76CC54BC + sample 115: + time = 2300000 + flags = 1 + data = length 24, hash D497ACF5 + sample 116: + time = 2320000 + flags = 1 + data = length 24, hash A1386080 + sample 117: + time = 2340000 + flags = 1 + data = length 24, hash 7ED36954 + sample 118: + time = 2360000 + flags = 1 + data = length 24, hash C11A3BF9 + sample 119: + time = 2380000 + flags = 1 + data = length 24, hash 8FB69488 + sample 120: + time = 2400000 + flags = 1 + data = length 24, hash C6225F59 + sample 121: + time = 2420000 + flags = 1 + data = length 24, hash 122AB6D2 + sample 122: + time = 2440000 + flags = 1 + data = length 24, hash 1E195E7D + sample 123: + time = 2460000 + flags = 1 + data = length 24, hash BD3DF418 + sample 124: + time = 2480000 + flags = 1 + data = length 24, hash D8AE4A5 + sample 125: + time = 2500000 + flags = 1 + data = length 24, hash 977BD182 + sample 126: + time = 2520000 + flags = 1 + data = length 24, hash F361F060 + sample 127: + time = 2540000 + flags = 1 + data = length 24, hash 11EC8CD0 + sample 128: + time = 2560000 + flags = 1 + data = length 24, hash 3798F3D2 + sample 129: + time = 2580000 + flags = 1 + data = length 24, hash B2C2517C + sample 130: + time = 2600000 + flags = 1 + data = length 24, hash FBE0D0D8 + sample 131: + time = 2620000 + flags = 1 + data = length 24, hash 7033172F + sample 132: + time = 2640000 + flags = 1 + data = length 24, hash BE760029 + sample 133: + time = 2660000 + flags = 1 + data = length 24, hash 590AF28C + sample 134: + time = 2680000 + flags = 1 + data = length 24, hash AD28C48F + sample 135: + time = 2700000 + flags = 1 + data = length 24, hash 640AA61B + sample 136: + time = 2720000 + flags = 1 + data = length 24, hash ABE659B + sample 137: + time = 2740000 + flags = 1 + data = length 24, hash ED2691D2 + sample 138: + time = 2760000 + flags = 1 + data = length 24, hash D998C80E + sample 139: + time = 2780000 + flags = 1 + data = length 24, hash 8DC0DF5C + sample 140: + time = 2800000 + flags = 1 + data = length 24, hash 7692247B + sample 141: + time = 2820000 + flags = 1 + data = length 24, hash C1D1CCB9 + sample 142: + time = 2840000 + flags = 1 + data = length 24, hash 362CE78E + sample 143: + time = 2860000 + flags = 1 + data = length 24, hash 54FA84A + sample 144: + time = 2880000 + flags = 1 + data = length 24, hash 29E88C84 + sample 145: + time = 2900000 + flags = 1 + data = length 24, hash 1CD848AC + sample 146: + time = 2920000 + flags = 1 + data = length 24, hash 5C3D4A79 + sample 147: + time = 2940000 + flags = 1 + data = length 24, hash 1AA8E604 + sample 148: + time = 2960000 + flags = 1 + data = length 24, hash 186A4316 + sample 149: + time = 2980000 + flags = 1 + data = length 24, hash 61ACE481 + sample 150: + time = 3000000 + flags = 1 + data = length 24, hash D0C42780 + sample 151: + time = 3020000 + flags = 1 + data = length 24, hash FAD51BA1 + sample 152: + time = 3040000 + flags = 1 + data = length 24, hash F1A9AC71 + sample 153: + time = 3060000 + flags = 1 + data = length 24, hash 24425449 + sample 154: + time = 3080000 + flags = 1 + data = length 24, hash 37AAC3E6 + sample 155: + time = 3100000 + flags = 1 + data = length 24, hash 91F68CB4 + sample 156: + time = 3120000 + flags = 1 + data = length 24, hash F8C92820 + sample 157: + time = 3140000 + flags = 1 + data = length 24, hash ECD39C3E + sample 158: + time = 3160000 + flags = 1 + data = length 24, hash B27D8F78 + sample 159: + time = 3180000 + flags = 1 + data = length 24, hash C9EB3DFB + sample 160: + time = 3200000 + flags = 1 + data = length 24, hash 88DC54A2 + sample 161: + time = 3220000 + flags = 1 + data = length 24, hash 7FC4C5BE + sample 162: + time = 3240000 + flags = 1 + data = length 24, hash E4F684EF + sample 163: + time = 3260000 + flags = 1 + data = length 24, hash 55C08B56 + sample 164: + time = 3280000 + flags = 1 + data = length 24, hash E5A0F006 + sample 165: + time = 3300000 + flags = 1 + data = length 24, hash DE3F3AA7 + sample 166: + time = 3320000 + flags = 1 + data = length 24, hash 3F28AE7F + sample 167: + time = 3340000 + flags = 1 + data = length 24, hash 3949CAFF + sample 168: + time = 3360000 + flags = 1 + data = length 24, hash 772665A0 +tracksEnded = true diff --git a/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java index 2320563750..829fa5a2b8 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java @@ -105,14 +105,10 @@ public final class AnalyticsCollectorTest { private static final int EVENT_DROPPED_VIDEO_FRAMES = 30; private static final int EVENT_VIDEO_SIZE_CHANGED = 31; private static final int EVENT_RENDERED_FIRST_FRAME = 32; - private static final int EVENT_AD_LOAD_ERROR = 33; - private static final int EVENT_INTERNAL_AD_LOAD_ERROR = 34; - private static final int EVENT_AD_CLICKED = 35; - private static final int EVENT_AD_TAPPED = 36; - private static final int EVENT_DRM_KEYS_LOADED = 37; - private static final int EVENT_DRM_ERROR = 38; - private static final int EVENT_DRM_KEYS_RESTORED = 39; - private static final int EVENT_DRM_KEYS_REMOVED = 40; + private static final int EVENT_DRM_KEYS_LOADED = 33; + private static final int EVENT_DRM_ERROR = 34; + private static final int EVENT_DRM_KEYS_RESTORED = 35; + private static final int EVENT_DRM_KEYS_REMOVED = 36; private static final int TIMEOUT_MS = 10000; private static final Timeline SINGLE_PERIOD_TIMELINE = new FakeTimeline(/* windowCount= */ 1); @@ -1089,26 +1085,6 @@ public final class AnalyticsCollectorTest { reportedEvents.add(new ReportedEvent(EVENT_RENDERED_FIRST_FRAME, eventTime)); } - @Override - public void onAdLoadError(EventTime eventTime, IOException error) { - reportedEvents.add(new ReportedEvent(EVENT_AD_LOAD_ERROR, eventTime)); - } - - @Override - public void onInternalAdLoadError(EventTime eventTime, RuntimeException error) { - reportedEvents.add(new ReportedEvent(EVENT_INTERNAL_AD_LOAD_ERROR, eventTime)); - } - - @Override - public void onAdClicked(EventTime eventTime) { - reportedEvents.add(new ReportedEvent(EVENT_AD_CLICKED, eventTime)); - } - - @Override - public void onAdTapped(EventTime eventTime) { - reportedEvents.add(new ReportedEvent(EVENT_AD_TAPPED, eventTime)); - } - @Override public void onDrmKeysLoaded(EventTime eventTime) { reportedEvents.add(new ReportedEvent(EVENT_DRM_KEYS_LOADED, eventTime)); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactoryTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactoryTest.java new file mode 100644 index 0000000000..148e04ca77 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactoryTest.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.extractor; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.android.exoplayer2.extractor.amr.AmrExtractor; +import com.google.android.exoplayer2.extractor.flv.FlvExtractor; +import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor; +import com.google.android.exoplayer2.extractor.mp3.Mp3Extractor; +import com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor; +import com.google.android.exoplayer2.extractor.mp4.Mp4Extractor; +import com.google.android.exoplayer2.extractor.ogg.OggExtractor; +import com.google.android.exoplayer2.extractor.ts.Ac3Extractor; +import com.google.android.exoplayer2.extractor.ts.AdtsExtractor; +import com.google.android.exoplayer2.extractor.ts.PsExtractor; +import com.google.android.exoplayer2.extractor.ts.TsExtractor; +import com.google.android.exoplayer2.extractor.wav.WavExtractor; +import java.util.ArrayList; +import java.util.List; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +/** Unit test for {@link DefaultExtractorsFactory}. */ +@RunWith(RobolectricTestRunner.class) +public final class DefaultExtractorsFactoryTest { + + @Test + public void testCreateExtractors_returnExpectedClasses() { + DefaultExtractorsFactory defaultExtractorsFactory = new DefaultExtractorsFactory(); + + Extractor[] extractors = defaultExtractorsFactory.createExtractors(); + List listCreatedExtractorClasses = new ArrayList<>(); + for (Extractor extractor : extractors) { + listCreatedExtractorClasses.add(extractor.getClass()); + } + + Class[] expectedExtractorClassses = + new Class[] { + MatroskaExtractor.class, + FragmentedMp4Extractor.class, + Mp4Extractor.class, + Mp3Extractor.class, + AdtsExtractor.class, + Ac3Extractor.class, + TsExtractor.class, + FlvExtractor.class, + OggExtractor.class, + PsExtractor.class, + WavExtractor.class, + AmrExtractor.class + }; + + assertThat(listCreatedExtractorClasses).containsNoDuplicates(); + assertThat(listCreatedExtractorClasses).containsExactlyElementsIn(expectedExtractorClassses); + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/amr/AmrExtractorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/amr/AmrExtractorTest.java new file mode 100644 index 0000000000..b46612e7c3 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/amr/AmrExtractorTest.java @@ -0,0 +1,246 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.extractor.amr; + +import static com.google.android.exoplayer2.extractor.amr.AmrExtractor.amrSignatureNb; +import static com.google.android.exoplayer2.extractor.amr.AmrExtractor.amrSignatureWb; +import static com.google.android.exoplayer2.extractor.amr.AmrExtractor.frameSizeBytesByTypeNb; +import static com.google.android.exoplayer2.extractor.amr.AmrExtractor.frameSizeBytesByTypeWb; +import static com.google.common.truth.Truth.assertThat; +import static junit.framework.Assert.fail; + +import android.support.annotation.NonNull; +import com.google.android.exoplayer2.ParserException; +import com.google.android.exoplayer2.extractor.Extractor; +import com.google.android.exoplayer2.extractor.PositionHolder; +import com.google.android.exoplayer2.testutil.ExtractorAsserts; +import com.google.android.exoplayer2.testutil.FakeExtractorInput; +import com.google.android.exoplayer2.testutil.FakeExtractorOutput; +import com.google.android.exoplayer2.util.Util; +import java.io.IOException; +import java.util.Random; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +/** Unit test for {@link AmrExtractor}. */ +@RunWith(RobolectricTestRunner.class) +public final class AmrExtractorTest { + + private static final Random RANDOM = new Random(1234); + + @Test + public void testSniff_nonAmrSignature_returnFalse() throws IOException, InterruptedException { + AmrExtractor amrExtractor = setupAmrExtractorWithOutput(); + FakeExtractorInput input = fakeExtractorInputWithData(Util.getUtf8Bytes("0#!AMR\n123")); + + boolean result = amrExtractor.sniff(input); + assertThat(result).isFalse(); + } + + @Test + public void testRead_nonAmrSignature_throwParserException() + throws IOException, InterruptedException { + AmrExtractor amrExtractor = setupAmrExtractorWithOutput(); + FakeExtractorInput input = fakeExtractorInputWithData(Util.getUtf8Bytes("0#!AMR-WB\n")); + + try { + amrExtractor.read(input, new PositionHolder()); + fail(); + } catch (ParserException e) { + // expected + } + } + + @Test + public void testRead_amrNb_returnParserException_forInvalidFrameType() + throws IOException, InterruptedException { + AmrExtractor amrExtractor = setupAmrExtractorWithOutput(); + + // Frame type 12-14 for narrow band is reserved for future usage. + byte[] amrFrame = newNarrowBandAmrFrameWithType(12); + byte[] data = joinData(amrSignatureNb(), amrFrame); + FakeExtractorInput input = fakeExtractorInputWithData(data); + + try { + amrExtractor.read(input, new PositionHolder()); + fail(); + } catch (ParserException e) { + // expected + } + } + + @Test + public void testRead_amrWb_returnParserException_forInvalidFrameType() + throws IOException, InterruptedException { + AmrExtractor amrExtractor = setupAmrExtractorWithOutput(); + + // Frame type 10-13 for wide band is reserved for future usage. + byte[] amrFrame = newWideBandAmrFrameWithType(13); + byte[] data = joinData(amrSignatureWb(), amrFrame); + FakeExtractorInput input = fakeExtractorInputWithData(data); + + try { + amrExtractor.read(input, new PositionHolder()); + fail(); + } catch (ParserException e) { + // expected + } + } + + @Test + public void testRead_amrNb_returnEndOfInput_ifInputEncountersEoF() + throws IOException, InterruptedException { + AmrExtractor amrExtractor = setupAmrExtractorWithOutput(); + + byte[] amrFrame = newNarrowBandAmrFrameWithType(3); + byte[] data = joinData(amrSignatureNb(), amrFrame); + FakeExtractorInput input = fakeExtractorInputWithData(data); + + // Read 1st frame, which will put the input at EoF. + amrExtractor.read(input, new PositionHolder()); + + int result = amrExtractor.read(input, new PositionHolder()); + assertThat(result).isEqualTo(Extractor.RESULT_END_OF_INPUT); + } + + @Test + public void testRead_amrWb_returnEndOfInput_ifInputEncountersEoF() + throws IOException, InterruptedException { + AmrExtractor amrExtractor = setupAmrExtractorWithOutput(); + + byte[] amrFrame = newWideBandAmrFrameWithType(5); + byte[] data = joinData(amrSignatureWb(), amrFrame); + FakeExtractorInput input = fakeExtractorInputWithData(data); + + // Read 1st frame, which will put the input at EoF. + amrExtractor.read(input, new PositionHolder()); + + int result = amrExtractor.read(input, new PositionHolder()); + assertThat(result).isEqualTo(Extractor.RESULT_END_OF_INPUT); + } + + @Test + public void testRead_amrNb_returnParserException_forInvalidFrameHeader() + throws IOException, InterruptedException { + AmrExtractor amrExtractor = setupAmrExtractorWithOutput(); + + byte[] invalidHeaderFrame = newNarrowBandAmrFrameWithType(4); + + // The padding bits are at bit-1 positions in the following pattern: 1000 0011 + // Padding bits must be 0. + invalidHeaderFrame[0] = (byte) (invalidHeaderFrame[0] | 0b01111101); + + byte[] data = joinData(amrSignatureNb(), invalidHeaderFrame); + FakeExtractorInput input = fakeExtractorInputWithData(data); + + try { + amrExtractor.read(input, new PositionHolder()); + fail(); + } catch (ParserException e) { + // expected + } + } + + @Test + public void testRead_amrWb_returnParserException_forInvalidFrameHeader() + throws IOException, InterruptedException { + AmrExtractor amrExtractor = setupAmrExtractorWithOutput(); + + byte[] invalidHeaderFrame = newWideBandAmrFrameWithType(6); + + // The padding bits are at bit-1 positions in the following pattern: 1000 0011 + // Padding bits must be 0. + invalidHeaderFrame[0] = (byte) (invalidHeaderFrame[0] | 0b01111110); + + byte[] data = joinData(amrSignatureWb(), invalidHeaderFrame); + FakeExtractorInput input = fakeExtractorInputWithData(data); + + try { + amrExtractor.read(input, new PositionHolder()); + fail(); + } catch (ParserException e) { + // expected + } + } + + @Test + public void testExtractingNarrowBandSamples() throws Exception { + ExtractorAsserts.assertBehavior(createAmrExtractorFactory(), "amr/sample_nb.amr"); + } + + @Test + public void testExtractingWideBandSamples() throws Exception { + ExtractorAsserts.assertBehavior(createAmrExtractorFactory(), "amr/sample_wb.amr"); + } + + private byte[] newWideBandAmrFrameWithType(int frameType) { + byte frameHeader = (byte) ((frameType << 3) & (0b01111100)); + int frameContentInBytes = frameSizeBytesByTypeWb(frameType) - 1; + + return joinData(new byte[] {frameHeader}, randomBytesArrayWithLength(frameContentInBytes)); + } + + private byte[] newNarrowBandAmrFrameWithType(int frameType) { + byte frameHeader = (byte) ((frameType << 3) & (0b01111100)); + int frameContentInBytes = frameSizeBytesByTypeNb(frameType) - 1; + + return joinData(new byte[] {frameHeader}, randomBytesArrayWithLength(frameContentInBytes)); + } + + private static byte[] randomBytesArrayWithLength(int length) { + byte[] result = new byte[length]; + RANDOM.nextBytes(result); + return result; + } + + private static byte[] joinData(byte[]... byteArrays) { + int totalLength = 0; + for (byte[] byteArray : byteArrays) { + totalLength += byteArray.length; + } + byte[] result = new byte[totalLength]; + int offset = 0; + for (byte[] byteArray : byteArrays) { + System.arraycopy(byteArray, /* srcPos= */ 0, result, offset, byteArray.length); + offset += byteArray.length; + } + return result; + } + + @NonNull + private static AmrExtractor setupAmrExtractorWithOutput() { + AmrExtractor amrExtractor = new AmrExtractor(); + FakeExtractorOutput output = new FakeExtractorOutput(); + amrExtractor.init(output); + return amrExtractor; + } + + @NonNull + private static FakeExtractorInput fakeExtractorInputWithData(byte[] data) { + return new FakeExtractorInput.Builder().setData(data).build(); + } + + @NonNull + private static ExtractorAsserts.ExtractorFactory createAmrExtractorFactory() { + return new ExtractorAsserts.ExtractorFactory() { + @Override + public Extractor create() { + return new AmrExtractor(); + } + }; + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/offline/ActionFileTest.java b/library/core/src/test/java/com/google/android/exoplayer2/offline/ActionFileTest.java index 466aea3795..e821bc34a0 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/offline/ActionFileTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/offline/ActionFileTest.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.offline; import static com.google.common.truth.Truth.assertThat; +import android.net.Uri; import com.google.android.exoplayer2.offline.DownloadAction.Deserializer; import com.google.android.exoplayer2.util.Util; import java.io.DataInputStream; @@ -63,7 +64,7 @@ public class ActionFileTest { @Test public void testLoadIncompleteHeaderThrowsIOException() throws Exception { try { - loadActions(new Object[] {DownloadAction.MASTER_VERSION}); + loadActions(new Object[] {ActionFile.VERSION}); Assert.fail(); } catch (IOException e) { // Expected exception. @@ -72,39 +73,83 @@ public class ActionFileTest { @Test public void testLoadCompleteHeaderZeroAction() throws Exception { - DownloadAction[] actions = - loadActions(new Object[] {DownloadAction.MASTER_VERSION, /*action count*/0}); + DownloadAction[] actions = loadActions(new Object[] {ActionFile.VERSION, 0}); assertThat(actions).isNotNull(); assertThat(actions).hasLength(0); } @Test public void testLoadAction() throws Exception { - DownloadAction[] actions = loadActions( - new Object[] {DownloadAction.MASTER_VERSION, /*action count*/1, /*action 1*/"type2", 321}, - new FakeDeserializer("type2")); + byte[] data = Util.getUtf8Bytes("321"); + DownloadAction[] actions = + loadActions( + new Object[] { + ActionFile.VERSION, + 1, // Action count + "type2", // Action 1 + FakeDownloadAction.VERSION, + data, + }, + new FakeDeserializer("type2")); assertThat(actions).isNotNull(); assertThat(actions).hasLength(1); - assertAction(actions[0], "type2", DownloadAction.MASTER_VERSION, 321); + assertAction(actions[0], "type2", FakeDownloadAction.VERSION, data); } @Test public void testLoadActions() throws Exception { - DownloadAction[] actions = loadActions( - new Object[] {DownloadAction.MASTER_VERSION, /*action count*/2, /*action 1*/"type1", 123, - /*action 2*/"type2", 321}, // Action 2 - new FakeDeserializer("type1"), new FakeDeserializer("type2")); + byte[] data1 = Util.getUtf8Bytes("123"); + byte[] data2 = Util.getUtf8Bytes("321"); + DownloadAction[] actions = + loadActions( + new Object[] { + ActionFile.VERSION, + 2, // Action count + "type1", // Action 1 + FakeDownloadAction.VERSION, + data1, + "type2", // Action 2 + FakeDownloadAction.VERSION, + data2, + }, + new FakeDeserializer("type1"), + new FakeDeserializer("type2")); assertThat(actions).isNotNull(); assertThat(actions).hasLength(2); - assertAction(actions[0], "type1", DownloadAction.MASTER_VERSION, 123); - assertAction(actions[1], "type2", DownloadAction.MASTER_VERSION, 321); + assertAction(actions[0], "type1", FakeDownloadAction.VERSION, data1); + assertAction(actions[1], "type2", FakeDownloadAction.VERSION, data2); } @Test public void testLoadNotSupportedVersion() throws Exception { try { - loadActions(new Object[] {DownloadAction.MASTER_VERSION + 1, /*action count*/1, - /*action 1*/"type2", 321}, new FakeDeserializer("type2")); + loadActions( + new Object[] { + ActionFile.VERSION + 1, + 1, // Action count + "type2", // Action 1 + FakeDownloadAction.VERSION, + Util.getUtf8Bytes("321"), + }, + new FakeDeserializer("type2")); + Assert.fail(); + } catch (IOException e) { + // Expected exception. + } + } + + @Test + public void testLoadNotSupportedActionVersion() throws Exception { + try { + loadActions( + new Object[] { + ActionFile.VERSION, + 1, // Action count + "type2", // Action 1 + FakeDownloadAction.VERSION + 1, + Util.getUtf8Bytes("321"), + }, + new FakeDeserializer("type2")); Assert.fail(); } catch (IOException e) { // Expected exception. @@ -114,8 +159,15 @@ public class ActionFileTest { @Test public void testLoadNotSupportedType() throws Exception { try { - loadActions(new Object[] {DownloadAction.MASTER_VERSION, /*action count*/1, - /*action 1*/"type2", 321}, new FakeDeserializer("type1")); + loadActions( + new Object[] { + ActionFile.VERSION, + 1, // Action count + "type2", // Action 1 + FakeDownloadAction.VERSION, + Util.getUtf8Bytes("321"), + }, + new FakeDeserializer("type1")); Assert.fail(); } catch (DownloadException e) { // Expected exception. @@ -129,10 +181,13 @@ public class ActionFileTest { @Test public void testStoreAndLoadActions() throws Exception { - doTestSerializationRoundTrip(new DownloadAction[] { - new FakeDownloadAction("type1", DownloadAction.MASTER_VERSION, 123), - new FakeDownloadAction("type2", DownloadAction.MASTER_VERSION, 321), - }, new FakeDeserializer("type1"), new FakeDeserializer("type2")); + doTestSerializationRoundTrip( + new DownloadAction[] { + new FakeDownloadAction("type1", Util.getUtf8Bytes("123")), + new FakeDownloadAction("type2", Util.getUtf8Bytes("321")), + }, + new FakeDeserializer("type1"), + new FakeDeserializer("type2")); } private void doTestSerializationRoundTrip(DownloadAction[] actions, @@ -149,9 +204,13 @@ public class ActionFileTest { try { for (Object value : values) { if (value instanceof Integer) { - dataOutputStream.writeInt((Integer) value); // Action count + dataOutputStream.writeInt((Integer) value); } else if (value instanceof String) { - dataOutputStream.writeUTF((String) value); // Action count + dataOutputStream.writeUTF((String) value); + } else if (value instanceof byte[]) { + byte[] data = (byte[]) value; + dataOutputStream.writeInt(data.length); + dataOutputStream.write(data); } else { throw new IllegalArgumentException(); } @@ -162,61 +221,40 @@ public class ActionFileTest { return new ActionFile(tempFile).load(deserializers); } - private static void assertAction(DownloadAction action, String type, int version, int data) { + private static void assertAction(DownloadAction action, String type, int version, byte[] data) { assertThat(action).isInstanceOf(FakeDownloadAction.class); - assertThat(action.getType()).isEqualTo(type); + assertThat(action.type).isEqualTo(type); assertThat(((FakeDownloadAction) action).version).isEqualTo(version); assertThat(((FakeDownloadAction) action).data).isEqualTo(data); } - private static class FakeDeserializer implements Deserializer { - final String type; + private static class FakeDeserializer extends Deserializer { FakeDeserializer(String type) { - this.type = type; - } - - @Override - public String getType() { - return type; + super(type, FakeDownloadAction.VERSION); } @Override public DownloadAction readFromStream(int version, DataInputStream input) throws IOException { - return new FakeDownloadAction(type, version, input.readInt()); + int dataLength = input.readInt(); + byte[] data = new byte[dataLength]; + input.readFully(data); + return new FakeDownloadAction(type, data); } } private static class FakeDownloadAction extends DownloadAction { - final String type; - final int version; - final int data; - private FakeDownloadAction(String type, int version, int data) { - super(null); - this.type = type; - this.version = version; - this.data = data; - } + public static final int VERSION = 0; - @Override - protected String getType() { - return type; + private FakeDownloadAction(String type, byte[] data) { + super(type, VERSION, Uri.parse("http://test.com"), /* isRemoveAction= */ false, data); } @Override protected void writeToStream(DataOutputStream output) throws IOException { - output.writeInt(data); - } - - @Override - public boolean isRemoveAction() { - return false; - } - - @Override - protected boolean isSameMedia(DownloadAction other) { - return false; + output.writeInt(data.length); + output.write(data); } @Override @@ -224,27 +262,6 @@ public class ActionFileTest { return null; } - // auto generated code - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - FakeDownloadAction that = (FakeDownloadAction) o; - return version == that.version && data == that.data && type.equals(that.type); - } - - @Override - public int hashCode() { - int result = type.hashCode(); - result = 31 * result + version; - result = 31 * result + data; - return result; - } } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java index 53f159ad35..0d0bf73d04 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java @@ -18,24 +18,19 @@ package com.google.android.exoplayer2.offline; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; -import android.os.ConditionVariable; -import android.support.annotation.Nullable; +import android.net.Uri; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.offline.DownloadManager.DownloadListener; -import com.google.android.exoplayer2.offline.DownloadManager.DownloadState; -import com.google.android.exoplayer2.offline.DownloadManager.DownloadState.State; +import com.google.android.exoplayer2.offline.DownloadManager.TaskState; +import com.google.android.exoplayer2.offline.DownloadManager.TaskState.State; import com.google.android.exoplayer2.testutil.DummyMainThread; import com.google.android.exoplayer2.testutil.RobolectricUtil; +import com.google.android.exoplayer2.testutil.TestDownloadManagerListener; import com.google.android.exoplayer2.upstream.DummyDataSource; import com.google.android.exoplayer2.upstream.cache.Cache; import com.google.android.exoplayer2.util.Util; import java.io.DataOutputStream; import java.io.File; import java.io.IOException; -import java.util.ArrayList; -import java.util.Locale; -import java.util.concurrent.ArrayBlockingQueue; -import java.util.concurrent.BlockingQueue; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import org.junit.After; @@ -62,17 +57,22 @@ public class DownloadManagerTest { private static final int MIN_RETRY_COUNT = 3; - private DownloadManager downloadManager; - private File actionFile; - private TestDownloadListener testDownloadListener; + private Uri uri1; + private Uri uri2; + private Uri uri3; private DummyMainThread dummyMainThread; + private File actionFile; + private TestDownloadManagerListener downloadManagerListener; + private DownloadManager downloadManager; @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); + uri1 = Uri.parse("http://abc.com/media1"); + uri2 = Uri.parse("http://abc.com/media2"); + uri3 = Uri.parse("http://abc.com/media3"); dummyMainThread = new DummyMainThread(); actionFile = Util.createTempFile(RuntimeEnvironment.application, "ExoPlayerTest"); - testDownloadListener = new TestDownloadListener(); setUpDownloadManager(100); } @@ -85,46 +85,46 @@ public class DownloadManagerTest { @Test public void testDownloadActionRuns() throws Throwable { - doTestActionRuns(createDownloadAction("media 1")); + doTestActionRuns(createDownloadAction(uri1)); } @Test public void testRemoveActionRuns() throws Throwable { - doTestActionRuns(createRemoveAction("media 1")); + doTestActionRuns(createRemoveAction(uri1)); } @Test public void testDownloadRetriesThenFails() throws Throwable { - FakeDownloadAction downloadAction = createDownloadAction("media 1"); + FakeDownloadAction downloadAction = createDownloadAction(uri1); downloadAction.post(); FakeDownloader fakeDownloader = downloadAction.getFakeDownloader(); fakeDownloader.enableDownloadIOException = true; for (int i = 0; i <= MIN_RETRY_COUNT; i++) { fakeDownloader.assertStarted(MAX_RETRY_DELAY).unblock(); } - downloadAction.assertError(); - testDownloadListener.clearDownloadError(); + downloadAction.assertFailed(); + downloadManagerListener.clearDownloadError(); - testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); + downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); } @Test - public void testDownloadNoRetryWhenCancelled() throws Throwable { - FakeDownloadAction downloadAction = createDownloadAction("media 1").ignoreInterrupts(); + public void testDownloadNoRetryWhenCanceled() throws Throwable { + FakeDownloadAction downloadAction = createDownloadAction(uri1).ignoreInterrupts(); downloadAction.getFakeDownloader().enableDownloadIOException = true; downloadAction.post().assertStarted(); - FakeDownloadAction removeAction = createRemoveAction("media 1").post(); + FakeDownloadAction removeAction = createRemoveAction(uri1).post(); - downloadAction.unblock().assertCancelled(); + downloadAction.unblock().assertCanceled(); removeAction.unblock(); - testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); + downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); } @Test public void testDownloadRetriesThenContinues() throws Throwable { - FakeDownloadAction downloadAction = createDownloadAction("media 1"); + FakeDownloadAction downloadAction = createDownloadAction(uri1); downloadAction.post(); FakeDownloader fakeDownloader = downloadAction.getFakeDownloader(); fakeDownloader.enableDownloadIOException = true; @@ -135,15 +135,15 @@ public class DownloadManagerTest { } fakeDownloader.unblock(); } - downloadAction.assertEnded(); + downloadAction.assertCompleted(); - testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); + downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); } @Test @SuppressWarnings({"NonAtomicVolatileUpdate", "NonAtomicOperationOnVolatileField"}) public void testDownloadRetryCountResetsOnProgress() throws Throwable { - FakeDownloadAction downloadAction = createDownloadAction("media 1"); + FakeDownloadAction downloadAction = createDownloadAction(uri1); downloadAction.post(); FakeDownloader fakeDownloader = downloadAction.getFakeDownloader(); fakeDownloader.enableDownloadIOException = true; @@ -156,48 +156,48 @@ public class DownloadManagerTest { } fakeDownloader.unblock(); } - downloadAction.assertEnded(); + downloadAction.assertCompleted(); - testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); + downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); } @Test public void testDifferentMediaDownloadActionsStartInParallel() throws Throwable { - doTestActionsRunInParallel(createDownloadAction("media 1"), createDownloadAction("media 2")); + doTestActionsRunInParallel(createDownloadAction(uri1), createDownloadAction(uri2)); } @Test public void testDifferentMediaDifferentActionsStartInParallel() throws Throwable { - doTestActionsRunInParallel(createDownloadAction("media 1"), createRemoveAction("media 2")); + doTestActionsRunInParallel(createDownloadAction(uri1), createRemoveAction(uri2)); } @Test public void testSameMediaDownloadActionsStartInParallel() throws Throwable { - doTestActionsRunInParallel(createDownloadAction("media 1"), createDownloadAction("media 1")); + doTestActionsRunInParallel(createDownloadAction(uri1), createDownloadAction(uri1)); } @Test public void testSameMediaRemoveActionWaitsDownloadAction() throws Throwable { - doTestActionsRunSequentially(createDownloadAction("media 1"), createRemoveAction("media 1")); + doTestActionsRunSequentially(createDownloadAction(uri1), createRemoveAction(uri1)); } @Test public void testSameMediaDownloadActionWaitsRemoveAction() throws Throwable { - doTestActionsRunSequentially(createRemoveAction("media 1"), createDownloadAction("media 1")); + doTestActionsRunSequentially(createRemoveAction(uri1), createDownloadAction(uri1)); } @Test public void testSameMediaRemoveActionWaitsRemoveAction() throws Throwable { - doTestActionsRunSequentially(createRemoveAction("media 1"), createRemoveAction("media 1")); + doTestActionsRunSequentially(createRemoveAction(uri1), createRemoveAction(uri1)); } @Test public void testSameMediaMultipleActions() throws Throwable { - FakeDownloadAction downloadAction1 = createDownloadAction("media 1").ignoreInterrupts(); - FakeDownloadAction downloadAction2 = createDownloadAction("media 1").ignoreInterrupts(); - FakeDownloadAction removeAction1 = createRemoveAction("media 1"); - FakeDownloadAction downloadAction3 = createDownloadAction("media 1"); - FakeDownloadAction removeAction2 = createRemoveAction("media 1"); + FakeDownloadAction downloadAction1 = createDownloadAction(uri1).ignoreInterrupts(); + FakeDownloadAction downloadAction2 = createDownloadAction(uri1).ignoreInterrupts(); + FakeDownloadAction removeAction1 = createRemoveAction(uri1); + FakeDownloadAction downloadAction3 = createDownloadAction(uri1); + FakeDownloadAction removeAction2 = createRemoveAction(uri1); // Two download actions run in parallel. downloadAction1.post().assertStarted(); @@ -207,13 +207,13 @@ public class DownloadManagerTest { removeAction1.post().assertDoesNotStart(); // downloadAction2 finishes but it isn't enough to start removeAction1. - downloadAction2.unblock().assertCancelled(); + downloadAction2.unblock().assertCanceled(); removeAction1.assertDoesNotStart(); // downloadAction3 is post to DownloadManager but it waits for removeAction1 to finish. downloadAction3.post().assertDoesNotStart(); // When downloadAction1 finishes, removeAction1 starts. - downloadAction1.unblock().assertCancelled(); + downloadAction1.unblock().assertCanceled(); removeAction1.assertStarted(); // downloadAction3 still waits removeAction1 downloadAction3.assertDoesNotStart(); @@ -221,111 +221,111 @@ public class DownloadManagerTest { // removeAction2 is posted. removeAction1 and downloadAction3 is canceled so removeAction2 // starts immediately. removeAction2.post(); - removeAction1.assertCancelled(); - downloadAction3.assertCancelled(); - removeAction2.assertStarted().unblock().assertEnded(); - testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); + removeAction1.assertCanceled(); + downloadAction3.assertCanceled(); + removeAction2.assertStarted().unblock().assertCompleted(); + downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); } @Test public void testMultipleRemoveActionWaitsLastCancelsAllOther() throws Throwable { - FakeDownloadAction removeAction1 = createRemoveAction("media 1").ignoreInterrupts(); - FakeDownloadAction removeAction2 = createRemoveAction("media 1"); - FakeDownloadAction removeAction3 = createRemoveAction("media 1"); + FakeDownloadAction removeAction1 = createRemoveAction(uri1).ignoreInterrupts(); + FakeDownloadAction removeAction2 = createRemoveAction(uri1); + FakeDownloadAction removeAction3 = createRemoveAction(uri1); removeAction1.post().assertStarted(); removeAction2.post().assertDoesNotStart(); removeAction3.post().assertDoesNotStart(); - removeAction2.assertCancelled(); + removeAction2.assertCanceled(); - removeAction1.unblock().assertCancelled(); - removeAction3.assertStarted().unblock().assertEnded(); + removeAction1.unblock().assertCanceled(); + removeAction3.assertStarted().unblock().assertCompleted(); - testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); + downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); } @Test public void testGetTasks() throws Throwable { - FakeDownloadAction removeAction = createRemoveAction("media 1"); - FakeDownloadAction downloadAction1 = createDownloadAction("media 1"); - FakeDownloadAction downloadAction2 = createDownloadAction("media 1"); + FakeDownloadAction removeAction = createRemoveAction(uri1); + FakeDownloadAction downloadAction1 = createDownloadAction(uri1); + FakeDownloadAction downloadAction2 = createDownloadAction(uri1); removeAction.post().assertStarted(); downloadAction1.post().assertDoesNotStart(); downloadAction2.post().assertDoesNotStart(); - DownloadState[] states = downloadManager.getDownloadStates(); + TaskState[] states = downloadManager.getAllTaskStates(); assertThat(states).hasLength(3); - assertThat(states[0].downloadAction).isEqualTo(removeAction); - assertThat(states[1].downloadAction).isEqualTo(downloadAction1); - assertThat(states[2].downloadAction).isEqualTo(downloadAction2); + assertThat(states[0].action).isEqualTo(removeAction); + assertThat(states[1].action).isEqualTo(downloadAction1); + assertThat(states[2].action).isEqualTo(downloadAction2); } @Test public void testMultipleWaitingDownloadActionStartsInParallel() throws Throwable { - FakeDownloadAction removeAction = createRemoveAction("media 1"); - FakeDownloadAction downloadAction1 = createDownloadAction("media 1"); - FakeDownloadAction downloadAction2 = createDownloadAction("media 1"); + FakeDownloadAction removeAction = createRemoveAction(uri1); + FakeDownloadAction downloadAction1 = createDownloadAction(uri1); + FakeDownloadAction downloadAction2 = createDownloadAction(uri1); removeAction.post().assertStarted(); downloadAction1.post().assertDoesNotStart(); downloadAction2.post().assertDoesNotStart(); - removeAction.unblock().assertEnded(); + removeAction.unblock().assertCompleted(); downloadAction1.assertStarted(); downloadAction2.assertStarted(); - downloadAction1.unblock().assertEnded(); - downloadAction2.unblock().assertEnded(); + downloadAction1.unblock().assertCompleted(); + downloadAction2.unblock().assertCompleted(); - testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); + downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); } @Test public void testDifferentMediaDownloadActionsPreserveOrder() throws Throwable { - FakeDownloadAction removeAction = createRemoveAction("media 1").ignoreInterrupts(); - FakeDownloadAction downloadAction1 = createDownloadAction("media 1"); - FakeDownloadAction downloadAction2 = createDownloadAction("media 2"); + FakeDownloadAction removeAction = createRemoveAction(uri1).ignoreInterrupts(); + FakeDownloadAction downloadAction1 = createDownloadAction(uri1); + FakeDownloadAction downloadAction2 = createDownloadAction(uri2); removeAction.post().assertStarted(); downloadAction1.post().assertDoesNotStart(); downloadAction2.post().assertDoesNotStart(); - removeAction.unblock().assertEnded(); + removeAction.unblock().assertCompleted(); downloadAction1.assertStarted(); downloadAction2.assertStarted(); - downloadAction1.unblock().assertEnded(); - downloadAction2.unblock().assertEnded(); + downloadAction1.unblock().assertCompleted(); + downloadAction2.unblock().assertCompleted(); - testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); + downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); } @Test public void testDifferentMediaRemoveActionsDoNotPreserveOrder() throws Throwable { - FakeDownloadAction downloadAction = createDownloadAction("media 1").ignoreInterrupts(); - FakeDownloadAction removeAction1 = createRemoveAction("media 1"); - FakeDownloadAction removeAction2 = createRemoveAction("media 2"); + FakeDownloadAction downloadAction = createDownloadAction(uri1).ignoreInterrupts(); + FakeDownloadAction removeAction1 = createRemoveAction(uri1); + FakeDownloadAction removeAction2 = createRemoveAction(uri2); downloadAction.post().assertStarted(); removeAction1.post().assertDoesNotStart(); removeAction2.post().assertStarted(); - downloadAction.unblock().assertCancelled(); - removeAction2.unblock().assertEnded(); + downloadAction.unblock().assertCanceled(); + removeAction2.unblock().assertCompleted(); removeAction1.assertStarted(); - removeAction1.unblock().assertEnded(); + removeAction1.unblock().assertCompleted(); - testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); + downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); } @Test public void testStopAndResume() throws Throwable { - FakeDownloadAction download1Action = createDownloadAction("media 1"); - FakeDownloadAction remove2Action = createRemoveAction("media 2"); - FakeDownloadAction download2Action = createDownloadAction("media 2"); - FakeDownloadAction remove1Action = createRemoveAction("media 1"); - FakeDownloadAction download3Action = createDownloadAction("media 3"); + FakeDownloadAction download1Action = createDownloadAction(uri1); + FakeDownloadAction remove2Action = createRemoveAction(uri2); + FakeDownloadAction download2Action = createDownloadAction(uri2); + FakeDownloadAction remove1Action = createRemoveAction(uri1); + FakeDownloadAction download3Action = createDownloadAction(uri3); download1Action.post().assertStarted(); remove2Action.post().assertStarted(); @@ -342,14 +342,14 @@ public class DownloadManagerTest { download1Action.assertStopped(); // remove actions aren't stopped. - remove2Action.unblock().assertEnded(); + remove2Action.unblock().assertCompleted(); // Although remove2Action is finished, download2Action doesn't start. download2Action.assertDoesNotStart(); // When a new remove action is added, it cancels stopped download actions with the same media. remove1Action.post(); - download1Action.assertCancelled(); - remove1Action.assertStarted().unblock().assertEnded(); + download1Action.assertCanceled(); + remove1Action.assertStarted().unblock().assertCompleted(); // New download actions can be added but they don't start. download3Action.post().assertDoesNotStart(); @@ -362,18 +362,18 @@ public class DownloadManagerTest { } }); - download2Action.assertStarted().unblock().assertEnded(); - download3Action.assertStarted().unblock().assertEnded(); + download2Action.assertStarted().unblock().assertCompleted(); + download3Action.assertStarted().unblock().assertCompleted(); - testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); + downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); } @Test public void testResumeBeforeTotallyStopped() throws Throwable { setUpDownloadManager(2); - FakeDownloadAction download1Action = createDownloadAction("media 1").ignoreInterrupts(); - FakeDownloadAction download2Action = createDownloadAction("media 2"); - FakeDownloadAction download3Action = createDownloadAction("media 3"); + FakeDownloadAction download1Action = createDownloadAction(uri1).ignoreInterrupts(); + FakeDownloadAction download2Action = createDownloadAction(uri2); + FakeDownloadAction download3Action = createDownloadAction(uri3); download1Action.post().assertStarted(); download2Action.post().assertStarted(); @@ -412,7 +412,7 @@ public class DownloadManagerTest { download2Action.unblock(); download3Action.unblock(); - testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); + downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); } private void setUpDownloadManager(final int maxActiveDownloadTasks) throws Exception { @@ -430,9 +430,11 @@ public class DownloadManagerTest { Mockito.mock(Cache.class), DummyDataSource.FACTORY), maxActiveDownloadTasks, MIN_RETRY_COUNT, - actionFile.getAbsolutePath(), + actionFile, ProgressiveDownloadAction.DESERIALIZER); - downloadManager.addListener(testDownloadListener); + downloadManagerListener = + new TestDownloadManagerListener(downloadManager, dummyMainThread); + downloadManager.addListener(downloadManagerListener); downloadManager.startDownloads(); } }); @@ -456,8 +458,8 @@ public class DownloadManagerTest { } private void doTestActionRuns(FakeDownloadAction action) throws Throwable { - action.post().assertStarted().unblock().assertEnded(); - testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); + action.post().assertStarted().unblock().assertCompleted(); + downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); } private void doTestActionsRunSequentially(FakeDownloadAction action1, FakeDownloadAction action2) @@ -468,102 +470,45 @@ public class DownloadManagerTest { action1.unblock(); action2.assertStarted(); - action2.unblock().assertEnded(); - testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); + action2.unblock().assertCompleted(); + downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); } private void doTestActionsRunInParallel(FakeDownloadAction action1, FakeDownloadAction action2) throws Throwable { action1.post().assertStarted(); action2.post().assertStarted(); - action1.unblock().assertEnded(); - action2.unblock().assertEnded(); - testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); + action1.unblock().assertCompleted(); + action2.unblock().assertCompleted(); + downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); } - private FakeDownloadAction createDownloadAction(String mediaId) { - return new FakeDownloadAction(mediaId, false); + private FakeDownloadAction createDownloadAction(Uri uri) { + return new FakeDownloadAction(uri, /* isRemoveAction= */ false); } - private FakeDownloadAction createRemoveAction(String mediaId) { - return new FakeDownloadAction(mediaId, true); + private FakeDownloadAction createRemoveAction(Uri uri) { + return new FakeDownloadAction(uri, /* isRemoveAction= */ true); } - private void runOnMainThread(final Runnable r) throws Throwable { + private void runOnMainThread(final Runnable r) { dummyMainThread.runOnMainThread(r); } - private static final class TestDownloadListener implements DownloadListener { - - private ConditionVariable downloadFinishedCondition; - private Throwable downloadError; - - private TestDownloadListener() { - downloadFinishedCondition = new ConditionVariable(); - } - - @Override - public void onStateChange(DownloadManager downloadManager, DownloadState downloadState) { - if (downloadState.state == DownloadState.STATE_ERROR && downloadError == null) { - downloadError = downloadState.error; - } - ((FakeDownloadAction) downloadState.downloadAction).onStateChange(downloadState.state); - } - - @Override - public void onIdle(DownloadManager downloadManager) { - downloadFinishedCondition.open(); - } - - private void clearDownloadError() { - this.downloadError = null; - } - - private void blockUntilTasksCompleteAndThrowAnyDownloadError() throws Throwable { - assertThat(downloadFinishedCondition.block(ASSERT_TRUE_TIMEOUT)).isTrue(); - downloadFinishedCondition.close(); - if (downloadError != null) { - throw new Exception(downloadError); - } - } - } - private class FakeDownloadAction extends DownloadAction { - private final String mediaId; - private final boolean removeAction; private final FakeDownloader downloader; - private final BlockingQueue states; - private FakeDownloadAction(String mediaId, boolean removeAction) { - super(mediaId); - this.mediaId = mediaId; - this.removeAction = removeAction; - this.downloader = new FakeDownloader(removeAction); - this.states = new ArrayBlockingQueue<>(10); + private FakeDownloadAction(Uri uri, boolean isRemoveAction) { + super("Fake", /* version= */ 0, uri, isRemoveAction, /* data= */ null); + this.downloader = new FakeDownloader(isRemoveAction); } @Override - protected String getType() { - return "FakeDownloadAction"; - } - - @Override - protected void writeToStream(DataOutputStream output) throws IOException { + protected void writeToStream(DataOutputStream output) { // do nothing. } - @Override - public boolean isRemoveAction() { - return removeAction; - } - - @Override - protected boolean isSameMedia(DownloadAction other) { - return other instanceof FakeDownloadAction - && mediaId.equals(((FakeDownloadAction) other).mediaId); - } - @Override protected Downloader createDownloader(DownloaderConstructorHelper downloaderConstructorHelper) { return downloader; @@ -573,7 +518,7 @@ public class DownloadManagerTest { return downloader; } - private FakeDownloadAction post() throws Throwable { + private FakeDownloadAction post() { runOnMainThread( new Runnable() { @Override @@ -592,53 +537,35 @@ public class DownloadManagerTest { private FakeDownloadAction assertStarted() throws InterruptedException { downloader.assertStarted(ASSERT_TRUE_TIMEOUT); - return assertState(DownloadState.STATE_STARTED); + return assertState(TaskState.STATE_STARTED); } - private FakeDownloadAction assertEnded() { - return assertState(DownloadState.STATE_ENDED); + private FakeDownloadAction assertCompleted() { + return assertState(TaskState.STATE_COMPLETED); } - private FakeDownloadAction assertError() { - return assertState(DownloadState.STATE_ERROR); + private FakeDownloadAction assertFailed() { + return assertState(TaskState.STATE_FAILED); } - private FakeDownloadAction assertCancelled() { - return assertState(DownloadState.STATE_CANCELED); + private FakeDownloadAction assertCanceled() { + return assertState(TaskState.STATE_CANCELED); } private FakeDownloadAction assertStopped() { - return assertState(DownloadState.STATE_QUEUED); + return assertState(TaskState.STATE_QUEUED); } private FakeDownloadAction assertState(@State int expectedState) { - ArrayList receivedStates = new ArrayList<>(); while (true) { Integer state = null; try { - state = states.poll(ASSERT_TRUE_TIMEOUT, TimeUnit.MILLISECONDS); + state = downloadManagerListener.pollStateChange(this, ASSERT_TRUE_TIMEOUT); } catch (InterruptedException e) { fail(e.getMessage()); } - if (state != null) { - if (expectedState == state) { - return this; - } - receivedStates.add(state); - } else { - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < receivedStates.size(); i++) { - if (i > 0) { - sb.append(','); - } - sb.append(DownloadState.getStateString(receivedStates.get(i))); - } - fail( - String.format( - Locale.US, - "expected:<%s> but was:<%s>", - DownloadState.getStateString(expectedState), - sb)); + if (expectedState == state) { + return this; } } } @@ -652,37 +579,27 @@ public class DownloadManagerTest { downloader.ignoreInterrupts = true; return this; } - - private void onStateChange(int state) { - states.add(state); - } } private static class FakeDownloader implements Downloader { private final com.google.android.exoplayer2.util.ConditionVariable blocker; - private final boolean removeAction; + private final boolean isRemoveAction; private CountDownLatch started; private boolean ignoreInterrupts; private volatile boolean enableDownloadIOException; private volatile int downloadedBytes = C.LENGTH_UNSET; - private FakeDownloader(boolean removeAction) { - this.removeAction = removeAction; + private FakeDownloader(boolean isRemoveAction) { + this.isRemoveAction = isRemoveAction; this.started = new CountDownLatch(1); this.blocker = new com.google.android.exoplayer2.util.ConditionVariable(); } @Override - public void init() throws InterruptedException, IOException { - // do nothing. - } - - @Override - public void download(@Nullable ProgressListener listener) - throws InterruptedException, IOException { - assertThat(removeAction).isFalse(); + public void download() throws InterruptedException, IOException { + assertThat(isRemoveAction).isFalse(); started.countDown(); block(); if (enableDownloadIOException) { @@ -690,9 +607,14 @@ public class DownloadManagerTest { } } + @Override + public void cancel() { + // Do nothing. + } + @Override public void remove() throws InterruptedException { - assertThat(removeAction).isTrue(); + assertThat(isRemoveAction).isTrue(); started.countDown(); block(); } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/offline/ProgressiveDownloadActionTest.java b/library/core/src/test/java/com/google/android/exoplayer2/offline/ProgressiveDownloadActionTest.java index e5b67a6da6..bc3732e3d3 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/offline/ProgressiveDownloadActionTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/offline/ProgressiveDownloadActionTest.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.offline; import static com.google.common.truth.Truth.assertThat; +import android.net.Uri; import com.google.android.exoplayer2.upstream.DummyDataSource; import com.google.android.exoplayer2.upstream.cache.Cache; import java.io.ByteArrayInputStream; @@ -24,6 +25,7 @@ import java.io.ByteArrayOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mockito; @@ -36,22 +38,31 @@ import org.robolectric.RobolectricTestRunner; @RunWith(RobolectricTestRunner.class) public class ProgressiveDownloadActionTest { - @Test - public void testDownloadActionIsNotRemoveAction() throws Exception { - ProgressiveDownloadAction action = new ProgressiveDownloadAction("uri", null, false, null); - assertThat(action.isRemoveAction()).isFalse(); + private Uri uri1; + private Uri uri2; + + @Before + public void setUp() { + uri1 = Uri.parse("http://test1.uri"); + uri2 = Uri.parse("http://test2.uri"); } @Test - public void testRemoveActionIsRemoveAction() throws Exception { - ProgressiveDownloadAction action2 = new ProgressiveDownloadAction("uri", null, true, null); - assertThat(action2.isRemoveAction()).isTrue(); + public void testDownloadActionIsNotRemoveAction() throws Exception { + ProgressiveDownloadAction action = new ProgressiveDownloadAction(uri1, false, null, null); + assertThat(action.isRemoveAction).isFalse(); + } + + @Test + public void testRemoveActionisRemoveAction() throws Exception { + ProgressiveDownloadAction action2 = new ProgressiveDownloadAction(uri1, true, null, null); + assertThat(action2.isRemoveAction).isTrue(); } @Test public void testCreateDownloader() throws Exception { MockitoAnnotations.initMocks(this); - ProgressiveDownloadAction action = new ProgressiveDownloadAction("uri", null, false, null); + ProgressiveDownloadAction action = new ProgressiveDownloadAction(uri1, false, null, null); DownloaderConstructorHelper constructorHelper = new DownloaderConstructorHelper( Mockito.mock(Cache.class), DummyDataSource.FACTORY); assertThat(action.createDownloader(constructorHelper)).isNotNull(); @@ -59,75 +70,75 @@ public class ProgressiveDownloadActionTest { @Test public void testSameUriCacheKeyDifferentAction_IsSameMedia() throws Exception { - ProgressiveDownloadAction action1 = new ProgressiveDownloadAction("uri", null, true, null); - ProgressiveDownloadAction action2 = new ProgressiveDownloadAction("uri", null, false, null); + ProgressiveDownloadAction action1 = new ProgressiveDownloadAction(uri1, true, null, null); + ProgressiveDownloadAction action2 = new ProgressiveDownloadAction(uri1, false, null, null); assertSameMedia(action1, action2); } @Test public void testNullCacheKeyDifferentUriAction_IsNotSameMedia() throws Exception { - ProgressiveDownloadAction action3 = new ProgressiveDownloadAction("uri2", null, true, null); - ProgressiveDownloadAction action4 = new ProgressiveDownloadAction("uri", null, false, null); + ProgressiveDownloadAction action3 = new ProgressiveDownloadAction(uri2, true, null, null); + ProgressiveDownloadAction action4 = new ProgressiveDownloadAction(uri1, false, null, null); assertNotSameMedia(action3, action4); } @Test public void testSameCacheKeyDifferentUriAction_IsSameMedia() throws Exception { - ProgressiveDownloadAction action5 = new ProgressiveDownloadAction("uri2", "key", true, null); - ProgressiveDownloadAction action6 = new ProgressiveDownloadAction("uri", "key", false, null); + ProgressiveDownloadAction action5 = new ProgressiveDownloadAction(uri2, true, null, "key"); + ProgressiveDownloadAction action6 = new ProgressiveDownloadAction(uri1, false, null, "key"); assertSameMedia(action5, action6); } @Test public void testSameUriDifferentCacheKeyAction_IsNotSameMedia() throws Exception { - ProgressiveDownloadAction action7 = new ProgressiveDownloadAction("uri", "key", true, null); - ProgressiveDownloadAction action8 = new ProgressiveDownloadAction("uri", "key2", false, null); + ProgressiveDownloadAction action7 = new ProgressiveDownloadAction(uri1, true, null, "key"); + ProgressiveDownloadAction action8 = new ProgressiveDownloadAction(uri1, false, null, "key2"); assertNotSameMedia(action7, action8); } @Test public void testSameUriNullCacheKeyAction_IsNotSameMedia() throws Exception { - ProgressiveDownloadAction action1 = new ProgressiveDownloadAction("uri", "key", true, null); - ProgressiveDownloadAction action2 = new ProgressiveDownloadAction("uri", null, false, null); + ProgressiveDownloadAction action1 = new ProgressiveDownloadAction(uri1, true, null, "key"); + ProgressiveDownloadAction action2 = new ProgressiveDownloadAction(uri1, false, null, null); assertNotSameMedia(action1, action2); } @Test public void testEquals() throws Exception { - ProgressiveDownloadAction action1 = new ProgressiveDownloadAction("uri", null, true, null); + ProgressiveDownloadAction action1 = new ProgressiveDownloadAction(uri1, true, null, null); assertThat(action1.equals(action1)).isTrue(); - ProgressiveDownloadAction action2 = new ProgressiveDownloadAction("uri", null, true, null); - ProgressiveDownloadAction action3 = new ProgressiveDownloadAction("uri", null, true, null); + ProgressiveDownloadAction action2 = new ProgressiveDownloadAction(uri1, true, null, null); + ProgressiveDownloadAction action3 = new ProgressiveDownloadAction(uri1, true, null, null); assertThat(action2.equals(action3)).isTrue(); - ProgressiveDownloadAction action4 = new ProgressiveDownloadAction("uri", null, true, null); - ProgressiveDownloadAction action5 = new ProgressiveDownloadAction("uri", null, false, null); + ProgressiveDownloadAction action4 = new ProgressiveDownloadAction(uri1, true, null, null); + ProgressiveDownloadAction action5 = new ProgressiveDownloadAction(uri1, false, null, null); assertThat(action4.equals(action5)).isFalse(); - ProgressiveDownloadAction action6 = new ProgressiveDownloadAction("uri", null, true, null); - ProgressiveDownloadAction action7 = new ProgressiveDownloadAction("uri", "key", true, null); + ProgressiveDownloadAction action6 = new ProgressiveDownloadAction(uri1, true, null, null); + ProgressiveDownloadAction action7 = new ProgressiveDownloadAction(uri1, true, null, "key"); assertThat(action6.equals(action7)).isFalse(); - ProgressiveDownloadAction action8 = new ProgressiveDownloadAction("uri", "key2", true, null); - ProgressiveDownloadAction action9 = new ProgressiveDownloadAction("uri", "key", true, null); + ProgressiveDownloadAction action8 = new ProgressiveDownloadAction(uri1, true, null, "key2"); + ProgressiveDownloadAction action9 = new ProgressiveDownloadAction(uri1, true, null, "key"); assertThat(action8.equals(action9)).isFalse(); - ProgressiveDownloadAction action10 = new ProgressiveDownloadAction("uri", null, true, null); - ProgressiveDownloadAction action11 = new ProgressiveDownloadAction("uri2", null, true, null); + ProgressiveDownloadAction action10 = new ProgressiveDownloadAction(uri1, true, null, null); + ProgressiveDownloadAction action11 = new ProgressiveDownloadAction(uri2, true, null, null); assertThat(action10.equals(action11)).isFalse(); } @Test public void testSerializerGetType() throws Exception { - ProgressiveDownloadAction action = new ProgressiveDownloadAction("uri", null, false, null); - assertThat(action.getType()).isNotNull(); + ProgressiveDownloadAction action = new ProgressiveDownloadAction(uri1, false, null, null); + assertThat(action.type).isNotNull(); } @Test public void testSerializerWriteRead() throws Exception { - doTestSerializationRoundTrip(new ProgressiveDownloadAction("uri1", null, false, null)); - doTestSerializationRoundTrip(new ProgressiveDownloadAction("uri2", "key", true, null)); + doTestSerializationRoundTrip(new ProgressiveDownloadAction(uri1, false, null, null)); + doTestSerializationRoundTrip(new ProgressiveDownloadAction(uri2, true, null, "key")); } private void assertSameMedia( @@ -142,18 +153,19 @@ public class ProgressiveDownloadActionTest { assertThat(action2.isSameMedia(action1)).isFalse(); } - private static void doTestSerializationRoundTrip(ProgressiveDownloadAction action1) + private static void doTestSerializationRoundTrip(ProgressiveDownloadAction action) throws IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(); DataOutputStream output = new DataOutputStream(out); - action1.writeToStream(output); + DownloadAction.serializeToStream(action, output); ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); DataInputStream input = new DataInputStream(in); DownloadAction action2 = - ProgressiveDownloadAction.DESERIALIZER.readFromStream(DownloadAction.MASTER_VERSION, input); + DownloadAction.deserializeFromStream( + new DownloadAction.Deserializer[] {ProgressiveDownloadAction.DESERIALIZER}, input); - assertThat(action2).isEqualTo(action1); + assertThat(action2).isEqualTo(action); } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java index 4415d641e3..2ba63d6773 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java @@ -25,6 +25,8 @@ import static org.mockito.Mockito.verify; import static org.mockito.MockitoAnnotations.initMocks; import android.os.Parcel; +import android.util.SparseArray; +import android.util.SparseBooleanArray; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; @@ -34,6 +36,7 @@ import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.Parameters; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.ParametersBuilder; +import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.SelectionOverride; import com.google.android.exoplayer2.trackselection.TrackSelector.InvalidationListener; import com.google.android.exoplayer2.util.MimeTypes; import java.util.HashMap; @@ -119,8 +122,18 @@ public final class DefaultTrackSelectorTest { /** Tests {@link Parameters} {@link android.os.Parcelable} implementation. */ @Test public void testParametersParcelable() { + SparseArray> selectionOverrides = new SparseArray<>(); + Map videoOverrides = new HashMap<>(); + videoOverrides.put(new TrackGroupArray(VIDEO_TRACK_GROUP), new SelectionOverride(0, 1)); + selectionOverrides.put(2, videoOverrides); + + SparseBooleanArray rendererDisabledFlags = new SparseBooleanArray(); + rendererDisabledFlags.put(3, true); + Parameters parametersToParcel = new Parameters( + selectionOverrides, + rendererDisabledFlags, /* preferredAudioLanguage= */ "en", /* preferredTextLanguage= */ "de", /* selectUndeterminedTextLanguage= */ false, @@ -135,7 +148,8 @@ public final class DefaultTrackSelectorTest { /* exceedRendererCapabilitiesIfNecessary= */ true, /* viewportWidth= */ 4, /* viewportHeight= */ 5, - /* viewportOrientationMayChange= */ false); + /* viewportOrientationMayChange= */ false, + /* tunnelingAudioSessionId= */ C.AUDIO_SESSION_ID_UNSET); Parcel parcel = Parcel.obtain(); parametersToParcel.writeToParcel(parcel, 0); @@ -147,11 +161,32 @@ public final class DefaultTrackSelectorTest { parcel.recycle(); } + /** Tests {@link SelectionOverride}'s {@link android.os.Parcelable} implementation. */ + @Test + public void testSelectionOverrideParcelable() { + int[] tracks = new int[] {2, 3}; + SelectionOverride selectionOverrideToParcel = + new SelectionOverride(/* groupIndex= */ 1, tracks); + + Parcel parcel = Parcel.obtain(); + selectionOverrideToParcel.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + + SelectionOverride selectionOverrideFromParcel = + SelectionOverride.CREATOR.createFromParcel(parcel); + assertThat(selectionOverrideFromParcel).isEqualTo(selectionOverrideToParcel); + + parcel.recycle(); + } + /** Tests that a null override clears a track selection. */ @Test public void testSelectTracksWithNullOverride() throws ExoPlaybackException { DefaultTrackSelector trackSelector = new DefaultTrackSelector(); - trackSelector.setSelectionOverride(0, new TrackGroupArray(VIDEO_TRACK_GROUP), null); + trackSelector.setParameters( + trackSelector + .buildUponParameters() + .setSelectionOverride(0, new TrackGroupArray(VIDEO_TRACK_GROUP), null)); TrackSelectorResult result = trackSelector.selectTracks(RENDERER_CAPABILITIES, TRACK_GROUPS); assertTrackSelections(result, new TrackSelection[] {null, TRACK_SELECTIONS[1]}); assertThat(result.rendererConfigurations) @@ -162,8 +197,11 @@ public final class DefaultTrackSelectorTest { @Test public void testSelectTracksWithClearedNullOverride() throws ExoPlaybackException { DefaultTrackSelector trackSelector = new DefaultTrackSelector(); - trackSelector.setSelectionOverride(0, new TrackGroupArray(VIDEO_TRACK_GROUP), null); - trackSelector.clearSelectionOverride(0, new TrackGroupArray(VIDEO_TRACK_GROUP)); + trackSelector.setParameters( + trackSelector + .buildUponParameters() + .setSelectionOverride(0, new TrackGroupArray(VIDEO_TRACK_GROUP), null) + .clearSelectionOverride(0, new TrackGroupArray(VIDEO_TRACK_GROUP))); TrackSelectorResult result = trackSelector.selectTracks(RENDERER_CAPABILITIES, TRACK_GROUPS); assertTrackSelections(result, TRACK_SELECTIONS); assertThat(result.rendererConfigurations) @@ -174,7 +212,10 @@ public final class DefaultTrackSelectorTest { @Test public void testSelectTracksWithNullOverrideForDifferentTracks() throws ExoPlaybackException { DefaultTrackSelector trackSelector = new DefaultTrackSelector(); - trackSelector.setSelectionOverride(0, new TrackGroupArray(VIDEO_TRACK_GROUP), null); + trackSelector.setParameters( + trackSelector + .buildUponParameters() + .setSelectionOverride(0, new TrackGroupArray(VIDEO_TRACK_GROUP), null)); TrackSelectorResult result = trackSelector.selectTracks( RENDERER_CAPABILITIES, @@ -188,7 +229,7 @@ public final class DefaultTrackSelectorTest { @Test public void testSelectTracksWithDisabledRenderer() throws ExoPlaybackException { DefaultTrackSelector trackSelector = new DefaultTrackSelector(); - trackSelector.setRendererDisabled(1, true); + trackSelector.setParameters(trackSelector.buildUponParameters().setRendererDisabled(1, true)); TrackSelectorResult result = trackSelector.selectTracks(RENDERER_CAPABILITIES, TRACK_GROUPS); assertTrackSelections(result, new TrackSelection[] {TRACK_SELECTIONS[0], null}); assertThat(new RendererConfiguration[] {DEFAULT, null}) @@ -199,8 +240,11 @@ public final class DefaultTrackSelectorTest { @Test public void testSelectTracksWithClearedDisabledRenderer() throws ExoPlaybackException { DefaultTrackSelector trackSelector = new DefaultTrackSelector(); - trackSelector.setRendererDisabled(1, true); - trackSelector.setRendererDisabled(1, false); + trackSelector.setParameters( + trackSelector + .buildUponParameters() + .setRendererDisabled(1, true) + .setRendererDisabled(1, false)); TrackSelectorResult result = trackSelector.selectTracks(RENDERER_CAPABILITIES, TRACK_GROUPS); assertTrackSelections(result, TRACK_SELECTIONS); assertThat(new RendererConfiguration[] {DEFAULT, DEFAULT}) @@ -222,7 +266,7 @@ public final class DefaultTrackSelectorTest { @Test public void testSelectTracksWithDisabledNoSampleRenderer() throws ExoPlaybackException { DefaultTrackSelector trackSelector = new DefaultTrackSelector(); - trackSelector.setRendererDisabled(1, true); + trackSelector.setParameters(trackSelector.buildUponParameters().setRendererDisabled(1, true)); TrackSelectorResult result = trackSelector.selectTracks(RENDERER_CAPABILITIES_WITH_NO_SAMPLE_RENDERER, TRACK_GROUPS); assertTrackSelections(result, TRACK_SELECTIONS_WITH_NO_SAMPLE_RENDERER); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/MappingTrackSelectorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/MappingTrackSelectorTest.java index ab6cea94ad..fa3d74b15f 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/MappingTrackSelectorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/MappingTrackSelectorTest.java @@ -93,7 +93,7 @@ public final class MappingTrackSelectorTest { /** * A {@link MappingTrackSelector} that stashes the {@link MappedTrackInfo} passed to {@link - * #selectTracks(RendererCapabilities[], MappedTrackInfo)}. + * #selectTracks(MappedTrackInfo, int[][][], int[])}. */ private static final class FakeMappingTrackSelector extends MappingTrackSelector { @@ -101,12 +101,14 @@ public final class MappingTrackSelectorTest { @Override protected Pair selectTracks( - RendererCapabilities[] rendererCapabilities, MappedTrackInfo mappedTrackInfo) + MappedTrackInfo mappedTrackInfo, + int[][][] rendererFormatSupports, + int[] rendererMixedMimeTypeAdaptationSupports) throws ExoPlaybackException { + int rendererCount = mappedTrackInfo.getRendererCount(); lastMappedTrackInfo = mappedTrackInfo; return Pair.create( - new RendererConfiguration[rendererCapabilities.length], - new TrackSelection[rendererCapabilities.length]); + new RendererConfiguration[rendererCount], new TrackSelection[rendererCount]); } public void assertMappedTrackGroups(int rendererIndex, TrackGroup... expected) { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java index 09be138abe..8dc702d3a3 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java @@ -220,7 +220,7 @@ public final class CacheDataSourceTest { .newDefaultData() .appendReadData(1024 * 1024) .endData()); - CacheUtil.cache(dataSpec, cache, upstream2, null); + CacheUtil.cache(dataSpec, cache, upstream2, /* counters= */ null, /* isCanceled= */ null); // Read the rest of the data. TestUtil.readToEnd(cacheDataSource); @@ -271,7 +271,7 @@ public final class CacheDataSourceTest { .newDefaultData() .appendReadData(1024 * 1024) .endData()); - CacheUtil.cache(dataSpec, cache, upstream2, null); + CacheUtil.cache(dataSpec, cache, upstream2, /* counters= */ null, /* isCanceled= */ null); // Read the rest of the data. TestUtil.readToEnd(cacheDataSource); @@ -287,7 +287,7 @@ public final class CacheDataSourceTest { // Cache the latter half of the data. DataSpec dataSpec = new DataSpec(testDataUri, 512, C.LENGTH_UNSET, testDataKey); - CacheUtil.cache(dataSpec, cache, upstream, null); + CacheUtil.cache(dataSpec, cache, upstream, /* counters= */ null, /* isCanceled= */ null); // Create cache read-only CacheDataSource. CacheDataSource cacheDataSource = @@ -318,7 +318,7 @@ public final class CacheDataSourceTest { // Cache the latter half of the data. int halfDataLength = 512; DataSpec dataSpec = new DataSpec(testDataUri, halfDataLength, C.LENGTH_UNSET, testDataKey); - CacheUtil.cache(dataSpec, cache, upstream, null); + CacheUtil.cache(dataSpec, cache, upstream, /* counters= */ null, /* isCanceled= */ null); // Create blocking CacheDataSource. CacheDataSource cacheDataSource = diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheUtilTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheUtilTest.java index 7237ecd50d..61c7f2b673 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheUtilTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheUtilTest.java @@ -174,7 +174,8 @@ public final class CacheUtilTest { FakeDataSource dataSource = new FakeDataSource(fakeDataSet); CachingCounters counters = new CachingCounters(); - CacheUtil.cache(new DataSpec(Uri.parse("test_data")), cache, dataSource, counters); + CacheUtil.cache( + new DataSpec(Uri.parse("test_data")), cache, dataSource, counters, /* isCanceled= */ null); assertCounters(counters, 0, 100, 100); assertCachedData(cache, fakeDataSet); @@ -188,11 +189,11 @@ public final class CacheUtilTest { Uri testUri = Uri.parse("test_data"); DataSpec dataSpec = new DataSpec(testUri, 10, 20, null); CachingCounters counters = new CachingCounters(); - CacheUtil.cache(dataSpec, cache, dataSource, counters); + CacheUtil.cache(dataSpec, cache, dataSource, counters, /* isCanceled= */ null); assertCounters(counters, 0, 20, 20); - CacheUtil.cache(new DataSpec(testUri), cache, dataSource, counters); + CacheUtil.cache(new DataSpec(testUri), cache, dataSource, counters, /* isCanceled= */ null); assertCounters(counters, 20, 80, 100); assertCachedData(cache, fakeDataSet); @@ -207,7 +208,7 @@ public final class CacheUtilTest { DataSpec dataSpec = new DataSpec(Uri.parse("test_data")); CachingCounters counters = new CachingCounters(); - CacheUtil.cache(dataSpec, cache, dataSource, counters); + CacheUtil.cache(dataSpec, cache, dataSource, counters, /* isCanceled= */ null); assertCounters(counters, 0, 100, 100); assertCachedData(cache, fakeDataSet); @@ -223,11 +224,11 @@ public final class CacheUtilTest { Uri testUri = Uri.parse("test_data"); DataSpec dataSpec = new DataSpec(testUri, 10, 20, null); CachingCounters counters = new CachingCounters(); - CacheUtil.cache(dataSpec, cache, dataSource, counters); + CacheUtil.cache(dataSpec, cache, dataSource, counters, /* isCanceled= */ null); assertCounters(counters, 0, 20, 20); - CacheUtil.cache(new DataSpec(testUri), cache, dataSource, counters); + CacheUtil.cache(new DataSpec(testUri), cache, dataSource, counters, /* isCanceled= */ null); assertCounters(counters, 20, 80, 100); assertCachedData(cache, fakeDataSet); @@ -241,7 +242,7 @@ public final class CacheUtilTest { Uri testUri = Uri.parse("test_data"); DataSpec dataSpec = new DataSpec(testUri, 0, 1000, null); CachingCounters counters = new CachingCounters(); - CacheUtil.cache(dataSpec, cache, dataSource, counters); + CacheUtil.cache(dataSpec, cache, dataSource, counters, /* isCanceled= */ null); assertCounters(counters, 0, 100, 1000); assertCachedData(cache, fakeDataSet); @@ -256,9 +257,16 @@ public final class CacheUtilTest { DataSpec dataSpec = new DataSpec(testUri, 0, 1000, null); try { - CacheUtil.cache(dataSpec, cache, new CacheDataSource(cache, dataSource), - new byte[CacheUtil.DEFAULT_BUFFER_SIZE_BYTES], null, 0, null, - /*enableEOFException*/ true); + CacheUtil.cache( + dataSpec, + cache, + new CacheDataSource(cache, dataSource), + new byte[CacheUtil.DEFAULT_BUFFER_SIZE_BYTES], + /* priorityTaskManager= */ null, + /* priority= */ 0, + /* counters= */ null, + /* isCanceled= */ null, + /* enableEOFException= */ true); fail(); } catch (EOFException e) { // Do nothing. @@ -286,7 +294,8 @@ public final class CacheUtilTest { .appendReadData(TestUtil.buildTestData(100)).endData(); FakeDataSource dataSource = new FakeDataSource(fakeDataSet); - CacheUtil.cache(new DataSpec(Uri.parse("test_data")), cache, dataSource, counters); + CacheUtil.cache( + new DataSpec(Uri.parse("test_data")), cache, dataSource, counters, /* isCanceled= */ null); assertCounters(counters, 0, 300, 300); assertCachedData(cache, fakeDataSet); @@ -298,10 +307,17 @@ public final class CacheUtilTest { FakeDataSource dataSource = new FakeDataSource(fakeDataSet); Uri uri = Uri.parse("test_data"); - CacheUtil.cache(new DataSpec(uri), cache, + CacheUtil.cache( + new DataSpec(uri), + cache, // set maxCacheFileSize to 10 to make sure there are multiple spans new CacheDataSource(cache, dataSource, 0, 10), - new byte[CacheUtil.DEFAULT_BUFFER_SIZE_BYTES], null, 0, null, true); + new byte[CacheUtil.DEFAULT_BUFFER_SIZE_BYTES], + /* priorityTaskManager= */ null, + /* priority= */ 0, + /* counters= */ null, + /* isCanceled= */ null, + true); CacheUtil.remove(cache, CacheUtil.generateKey(uri)); assertCacheEmpty(cache); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java index a35a86f933..15e2b80f59 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java @@ -296,17 +296,17 @@ public class SimpleCacheTest { } } - // @Test - // public void testMultipleSimpleCacheWithSameCacheDirThrowsException() throws Exception { - // new SimpleCache(cacheDir, new NoOpCacheEvictor()); - // - // try { - // new SimpleCache(cacheDir, new NoOpCacheEvictor()); - // assertWithMessage("Exception was expected").fail(); - // } catch (IllegalStateException e) { - // // Expected. Do nothing. - // } - // } + @Test + public void testMultipleSimpleCacheWithSameCacheDirThrowsException() throws Exception { + new SimpleCache(cacheDir, new NoOpCacheEvictor()); + + try { + new SimpleCache(cacheDir, new NoOpCacheEvictor()); + assertWithMessage("Exception was expected").fail(); + } catch (IllegalStateException e) { + // Expected. Do nothing. + } + } @Test public void testMultipleSimpleCacheWithSameCacheDirDoesNotThrowsExceptionAfterRelease() diff --git a/library/dash/build.gradle b/library/dash/build.gradle index d2692eb7d9..81b247d047 100644 --- a/library/dash/build.gradle +++ b/library/dash/build.gradle @@ -30,10 +30,15 @@ android { // testCoverageEnabled = true // } } + + lintOptions { + lintConfig file("../../checker-framework-lint.xml") + } } dependencies { implementation project(modulePrefix + 'library-core') + implementation 'org.checkerframework:checker-qual:' + checkerframeworkVersion implementation 'com.android.support:support-annotations:' + supportLibraryVersion testImplementation project(modulePrefix + 'testutils-robolectric') } diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashUtil.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashUtil.java index 2227044da7..6b481df46d 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashUtil.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashUtil.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.source.dash; import android.net.Uri; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.drm.DrmInitData; @@ -53,10 +54,7 @@ public final class DashUtil { */ public static DashManifest loadManifest(DataSource dataSource, Uri uri) throws IOException { - ParsingLoadable loadable = - new ParsingLoadable<>(dataSource, uri, C.DATA_TYPE_MANIFEST, new DashManifestParser()); - loadable.load(); - return loadable.getResult(); + return ParsingLoadable.load(dataSource, new DashManifestParser(), uri); } /** @@ -68,7 +66,7 @@ public final class DashUtil { * @throws IOException Thrown when there is an error while loading. * @throws InterruptedException Thrown if the thread was interrupted. */ - public static DrmInitData loadDrmInitData(DataSource dataSource, Period period) + public static @Nullable DrmInitData loadDrmInitData(DataSource dataSource, Period period) throws IOException, InterruptedException { int primaryTrackType = C.TRACK_TYPE_VIDEO; Representation representation = getFirstRepresentation(period, primaryTrackType); @@ -90,15 +88,16 @@ public final class DashUtil { * Loads initialization data for the {@code representation} and returns the sample {@link Format}. * * @param dataSource The source from which the data should be loaded. - * @param trackType The type of the representation. Typically one of the - * {@link com.google.android.exoplayer2.C} {@code TRACK_TYPE_*} constants. + * @param trackType The type of the representation. Typically one of the {@link + * com.google.android.exoplayer2.C} {@code TRACK_TYPE_*} constants. * @param representation The representation which initialization chunk belongs to. * @return the sample {@link Format} of the given representation. * @throws IOException Thrown when there is an error while loading. * @throws InterruptedException Thrown if the thread was interrupted. */ - public static Format loadSampleFormat(DataSource dataSource, int trackType, - Representation representation) throws IOException, InterruptedException { + public static @Nullable Format loadSampleFormat( + DataSource dataSource, int trackType, Representation representation) + throws IOException, InterruptedException { ChunkExtractorWrapper extractorWrapper = loadInitializationData(dataSource, trackType, representation, false); return extractorWrapper == null ? null : extractorWrapper.getSampleFormats()[0]; @@ -109,28 +108,29 @@ public final class DashUtil { * ChunkIndex}. * * @param dataSource The source from which the data should be loaded. - * @param trackType The type of the representation. Typically one of the - * {@link com.google.android.exoplayer2.C} {@code TRACK_TYPE_*} constants. + * @param trackType The type of the representation. Typically one of the {@link + * com.google.android.exoplayer2.C} {@code TRACK_TYPE_*} constants. * @param representation The representation which initialization chunk belongs to. * @return The {@link ChunkIndex} of the given representation, or null if no initialization or * index data exists. * @throws IOException Thrown when there is an error while loading. * @throws InterruptedException Thrown if the thread was interrupted. */ - public static ChunkIndex loadChunkIndex(DataSource dataSource, int trackType, - Representation representation) throws IOException, InterruptedException { + public static @Nullable ChunkIndex loadChunkIndex( + DataSource dataSource, int trackType, Representation representation) + throws IOException, InterruptedException { ChunkExtractorWrapper extractorWrapper = loadInitializationData(dataSource, trackType, representation, true); return extractorWrapper == null ? null : (ChunkIndex) extractorWrapper.getSeekMap(); } /** - * Loads initialization data for the {@code representation} and optionally index data then - * returns a {@link ChunkExtractorWrapper} which contains the output. + * Loads initialization data for the {@code representation} and optionally index data then returns + * a {@link ChunkExtractorWrapper} which contains the output. * * @param dataSource The source from which the data should be loaded. - * @param trackType The type of the representation. Typically one of the - * {@link com.google.android.exoplayer2.C} {@code TRACK_TYPE_*} constants. + * @param trackType The type of the representation. Typically one of the {@link + * com.google.android.exoplayer2.C} {@code TRACK_TYPE_*} constants. * @param representation The representation which initialization chunk belongs to. * @param loadIndex Whether to load index data too. * @return A {@link ChunkExtractorWrapper} for the {@code representation}, or null if no @@ -138,8 +138,9 @@ public final class DashUtil { * @throws IOException Thrown when there is an error while loading. * @throws InterruptedException Thrown if the thread was interrupted. */ - private static ChunkExtractorWrapper loadInitializationData(DataSource dataSource, int trackType, - Representation representation, boolean loadIndex) throws IOException, InterruptedException { + private static @Nullable ChunkExtractorWrapper loadInitializationData( + DataSource dataSource, int trackType, Representation representation, boolean loadIndex) + throws IOException, InterruptedException { RangedUri initializationUri = representation.getInitializationUri(); if (initializationUri == null) { return null; @@ -178,13 +179,15 @@ public final class DashUtil { private static ChunkExtractorWrapper newWrappedExtractor(int trackType, Format format) { String mimeType = format.containerMimeType; - boolean isWebm = mimeType.startsWith(MimeTypes.VIDEO_WEBM) - || mimeType.startsWith(MimeTypes.AUDIO_WEBM); + boolean isWebm = + mimeType != null + && (mimeType.startsWith(MimeTypes.VIDEO_WEBM) + || mimeType.startsWith(MimeTypes.AUDIO_WEBM)); Extractor extractor = isWebm ? new MatroskaExtractor() : new FragmentedMp4Extractor(); return new ChunkExtractorWrapper(extractor, trackType, format); } - private static Representation getFirstRepresentation(Period period, int type) { + private static @Nullable Representation getFirstRepresentation(Period period, int type) { int index = period.getAdaptationSetIndex(type); if (index == C.INDEX_UNSET) { return null; diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/EventSampleStream.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/EventSampleStream.java index 694f9f843e..7fef59f6a1 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/EventSampleStream.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/EventSampleStream.java @@ -45,8 +45,10 @@ import java.io.IOException; EventSampleStream(EventStream eventStream, Format upstreamFormat, boolean eventStreamUpdatable) { this.upstreamFormat = upstreamFormat; + this.eventStream = eventStream; eventMessageEncoder = new EventMessageEncoder(); pendingSeekPositionUs = C.TIME_UNSET; + eventTimesUs = eventStream.presentationTimesUs; updateEventStream(eventStream, eventStreamUpdatable); } diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/PlayerEmsgHandler.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/PlayerEmsgHandler.java index affeeafe50..1bb08c4398 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/PlayerEmsgHandler.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/PlayerEmsgHandler.java @@ -100,6 +100,7 @@ public final class PlayerEmsgHandler implements Handler.Callback { * messages that generate DASH media source events. * @param allocator An {@link Allocator} from which allocations can be obtained. */ + @SuppressWarnings("nullness") public PlayerEmsgHandler( DashManifest manifest, PlayerEmsgCallback playerEmsgCallback, Allocator allocator) { this.manifest = manifest; @@ -237,11 +238,10 @@ public final class PlayerEmsgHandler implements Handler.Callback { // Internal methods. private void handleManifestExpiredMessage(long eventTimeUs, long manifestPublishTimeMsInEmsg) { - if (!manifestPublishTimeToExpiryTimeUs.containsKey(manifestPublishTimeMsInEmsg)) { + Long previousExpiryTimeUs = manifestPublishTimeToExpiryTimeUs.get(manifestPublishTimeMsInEmsg); + if (previousExpiryTimeUs == null) { manifestPublishTimeToExpiryTimeUs.put(manifestPublishTimeMsInEmsg, eventTimeUs); } else { - long previousExpiryTimeUs = - manifestPublishTimeToExpiryTimeUs.get(manifestPublishTimeMsInEmsg); if (previousExpiryTimeUs > eventTimeUs) { manifestPublishTimeToExpiryTimeUs.put(manifestPublishTimeMsInEmsg, eventTimeUs); } @@ -253,10 +253,7 @@ public final class PlayerEmsgHandler implements Handler.Callback { notifySourceMediaPresentationEnded(); } - private Map.Entry ceilingExpiryEntryForPublishTime(long publishTimeMs) { - if (manifestPublishTimeToExpiryTimeUs.isEmpty()) { - return null; - } + private @Nullable Map.Entry ceilingExpiryEntryForPublishTime(long publishTimeMs) { return manifestPublishTimeToExpiryTimeUs.ceilingEntry(publishTimeMs); } diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java index 95fe938fa4..639ad32d78 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.source.dash.manifest; import android.net.Uri; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.offline.FilterableManifest; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedList; @@ -26,7 +27,7 @@ import java.util.List; * Represents a DASH media presentation description (mpd), as defined by ISO/IEC 23009-1:2014 * Section 5.3.1.2. */ -public class DashManifest { +public class DashManifest implements FilterableManifest { /** * The {@code availabilityStartTime} value in milliseconds since epoch, or {@link C#TIME_UNSET} if @@ -121,16 +122,9 @@ public class DashManifest { return C.msToUs(getPeriodDurationMs(index)); } - /** - * Creates a copy of this manifest which includes only the representations identified by the given - * keys. - * - * @param representationKeys List of keys for the representations to be included in the copy. - * @return A copy of this manifest with the selected representations. - * @throws IndexOutOfBoundsException If a key has an invalid index. - */ - public final DashManifest copy(List representationKeys) { - LinkedList keys = new LinkedList<>(representationKeys); + @Override + public final DashManifest copy(List streamKeys) { + LinkedList keys = new LinkedList<>(streamKeys); Collections.sort(keys); keys.add(new RepresentationKey(-1, -1, -1)); // Add a stopper key to the end diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Descriptor.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Descriptor.java index 18d0a937ab..0e21df64bb 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Descriptor.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Descriptor.java @@ -49,7 +49,7 @@ public final class Descriptor { } @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/FilteringDashManifestParser.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/FilteringDashManifestParser.java deleted file mode 100644 index 4e45a31183..0000000000 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/FilteringDashManifestParser.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright (C) 2018 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.source.dash.manifest; - -import android.net.Uri; -import android.support.annotation.Nullable; -import com.google.android.exoplayer2.upstream.ParsingLoadable.Parser; -import java.io.IOException; -import java.io.InputStream; -import java.util.List; - -/** - * A parser of media presentation description files which includes only the representations - * identified by the given keys. - */ -public final class FilteringDashManifestParser implements Parser { - - private final DashManifestParser dashManifestParser; - private final List filter; - - /** - * @param filter The representation keys that should be retained in the parsed manifests. If null, - * all representation are retained. - */ - public FilteringDashManifestParser(@Nullable List filter) { - this.dashManifestParser = new DashManifestParser(); - this.filter = filter; - } - - @Override - public DashManifest parse(Uri uri, InputStream inputStream) throws IOException { - DashManifest manifest = dashManifestParser.parse(uri, inputStream); - return filter != null ? manifest.copy(filter) : manifest; - } -} diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/RangedUri.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/RangedUri.java index c2a64718df..e226667337 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/RangedUri.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/RangedUri.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.source.dash.manifest; import android.net.Uri; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.UriUtil; @@ -46,7 +47,7 @@ public final class RangedUri { * @param length The length of the range, or {@link C#LENGTH_UNSET} to indicate that the range is * unbounded. */ - public RangedUri(String referenceUri, long start, long length) { + public RangedUri(@Nullable String referenceUri, long start, long length) { this.referenceUri = referenceUri == null ? "" : referenceUri; this.start = start; this.length = length; @@ -74,18 +75,18 @@ public final class RangedUri { /** * Attempts to merge this {@link RangedUri} with another and an optional common base uri. - *

- * A merge is successful if both instances define the same {@link Uri} after resolution with the - * base uri, and if one starts the byte after the other ends, forming a contiguous region with + * + *

A merge is successful if both instances define the same {@link Uri} after resolution with + * the base uri, and if one starts the byte after the other ends, forming a contiguous region with * no overlap. - *

- * If {@code other} is null then the merge is considered unsuccessful, and null is returned. + * + *

If {@code other} is null then the merge is considered unsuccessful, and null is returned. * * @param other The {@link RangedUri} to merge. * @param baseUri The optional base Uri. * @return The merged {@link RangedUri} if the merge was successful. Null otherwise. */ - public RangedUri attemptMerge(RangedUri other, String baseUri) { + public @Nullable RangedUri attemptMerge(@Nullable RangedUri other, String baseUri) { final String resolvedUri = resolveUriString(baseUri); if (other == null || !resolvedUri.equals(other.resolveUriString(baseUri))) { return null; @@ -113,7 +114,7 @@ public final class RangedUri { } @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } @@ -126,4 +127,15 @@ public final class RangedUri { && referenceUri.equals(other.referenceUri); } + @Override + public String toString() { + return "RangedUri(" + + "referenceUri=" + + referenceUri + + ", start=" + + start + + ", length=" + + length + + ")"; + } } diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/RepresentationKey.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/RepresentationKey.java index 4ce1d06700..fd9488af55 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/RepresentationKey.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/RepresentationKey.java @@ -15,14 +15,11 @@ */ package com.google.android.exoplayer2.source.dash.manifest; -import android.os.Parcel; -import android.os.Parcelable; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; -/** - * Uniquely identifies a {@link Representation} in a {@link DashManifest}. - */ -public final class RepresentationKey implements Parcelable, Comparable { +/** Uniquely identifies a {@link Representation} in a {@link DashManifest}. */ +public final class RepresentationKey implements Comparable { public final int periodIndex; public final int adaptationSetIndex; @@ -39,49 +36,8 @@ public final class RepresentationKey implements Parcelable, Comparable CREATOR = - new Creator() { - @Override - public RepresentationKey createFromParcel(Parcel in) { - return new RepresentationKey(in.readInt(), in.readInt(), in.readInt()); - } - - @Override - public RepresentationKey[] newArray(int size) { - return new RepresentationKey[size]; - } - }; - - // Comparable implementation. - - @Override - public int compareTo(@NonNull RepresentationKey o) { - int result = periodIndex - o.periodIndex; - if (result == 0) { - result = adaptationSetIndex - o.adaptationSetIndex; - if (result == 0) { - result = representationIndex - o.representationIndex; - } - } - return result; - } - - @Override - public boolean equals(Object o) { + public boolean equals(@Nullable Object o) { if (this == o) { return true; } @@ -103,4 +59,18 @@ public final class RepresentationKey implements Parcelable, Comparable { + private static final String TYPE = "dash"; + private static final int VERSION = 0; + public static final Deserializer DESERIALIZER = - new SegmentDownloadActionDeserializer() { + new SegmentDownloadActionDeserializer(TYPE, VERSION) { - @Override - public String getType() { - return TYPE; - } + @Override + protected RepresentationKey readKey(DataInputStream input) throws IOException { + return new RepresentationKey(input.readInt(), input.readInt(), input.readInt()); + } - @Override - protected RepresentationKey readKey(DataInputStream input) throws IOException { - return new RepresentationKey(input.readInt(), input.readInt(), input.readInt()); - } + @Override + protected DownloadAction createDownloadAction( + Uri uri, boolean isRemoveAction, byte[] data, List keys) { + return new DashDownloadAction(uri, isRemoveAction, data, keys); + } + }; - @Override - protected RepresentationKey[] createKeyArray(int keyCount) { - return new RepresentationKey[keyCount]; - } - - @Override - protected DownloadAction createDownloadAction(Uri manifestUri, boolean removeAction, - String data, RepresentationKey[] keys) { - return new DashDownloadAction(manifestUri, removeAction, data, keys); - } - - }; - - private static final String TYPE = "DashDownloadAction"; - - /** @see SegmentDownloadAction#SegmentDownloadAction(Uri, boolean, String, Object[]) */ + /** + * @param uri The DASH manifest URI. + * @param isRemoveAction Whether the data will be removed. If {@code false} it will be downloaded. + * @param data Optional custom data for this action. + * @param keys Keys of representations to be downloaded. If empty, all representations are + * downloaded. If {@code removeAction} is true, {@code keys} must be empty. + */ public DashDownloadAction( - Uri manifestUri, boolean removeAction, @Nullable String data, RepresentationKey... keys) { - super(manifestUri, removeAction, data, keys); - } - - @Override - protected String getType() { - return TYPE; + Uri uri, boolean isRemoveAction, @Nullable byte[] data, List keys) { + super(TYPE, VERSION, uri, isRemoveAction, data, keys); } @Override protected DashDownloader createDownloader(DownloaderConstructorHelper constructorHelper) { - DashDownloader downloader = new DashDownloader(manifestUri, constructorHelper); - if (!isRemoveAction()) { - downloader.selectRepresentations(keys); - } - return downloader; + return new DashDownloader(uri, keys, constructorHelper); } @Override diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadHelper.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadHelper.java new file mode 100644 index 0000000000..8a6069e477 --- /dev/null +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadHelper.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.source.dash.offline; + +import android.net.Uri; +import android.support.annotation.Nullable; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.offline.DownloadHelper; +import com.google.android.exoplayer2.offline.TrackKey; +import com.google.android.exoplayer2.source.TrackGroup; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.source.dash.manifest.AdaptationSet; +import com.google.android.exoplayer2.source.dash.manifest.DashManifest; +import com.google.android.exoplayer2.source.dash.manifest.DashManifestParser; +import com.google.android.exoplayer2.source.dash.manifest.Representation; +import com.google.android.exoplayer2.source.dash.manifest.RepresentationKey; +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.ParsingLoadable; +import com.google.android.exoplayer2.util.Assertions; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; + +/** A {@link DownloadHelper} for DASH streams. */ +public final class DashDownloadHelper extends DownloadHelper { + + private final Uri uri; + private final DataSource.Factory manifestDataSourceFactory; + + private @MonotonicNonNull DashManifest manifest; + + public DashDownloadHelper(Uri uri, DataSource.Factory manifestDataSourceFactory) { + this.uri = uri; + this.manifestDataSourceFactory = manifestDataSourceFactory; + } + + @Override + protected void prepareInternal() throws IOException { + manifest = + ParsingLoadable.load( + manifestDataSourceFactory.createDataSource(), new DashManifestParser(), uri); + } + + @Override + public int getPeriodCount() { + Assertions.checkNotNull(manifest); + return manifest.getPeriodCount(); + } + + @Override + public TrackGroupArray getTrackGroups(int periodIndex) { + Assertions.checkNotNull(manifest); + List adaptationSets = manifest.getPeriod(periodIndex).adaptationSets; + TrackGroup[] trackGroups = new TrackGroup[adaptationSets.size()]; + for (int i = 0; i < trackGroups.length; i++) { + List representations = adaptationSets.get(i).representations; + Format[] formats = new Format[representations.size()]; + int representationsCount = representations.size(); + for (int j = 0; j < representationsCount; j++) { + formats[j] = representations.get(j).format; + } + trackGroups[i] = new TrackGroup(formats); + } + return new TrackGroupArray(trackGroups); + } + + @Override + public DashDownloadAction getDownloadAction(@Nullable byte[] data, List trackKeys) { + return new DashDownloadAction( + uri, /* isRemoveAction= */ false, data, toRepresentationKeys(trackKeys)); + } + + @Override + public DashDownloadAction getRemoveAction(@Nullable byte[] data) { + return new DashDownloadAction( + uri, /* isRemoveAction= */ true, data, Collections.emptyList()); + } + + private static List toRepresentationKeys(List trackKeys) { + List representationKeys = new ArrayList<>(trackKeys.size()); + for (int i = 0; i < trackKeys.size(); i++) { + TrackKey trackKey = trackKeys.get(i); + representationKeys.add( + new RepresentationKey(trackKey.periodIndex, trackKey.groupIndex, trackKey.trackIndex)); + } + return representationKeys; + } +} diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloader.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloader.java index d042788814..6922e56b84 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloader.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloader.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.source.dash.offline; import android.net.Uri; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.extractor.ChunkIndex; import com.google.android.exoplayer2.offline.DownloadException; @@ -37,10 +38,7 @@ import java.util.ArrayList; import java.util.List; /** - * Helper class to download DASH streams. - * - *

Except {@link #getTotalSegments()}, {@link #getDownloadedSegments()} and {@link - * #getDownloadedBytes()}, this class isn't thread safe. + * A downloader for DASH streams. * *

Example usage: * @@ -49,16 +47,15 @@ import java.util.List; * DefaultHttpDataSourceFactory factory = new DefaultHttpDataSourceFactory("ExoPlayer", null); * DownloaderConstructorHelper constructorHelper = * new DownloaderConstructorHelper(cache, factory); - * DashDownloader dashDownloader = new DashDownloader(manifestUrl, constructorHelper); - * // Select the first representation of the first adaptation set of the first period - * dashDownloader.selectRepresentations(new RepresentationKey[] {new RepresentationKey(0, 0, 0)}); - * dashDownloader.download(new ProgressListener() { - * {@literal @}Override - * public void onDownloadProgress(Downloader downloader, float downloadPercentage, - * long downloadedBytes) { - * // Invoked periodically during the download. - * } - * }); + * // Create a downloader for the first representation of the first adaptation set of the first + * // period. + * DashDownloader dashDownloader = + * new DashDownloader( + * manifestUrl, + * Collections.singletonList(new RepresentationKey(0, 0, 0)), + * constructorHelper); + * // Perform the download. + * dashDownloader.download(); * // Access downloaded data using CacheDataSource * CacheDataSource cacheDataSource = * new CacheDataSource(cache, factory.createDataSource(), CacheDataSource.FLAG_BLOCK_ON_CACHE); @@ -67,26 +64,16 @@ import java.util.List; public final class DashDownloader extends SegmentDownloader { /** - * @see SegmentDownloader#SegmentDownloader(Uri, DownloaderConstructorHelper) + * @param manifestUri The {@link Uri} of the manifest to be downloaded. + * @param representationKeys Keys defining which representations in the manifest should be + * selected for download. If empty, all representations are downloaded. + * @param constructorHelper A {@link DownloaderConstructorHelper} instance. */ - public DashDownloader(Uri manifestUri, DownloaderConstructorHelper constructorHelper) { - super(manifestUri, constructorHelper); - } - - @Override - public RepresentationKey[] getAllRepresentationKeys() throws IOException { - ArrayList keys = new ArrayList<>(); - DashManifest manifest = getManifest(); - for (int periodIndex = 0; periodIndex < manifest.getPeriodCount(); periodIndex++) { - List adaptationSets = manifest.getPeriod(periodIndex).adaptationSets; - for (int adaptationIndex = 0; adaptationIndex < adaptationSets.size(); adaptationIndex++) { - int representationsCount = adaptationSets.get(adaptationIndex).representations.size(); - for (int i = 0; i < representationsCount; i++) { - keys.add(new RepresentationKey(periodIndex, adaptationIndex, i)); - } - } - } - return keys.toArray(new RepresentationKey[keys.size()]); + public DashDownloader( + Uri manifestUri, + List representationKeys, + DownloaderConstructorHelper constructorHelper) { + super(manifestUri, representationKeys, constructorHelper); } @Override @@ -95,77 +82,92 @@ public final class DashDownloader extends SegmentDownloader getSegments(DataSource dataSource, DashManifest manifest, - RepresentationKey[] keys, boolean allowIndexLoadErrors) + protected List getSegments( + DataSource dataSource, DashManifest manifest, boolean allowIncompleteList) throws InterruptedException, IOException { ArrayList segments = new ArrayList<>(); - for (RepresentationKey key : keys) { - DashSegmentIndex index; - try { - index = getSegmentIndex(dataSource, manifest, key); - if (index == null) { - // Loading succeeded but there was no index. This is always a failure. - throw new DownloadException("No index for representation: " + key); - } - } catch (IOException e) { - if (allowIndexLoadErrors) { - // Loading failed, but load errors are allowed. Advance to the next key. - continue; - } else { - throw e; - } - } - - long periodDurationUs = manifest.getPeriodDurationUs(key.periodIndex); - int segmentCount = index.getSegmentCount(periodDurationUs); - if (segmentCount == DashSegmentIndex.INDEX_UNBOUNDED) { - throw new DownloadException("Unbounded index for representation: " + key); - } - - Period period = manifest.getPeriod(key.periodIndex); - Representation representation = period.adaptationSets.get(key.adaptationSetIndex) - .representations.get(key.representationIndex); - long startUs = C.msToUs(period.startMs); - String baseUrl = representation.baseUrl; - RangedUri initializationUri = representation.getInitializationUri(); - if (initializationUri != null) { - addSegment(segments, startUs, baseUrl, initializationUri); - } - RangedUri indexUri = representation.getIndexUri(); - if (indexUri != null) { - addSegment(segments, startUs, baseUrl, indexUri); - } - - long firstSegmentNum = index.getFirstSegmentNum(); - long lastSegmentNum = firstSegmentNum + segmentCount - 1; - for (long j = firstSegmentNum; j <= lastSegmentNum; j++) { - addSegment(segments, startUs + index.getTimeUs(j), baseUrl, index.getSegmentUrl(j)); + for (int i = 0; i < manifest.getPeriodCount(); i++) { + Period period = manifest.getPeriod(i); + long periodStartUs = C.msToUs(period.startMs); + long periodDurationUs = manifest.getPeriodDurationUs(i); + List adaptationSets = period.adaptationSets; + for (int j = 0; j < adaptationSets.size(); j++) { + addSegmentsForAdaptationSet( + dataSource, + adaptationSets.get(j), + periodStartUs, + periodDurationUs, + allowIncompleteList, + segments); } } return segments; } - /** - * Returns DashSegmentIndex for given representation. - */ - private DashSegmentIndex getSegmentIndex(DataSource dataSource, DashManifest manifest, - RepresentationKey key) throws IOException, InterruptedException { - AdaptationSet adaptationSet = manifest.getPeriod(key.periodIndex).adaptationSets.get( - key.adaptationSetIndex); - Representation representation = adaptationSet.representations.get(key.representationIndex); + private static void addSegmentsForAdaptationSet( + DataSource dataSource, + AdaptationSet adaptationSet, + long periodStartUs, + long periodDurationUs, + boolean allowIncompleteList, + ArrayList out) + throws IOException, InterruptedException { + for (int i = 0; i < adaptationSet.representations.size(); i++) { + Representation representation = adaptationSet.representations.get(i); + DashSegmentIndex index; + try { + index = getSegmentIndex(dataSource, adaptationSet.type, representation); + if (index == null) { + // Loading succeeded but there was no index. + throw new DownloadException("Missing segment index"); + } + } catch (IOException e) { + if (!allowIncompleteList) { + throw e; + } + // Loading failed, but generating an incomplete segment list is allowed. Advance to the next + // representation. + continue; + } + + int segmentCount = index.getSegmentCount(periodDurationUs); + if (segmentCount == DashSegmentIndex.INDEX_UNBOUNDED) { + throw new DownloadException("Unbounded segment index"); + } + + String baseUrl = representation.baseUrl; + RangedUri initializationUri = representation.getInitializationUri(); + if (initializationUri != null) { + addSegment(periodStartUs, baseUrl, initializationUri, out); + } + RangedUri indexUri = representation.getIndexUri(); + if (indexUri != null) { + addSegment(periodStartUs, baseUrl, indexUri, out); + } + long firstSegmentNum = index.getFirstSegmentNum(); + long lastSegmentNum = firstSegmentNum + segmentCount - 1; + for (long j = firstSegmentNum; j <= lastSegmentNum; j++) { + addSegment(periodStartUs + index.getTimeUs(j), baseUrl, index.getSegmentUrl(j), out); + } + } + } + + private static void addSegment( + long startTimeUs, String baseUrl, RangedUri rangedUri, ArrayList out) { + DataSpec dataSpec = + new DataSpec(rangedUri.resolveUri(baseUrl), rangedUri.start, rangedUri.length, null); + out.add(new Segment(startTimeUs, dataSpec)); + } + + private static @Nullable DashSegmentIndex getSegmentIndex( + DataSource dataSource, int trackType, Representation representation) + throws IOException, InterruptedException { DashSegmentIndex index = representation.getIndex(); if (index != null) { return index; } - ChunkIndex seekMap = DashUtil.loadChunkIndex(dataSource, adaptationSet.type, representation); + ChunkIndex seekMap = DashUtil.loadChunkIndex(dataSource, trackType, representation); return seekMap == null ? null : new DashWrappingSegmentIndex(seekMap); } - private static void addSegment(ArrayList segments, long startTimeUs, String baseUrl, - RangedUri rangedUri) { - DataSpec dataSpec = new DataSpec(rangedUri.resolveUri(baseUrl), rangedUri.start, - rangedUri.length, null); - segments.add(new Segment(startTimeUs, dataSpec)); - } - } diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadActionTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadActionTest.java index ea47722b69..43d9bd9965 100644 --- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadActionTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadActionTest.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.source.dash.offline; import static com.google.common.truth.Truth.assertThat; import android.net.Uri; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.offline.DownloadAction; import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; import com.google.android.exoplayer2.source.dash.manifest.RepresentationKey; @@ -28,6 +29,9 @@ import java.io.ByteArrayOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mockito; @@ -40,100 +44,132 @@ import org.robolectric.RobolectricTestRunner; @RunWith(RobolectricTestRunner.class) public class DashDownloadActionTest { - @Test - public void testDownloadActionIsNotRemoveAction() throws Exception { - DashDownloadAction action = new DashDownloadAction(Uri.parse("uri"), false, null); - assertThat(action.isRemoveAction()).isFalse(); + private Uri uri1; + private Uri uri2; + + @Before + public void setUp() { + uri1 = Uri.parse("http://test1.uri"); + uri2 = Uri.parse("http://test2.uri"); } @Test - public void testRemoveActionIsRemoveAction() throws Exception { - DashDownloadAction action2 = new DashDownloadAction(Uri.parse("uri"), true, null); - assertThat(action2.isRemoveAction()).isTrue(); + public void testDownloadActionIsNotRemoveAction() { + DashDownloadAction action = newAction(uri1, /* isRemoveAction= */ false, /* data= */ null); + assertThat(action.isRemoveAction).isFalse(); } @Test - public void testCreateDownloader() throws Exception { + public void testRemoveActionisRemoveAction() { + DashDownloadAction action2 = newAction(uri1, /* isRemoveAction= */ true, /* data= */ null); + assertThat(action2.isRemoveAction).isTrue(); + } + + @Test + public void testCreateDownloader() { MockitoAnnotations.initMocks(this); - DashDownloadAction action = new DashDownloadAction(Uri.parse("uri"), false, null); + DashDownloadAction action = newAction(uri1, /* isRemoveAction= */ false, /* data= */ null); DownloaderConstructorHelper constructorHelper = new DownloaderConstructorHelper( Mockito.mock(Cache.class), DummyDataSource.FACTORY); assertThat(action.createDownloader(constructorHelper)).isNotNull(); } @Test - public void testSameUriDifferentAction_IsSameMedia() throws Exception { - DashDownloadAction action1 = new DashDownloadAction(Uri.parse("uri"), true, null); - DashDownloadAction action2 = new DashDownloadAction(Uri.parse("uri"), false, null); + public void testSameUriDifferentAction_IsSameMedia() { + DashDownloadAction action1 = newAction(uri1, /* isRemoveAction= */ true, /* data= */ null); + DashDownloadAction action2 = newAction(uri1, /* isRemoveAction= */ false, /* data= */ null); assertThat(action1.isSameMedia(action2)).isTrue(); } @Test - public void testDifferentUriAndAction_IsNotSameMedia() throws Exception { - DashDownloadAction action3 = new DashDownloadAction(Uri.parse("uri2"), true, null); - DashDownloadAction action4 = new DashDownloadAction(Uri.parse("uri"), false, null); + public void testDifferentUriAndAction_IsNotSameMedia() { + DashDownloadAction action3 = newAction(uri2, /* isRemoveAction= */ true, /* data= */ null); + DashDownloadAction action4 = newAction(uri1, /* isRemoveAction= */ false, /* data= */ null); assertThat(action3.isSameMedia(action4)).isFalse(); } @SuppressWarnings("EqualsWithItself") @Test - public void testEquals() throws Exception { - DashDownloadAction action1 = new DashDownloadAction(Uri.parse("uri"), true, null); + public void testEquals() { + DashDownloadAction action1 = newAction(uri1, /* isRemoveAction= */ true, /* data= */ null); assertThat(action1.equals(action1)).isTrue(); - DashDownloadAction action2 = new DashDownloadAction(Uri.parse("uri"), true, null); - DashDownloadAction action3 = new DashDownloadAction(Uri.parse("uri"), true, null); + DashDownloadAction action2 = newAction(uri1, /* isRemoveAction= */ true, /* data= */ null); + DashDownloadAction action3 = newAction(uri1, /* isRemoveAction= */ true, /* data= */ null); assertEqual(action2, action3); - DashDownloadAction action4 = new DashDownloadAction(Uri.parse("uri"), true, null); - DashDownloadAction action5 = new DashDownloadAction(Uri.parse("uri"), false, null); + DashDownloadAction action4 = newAction(uri1, /* isRemoveAction= */ true, /* data= */ null); + DashDownloadAction action5 = newAction(uri1, /* isRemoveAction= */ false, /* data= */ null); assertNotEqual(action4, action5); - DashDownloadAction action6 = new DashDownloadAction(Uri.parse("uri"), false, null); + DashDownloadAction action6 = newAction(uri1, /* isRemoveAction= */ false, /* data= */ null); DashDownloadAction action7 = - new DashDownloadAction(Uri.parse("uri"), false, null, new RepresentationKey(0, 0, 0)); + newAction( + uri1, /* isRemoveAction= */ false, /* data= */ null, new RepresentationKey(0, 0, 0)); assertNotEqual(action6, action7); DashDownloadAction action8 = - new DashDownloadAction(Uri.parse("uri"), false, null, new RepresentationKey(1, 1, 1)); + newAction( + uri1, /* isRemoveAction= */ false, /* data= */ null, new RepresentationKey(1, 1, 1)); DashDownloadAction action9 = - new DashDownloadAction(Uri.parse("uri"), false, null, new RepresentationKey(0, 0, 0)); + newAction( + uri1, /* isRemoveAction= */ false, /* data= */ null, new RepresentationKey(0, 0, 0)); assertNotEqual(action8, action9); - DashDownloadAction action10 = new DashDownloadAction(Uri.parse("uri"), true, null); - DashDownloadAction action11 = new DashDownloadAction(Uri.parse("uri2"), true, null); + DashDownloadAction action10 = newAction(uri1, /* isRemoveAction= */ true, /* data= */ null); + DashDownloadAction action11 = newAction(uri2, /* isRemoveAction= */ true, /* data= */ null); assertNotEqual(action10, action11); - DashDownloadAction action12 = new DashDownloadAction(Uri.parse("uri"), false, null, - new RepresentationKey(0, 0, 0), new RepresentationKey(1, 1, 1)); - DashDownloadAction action13 = new DashDownloadAction(Uri.parse("uri"), false, null, - new RepresentationKey(1, 1, 1), new RepresentationKey(0, 0, 0)); + DashDownloadAction action12 = + newAction( + uri1, + /* isRemoveAction= */ false, + /* data= */ null, + new RepresentationKey(0, 0, 0), + new RepresentationKey(1, 1, 1)); + DashDownloadAction action13 = + newAction( + uri1, + /* isRemoveAction= */ false, + /* data= */ null, + new RepresentationKey(1, 1, 1), + new RepresentationKey(0, 0, 0)); assertEqual(action12, action13); - DashDownloadAction action14 = new DashDownloadAction(Uri.parse("uri"), false, null, - new RepresentationKey(0, 0, 0)); - DashDownloadAction action15 = new DashDownloadAction(Uri.parse("uri"), false, null, - new RepresentationKey(1, 1, 1), new RepresentationKey(0, 0, 0)); + DashDownloadAction action14 = + newAction( + uri1, /* isRemoveAction= */ false, /* data= */ null, new RepresentationKey(0, 0, 0)); + DashDownloadAction action15 = + newAction( + uri1, + /* isRemoveAction= */ false, + /* data= */ null, + new RepresentationKey(1, 1, 1), + new RepresentationKey(0, 0, 0)); assertNotEqual(action14, action15); - DashDownloadAction action16 = new DashDownloadAction(Uri.parse("uri"), false, null); - DashDownloadAction action17 = - new DashDownloadAction(Uri.parse("uri"), false, null, new RepresentationKey[0]); + DashDownloadAction action16 = newAction(uri1, /* isRemoveAction= */ false, /* data= */ null); + DashDownloadAction action17 = newAction(uri1, /* isRemoveAction= */ false, /* data= */ null); assertEqual(action16, action17); } @Test - public void testSerializerGetType() throws Exception { - DashDownloadAction action = new DashDownloadAction(Uri.parse("uri"), false, null); - assertThat(action.getType()).isNotNull(); + public void testSerializerGetType() { + DashDownloadAction action = newAction(uri1, /* isRemoveAction= */ false, /* data= */ null); + assertThat(action.type).isNotNull(); } @Test public void testSerializerWriteRead() throws Exception { - doTestSerializationRoundTrip(new DashDownloadAction(Uri.parse("uri"), false, null)); - doTestSerializationRoundTrip(new DashDownloadAction(Uri.parse("uri"), true, null)); - doTestSerializationRoundTrip(new DashDownloadAction(Uri.parse("uri2"), false, null, - new RepresentationKey(0, 0, 0), new RepresentationKey(1, 1, 1))); + doTestSerializationRoundTrip(newAction(uri1, /* isRemoveAction= */ false, /* data= */ null)); + doTestSerializationRoundTrip(newAction(uri1, /* isRemoveAction= */ true, /* data= */ null)); + doTestSerializationRoundTrip( + newAction( + uri2, + /* isRemoveAction= */ false, + /* data= */ null, + new RepresentationKey(0, 0, 0), + new RepresentationKey(1, 1, 1))); } private static void assertNotEqual(DashDownloadAction action1, DashDownloadAction action2) { @@ -146,17 +182,24 @@ public class DashDownloadActionTest { assertThat(action2).isEqualTo(action1); } - private static void doTestSerializationRoundTrip(DashDownloadAction action1) throws IOException { + private static void doTestSerializationRoundTrip(DashDownloadAction action) throws IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(); DataOutputStream output = new DataOutputStream(out); - action1.writeToStream(output); + DownloadAction.serializeToStream(action, output); ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); DataInputStream input = new DataInputStream(in); DownloadAction action2 = - DashDownloadAction.DESERIALIZER.readFromStream(DownloadAction.MASTER_VERSION, input); + DownloadAction.deserializeFromStream( + new DownloadAction.Deserializer[] {DashDownloadAction.DESERIALIZER}, input); - assertThat(action1).isEqualTo(action2); + assertThat(action).isEqualTo(action2); } + private static DashDownloadAction newAction( + Uri uri, boolean isRemoveAction, @Nullable byte[] data, RepresentationKey... keys) { + ArrayList keysList = new ArrayList<>(); + Collections.addAll(keysList, keys); + return new DashDownloadAction(uri, isRemoveAction, data, keysList); + } } diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DashDownloaderTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DashDownloaderTest.java index 1d3fcb2135..4c96357528 100644 --- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DashDownloaderTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DashDownloaderTest.java @@ -20,17 +20,13 @@ import static com.google.android.exoplayer2.source.dash.offline.DashDownloadTest import static com.google.android.exoplayer2.source.dash.offline.DashDownloadTestData.TEST_MPD_URI; import static com.google.android.exoplayer2.testutil.CacheAsserts.assertCacheEmpty; import static com.google.android.exoplayer2.testutil.CacheAsserts.assertCachedData; -import static com.google.android.exoplayer2.testutil.CacheAsserts.assertDataCached; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.offline.DownloadException; -import com.google.android.exoplayer2.offline.Downloader.ProgressListener; import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; -import com.google.android.exoplayer2.source.dash.manifest.DashManifest; import com.google.android.exoplayer2.source.dash.manifest.RepresentationKey; import com.google.android.exoplayer2.testutil.FakeDataSet; import com.google.android.exoplayer2.testutil.FakeDataSource; @@ -42,13 +38,12 @@ import com.google.android.exoplayer2.upstream.cache.SimpleCache; import com.google.android.exoplayer2.util.Util; import java.io.File; import java.io.IOException; -import java.util.Arrays; +import java.util.ArrayList; +import java.util.Collections; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.InOrder; -import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; @@ -68,51 +63,10 @@ public class DashDownloaderTest { } @After - public void tearDown() throws Exception { + public void tearDown() { Util.recursiveDelete(tempFolder); } - @Test - public void testGetManifest() throws Exception { - FakeDataSet fakeDataSet = new FakeDataSet().setData(TEST_MPD_URI, TEST_MPD); - DashDownloader dashDownloader = getDashDownloader(fakeDataSet); - - DashManifest manifest = dashDownloader.getManifest(); - - assertThat(manifest).isNotNull(); - assertCachedData(cache, fakeDataSet); - } - - @Test - public void testDownloadManifestFailure() throws Exception { - byte[] testMpdFirstPart = Arrays.copyOf(TEST_MPD, 10); - byte[] testMpdSecondPart = Arrays.copyOfRange(TEST_MPD, 10, TEST_MPD.length); - FakeDataSet fakeDataSet = - new FakeDataSet() - .newData(TEST_MPD_URI) - .appendReadData(testMpdFirstPart) - .appendReadError(new IOException()) - .appendReadData(testMpdSecondPart) - .endData(); - DashDownloader dashDownloader = getDashDownloader(fakeDataSet); - - // fails on the first try - try { - dashDownloader.getManifest(); - fail(); - } catch (IOException e) { - // ignore - } - DataSpec dataSpec = new DataSpec(TEST_MPD_URI, 0, testMpdFirstPart.length, null); - assertDataCached(cache, dataSpec, testMpdFirstPart); - - // on the second try it downloads the rest of the data - DashManifest manifest = dashDownloader.getManifest(); - - assertThat(manifest).isNotNull(); - assertCachedData(cache, fakeDataSet); - } - @Test public void testDownloadRepresentation() throws Exception { FakeDataSet fakeDataSet = @@ -122,11 +76,9 @@ public class DashDownloaderTest { .setRandomData("audio_segment_1", 4) .setRandomData("audio_segment_2", 5) .setRandomData("audio_segment_3", 6); - DashDownloader dashDownloader = getDashDownloader(fakeDataSet); - - dashDownloader.selectRepresentations(new RepresentationKey[] {new RepresentationKey(0, 0, 0)}); - dashDownloader.download(null); + DashDownloader dashDownloader = getDashDownloader(fakeDataSet, new RepresentationKey(0, 0, 0)); + dashDownloader.download(); assertCachedData(cache, fakeDataSet); } @@ -143,11 +95,9 @@ public class DashDownloaderTest { .endData() .setRandomData("audio_segment_2", 5) .setRandomData("audio_segment_3", 6); - DashDownloader dashDownloader = getDashDownloader(fakeDataSet); - - dashDownloader.selectRepresentations(new RepresentationKey[] {new RepresentationKey(0, 0, 0)}); - dashDownloader.download(null); + DashDownloader dashDownloader = getDashDownloader(fakeDataSet, new RepresentationKey(0, 0, 0)); + dashDownloader.download(); assertCachedData(cache, fakeDataSet); } @@ -163,12 +113,11 @@ public class DashDownloaderTest { .setRandomData("text_segment_1", 1) .setRandomData("text_segment_2", 2) .setRandomData("text_segment_3", 3); - DashDownloader dashDownloader = getDashDownloader(fakeDataSet); - - dashDownloader.selectRepresentations( - new RepresentationKey[] {new RepresentationKey(0, 0, 0), new RepresentationKey(0, 1, 0)}); - dashDownloader.download(null); + DashDownloader dashDownloader = + getDashDownloader( + fakeDataSet, new RepresentationKey(0, 0, 0), new RepresentationKey(0, 1, 0)); + dashDownloader.download(); assertCachedData(cache, fakeDataSet); } @@ -187,20 +136,10 @@ public class DashDownloaderTest { .setRandomData("period_2_segment_1", 1) .setRandomData("period_2_segment_2", 2) .setRandomData("period_2_segment_3", 3); + DashDownloader dashDownloader = getDashDownloader(fakeDataSet); - - // dashDownloader.selectRepresentations() isn't called - dashDownloader.download(null); + dashDownloader.download(); assertCachedData(cache, fakeDataSet); - dashDownloader.remove(); - - // select something random - dashDownloader.selectRepresentations(new RepresentationKey[] {new RepresentationKey(0, 0, 0)}); - // clear selection - dashDownloader.selectRepresentations(new RepresentationKey[0]); - dashDownloader.download(null); - assertCachedData(cache, fakeDataSet); - dashDownloader.remove(); } @Test @@ -218,12 +157,10 @@ public class DashDownloaderTest { FakeDataSource fakeDataSource = new FakeDataSource(fakeDataSet); Factory factory = mock(Factory.class); when(factory.createDataSource()).thenReturn(fakeDataSource); - DashDownloader dashDownloader = - new DashDownloader(TEST_MPD_URI, new DownloaderConstructorHelper(cache, factory)); - dashDownloader.selectRepresentations( - new RepresentationKey[] {new RepresentationKey(0, 0, 0), new RepresentationKey(0, 1, 0)}); - dashDownloader.download(null); + DashDownloader dashDownloader = + getDashDownloader(factory, new RepresentationKey(0, 0, 0), new RepresentationKey(0, 1, 0)); + dashDownloader.download(); DataSpec[] openedDataSpecs = fakeDataSource.getAndClearOpenedDataSpecs(); assertThat(openedDataSpecs.length).isEqualTo(8); @@ -252,12 +189,10 @@ public class DashDownloaderTest { FakeDataSource fakeDataSource = new FakeDataSource(fakeDataSet); Factory factory = mock(Factory.class); when(factory.createDataSource()).thenReturn(fakeDataSource); - DashDownloader dashDownloader = - new DashDownloader(TEST_MPD_URI, new DownloaderConstructorHelper(cache, factory)); - dashDownloader.selectRepresentations( - new RepresentationKey[] {new RepresentationKey(0, 0, 0), new RepresentationKey(1, 0, 0)}); - dashDownloader.download(null); + DashDownloader dashDownloader = + getDashDownloader(factory, new RepresentationKey(0, 0, 0), new RepresentationKey(1, 0, 0)); + dashDownloader.download(); DataSpec[] openedDataSpecs = fakeDataSource.getAndClearOpenedDataSpecs(); assertThat(openedDataSpecs.length).isEqualTo(8); @@ -284,18 +219,15 @@ public class DashDownloaderTest { .appendReadData(TestUtil.buildTestData(3)) .endData() .setRandomData("audio_segment_3", 6); - DashDownloader dashDownloader = getDashDownloader(fakeDataSet); - dashDownloader.selectRepresentations(new RepresentationKey[] {new RepresentationKey(0, 0, 0)}); - // downloadRepresentations fails on the first try + DashDownloader dashDownloader = getDashDownloader(fakeDataSet, new RepresentationKey(0, 0, 0)); try { - dashDownloader.download(null); + dashDownloader.download(); fail(); } catch (IOException e) { - // ignore + // Expected. } - dashDownloader.download(null); - + dashDownloader.download(); assertCachedData(cache, fakeDataSet); } @@ -312,54 +244,24 @@ public class DashDownloaderTest { .appendReadData(TestUtil.buildTestData(3)) .endData() .setRandomData("audio_segment_3", 6); - DashDownloader dashDownloader = getDashDownloader(fakeDataSet); - assertCounters(dashDownloader, C.LENGTH_UNSET, C.LENGTH_UNSET, C.LENGTH_UNSET); + DashDownloader dashDownloader = getDashDownloader(fakeDataSet, new RepresentationKey(0, 0, 0)); + assertThat(dashDownloader.getDownloadedBytes()).isEqualTo(0); - dashDownloader.selectRepresentations(new RepresentationKey[] {new RepresentationKey(0, 0, 0)}); - dashDownloader.init(); - assertCounters(dashDownloader, C.LENGTH_UNSET, C.LENGTH_UNSET, C.LENGTH_UNSET); - - // downloadRepresentations fails after downloading init data, segment 1 and 2 bytes in segment 2 try { - dashDownloader.download(null); + dashDownloader.download(); fail(); } catch (IOException e) { - // ignore + // Failure expected after downloading init data, segment 1 and 2 bytes in segment 2. } - dashDownloader.init(); - assertCounters(dashDownloader, 4, 2, 10 + 4 + 2); + assertThat(dashDownloader.getDownloadedBytes()).isEqualTo(10 + 4 + 2); - dashDownloader.download(null); - - assertCounters(dashDownloader, 4, 4, 10 + 4 + 5 + 6); + dashDownloader.download(); + assertThat(dashDownloader.getDownloadedBytes()).isEqualTo(10 + 4 + 5 + 6); } @Test - public void testListener() throws Exception { - FakeDataSet fakeDataSet = - new FakeDataSet() - .setData(TEST_MPD_URI, TEST_MPD) - .setRandomData("audio_init_data", 10) - .setRandomData("audio_segment_1", 4) - .setRandomData("audio_segment_2", 5) - .setRandomData("audio_segment_3", 6); - DashDownloader dashDownloader = getDashDownloader(fakeDataSet); - - dashDownloader.selectRepresentations(new RepresentationKey[] {new RepresentationKey(0, 0, 0)}); - ProgressListener mockListener = Mockito.mock(ProgressListener.class); - dashDownloader.download(mockListener); - InOrder inOrder = Mockito.inOrder(mockListener); - inOrder.verify(mockListener).onDownloadProgress(dashDownloader, 0.0f, 0); - inOrder.verify(mockListener).onDownloadProgress(dashDownloader, 25.0f, 10); - inOrder.verify(mockListener).onDownloadProgress(dashDownloader, 50.0f, 14); - inOrder.verify(mockListener).onDownloadProgress(dashDownloader, 75.0f, 19); - inOrder.verify(mockListener).onDownloadProgress(dashDownloader, 100.0f, 25); - inOrder.verifyNoMoreInteractions(); - } - - @Test - public void testRemoveAll() throws Exception { + public void testRemove() throws Exception { FakeDataSet fakeDataSet = new FakeDataSet() .setData(TEST_MPD_URI, TEST_MPD) @@ -370,13 +272,12 @@ public class DashDownloaderTest { .setRandomData("text_segment_1", 1) .setRandomData("text_segment_2", 2) .setRandomData("text_segment_3", 3); - DashDownloader dashDownloader = getDashDownloader(fakeDataSet); - dashDownloader.selectRepresentations( - new RepresentationKey[] {new RepresentationKey(0, 0, 0), new RepresentationKey(0, 1, 0)}); - dashDownloader.download(null); + DashDownloader dashDownloader = + getDashDownloader( + fakeDataSet, new RepresentationKey(0, 0, 0), new RepresentationKey(0, 1, 0)); + dashDownloader.download(); dashDownloader.remove(); - assertCacheEmpty(cache); } @@ -386,52 +287,31 @@ public class DashDownloaderTest { new FakeDataSet() .setData(TEST_MPD_URI, TEST_MPD_NO_INDEX) .setRandomData("test_segment_1", 4); - DashDownloader dashDownloader = getDashDownloader(fakeDataSet); - dashDownloader.selectRepresentations(new RepresentationKey[] {new RepresentationKey(0, 0, 0)}); - dashDownloader.init(); + DashDownloader dashDownloader = getDashDownloader(fakeDataSet, new RepresentationKey(0, 0, 0)); try { - dashDownloader.download(null); + dashDownloader.download(); fail(); } catch (DownloadException e) { - // expected exception. + // Expected. } dashDownloader.remove(); - assertCacheEmpty(cache); } - @Test - public void testSelectRepresentationsClearsPreviousSelection() throws Exception { - FakeDataSet fakeDataSet = - new FakeDataSet() - .setData(TEST_MPD_URI, TEST_MPD) - .setRandomData("audio_init_data", 10) - .setRandomData("audio_segment_1", 4) - .setRandomData("audio_segment_2", 5) - .setRandomData("audio_segment_3", 6); - DashDownloader dashDownloader = getDashDownloader(fakeDataSet); - - dashDownloader.selectRepresentations( - new RepresentationKey[] {new RepresentationKey(0, 0, 0), new RepresentationKey(0, 1, 0)}); - dashDownloader.selectRepresentations(new RepresentationKey[] {new RepresentationKey(0, 0, 0)}); - dashDownloader.download(null); - - assertCachedData(cache, fakeDataSet); + private DashDownloader getDashDownloader(FakeDataSet fakeDataSet, RepresentationKey... keys) { + return getDashDownloader(new Factory(null).setFakeDataSet(fakeDataSet), keys); } - private DashDownloader getDashDownloader(FakeDataSet fakeDataSet) { - Factory factory = new Factory(null).setFakeDataSet(fakeDataSet); - return new DashDownloader(TEST_MPD_URI, new DownloaderConstructorHelper(cache, factory)); + private DashDownloader getDashDownloader(Factory factory, RepresentationKey... keys) { + return new DashDownloader( + TEST_MPD_URI, keysList(keys), new DownloaderConstructorHelper(cache, factory)); } - private static void assertCounters( - DashDownloader dashDownloader, - int totalSegments, - int downloadedSegments, - int downloadedBytes) { - assertThat(dashDownloader.getTotalSegments()).isEqualTo(totalSegments); - assertThat(dashDownloader.getDownloadedSegments()).isEqualTo(downloadedSegments); - assertThat(dashDownloader.getDownloadedBytes()).isEqualTo(downloadedBytes); + private static ArrayList keysList(RepresentationKey... keys) { + ArrayList keysList = new ArrayList<>(); + Collections.addAll(keysList, keys); + return keysList; } + } diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadManagerDashTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadManagerDashTest.java index 84622f4828..8ca2aa083b 100644 --- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadManagerDashTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadManagerDashTest.java @@ -22,7 +22,9 @@ import static com.google.android.exoplayer2.testutil.CacheAsserts.assertCachedDa import static com.google.common.truth.Truth.assertThat; import android.content.Context; +import android.net.Uri; import android.os.ConditionVariable; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.offline.DownloadManager; import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; import com.google.android.exoplayer2.source.dash.manifest.RepresentationKey; @@ -30,12 +32,15 @@ import com.google.android.exoplayer2.testutil.DummyMainThread; import com.google.android.exoplayer2.testutil.FakeDataSet; import com.google.android.exoplayer2.testutil.FakeDataSource; import com.google.android.exoplayer2.testutil.RobolectricUtil; +import com.google.android.exoplayer2.testutil.TestDownloadManagerListener; import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.upstream.DataSource.Factory; import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor; import com.google.android.exoplayer2.upstream.cache.SimpleCache; import com.google.android.exoplayer2.util.Util; import java.io.File; +import java.util.ArrayList; +import java.util.Collections; import org.junit.After; import org.junit.Before; import org.junit.Ignore; @@ -59,7 +64,7 @@ public class DownloadManagerDashTest { private DownloadManager downloadManager; private RepresentationKey fakeRepresentationKey1; private RepresentationKey fakeRepresentationKey2; - private TestDownloadListener downloadListener; + private TestDownloadManagerListener downloadManagerListener; private File actionFile; private DummyMainThread dummyMainThread; @@ -240,15 +245,15 @@ public class DownloadManagerDashTest { } private void blockUntilTasksCompleteAndThrowAnyDownloadError() throws Throwable { - downloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); + downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); } private void handleDownloadAction(RepresentationKey... keys) { - downloadManager.handleAction(new DashDownloadAction(TEST_MPD_URI, false, null, keys)); + downloadManager.handleAction(newAction(TEST_MPD_URI, false, null, keys)); } private void handleRemoveAction() { - downloadManager.handleAction(new DashDownloadAction(TEST_MPD_URI, true, null)); + downloadManager.handleAction(newAction(TEST_MPD_URI, true, null)); } private void createDownloadManager() { @@ -261,15 +266,23 @@ public class DownloadManagerDashTest { downloadManager = new DownloadManager( new DownloaderConstructorHelper(cache, fakeDataSourceFactory), - 1, - 3, - actionFile.getAbsolutePath(), + /* maxSimultaneousDownloads= */ 1, + /* minRetryCount= */ 3, + actionFile, DashDownloadAction.DESERIALIZER); - downloadListener = new TestDownloadListener(downloadManager, dummyMainThread); - downloadManager.addListener(downloadListener); + downloadManagerListener = + new TestDownloadManagerListener(downloadManager, dummyMainThread); + downloadManager.addListener(downloadManagerListener); downloadManager.startDownloads(); } }); } + + private static DashDownloadAction newAction( + Uri uri, boolean isRemoveAction, @Nullable byte[] data, RepresentationKey... keys) { + ArrayList keysList = new ArrayList<>(); + Collections.addAll(keysList, keys); + return new DashDownloadAction(uri, isRemoveAction, data, keysList); + } } diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadServiceDashTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadServiceDashTest.java index e8044e57c1..745acd9bbf 100644 --- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadServiceDashTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadServiceDashTest.java @@ -20,9 +20,13 @@ import static com.google.android.exoplayer2.source.dash.offline.DashDownloadTest import static com.google.android.exoplayer2.testutil.CacheAsserts.assertCacheEmpty; import static com.google.android.exoplayer2.testutil.CacheAsserts.assertCachedData; +import android.app.Notification; import android.content.Context; import android.content.Intent; +import android.net.Uri; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.offline.DownloadManager; +import com.google.android.exoplayer2.offline.DownloadManager.TaskState; import com.google.android.exoplayer2.offline.DownloadService; import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; import com.google.android.exoplayer2.scheduler.Requirements; @@ -32,6 +36,7 @@ import com.google.android.exoplayer2.testutil.DummyMainThread; import com.google.android.exoplayer2.testutil.FakeDataSet; import com.google.android.exoplayer2.testutil.FakeDataSource; import com.google.android.exoplayer2.testutil.RobolectricUtil; +import com.google.android.exoplayer2.testutil.TestDownloadManagerListener; import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor; @@ -40,10 +45,14 @@ import com.google.android.exoplayer2.util.ConditionVariable; import com.google.android.exoplayer2.util.Util; import java.io.File; import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; import org.junit.After; import org.junit.Before; +import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Mockito; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; @@ -61,11 +70,11 @@ public class DownloadServiceDashTest { private Context context; private DownloadService dashDownloadService; private ConditionVariable pauseDownloadCondition; - private TestDownloadListener testDownloadListener; + private TestDownloadManagerListener downloadManagerListener; private DummyMainThread dummyMainThread; @Before - public void setUp() throws Exception { + public void setUp() throws IOException { dummyMainThread = new DummyMainThread(); context = RuntimeEnvironment.application; tempFolder = Util.createTempDirectory(context, "ExoPlayerTest"); @@ -102,100 +111,98 @@ public class DownloadServiceDashTest { fakeRepresentationKey1 = new RepresentationKey(0, 0, 0); fakeRepresentationKey2 = new RepresentationKey(0, 1, 0); - try { - dummyMainThread.runOnMainThread( - new Runnable() { - @Override - public void run() { - File actionFile = null; - try { - actionFile = Util.createTempFile(context, "ExoPlayerTest"); - } catch (IOException e) { - throw new RuntimeException(e); - } - actionFile.delete(); - final DownloadManager dashDownloadManager = - new DownloadManager( - new DownloaderConstructorHelper(cache, fakeDataSourceFactory), - 1, - 3, - actionFile.getAbsolutePath(), - DashDownloadAction.DESERIALIZER); - testDownloadListener = new TestDownloadListener(dashDownloadManager, dummyMainThread); - dashDownloadManager.addListener(testDownloadListener); - dashDownloadManager.startDownloads(); - - dashDownloadService = - new DownloadService(101010) { - - @Override - protected DownloadManager getDownloadManager() { - return dashDownloadManager; - } - - @Override - protected String getNotificationChannelId() { - return ""; - } - - @Override - protected Scheduler getScheduler() { - return null; - } - - @Override - protected Requirements getRequirements() { - return null; - } - }; - dashDownloadService.onCreate(); + dummyMainThread.runOnMainThread( + new Runnable() { + @Override + public void run() { + File actionFile; + try { + actionFile = Util.createTempFile(context, "ExoPlayerTest"); + } catch (IOException e) { + throw new RuntimeException(e); } - }); - } catch (Throwable throwable) { - throw new Exception(throwable); - } + actionFile.delete(); + final DownloadManager dashDownloadManager = + new DownloadManager( + new DownloaderConstructorHelper(cache, fakeDataSourceFactory), + 1, + 3, + actionFile, + DashDownloadAction.DESERIALIZER); + downloadManagerListener = + new TestDownloadManagerListener(dashDownloadManager, dummyMainThread); + dashDownloadManager.addListener(downloadManagerListener); + dashDownloadManager.startDownloads(); + + dashDownloadService = + new DownloadService(/*foregroundNotificationId=*/ 1) { + + @Override + protected DownloadManager getDownloadManager() { + return dashDownloadManager; + } + + @Override + protected Notification getForegroundNotification(TaskState[] taskStates) { + return Mockito.mock(Notification.class); + } + + @Nullable + @Override + protected Scheduler getScheduler() { + return null; + } + + @Nullable + @Override + protected Requirements getRequirements() { + return null; + } + }; + dashDownloadService.onCreate(); + } + }); } @After - public void tearDown() throws Exception { - try { - dummyMainThread.runOnMainThread( - new Runnable() { - @Override - public void run() { - dashDownloadService.onDestroy(); - } - }); - } catch (Throwable throwable) { - throw new Exception(throwable); - } + public void tearDown() { + dummyMainThread.runOnMainThread( + new Runnable() { + @Override + public void run() { + dashDownloadService.onDestroy(); + } + }); Util.recursiveDelete(tempFolder); dummyMainThread.release(); } + @Ignore // b/78877092 @Test public void testMultipleDownloadAction() throws Throwable { downloadKeys(fakeRepresentationKey1); downloadKeys(fakeRepresentationKey2); - testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); + downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); assertCachedData(cache, fakeDataSet); } + @Ignore // b/78877092 @Test public void testRemoveAction() throws Throwable { downloadKeys(fakeRepresentationKey1, fakeRepresentationKey2); - testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); + downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); removeAll(); - testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); + downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); assertCacheEmpty(cache); } + @Ignore // b/78877092 @Test public void testRemoveBeforeDownloadComplete() throws Throwable { pauseDownloadCondition = new ConditionVariable(); @@ -203,29 +210,35 @@ public class DownloadServiceDashTest { removeAll(); - testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); + downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); assertCacheEmpty(cache); } private void removeAll() throws Throwable { - callDownloadServiceOnStart(new DashDownloadAction(TEST_MPD_URI, true, null)); + callDownloadServiceOnStart(newAction(TEST_MPD_URI, true, null)); } - private void downloadKeys(RepresentationKey... keys) throws Throwable { - callDownloadServiceOnStart(new DashDownloadAction(TEST_MPD_URI, false, null, keys)); + private void downloadKeys(RepresentationKey... keys) { + callDownloadServiceOnStart(newAction(TEST_MPD_URI, false, null, keys)); } - private void callDownloadServiceOnStart(final DashDownloadAction action) throws Throwable { + private void callDownloadServiceOnStart(final DashDownloadAction action) { dummyMainThread.runOnMainThread( new Runnable() { @Override public void run() { Intent startIntent = - DownloadService.createAddDownloadActionIntent( - context, DownloadService.class, action); + DownloadService.buildAddActionIntent(context, DownloadService.class, action, false); dashDownloadService.onStartCommand(startIntent, 0, 0); } }); } + + private static DashDownloadAction newAction( + Uri uri, boolean isRemoveAction, @Nullable byte[] data, RepresentationKey... keys) { + ArrayList keysList = new ArrayList<>(); + Collections.addAll(keysList, keys); + return new DashDownloadAction(uri, isRemoveAction, data, keysList); + } } diff --git a/library/hls/build.gradle b/library/hls/build.gradle index c2268a3007..c599931a68 100644 --- a/library/hls/build.gradle +++ b/library/hls/build.gradle @@ -30,10 +30,15 @@ android { // testCoverageEnabled = true // } } + + lintOptions { + lintConfig file("../../checker-framework-lint.xml") + } } dependencies { implementation 'com.android.support:support-annotations:' + supportLibraryVersion + implementation 'org.checkerframework:checker-qual:' + checkerframeworkVersion implementation project(modulePrefix + 'library-core') testImplementation project(modulePrefix + 'testutils-robolectric') } diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/Aes128DataSource.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/Aes128DataSource.java index 142b846a97..3f57cba1b0 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/Aes128DataSource.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/Aes128DataSource.java @@ -78,16 +78,19 @@ import javax.crypto.spec.SecretKeySpec; throw new RuntimeException(e); } - cipherInputStream = new CipherInputStream( - new DataSourceInputStream(upstream, dataSpec), cipher); + DataSourceInputStream inputStream = new DataSourceInputStream(upstream, dataSpec); + cipherInputStream = new CipherInputStream(inputStream, cipher); + inputStream.open(); return C.LENGTH_UNSET; } @Override public void close() throws IOException { - cipherInputStream = null; - upstream.close(); + if (cipherInputStream != null) { + cipherInputStream = null; + upstream.close(); + } } @Override diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java index f3cc43b8a1..9a02bd785a 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java @@ -261,10 +261,12 @@ import java.util.List; // If the playlist is too old to contain the chunk, we need to refresh it. chunkMediaSequence = mediaPlaylist.mediaSequence + mediaPlaylist.segments.size(); } else { + // The playlist start time is subtracted from the target position because the segment start + // times are relative to the start of the playlist, but the target position is not. chunkMediaSequence = Util.binarySearchFloor( mediaPlaylist.segments, - targetPositionUs, + /* value= */ targetPositionUs - mediaPlaylist.startTimeUs, /* inclusive= */ true, /* stayInBounds= */ !playlistTracker.isLive() || previous == null) + mediaPlaylist.mediaSequence; @@ -320,7 +322,7 @@ import java.util.List; } DataSpec initDataSpec = null; - Segment initSegment = mediaPlaylist.initializationSegment; + Segment initSegment = segment.initializationSegment; if (initSegment != null) { Uri initSegmentUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, initSegment.url); initDataSpec = new DataSpec(initSegmentUri, initSegment.byterangeOffset, diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java index 52198891e0..1a3f41fffc 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java @@ -344,7 +344,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper Format renditionFormat = audioRendition.format; if (allowChunklessPreparation && renditionFormat.codecs != null) { sampleStreamWrapper.prepareWithMasterPlaylistInfo( - new TrackGroupArray(new TrackGroup(audioRendition.format)), 0); + new TrackGroupArray(new TrackGroup(audioRendition.format)), 0, TrackGroupArray.EMPTY); } else { sampleStreamWrapper.continuePreparing(); } @@ -362,7 +362,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper positionUs); sampleStreamWrappers[currentWrapperIndex++] = sampleStreamWrapper; sampleStreamWrapper.prepareWithMasterPlaylistInfo( - new TrackGroupArray(new TrackGroup(url.format)), 0); + new TrackGroupArray(new TrackGroup(url.format)), 0, TrackGroupArray.EMPTY); } // All wrappers are enabled during preparation. @@ -386,7 +386,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper * master playlist either contains an EXT-X-MEDIA tag without the URI attribute or does not * contain any EXT-X-MEDIA tag. *

  • Closed captions will only be exposed if they are declared by the master playlist. - *
  • ID3 tracks are not exposed. + *
  • An ID3 track is exposed preemptively, in case the segments contain an ID3 track. * * * @param masterPlaylist The HLS master playlist. @@ -463,8 +463,21 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper // Variants contain codecs but no video or audio entries could be identified. throw new IllegalArgumentException("Unexpected codecs attribute: " + codecs); } + + TrackGroup id3TrackGroup = + new TrackGroup( + Format.createSampleFormat( + /* id= */ "ID3", + MimeTypes.APPLICATION_ID3, + /* codecs= */ null, + /* bitrate= */ Format.NO_VALUE, + /* drmInitData= */ null)); + muxedTrackGroups.add(id3TrackGroup); + sampleStreamWrapper.prepareWithMasterPlaylistInfo( - new TrackGroupArray(muxedTrackGroups.toArray(new TrackGroup[0])), 0); + new TrackGroupArray(muxedTrackGroups.toArray(new TrackGroup[0])), + 0, + new TrackGroupArray(id3TrackGroup)); } else { sampleStreamWrapper.setIsTimestampMaster(true); sampleStreamWrapper.continuePreparing(); diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java index 6563a5fba0..5d4d953372 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java @@ -33,13 +33,13 @@ import java.io.IOException; public HlsSampleStream(HlsSampleStreamWrapper sampleStreamWrapper, int trackGroupIndex) { this.sampleStreamWrapper = sampleStreamWrapper; this.trackGroupIndex = trackGroupIndex; - sampleQueueIndex = C.INDEX_UNSET; + sampleQueueIndex = HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_PENDING; } public void unbindSampleQueue() { - if (sampleQueueIndex != C.INDEX_UNSET) { + if (sampleQueueIndex != HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_PENDING) { sampleStreamWrapper.unbindSampleQueue(trackGroupIndex); - sampleQueueIndex = C.INDEX_UNSET; + sampleQueueIndex = HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_PENDING; } } @@ -47,12 +47,14 @@ import java.io.IOException; @Override public boolean isReady() { - return ensureBoundSampleQueue() && sampleStreamWrapper.isReady(sampleQueueIndex); + return sampleQueueIndex == HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_NO_MAPPING_NON_FATAL + || (maybeMapToSampleQueue() && sampleStreamWrapper.isReady(sampleQueueIndex)); } @Override public void maybeThrowError() throws IOException { - if (!ensureBoundSampleQueue() && sampleStreamWrapper.isMappingFinished()) { + maybeMapToSampleQueue(); + if (sampleQueueIndex == HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_NO_MAPPING_FATAL) { throw new SampleQueueMappingException( sampleStreamWrapper.getTrackGroups().get(trackGroupIndex).getFormat(0).sampleMimeType); } @@ -61,27 +63,24 @@ import java.io.IOException; @Override public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, boolean requireFormat) { - if (!ensureBoundSampleQueue()) { - return C.RESULT_NOTHING_READ; - } - return sampleStreamWrapper.readData(sampleQueueIndex, formatHolder, buffer, requireFormat); + return maybeMapToSampleQueue() + ? sampleStreamWrapper.readData(sampleQueueIndex, formatHolder, buffer, requireFormat) + : C.RESULT_NOTHING_READ; } @Override public int skipData(long positionUs) { - if (!ensureBoundSampleQueue()) { - return 0; - } - return sampleStreamWrapper.skipData(sampleQueueIndex, positionUs); + return maybeMapToSampleQueue() ? sampleStreamWrapper.skipData(sampleQueueIndex, positionUs) : 0; } // Internal methods. - private boolean ensureBoundSampleQueue() { - if (sampleQueueIndex != C.INDEX_UNSET) { - return true; + private boolean maybeMapToSampleQueue() { + if (sampleQueueIndex == HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_PENDING) { + sampleQueueIndex = sampleStreamWrapper.bindSampleQueueToSampleStream(trackGroupIndex); } - sampleQueueIndex = sampleStreamWrapper.bindSampleQueueToSampleStream(trackGroupIndex); - return sampleQueueIndex != C.INDEX_UNSET; + return sampleQueueIndex != HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_PENDING + && sampleQueueIndex != HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_NO_MAPPING_NON_FATAL + && sampleQueueIndex != HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_NO_MAPPING_FATAL; } } diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java index 4df213a7f0..0de4faa9c0 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java @@ -76,6 +76,10 @@ import java.util.Arrays; private static final String TAG = "HlsSampleStreamWrapper"; + public static final int SAMPLE_QUEUE_INDEX_PENDING = -1; + public static final int SAMPLE_QUEUE_INDEX_NO_MAPPING_FATAL = -2; + public static final int SAMPLE_QUEUE_INDEX_NO_MAPPING_NON_FATAL = -3; + @Retention(RetentionPolicy.SOURCE) @IntDef({PRIMARY_TYPE_NONE, PRIMARY_TYPE_TEXT, PRIMARY_TYPE_AUDIO, PRIMARY_TYPE_VIDEO}) private @interface PrimaryTrackType {} @@ -114,6 +118,7 @@ import java.util.Arrays; // Tracks are complicated in HLS. See documentation of buildTracks for details. // Indexed by track (as exposed by this source). private TrackGroupArray trackGroups; + private TrackGroupArray optionalTrackGroups; // Indexed by track group. private int[] trackGroupToSampleQueueIndex; private int primaryTrackGroupIndex; @@ -189,13 +194,18 @@ import java.util.Arrays; /** * Prepares the sample stream wrapper with master playlist information. * - * @param trackGroups This {@link TrackGroupArray} to expose. + * @param trackGroups The {@link TrackGroupArray} to expose. * @param primaryTrackGroupIndex The index of the adaptive track group. + * @param optionalTrackGroups A subset of {@code trackGroups} that should not trigger a failure if + * not found in the media playlist's segments. */ public void prepareWithMasterPlaylistInfo( - TrackGroupArray trackGroups, int primaryTrackGroupIndex) { + TrackGroupArray trackGroups, + int primaryTrackGroupIndex, + TrackGroupArray optionalTrackGroups) { prepared = true; this.trackGroups = trackGroups; + this.optionalTrackGroups = optionalTrackGroups; this.primaryTrackGroupIndex = primaryTrackGroupIndex; callback.onPrepared(); } @@ -208,21 +218,19 @@ import java.util.Arrays; return trackGroups; } - public boolean isMappingFinished() { - return trackGroupToSampleQueueIndex != null; - } - public int bindSampleQueueToSampleStream(int trackGroupIndex) { - if (!isMappingFinished()) { - return C.INDEX_UNSET; + if (trackGroupToSampleQueueIndex == null) { + return SAMPLE_QUEUE_INDEX_PENDING; } int sampleQueueIndex = trackGroupToSampleQueueIndex[trackGroupIndex]; if (sampleQueueIndex == C.INDEX_UNSET) { - return C.INDEX_UNSET; + return optionalTrackGroups.indexOf(trackGroups.get(trackGroupIndex)) == C.INDEX_UNSET + ? SAMPLE_QUEUE_INDEX_NO_MAPPING_FATAL + : SAMPLE_QUEUE_INDEX_NO_MAPPING_NON_FATAL; } if (sampleQueuesEnabledStates[sampleQueueIndex]) { // This sample queue is already bound to a different sample stream. - return C.INDEX_UNSET; + return SAMPLE_QUEUE_INDEX_NO_MAPPING_FATAL; } sampleQueuesEnabledStates[sampleQueueIndex] = true; return sampleQueueIndex; @@ -780,7 +788,7 @@ import java.util.Arrays; mapSampleQueuesToMatchTrackGroups(); } else { // Tracks are created using media segment information. - buildTracks(); + buildTracksFromSampleStreams(); prepared = true; callback.onPrepared(); } @@ -804,33 +812,34 @@ import java.util.Arrays; /** * Builds tracks that are exposed by this {@link HlsSampleStreamWrapper} instance, as well as * internal data-structures required for operation. - *

    - * Tracks in HLS are complicated. A HLS master playlist contains a number of "variants". Each + * + *

    Tracks in HLS are complicated. A HLS master playlist contains a number of "variants". Each * variant stream typically contains muxed video, audio and (possibly) additional audio, metadata * and caption tracks. We wish to allow the user to select between an adaptive track that spans * all variants, as well as each individual variant. If multiple audio tracks are present within * each variant then we wish to allow the user to select between those also. - *

    - * To do this, tracks are constructed as follows. The {@link HlsChunkSource} exposes (N+1) tracks, - * where N is the number of variants defined in the HLS master playlist. These consist of one - * adaptive track defined to span all variants and a track for each individual variant. The + * + *

    To do this, tracks are constructed as follows. The {@link HlsChunkSource} exposes (N+1) + * tracks, where N is the number of variants defined in the HLS master playlist. These consist of + * one adaptive track defined to span all variants and a track for each individual variant. The * adaptive track is initially selected. The extractor is then prepared to discover the tracks * inside of each variant stream. The two sets of tracks are then combined by this method to * create a third set, which is the set exposed by this {@link HlsSampleStreamWrapper}: + * *

      - *
    • The extractor tracks are inspected to infer a "primary" track type. If a video track is - * present then it is always the primary type. If not, audio is the primary type if present. - * Else text is the primary type if present. Else there is no primary type.
    • - *
    • If there is exactly one extractor track of the primary type, it's expanded into (N+1) - * exposed tracks, all of which correspond to the primary extractor track and each of which - * corresponds to a different chunk source track. Selecting one of these tracks has the effect - * of switching the selected track on the chunk source.
    • - *
    • All other extractor tracks are exposed directly. Selecting one of these tracks has the - * effect of selecting an extractor track, leaving the selected track on the chunk source - * unchanged.
    • + *
    • The extractor tracks are inspected to infer a "primary" track type. If a video track is + * present then it is always the primary type. If not, audio is the primary type if present. + * Else text is the primary type if present. Else there is no primary type. + *
    • If there is exactly one extractor track of the primary type, it's expanded into (N+1) + * exposed tracks, all of which correspond to the primary extractor track and each of which + * corresponds to a different chunk source track. Selecting one of these tracks has the + * effect of switching the selected track on the chunk source. + *
    • All other extractor tracks are exposed directly. Selecting one of these tracks has the + * effect of selecting an extractor track, leaving the selected track on the chunk source + * unchanged. *
    */ - private void buildTracks() { + private void buildTracksFromSampleStreams() { // Iterate through the extractor tracks to discover the "primary" track type, and the index // of the single track of this type. @PrimaryTrackType int primaryExtractorTrackType = PRIMARY_TYPE_NONE; @@ -887,6 +896,8 @@ import java.util.Arrays; } } this.trackGroups = new TrackGroupArray(trackGroups); + Assertions.checkState(optionalTrackGroups == null); + optionalTrackGroups = TrackGroupArray.EMPTY; } private HlsMediaChunk getLastMediaChunk() { diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadAction.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadAction.java index aaeb64ab06..e56bf66efd 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadAction.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadAction.java @@ -20,63 +20,56 @@ import android.support.annotation.Nullable; import com.google.android.exoplayer2.offline.DownloadAction; import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; import com.google.android.exoplayer2.offline.SegmentDownloadAction; +import com.google.android.exoplayer2.source.hls.playlist.RenditionKey; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; +import java.util.List; /** An action to download or remove downloaded HLS streams. */ -public final class HlsDownloadAction extends SegmentDownloadAction { +public final class HlsDownloadAction extends SegmentDownloadAction { + + private static final String TYPE = "hls"; + private static final int VERSION = 0; public static final Deserializer DESERIALIZER = - new SegmentDownloadActionDeserializer() { + new SegmentDownloadActionDeserializer(TYPE, VERSION) { @Override - public String getType() { - return TYPE; - } - - @Override - protected String readKey(DataInputStream input) throws IOException { - return input.readUTF(); - } - - @Override - protected String[] createKeyArray(int keyCount) { - return new String[keyCount]; + protected RenditionKey readKey(DataInputStream input) throws IOException { + int renditionGroup = input.readInt(); + int trackIndex = input.readInt(); + return new RenditionKey(renditionGroup, trackIndex); } @Override protected DownloadAction createDownloadAction( - Uri manifestUri, boolean removeAction, String data, String[] keys) { - return new HlsDownloadAction(manifestUri, removeAction, data, keys); + Uri uri, boolean isRemoveAction, byte[] data, List keys) { + return new HlsDownloadAction(uri, isRemoveAction, data, keys); } }; - private static final String TYPE = "HlsDownloadAction"; - - /** @see SegmentDownloadAction#SegmentDownloadAction(Uri, boolean, String, Object[]) */ + /** + * @param uri The HLS playlist URI. + * @param isRemoveAction Whether the data will be removed. If {@code false} it will be downloaded. + * @param data Optional custom data for this action. + * @param keys Keys of renditions to be downloaded. If empty, all renditions are downloaded. If + * {@code removeAction} is true, {@code keys} must empty. + */ public HlsDownloadAction( - Uri manifestUri, boolean removeAction, @Nullable String data, String... keys) { - super(manifestUri, removeAction, data, keys); - } - - @Override - protected String getType() { - return TYPE; + Uri uri, boolean isRemoveAction, @Nullable byte[] data, List keys) { + super(TYPE, VERSION, uri, isRemoveAction, data, keys); } @Override protected HlsDownloader createDownloader(DownloaderConstructorHelper constructorHelper) { - HlsDownloader downloader = new HlsDownloader(manifestUri, constructorHelper); - if (!isRemoveAction()) { - downloader.selectRepresentations(keys); - } - return downloader; + return new HlsDownloader(uri, keys, constructorHelper); } @Override - protected void writeKey(DataOutputStream output, String key) throws IOException { - output.writeUTF(key); + protected void writeKey(DataOutputStream output, RenditionKey key) throws IOException { + output.writeInt(key.type); + output.writeInt(key.trackIndex); } } diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadHelper.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadHelper.java new file mode 100644 index 0000000000..773aec49ee --- /dev/null +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadHelper.java @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.source.hls.offline; + +import android.net.Uri; +import android.support.annotation.Nullable; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.offline.DownloadHelper; +import com.google.android.exoplayer2.offline.TrackKey; +import com.google.android.exoplayer2.source.TrackGroup; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist; +import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist; +import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylist; +import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistParser; +import com.google.android.exoplayer2.source.hls.playlist.RenditionKey; +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.ParsingLoadable; +import com.google.android.exoplayer2.util.Assertions; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; + +/** A {@link DownloadHelper} for HLS streams. */ +public final class HlsDownloadHelper extends DownloadHelper { + + private final Uri uri; + private final DataSource.Factory manifestDataSourceFactory; + + private @MonotonicNonNull HlsPlaylist playlist; + private int[] renditionTypes; + + public HlsDownloadHelper(Uri uri, DataSource.Factory manifestDataSourceFactory) { + this.uri = uri; + this.manifestDataSourceFactory = manifestDataSourceFactory; + } + + @Override + protected void prepareInternal() throws IOException { + DataSource dataSource = manifestDataSourceFactory.createDataSource(); + playlist = ParsingLoadable.load(dataSource, new HlsPlaylistParser(), uri); + } + + @Override + public int getPeriodCount() { + Assertions.checkNotNull(playlist); + return 1; + } + + @Override + public TrackGroupArray getTrackGroups(int periodIndex) { + Assertions.checkNotNull(playlist); + if (playlist instanceof HlsMediaPlaylist) { + return TrackGroupArray.EMPTY; + } + // TODO: Generate track groups as in playback. Reverse the mapping in getDownloadAction. + HlsMasterPlaylist masterPlaylist = (HlsMasterPlaylist) playlist; + TrackGroup[] trackGroups = new TrackGroup[3]; + renditionTypes = new int[3]; + int trackGroupIndex = 0; + if (!masterPlaylist.variants.isEmpty()) { + renditionTypes[trackGroupIndex] = RenditionKey.TYPE_VARIANT; + trackGroups[trackGroupIndex++] = new TrackGroup(toFormats(masterPlaylist.variants)); + } + if (!masterPlaylist.audios.isEmpty()) { + renditionTypes[trackGroupIndex] = RenditionKey.TYPE_AUDIO; + trackGroups[trackGroupIndex++] = new TrackGroup(toFormats(masterPlaylist.audios)); + } + if (!masterPlaylist.subtitles.isEmpty()) { + renditionTypes[trackGroupIndex] = RenditionKey.TYPE_SUBTITLE; + trackGroups[trackGroupIndex++] = new TrackGroup(toFormats(masterPlaylist.subtitles)); + } + return new TrackGroupArray(Arrays.copyOf(trackGroups, trackGroupIndex)); + } + + @Override + public HlsDownloadAction getDownloadAction(@Nullable byte[] data, List trackKeys) { + Assertions.checkNotNull(renditionTypes); + return new HlsDownloadAction( + uri, /* isRemoveAction= */ false, data, toRenditionKeys(trackKeys, renditionTypes)); + } + + @Override + public HlsDownloadAction getRemoveAction(@Nullable byte[] data) { + return new HlsDownloadAction( + uri, /* isRemoveAction= */ true, data, Collections.emptyList()); + } + + private static Format[] toFormats(List hlsUrls) { + Format[] formats = new Format[hlsUrls.size()]; + for (int i = 0; i < hlsUrls.size(); i++) { + formats[i] = hlsUrls.get(i).format; + } + return formats; + } + + private static List toRenditionKeys(List trackKeys, int[] groups) { + List representationKeys = new ArrayList<>(trackKeys.size()); + for (int i = 0; i < trackKeys.size(); i++) { + TrackKey trackKey = trackKeys.get(i); + representationKeys.add(new RenditionKey(groups[trackKey.groupIndex], trackKey.trackIndex)); + } + return representationKeys; + } +} diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloader.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloader.java index 3d14283e86..bd59eed447 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloader.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloader.java @@ -24,6 +24,7 @@ import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.HlsUr import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist; import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylist; import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistParser; +import com.google.android.exoplayer2.source.hls.playlist.RenditionKey; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.ParsingLoadable; @@ -34,70 +35,85 @@ import java.util.HashSet; import java.util.List; /** - * Helper class to download HLS streams. + * A downloader for HLS streams. * - *

    A subset of renditions can be downloaded by selecting them using {@link - * #selectRepresentations(Object[])}. As key, string form of the rendition's url is used. The urls - * can be absolute or relative to the master playlist url. + *

    Example usage: + * + *

    {@code
    + * SimpleCache cache = new SimpleCache(downloadFolder, new NoOpCacheEvictor());
    + * DefaultHttpDataSourceFactory factory = new DefaultHttpDataSourceFactory("ExoPlayer", null);
    + * DownloaderConstructorHelper constructorHelper =
    + *     new DownloaderConstructorHelper(cache, factory);
    + * // Create a downloader for the first variant in a master playlist.
    + * HlsDownloader hlsDownloader =
    + *     new HlsDownloader(
    + *         playlistUri,
    + *         Collections.singletonList(new RenditionKey(RenditionKey.TYPE_VARIANT, 0)),
    + *         constructorHelper);
    + * // Perform the download.
    + * hlsDownloader.download();
    + * // Access downloaded data using CacheDataSource
    + * CacheDataSource cacheDataSource =
    + *     new CacheDataSource(cache, factory.createDataSource(), CacheDataSource.FLAG_BLOCK_ON_CACHE);
    + * }
    */ -public final class HlsDownloader extends SegmentDownloader { +public final class HlsDownloader extends SegmentDownloader { /** - * @see SegmentDownloader#SegmentDownloader(Uri, DownloaderConstructorHelper) + * @param playlistUri The {@link Uri} of the playlist to be downloaded. + * @param renditionKeys Keys defining which renditions in the playlist should be selected for + * download. If empty, all renditions are downloaded. + * @param constructorHelper A {@link DownloaderConstructorHelper} instance. */ - public HlsDownloader(Uri manifestUri, DownloaderConstructorHelper constructorHelper) { - super(manifestUri, constructorHelper); + public HlsDownloader( + Uri playlistUri, + List renditionKeys, + DownloaderConstructorHelper constructorHelper) { + super(playlistUri, renditionKeys, constructorHelper); } @Override - public String[] getAllRepresentationKeys() throws IOException { - ArrayList urls = new ArrayList<>(); - HlsMasterPlaylist manifest = getManifest(); - extractUrls(manifest.variants, urls); - extractUrls(manifest.audios, urls); - extractUrls(manifest.subtitles, urls); - return urls.toArray(new String[urls.size()]); + protected HlsPlaylist getManifest(DataSource dataSource, Uri uri) throws IOException { + return loadManifest(dataSource, uri); } @Override - protected HlsMasterPlaylist getManifest(DataSource dataSource, Uri uri) throws IOException { - HlsPlaylist hlsPlaylist = loadManifest(dataSource, uri); - if (hlsPlaylist instanceof HlsMasterPlaylist) { - return (HlsMasterPlaylist) hlsPlaylist; + protected List getSegments( + DataSource dataSource, HlsPlaylist playlist, boolean allowIncompleteList) throws IOException { + ArrayList mediaPlaylistUris = new ArrayList<>(); + if (playlist instanceof HlsMasterPlaylist) { + HlsMasterPlaylist masterPlaylist = (HlsMasterPlaylist) playlist; + addResolvedUris(masterPlaylist.baseUri, masterPlaylist.variants, mediaPlaylistUris); + addResolvedUris(masterPlaylist.baseUri, masterPlaylist.audios, mediaPlaylistUris); + addResolvedUris(masterPlaylist.baseUri, masterPlaylist.subtitles, mediaPlaylistUris); } else { - return HlsMasterPlaylist.createSingleVariantMasterPlaylist(hlsPlaylist.baseUri); + mediaPlaylistUris.add(Uri.parse(playlist.baseUri)); } - } - - @Override - protected List getSegments(DataSource dataSource, HlsMasterPlaylist manifest, - String[] keys, boolean allowIndexLoadErrors) throws InterruptedException, IOException { - HashSet encryptionKeyUris = new HashSet<>(); ArrayList segments = new ArrayList<>(); - for (String playlistUrl : keys) { - HlsMediaPlaylist mediaPlaylist = null; - Uri uri = UriUtil.resolveToUri(manifest.baseUri, playlistUrl); + + HashSet seenEncryptionKeyUris = new HashSet<>(); + for (Uri mediaPlaylistUri : mediaPlaylistUris) { + HlsMediaPlaylist mediaPlaylist; try { - mediaPlaylist = (HlsMediaPlaylist) loadManifest(dataSource, uri); + mediaPlaylist = (HlsMediaPlaylist) loadManifest(dataSource, mediaPlaylistUri); + segments.add(new Segment(mediaPlaylist.startTimeUs, new DataSpec(mediaPlaylistUri))); } catch (IOException e) { - if (!allowIndexLoadErrors) { + if (!allowIncompleteList) { throw e; } - } - segments.add(new Segment(mediaPlaylist != null ? mediaPlaylist.startTimeUs : Long.MIN_VALUE, - new DataSpec(uri))); - if (mediaPlaylist == null) { + segments.add(new Segment(0, new DataSpec(mediaPlaylistUri))); continue; } - - HlsMediaPlaylist.Segment initSegment = mediaPlaylist.initializationSegment; - if (initSegment != null) { - addSegment(segments, mediaPlaylist, initSegment, encryptionKeyUris); - } - + HlsMediaPlaylist.Segment lastInitSegment = null; List hlsSegments = mediaPlaylist.segments; for (int i = 0; i < hlsSegments.size(); i++) { - addSegment(segments, mediaPlaylist, hlsSegments.get(i), encryptionKeyUris); + HlsMediaPlaylist.Segment segment = hlsSegments.get(i); + HlsMediaPlaylist.Segment initSegment = segment.initializationSegment; + if (initSegment != null && initSegment != lastInitSegment) { + lastInitSegment = initSegment; + addSegment(segments, mediaPlaylist, initSegment, seenEncryptionKeyUris); + } + addSegment(segments, mediaPlaylist, segment, seenEncryptionKeyUris); } } return segments; @@ -114,12 +130,12 @@ public final class HlsDownloader extends SegmentDownloader segments, HlsMediaPlaylist mediaPlaylist, HlsMediaPlaylist.Segment hlsSegment, - HashSet encryptionKeyUris) { + HashSet seenEncryptionKeyUris) { long startTimeUs = mediaPlaylist.startTimeUs + hlsSegment.relativeStartTimeUs; if (hlsSegment.fullSegmentEncryptionKeyUri != null) { Uri keyUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, hlsSegment.fullSegmentEncryptionKeyUri); - if (encryptionKeyUris.add(keyUri)) { + if (seenEncryptionKeyUris.add(keyUri)) { segments.add(new Segment(startTimeUs, new DataSpec(keyUri))); } } @@ -128,10 +144,9 @@ public final class HlsDownloader extends SegmentDownloader hlsUrls, ArrayList urls) { - for (int i = 0; i < hlsUrls.size(); i++) { - urls.add(hlsUrls.get(i).url); + private static void addResolvedUris(String baseUri, List urls, List out) { + for (int i = 0; i < urls.size(); i++) { + out.add(UriUtil.resolveToUri(baseUri, urls.get(i).url)); } } - } diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/FilteringHlsPlaylistParser.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/FilteringHlsPlaylistParser.java deleted file mode 100644 index db490f2518..0000000000 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/FilteringHlsPlaylistParser.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright (C) 2018 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.source.hls.playlist; - -import android.net.Uri; -import android.support.annotation.Nullable; -import com.google.android.exoplayer2.upstream.ParsingLoadable.Parser; -import java.io.IOException; -import java.io.InputStream; -import java.util.List; - -/** A HLS playlists parser which includes only the renditions identified by the given urls. */ -public final class FilteringHlsPlaylistParser implements Parser { - - private final HlsPlaylistParser hlsPlaylistParser; - private final List filter; - - /** - * @param filter The urls to renditions that should be retained in the parsed playlists. If null, - * all renditions are retained. - */ - public FilteringHlsPlaylistParser(@Nullable List filter) { - this.hlsPlaylistParser = new HlsPlaylistParser(); - this.filter = filter; - } - - @Override - public HlsPlaylist parse(Uri uri, InputStream inputStream) throws IOException { - HlsPlaylist hlsPlaylist = hlsPlaylistParser.parse(uri, inputStream); - if (hlsPlaylist instanceof HlsMasterPlaylist) { - HlsMasterPlaylist masterPlaylist = (HlsMasterPlaylist) hlsPlaylist; - return filter != null ? masterPlaylist.copy(filter) : masterPlaylist; - } else { - return hlsPlaylist; - } - } -} diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java index 04192def9d..5c29dca38e 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java @@ -21,9 +21,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; -/** - * Represents an HLS master playlist. - */ +/** Represents an HLS master playlist. */ public final class HlsMasterPlaylist extends HlsPlaylist { /** @@ -109,18 +107,16 @@ public final class HlsMasterPlaylist extends HlsPlaylist { ? Collections.unmodifiableList(muxedCaptionFormats) : null; } - /** - * Returns a copy of this playlist which includes only the renditions identified by the given - * urls. - * - * @param renditionUrls List of rendition urls. - * @return A copy of this playlist which includes only the renditions identified by the given - * urls. - */ - public HlsMasterPlaylist copy(List renditionUrls) { - return new HlsMasterPlaylist(baseUri, tags, copyRenditionsList(variants, renditionUrls), - copyRenditionsList(audios, renditionUrls), copyRenditionsList(subtitles, renditionUrls), - muxedAudioFormat, muxedCaptionFormats); + @Override + public HlsMasterPlaylist copy(List renditionKeys) { + return new HlsMasterPlaylist( + baseUri, + tags, + copyRenditionsList(variants, RenditionKey.TYPE_VARIANT, renditionKeys), + copyRenditionsList(audios, RenditionKey.TYPE_AUDIO, renditionKeys), + copyRenditionsList(subtitles, RenditionKey.TYPE_SUBTITLE, renditionKeys), + muxedAudioFormat, + muxedCaptionFormats); } /** @@ -136,12 +132,17 @@ public final class HlsMasterPlaylist extends HlsPlaylist { emptyList, null, null); } - private static List copyRenditionsList(List renditions, List urls) { - List copiedRenditions = new ArrayList<>(urls.size()); + private static List copyRenditionsList( + List renditions, int renditionType, List renditionKeys) { + List copiedRenditions = new ArrayList<>(renditionKeys.size()); for (int i = 0; i < renditions.size(); i++) { HlsUrl rendition = renditions.get(i); - if (urls.contains(rendition.url)) { - copiedRenditions.add(rendition); + for (int j = 0; j < renditionKeys.size(); j++) { + RenditionKey renditionKey = renditionKeys.get(j); + if (renditionKey.type == renditionType && renditionKey.trackIndex == i) { + copiedRenditions.add(rendition); + break; + } } } return copiedRenditions; diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java index 9a9517e2d4..f905def54b 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.source.hls.playlist; import android.support.annotation.IntDef; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.drm.DrmInitData; import java.lang.annotation.Retention; @@ -24,9 +25,7 @@ import java.lang.annotation.RetentionPolicy; import java.util.Collections; import java.util.List; -/** - * Represents an HLS media playlist. - */ +/** Represents an HLS media playlist. */ public final class HlsMediaPlaylist extends HlsPlaylist { /** Media segment reference. */ @@ -38,8 +37,12 @@ public final class HlsMediaPlaylist extends HlsPlaylist { */ public final String url; /** - * The duration of the segment in microseconds, as defined by #EXTINF. + * The media initialization section for this segment, as defined by #EXT-X-MAP. May be null if + * the media playlist does not define a media section for this segment. The same instance is + * used for all segments that share an EXT-X-MAP tag. */ + @Nullable public final Segment initializationSegment; + /** The duration of the segment in microseconds, as defined by #EXTINF. */ public final long durationUs; /** * The number of #EXT-X-DISCONTINUITY tags in the playlist before the segment. @@ -78,11 +81,12 @@ public final class HlsMediaPlaylist extends HlsPlaylist { * @param byterangeLength See {@link #byterangeLength}. */ public Segment(String uri, long byterangeOffset, long byterangeLength) { - this(uri, 0, -1, C.TIME_UNSET, null, null, byterangeOffset, byterangeLength, false); + this(uri, null, 0, -1, C.TIME_UNSET, null, null, byterangeOffset, byterangeLength, false); } /** * @param url See {@link #url}. + * @param initializationSegment See {@link #initializationSegment}. * @param durationUs See {@link #durationUs}. * @param relativeDiscontinuitySequence See {@link #relativeDiscontinuitySequence}. * @param relativeStartTimeUs See {@link #relativeStartTimeUs}. @@ -94,6 +98,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist { */ public Segment( String url, + Segment initializationSegment, long durationUs, int relativeDiscontinuitySequence, long relativeStartTimeUs, @@ -103,6 +108,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist { long byterangeLength, boolean hasGapTag) { this.url = url; + this.initializationSegment = initializationSegment; this.durationUs = durationUs; this.relativeDiscontinuitySequence = relativeDiscontinuitySequence; this.relativeStartTimeUs = relativeStartTimeUs; @@ -182,10 +188,6 @@ public final class HlsMediaPlaylist extends HlsPlaylist { * encryption. */ public final DrmInitData drmInitData; - /** - * The initialization segment, as defined by #EXT-X-MAP. - */ - public final Segment initializationSegment; /** * The list of segments in the playlist. */ @@ -210,7 +212,6 @@ public final class HlsMediaPlaylist extends HlsPlaylist { * @param hasEndTag See {@link #hasEndTag}. * @param hasProgramDateTime See {@link #hasProgramDateTime}. * @param drmInitData See {@link #drmInitData}. - * @param initializationSegment See {@link #initializationSegment}. * @param segments See {@link #segments}. */ public HlsMediaPlaylist( @@ -228,7 +229,6 @@ public final class HlsMediaPlaylist extends HlsPlaylist { boolean hasEndTag, boolean hasProgramDateTime, DrmInitData drmInitData, - Segment initializationSegment, List segments) { super(baseUri, tags); this.playlistType = playlistType; @@ -242,7 +242,6 @@ public final class HlsMediaPlaylist extends HlsPlaylist { this.hasEndTag = hasEndTag; this.hasProgramDateTime = hasProgramDateTime; this.drmInitData = drmInitData; - this.initializationSegment = initializationSegment; this.segments = Collections.unmodifiableList(segments); if (!segments.isEmpty()) { Segment last = segments.get(segments.size() - 1); @@ -254,6 +253,11 @@ public final class HlsMediaPlaylist extends HlsPlaylist { : startOffsetUs >= 0 ? startOffsetUs : durationUs + startOffsetUs; } + @Override + public HlsMediaPlaylist copy(List renditionKeys) { + return this; + } + /** * Returns whether this playlist is newer than {@code other}. * @@ -291,9 +295,22 @@ public final class HlsMediaPlaylist extends HlsPlaylist { * @return The playlist. */ public HlsMediaPlaylist copyWith(long startTimeUs, int discontinuitySequence) { - return new HlsMediaPlaylist(playlistType, baseUri, tags, startOffsetUs, startTimeUs, true, - discontinuitySequence, mediaSequence, version, targetDurationUs, hasIndependentSegmentsTag, - hasEndTag, hasProgramDateTime, drmInitData, initializationSegment, segments); + return new HlsMediaPlaylist( + playlistType, + baseUri, + tags, + startOffsetUs, + startTimeUs, + /* hasDiscontinuitySequence= */ true, + discontinuitySequence, + mediaSequence, + version, + targetDurationUs, + hasIndependentSegmentsTag, + hasEndTag, + hasProgramDateTime, + drmInitData, + segments); } /** @@ -306,9 +323,21 @@ public final class HlsMediaPlaylist extends HlsPlaylist { if (this.hasEndTag) { return this; } - return new HlsMediaPlaylist(playlistType, baseUri, tags, startOffsetUs, startTimeUs, - hasDiscontinuitySequence, discontinuitySequence, mediaSequence, version, targetDurationUs, - hasIndependentSegmentsTag, true, hasProgramDateTime, drmInitData, initializationSegment, + return new HlsMediaPlaylist( + playlistType, + baseUri, + tags, + startOffsetUs, + startTimeUs, + hasDiscontinuitySequence, + discontinuitySequence, + mediaSequence, + version, + targetDurationUs, + hasIndependentSegmentsTag, + /* hasEndTag= */ true, + hasProgramDateTime, + drmInitData, segments); } diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylist.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylist.java index a490c9477c..34ecde229d 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylist.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylist.java @@ -15,13 +15,12 @@ */ package com.google.android.exoplayer2.source.hls.playlist; +import com.google.android.exoplayer2.offline.FilterableManifest; import java.util.Collections; import java.util.List; -/** - * Represents an HLS playlist. - */ -public abstract class HlsPlaylist { +/** Represents an HLS playlist. */ +public abstract class HlsPlaylist implements FilterableManifest { /** * The base uri. Used to resolve relative paths. diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java index acd0746e72..7187bdb0ca 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java @@ -123,7 +123,8 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser { + + /** Types of rendition. */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({TYPE_VARIANT, TYPE_AUDIO, TYPE_SUBTITLE}) + public @interface Type {} + + public static final int TYPE_VARIANT = 0; + public static final int TYPE_AUDIO = 1; + public static final int TYPE_SUBTITLE = 2; + + public final @Type int type; + public final int trackIndex; + + public RenditionKey(@Type int type, int trackIndex) { + this.type = type; + this.trackIndex = trackIndex; + } + + @Override + public String toString() { + return type + "." + trackIndex; + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + RenditionKey that = (RenditionKey) o; + return type == that.type && trackIndex == that.trackIndex; + } + + @Override + public int hashCode() { + int result = type; + result = 31 * result + trackIndex; + return result; + } + + // Comparable implementation. + + @Override + public int compareTo(@NonNull RenditionKey other) { + int result = type - other.type; + if (result == 0) { + result = trackIndex - other.trackIndex; + } + return result; + } +} diff --git a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/Aes128DataSourceTest.java b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/Aes128DataSourceTest.java new file mode 100644 index 0000000000..86bffc7762 --- /dev/null +++ b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/Aes128DataSourceTest.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.source.hls; + +import static com.google.common.truth.Truth.assertThat; + +import android.net.Uri; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DataSpec; +import java.io.IOException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +/** Test for {@link Aes128DataSource}. */ +@RunWith(RobolectricTestRunner.class) +public class Aes128DataSourceTest { + + @Test + public void test_OpenCallsUpstreamOpen_CloseCallsUpstreamClose() throws IOException { + UpstreamDataSource upstream = new UpstreamDataSource(); + Aes128DataSource testInstance = new Aes128DataSource(upstream, new byte[16], new byte[16]); + assertThat(upstream.opened).isFalse(); + + Uri uri = Uri.parse("http.abc.com/def"); + testInstance.open(new DataSpec(uri)); + assertThat(upstream.opened).isTrue(); + + testInstance.close(); + assertThat(upstream.opened).isFalse(); + } + + @Test + public void test_OpenCallsUpstreamThrowingOpen_CloseCallsUpstreamClose() throws IOException { + UpstreamDataSource upstream = + new UpstreamDataSource() { + @Override + public long open(DataSpec dataSpec) throws IOException { + throw new IOException(); + } + }; + Aes128DataSource testInstance = new Aes128DataSource(upstream, new byte[16], new byte[16]); + assertThat(upstream.opened).isFalse(); + + Uri uri = Uri.parse("http.abc.com/def"); + try { + testInstance.open(new DataSpec(uri)); + } catch (IOException e) { + // Expected. + } + assertThat(upstream.opened).isFalse(); + assertThat(upstream.closedCalled).isFalse(); + + // Even though the upstream open call failed, close should still call close on the upstream as + // per the contract of DataSource. + testInstance.close(); + assertThat(upstream.closedCalled).isTrue(); + } + + private static class UpstreamDataSource implements DataSource { + + public boolean opened; + public boolean closedCalled; + + @Override + public long open(DataSpec dataSpec) throws IOException { + opened = true; + return C.LENGTH_UNSET; + } + + @Override + public int read(byte[] buffer, int offset, int readLength) { + return C.RESULT_END_OF_INPUT; + } + + @Override + public Uri getUri() { + return null; + } + + @Override + public void close() { + opened = false; + closedCalled = true; + } + } +} diff --git a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadTestData.java b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadTestData.java index 55db28a59a..f38a4577be 100644 --- a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadTestData.java +++ b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadTestData.java @@ -22,6 +22,10 @@ import java.nio.charset.Charset; /* package */ interface HlsDownloadTestData { String MASTER_PLAYLIST_URI = "test.m3u8"; + int MASTER_MEDIA_PLAYLIST_1_INDEX = 0; + int MASTER_MEDIA_PLAYLIST_2_INDEX = 1; + int MASTER_MEDIA_PLAYLIST_3_INDEX = 2; + int MASTER_MEDIA_PLAYLIST_0_INDEX = 3; String MEDIA_PLAYLIST_0_DIR = "gear0/"; String MEDIA_PLAYLIST_0_URI = MEDIA_PLAYLIST_0_DIR + "prog_index.m3u8"; diff --git a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloaderTest.java b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloaderTest.java index 9d8d3d28ee..6e816dd8a7 100644 --- a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloaderTest.java +++ b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloaderTest.java @@ -17,6 +17,8 @@ package com.google.android.exoplayer2.source.hls.offline; import static com.google.android.exoplayer2.source.hls.offline.HlsDownloadTestData.ENC_MEDIA_PLAYLIST_DATA; import static com.google.android.exoplayer2.source.hls.offline.HlsDownloadTestData.ENC_MEDIA_PLAYLIST_URI; +import static com.google.android.exoplayer2.source.hls.offline.HlsDownloadTestData.MASTER_MEDIA_PLAYLIST_1_INDEX; +import static com.google.android.exoplayer2.source.hls.offline.HlsDownloadTestData.MASTER_MEDIA_PLAYLIST_2_INDEX; import static com.google.android.exoplayer2.source.hls.offline.HlsDownloadTestData.MASTER_PLAYLIST_DATA; import static com.google.android.exoplayer2.source.hls.offline.HlsDownloadTestData.MASTER_PLAYLIST_URI; import static com.google.android.exoplayer2.source.hls.offline.HlsDownloadTestData.MEDIA_PLAYLIST_0_DIR; @@ -34,13 +36,15 @@ import static com.google.common.truth.Truth.assertThat; import android.net.Uri; import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; -import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist; +import com.google.android.exoplayer2.source.hls.playlist.RenditionKey; import com.google.android.exoplayer2.testutil.FakeDataSet; import com.google.android.exoplayer2.testutil.FakeDataSource.Factory; import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor; import com.google.android.exoplayer2.upstream.cache.SimpleCache; import com.google.android.exoplayer2.util.Util; import java.io.File; +import java.util.ArrayList; +import java.util.List; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -55,7 +59,6 @@ public class HlsDownloaderTest { private SimpleCache cache; private File tempFolder; private FakeDataSet fakeDataSet; - private HlsDownloader hlsDownloader; @Before public void setUp() throws Exception { @@ -73,68 +76,28 @@ public class HlsDownloaderTest { .setRandomData(MEDIA_PLAYLIST_2_DIR + "fileSequence0.ts", 13) .setRandomData(MEDIA_PLAYLIST_2_DIR + "fileSequence1.ts", 14) .setRandomData(MEDIA_PLAYLIST_2_DIR + "fileSequence2.ts", 15); - hlsDownloader = getHlsDownloader(MASTER_PLAYLIST_URI); } @After - public void tearDown() throws Exception { + public void tearDown() { Util.recursiveDelete(tempFolder); } - @Test - public void testDownloadManifest() throws Exception { - HlsMasterPlaylist manifest = hlsDownloader.getManifest(); - - assertThat(manifest).isNotNull(); - assertCachedData(cache, fakeDataSet, MASTER_PLAYLIST_URI); - } - - @Test - public void testSelectRepresentationsClearsPreviousSelection() throws Exception { - hlsDownloader.selectRepresentations(new String[] {MEDIA_PLAYLIST_1_URI}); - hlsDownloader.selectRepresentations(new String[] {MEDIA_PLAYLIST_2_URI}); - hlsDownloader.download(null); - - assertCachedData( - cache, - fakeDataSet, - MASTER_PLAYLIST_URI, - MEDIA_PLAYLIST_2_URI, - MEDIA_PLAYLIST_2_DIR + "fileSequence0.ts", - MEDIA_PLAYLIST_2_DIR + "fileSequence1.ts", - MEDIA_PLAYLIST_2_DIR + "fileSequence2.ts"); - } - @Test public void testCounterMethods() throws Exception { - hlsDownloader.selectRepresentations(new String[] {MEDIA_PLAYLIST_1_URI}); - hlsDownloader.download(null); + HlsDownloader downloader = + getHlsDownloader(MASTER_PLAYLIST_URI, getKeys(MASTER_MEDIA_PLAYLIST_1_INDEX)); + downloader.download(); - assertThat(hlsDownloader.getTotalSegments()).isEqualTo(4); - assertThat(hlsDownloader.getDownloadedSegments()).isEqualTo(4); - assertThat(hlsDownloader.getDownloadedBytes()) - .isEqualTo(MEDIA_PLAYLIST_DATA.length + 10 + 11 + 12); - } - - @Test - public void testInitStatus() throws Exception { - hlsDownloader.selectRepresentations(new String[] {MEDIA_PLAYLIST_1_URI}); - hlsDownloader.download(null); - - HlsDownloader newHlsDownloader = getHlsDownloader(MASTER_PLAYLIST_URI); - newHlsDownloader.selectRepresentations(new String[] {MEDIA_PLAYLIST_1_URI}); - newHlsDownloader.init(); - - assertThat(newHlsDownloader.getTotalSegments()).isEqualTo(4); - assertThat(newHlsDownloader.getDownloadedSegments()).isEqualTo(4); - assertThat(newHlsDownloader.getDownloadedBytes()) + assertThat(downloader.getDownloadedBytes()) .isEqualTo(MEDIA_PLAYLIST_DATA.length + 10 + 11 + 12); } @Test public void testDownloadRepresentation() throws Exception { - hlsDownloader.selectRepresentations(new String[] {MEDIA_PLAYLIST_1_URI}); - hlsDownloader.download(null); + HlsDownloader downloader = + getHlsDownloader(MASTER_PLAYLIST_URI, getKeys(MASTER_MEDIA_PLAYLIST_1_INDEX)); + downloader.download(); assertCachedData( cache, @@ -148,8 +111,11 @@ public class HlsDownloaderTest { @Test public void testDownloadMultipleRepresentations() throws Exception { - hlsDownloader.selectRepresentations(new String[] {MEDIA_PLAYLIST_1_URI, MEDIA_PLAYLIST_2_URI}); - hlsDownloader.download(null); + HlsDownloader downloader = + getHlsDownloader( + MASTER_PLAYLIST_URI, + getKeys(MASTER_MEDIA_PLAYLIST_1_INDEX, MASTER_MEDIA_PLAYLIST_2_INDEX)); + downloader.download(); assertCachedData(cache, fakeDataSet); } @@ -166,36 +132,29 @@ public class HlsDownloaderTest { .setRandomData(MEDIA_PLAYLIST_3_DIR + "fileSequence0.ts", 13) .setRandomData(MEDIA_PLAYLIST_3_DIR + "fileSequence1.ts", 14) .setRandomData(MEDIA_PLAYLIST_3_DIR + "fileSequence2.ts", 15); - hlsDownloader = getHlsDownloader(MASTER_PLAYLIST_URI); - // hlsDownloader.selectRepresentations() isn't called - hlsDownloader.download(null); - assertCachedData(cache, fakeDataSet); - hlsDownloader.remove(); + HlsDownloader downloader = getHlsDownloader(MASTER_PLAYLIST_URI, getKeys()); + downloader.download(); - // select something random - hlsDownloader.selectRepresentations(new String[] {MEDIA_PLAYLIST_1_URI}); - // clear selection - hlsDownloader.selectRepresentations(new String[0]); - hlsDownloader.download(null); assertCachedData(cache, fakeDataSet); - hlsDownloader.remove(); } @Test - public void testRemoveAll() throws Exception { - hlsDownloader.selectRepresentations(new String[] {MEDIA_PLAYLIST_1_URI, MEDIA_PLAYLIST_2_URI}); - hlsDownloader.download(null); - hlsDownloader.remove(); + public void testRemove() throws Exception { + HlsDownloader downloader = + getHlsDownloader( + MASTER_PLAYLIST_URI, + getKeys(MASTER_MEDIA_PLAYLIST_1_INDEX, MASTER_MEDIA_PLAYLIST_2_INDEX)); + downloader.download(); + downloader.remove(); assertCacheEmpty(cache); } @Test public void testDownloadMediaPlaylist() throws Exception { - hlsDownloader = getHlsDownloader(MEDIA_PLAYLIST_1_URI); - hlsDownloader.selectRepresentations(new String[] {MEDIA_PLAYLIST_1_URI}); - hlsDownloader.download(null); + HlsDownloader downloader = getHlsDownloader(MEDIA_PLAYLIST_1_URI, getKeys()); + downloader.download(); assertCachedData( cache, @@ -216,16 +175,23 @@ public class HlsDownloaderTest { .setRandomData("fileSequence0.ts", 10) .setRandomData("fileSequence1.ts", 11) .setRandomData("fileSequence2.ts", 12); - hlsDownloader = getHlsDownloader(ENC_MEDIA_PLAYLIST_URI); - hlsDownloader.selectRepresentations(new String[] {ENC_MEDIA_PLAYLIST_URI}); - hlsDownloader.download(null); + HlsDownloader downloader = getHlsDownloader(ENC_MEDIA_PLAYLIST_URI, getKeys()); + downloader.download(); assertCachedData(cache, fakeDataSet); } - private HlsDownloader getHlsDownloader(String mediaPlaylistUri) { + private HlsDownloader getHlsDownloader(String mediaPlaylistUri, List keys) { Factory factory = new Factory(null).setFakeDataSet(fakeDataSet); return new HlsDownloader( - Uri.parse(mediaPlaylistUri), new DownloaderConstructorHelper(cache, factory)); + Uri.parse(mediaPlaylistUri), keys, new DownloaderConstructorHelper(cache, factory)); + } + + private static ArrayList getKeys(int... variantIndices) { + ArrayList renditionKeys = new ArrayList<>(); + for (int variantIndex : variantIndices) { + renditionKeys.add(new RenditionKey(RenditionKey.TYPE_VARIANT, variantIndex)); + } + return renditionKeys; } } diff --git a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java index 5ba6f0c7f4..7a8a4d7925 100644 --- a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java +++ b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java @@ -140,6 +140,78 @@ public class HlsMediaPlaylistParserTest { assertThat(segment.url).isEqualTo("https://priv.example.com/fileSequence2683.ts"); } + @Test + public void testParseSampleAesMethod() throws Exception { + Uri playlistUri = Uri.parse("https://example.com/test.m3u8"); + String playlistString = + "#EXTM3U\n" + + "#EXT-X-MEDIA-SEQUENCE:0\n" + + "#EXTINF:8,\n" + + "https://priv.example.com/1.ts\n" + + "\n" + + "#EXT-X-KEY:METHOD=SAMPLE-AES,URI=" + + "\"data:text/plain;base64,VGhpcyBpcyBhbiBlYXN0ZXIgZWdn\"," + + "IV=0x9358382AEB449EE23C3D809DA0B9CCD3,KEYFORMATVERSIONS=\"1\"," + + "KEYFORMAT=\"com.widevine\",IV=0x1566B\n" + + "#EXTINF:8,\n" + + "https://priv.example.com/2.ts\n" + + "#EXT-X-ENDLIST\n"; + InputStream inputStream = + new ByteArrayInputStream(playlistString.getBytes(Charset.forName(C.UTF8_NAME))); + HlsMediaPlaylist playlist = + (HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream); + assertThat(playlist.drmInitData.schemeType).isEqualTo(C.CENC_TYPE_cbcs); + assertThat(playlist.drmInitData.get(0).matches(C.WIDEVINE_UUID)).isTrue(); + } + + @Test + public void testParseSampleAesCencMethod() throws Exception { + Uri playlistUri = Uri.parse("https://example.com/test.m3u8"); + String playlistString = + "#EXTM3U\n" + + "#EXT-X-MEDIA-SEQUENCE:0\n" + + "#EXTINF:8,\n" + + "https://priv.example.com/1.ts\n" + + "\n" + + "#EXT-X-KEY:URI=\"data:text/plain;base64,VGhpcyBpcyBhbiBlYXN0ZXIgZWdn\"," + + "IV=0x9358382AEB449EE23C3D809DA0B9CCD3,KEYFORMATVERSIONS=\"1\"," + + "KEYFORMAT=\"urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed\"," + + "IV=0x1566B,METHOD=SAMPLE-AES-CENC \n" + + "#EXTINF:8,\n" + + "https://priv.example.com/2.ts\n" + + "#EXT-X-ENDLIST\n"; + InputStream inputStream = + new ByteArrayInputStream(playlistString.getBytes(Charset.forName(C.UTF8_NAME))); + HlsMediaPlaylist playlist = + (HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream); + assertThat(playlist.drmInitData.schemeType).isEqualTo(C.CENC_TYPE_cenc); + assertThat(playlist.drmInitData.get(0).matches(C.WIDEVINE_UUID)).isTrue(); + } + + @Test + public void testParseSampleAesCtrMethod() throws Exception { + Uri playlistUri = Uri.parse("https://example.com/test.m3u8"); + String playlistString = + "#EXTM3U\n" + + "#EXT-X-MEDIA-SEQUENCE:0\n" + + "#EXTINF:8,\n" + + "https://priv.example.com/1.ts\n" + + "\n" + + "#EXT-X-KEY:METHOD=SAMPLE-AES-CTR,URI=" + + "\"data:text/plain;base64,VGhpcyBpcyBhbiBlYXN0ZXIgZWdn\"," + + "IV=0x9358382AEB449EE23C3D809DA0B9CCD3,KEYFORMATVERSIONS=\"1\"," + + "KEYFORMAT=\"com.widevine\",IV=0x1566B\n" + + "#EXTINF:8,\n" + + "https://priv.example.com/2.ts\n" + + "#EXT-X-ENDLIST\n"; + InputStream inputStream = + new ByteArrayInputStream(playlistString.getBytes(Charset.forName(C.UTF8_NAME))); + HlsMediaPlaylist playlist = + (HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream); + assertThat(playlist.drmInitData.schemeType).isEqualTo(C.CENC_TYPE_cenc); + assertThat(playlist.drmInitData.get(0).matches(C.WIDEVINE_UUID)).isTrue(); + } + @Test public void testGapTag() throws IOException { Uri playlistUri = Uri.parse("https://example.com/test2.m3u8"); @@ -175,4 +247,35 @@ public class HlsMediaPlaylistParserTest { assertThat(playlist.segments.get(2).hasGapTag).isTrue(); assertThat(playlist.segments.get(3).hasGapTag).isFalse(); } + + @Test + public void testMapTag() throws IOException { + Uri playlistUri = Uri.parse("https://example.com/test3.m3u8"); + String playlistString = + "#EXTM3U\n" + + "#EXT-X-VERSION:3\n" + + "#EXT-X-TARGETDURATION:5\n" + + "#EXT-X-MEDIA-SEQUENCE:10\n" + + "#EXTINF:5.005,\n" + + "02/00/27.ts\n" + + "#EXT-X-MAP:URI=\"init1.ts\"" + + "#EXTINF:5.005,\n" + + "02/00/32.ts\n" + + "#EXTINF:5.005,\n" + + "02/00/42.ts\n" + + "#EXT-X-MAP:URI=\"init2.ts\"" + + "#EXTINF:5.005,\n" + + "02/00/47.ts\n"; + InputStream inputStream = + new ByteArrayInputStream(playlistString.getBytes(Charset.forName(C.UTF8_NAME))); + HlsMediaPlaylist playlist = + (HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream); + + List segments = playlist.segments; + assertThat(segments.get(0).initializationSegment).isNull(); + assertThat(segments.get(1).initializationSegment) + .isSameAs(segments.get(2).initializationSegment); + assertThat(segments.get(1).initializationSegment.url).isEqualTo("init1.ts"); + assertThat(segments.get(3).initializationSegment.url).isEqualTo("init2.ts"); + } } diff --git a/library/smoothstreaming/build.gradle b/library/smoothstreaming/build.gradle index 6ca5570a93..e71f9baa99 100644 --- a/library/smoothstreaming/build.gradle +++ b/library/smoothstreaming/build.gradle @@ -30,10 +30,15 @@ android { // testCoverageEnabled = true // } } + + lintOptions { + lintConfig file("../../checker-framework-lint.xml") + } } dependencies { implementation project(modulePrefix + 'library-core') + implementation 'org.checkerframework:checker-qual:' + checkerframeworkVersion implementation 'com.android.support:support-annotations:' + supportLibraryVersion testImplementation project(modulePrefix + 'testutils-robolectric') } diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/FilteringSsManifestParser.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/FilteringSsManifestParser.java deleted file mode 100644 index 3f88247ac9..0000000000 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/FilteringSsManifestParser.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.source.smoothstreaming.manifest; - -import android.net.Uri; -import android.support.annotation.Nullable; -import com.google.android.exoplayer2.upstream.ParsingLoadable.Parser; -import java.io.IOException; -import java.io.InputStream; -import java.util.List; - -/** - * A parser of SmoothStreaming manifest which includes only the tracks identified by the given keys. - */ -public final class FilteringSsManifestParser implements Parser { - - private final SsManifestParser ssManifestParser; - private final List filter; - - /** - * @param filter The track keys that should be retained in the parsed manifests. If null, all - * tracks are retained. - */ - public FilteringSsManifestParser(@Nullable List filter) { - this.ssManifestParser = new SsManifestParser(); - this.filter = filter; - } - - @Override - public SsManifest parse(Uri uri, InputStream inputStream) throws IOException { - SsManifest manifest = ssManifestParser.parse(uri, inputStream); - return filter != null ? manifest.copy(filter) : manifest; - } -} diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifest.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifest.java index 0df180a5a6..396d29fb75 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifest.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifest.java @@ -18,22 +18,22 @@ package com.google.android.exoplayer2.source.smoothstreaming.manifest; import android.net.Uri; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.offline.FilterableManifest; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.UriUtil; import com.google.android.exoplayer2.util.Util; import java.util.ArrayList; import java.util.Collections; -import java.util.LinkedList; import java.util.List; import java.util.UUID; /** * Represents a SmoothStreaming manifest. * - * @see - * IIS Smooth Streaming Client Manifest Format + * @see IIS Smooth + * Streaming Client Manifest Format */ -public class SsManifest { +public class SsManifest implements FilterableManifest { public static final int UNSET_LOOKAHEAD = -1; @@ -120,22 +120,16 @@ public class SsManifest { this.streamElements = streamElements; } - /** - * Creates a copy of this manifest which includes only the tracks identified by the given keys. - * - * @param trackKeys List of keys for the tracks to be included in the copy. - * @return A copy of this manifest with the selected tracks. - * @throws IndexOutOfBoundsException If a key has an invalid index. - */ - public final SsManifest copy(List trackKeys) { - LinkedList sortedKeys = new LinkedList<>(trackKeys); + @Override + public final SsManifest copy(List streamKeys) { + ArrayList sortedKeys = new ArrayList<>(streamKeys); Collections.sort(sortedKeys); StreamElement currentStreamElement = null; List copiedStreamElements = new ArrayList<>(); List copiedFormats = new ArrayList<>(); for (int i = 0; i < sortedKeys.size(); i++) { - TrackKey key = sortedKeys.get(i); + StreamKey key = sortedKeys.get(i); StreamElement streamElement = streamElements[key.streamElementIndex]; if (streamElement != currentStreamElement && currentStreamElement != null) { // We're advancing to a new stream element. Add the current one. diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/TrackKey.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/StreamKey.java similarity index 60% rename from library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/TrackKey.java rename to library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/StreamKey.java index ed52e6fa12..6667a3df27 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/TrackKey.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/StreamKey.java @@ -15,19 +15,16 @@ */ package com.google.android.exoplayer2.source.smoothstreaming.manifest; -import android.os.Parcel; -import android.os.Parcelable; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; -/** - * Uniquely identifies a track in a {@link SsManifest}. - */ -public final class TrackKey implements Parcelable, Comparable { +/** Uniquely identifies a track in a {@link SsManifest}. */ +public final class StreamKey implements Comparable { public final int streamElementIndex; public final int trackIndex; - public TrackKey(int streamElementIndex, int trackIndex) { + public StreamKey(int streamElementIndex, int trackIndex) { this.streamElementIndex = streamElementIndex; this.trackIndex = trackIndex; } @@ -37,40 +34,34 @@ public final class TrackKey implements Parcelable, Comparable { return streamElementIndex + "." + trackIndex; } - // Parcelable implementation. - @Override - public int describeContents() { - return 0; + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + StreamKey that = (StreamKey) o; + return streamElementIndex == that.streamElementIndex && trackIndex == that.trackIndex; } @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(streamElementIndex); - dest.writeInt(trackIndex); + public int hashCode() { + int result = streamElementIndex; + result = 31 * result + trackIndex; + return result; } - public static final Creator CREATOR = new Creator() { - @Override - public TrackKey createFromParcel(Parcel in) { - return new TrackKey(in.readInt(), in.readInt()); - } - - @Override - public TrackKey[] newArray(int size) { - return new TrackKey[size]; - } - }; - // Comparable implementation. @Override - public int compareTo(@NonNull TrackKey o) { + public int compareTo(@NonNull StreamKey o) { int result = streamElementIndex - o.streamElementIndex; if (result == 0) { result = trackIndex - o.trackIndex; } return result; } - } diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadAction.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadAction.java index 84c0eb5c4a..d4b3ef6622 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadAction.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadAction.java @@ -20,64 +20,52 @@ import android.support.annotation.Nullable; import com.google.android.exoplayer2.offline.DownloadAction; import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; import com.google.android.exoplayer2.offline.SegmentDownloadAction; -import com.google.android.exoplayer2.source.smoothstreaming.manifest.TrackKey; +import com.google.android.exoplayer2.source.smoothstreaming.manifest.StreamKey; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; +import java.util.List; /** An action to download or remove downloaded SmoothStreaming streams. */ -public final class SsDownloadAction extends SegmentDownloadAction { +public final class SsDownloadAction extends SegmentDownloadAction { + + private static final String TYPE = "ss"; + private static final int VERSION = 0; public static final Deserializer DESERIALIZER = - new SegmentDownloadActionDeserializer() { + new SegmentDownloadActionDeserializer(TYPE, VERSION) { - @Override - public String getType() { - return TYPE; - } + @Override + protected StreamKey readKey(DataInputStream input) throws IOException { + return new StreamKey(input.readInt(), input.readInt()); + } - @Override - protected TrackKey readKey(DataInputStream input) throws IOException { - return new TrackKey(input.readInt(), input.readInt()); - } + @Override + protected DownloadAction createDownloadAction( + Uri uri, boolean isRemoveAction, byte[] data, List keys) { + return new SsDownloadAction(uri, isRemoveAction, data, keys); + } + }; - @Override - protected TrackKey[] createKeyArray(int keyCount) { - return new TrackKey[keyCount]; - } - - @Override - protected DownloadAction createDownloadAction(Uri manifestUri, boolean removeAction, - String data, TrackKey[] keys) { - return new SsDownloadAction(manifestUri, removeAction, data, keys); - } - - }; - - private static final String TYPE = "SsDownloadAction"; - - /** @see SegmentDownloadAction#SegmentDownloadAction(Uri, boolean, String, Object[]) */ + /** + * @param uri The SmoothStreaming manifest URI. + * @param isRemoveAction Whether the data will be removed. If {@code false} it will be downloaded. + * @param data Optional custom data for this action. + * @param keys Keys of streams to be downloaded. If empty, all streams are downloaded. If {@code + * removeAction} is true, {@code keys} must be empty. + */ public SsDownloadAction( - Uri manifestUri, boolean removeAction, @Nullable String data, TrackKey... keys) { - super(manifestUri, removeAction, data, keys); - } - - @Override - protected String getType() { - return TYPE; + Uri uri, boolean isRemoveAction, @Nullable byte[] data, List keys) { + super(TYPE, VERSION, uri, isRemoveAction, data, keys); } @Override protected SsDownloader createDownloader(DownloaderConstructorHelper constructorHelper) { - SsDownloader downloader = new SsDownloader(manifestUri, constructorHelper); - if (!isRemoveAction()) { - downloader.selectRepresentations(keys); - } - return downloader; + return new SsDownloader(uri, keys, constructorHelper); } @Override - protected void writeKey(DataOutputStream output, TrackKey key) throws IOException { + protected void writeKey(DataOutputStream output, StreamKey key) throws IOException { output.writeInt(key.streamElementIndex); output.writeInt(key.trackIndex); } diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadHelper.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadHelper.java new file mode 100644 index 0000000000..82464101d6 --- /dev/null +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadHelper.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.source.smoothstreaming.offline; + +import android.net.Uri; +import android.support.annotation.Nullable; +import com.google.android.exoplayer2.offline.DownloadHelper; +import com.google.android.exoplayer2.offline.TrackKey; +import com.google.android.exoplayer2.source.TrackGroup; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest; +import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifestParser; +import com.google.android.exoplayer2.source.smoothstreaming.manifest.StreamKey; +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.ParsingLoadable; +import com.google.android.exoplayer2.util.Assertions; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; + +/** A {@link DownloadHelper} for SmoothStreaming streams. */ +public final class SsDownloadHelper extends DownloadHelper { + + private final Uri uri; + private final DataSource.Factory manifestDataSourceFactory; + + private @MonotonicNonNull SsManifest manifest; + + public SsDownloadHelper(Uri uri, DataSource.Factory manifestDataSourceFactory) { + this.uri = uri; + this.manifestDataSourceFactory = manifestDataSourceFactory; + } + + @Override + protected void prepareInternal() throws IOException { + DataSource dataSource = manifestDataSourceFactory.createDataSource(); + manifest = ParsingLoadable.load(dataSource, new SsManifestParser(), uri); + } + + @Override + public int getPeriodCount() { + Assertions.checkNotNull(manifest); + return 1; + } + + @Override + public TrackGroupArray getTrackGroups(int periodIndex) { + Assertions.checkNotNull(manifest); + SsManifest.StreamElement[] streamElements = manifest.streamElements; + TrackGroup[] trackGroups = new TrackGroup[streamElements.length]; + for (int i = 0; i < streamElements.length; i++) { + trackGroups[i] = new TrackGroup(streamElements[i].formats); + } + return new TrackGroupArray(trackGroups); + } + + @Override + public SsDownloadAction getDownloadAction(@Nullable byte[] data, List trackKeys) { + return new SsDownloadAction(uri, /* isRemoveAction= */ false, data, toStreamKeys(trackKeys)); + } + + @Override + public SsDownloadAction getRemoveAction(@Nullable byte[] data) { + return new SsDownloadAction( + uri, /* isRemoveAction= */ true, data, Collections.emptyList()); + } + + private static List toStreamKeys(List trackKeys) { + List representationKeys = new ArrayList<>(trackKeys.size()); + for (int i = 0; i < trackKeys.size(); i++) { + TrackKey trackKey = trackKeys.get(i); + representationKeys.add(new StreamKey(trackKey.groupIndex, trackKey.trackIndex)); + } + return representationKeys; + } +} diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloader.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloader.java index 7988523bed..4fef3eb469 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloader.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloader.java @@ -23,7 +23,7 @@ import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest; import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest.StreamElement; import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifestParser; import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsUtil; -import com.google.android.exoplayer2.source.smoothstreaming.manifest.TrackKey; +import com.google.android.exoplayer2.source.smoothstreaming.manifest.StreamKey; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.ParsingLoadable; @@ -32,10 +32,7 @@ import java.util.ArrayList; import java.util.List; /** - * Helper class to download SmoothStreaming streams. - * - *

    Except {@link #getTotalSegments()}, {@link #getDownloadedSegments()} and {@link - * #getDownloadedBytes()}, this class isn't thread safe. + * A downloader for SmoothStreaming streams. * *

    Example usage: * @@ -44,41 +41,30 @@ import java.util.List; * DefaultHttpDataSourceFactory factory = new DefaultHttpDataSourceFactory("ExoPlayer", null); * DownloaderConstructorHelper constructorHelper = * new DownloaderConstructorHelper(cache, factory); - * SsDownloader ssDownloader = new SsDownloader(manifestUrl, constructorHelper); - * // Select the first track of the first stream element - * ssDownloader.selectRepresentations(new TrackKey[] {new TrackKey(0, 0)}); - * ssDownloader.download(new ProgressListener() { - * {@literal @}Override - * public void onDownloadProgress(Downloader downloader, float downloadPercentage, - * long downloadedBytes) { - * // Invoked periodically during the download. - * } - * }); + * // Create a downloader for the first track of the first stream element. + * SsDownloader ssDownloader = + * new SsDownloader( + * manifestUrl, + * Collections.singletonList(new StreamKey(0, 0)), + * constructorHelper); + * // Perform the download. + * ssDownloader.download(); * // Access downloaded data using CacheDataSource * CacheDataSource cacheDataSource = * new CacheDataSource(cache, factory.createDataSource(), CacheDataSource.FLAG_BLOCK_ON_CACHE); * } */ -public final class SsDownloader extends SegmentDownloader { +public final class SsDownloader extends SegmentDownloader { /** - * @see SegmentDownloader#SegmentDownloader(Uri, DownloaderConstructorHelper) + * @param manifestUri The {@link Uri} of the manifest to be downloaded. + * @param streamKeys Keys defining which streams in the manifest should be selected for download. + * If empty, all streams are downloaded. + * @param constructorHelper A {@link DownloaderConstructorHelper} instance. */ - public SsDownloader(Uri manifestUri, DownloaderConstructorHelper constructorHelper) { - super(SsUtil.fixManifestUri(manifestUri), constructorHelper); - } - - @Override - public TrackKey[] getAllRepresentationKeys() throws IOException { - ArrayList keys = new ArrayList<>(); - SsManifest manifest = getManifest(); - for (int i = 0; i < manifest.streamElements.length; i++) { - StreamElement streamElement = manifest.streamElements[i]; - for (int j = 0; j < streamElement.formats.length; j++) { - keys.add(new TrackKey(i, j)); - } - } - return keys.toArray(new TrackKey[keys.size()]); + public SsDownloader( + Uri manifestUri, List streamKeys, DownloaderConstructorHelper constructorHelper) { + super(SsUtil.fixManifestUri(manifestUri), streamKeys, constructorHelper); } @Override @@ -90,14 +76,17 @@ public final class SsDownloader extends SegmentDownloader } @Override - protected List getSegments(DataSource dataSource, SsManifest manifest, - TrackKey[] keys, boolean allowIndexLoadErrors) throws InterruptedException, IOException { + protected List getSegments( + DataSource dataSource, SsManifest manifest, boolean allowIncompleteList) { ArrayList segments = new ArrayList<>(); - for (TrackKey key : keys) { - StreamElement streamElement = manifest.streamElements[key.streamElementIndex]; - for (int i = 0; i < streamElement.chunkCount; i++) { - segments.add(new Segment(streamElement.getStartTimeUs(i), - new DataSpec(streamElement.buildRequestUri(key.trackIndex, i)))); + for (StreamElement streamElement : manifest.streamElements) { + for (int i = 0; i < streamElement.formats.length; i++) { + for (int j = 0; j < streamElement.chunkCount; j++) { + segments.add( + new Segment( + streamElement.getStartTimeUs(j), + new DataSpec(streamElement.buildRequestUri(i, j)))); + } } } return segments; diff --git a/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestTest.java b/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestTest.java index fbb2c3d4c4..c7c6c6f3fb 100644 --- a/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestTest.java +++ b/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestTest.java @@ -43,7 +43,8 @@ public class SsManifestTest { SsManifest sourceManifest = newSsManifest(newStreamElement("1", formats[0]), newStreamElement("2", formats[1])); - List keys = Arrays.asList(new TrackKey(0, 0), new TrackKey(0, 2), new TrackKey(1, 0)); + List keys = + Arrays.asList(new StreamKey(0, 0), new StreamKey(0, 2), new StreamKey(1, 0)); // Keys don't need to be in any particular order Collections.shuffle(keys, new Random(0)); @@ -62,7 +63,7 @@ public class SsManifestTest { SsManifest sourceManifest = newSsManifest(newStreamElement("1", formats[0]), newStreamElement("2", formats[1])); - List keys = Arrays.asList(new TrackKey(1, 0)); + List keys = Arrays.asList(new StreamKey(1, 0)); // Keys don't need to be in any particular order Collections.shuffle(keys, new Random(0)); diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTrackNameProvider.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTrackNameProvider.java new file mode 100644 index 0000000000..b36941e999 --- /dev/null +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTrackNameProvider.java @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.ui; + +import android.content.res.Resources; +import android.text.TextUtils; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.MimeTypes; +import com.google.android.exoplayer2.util.Util; +import java.util.Locale; + +/** A default {@link TrackNameProvider}. */ +public class DefaultTrackNameProvider implements TrackNameProvider { + + private final Resources resources; + + /** @param resources Resources from which to obtain strings. */ + public DefaultTrackNameProvider(Resources resources) { + this.resources = Assertions.checkNotNull(resources); + } + + @Override + public String getTrackName(Format format) { + String trackName; + int trackType = inferPrimaryTrackType(format); + if (trackType == C.TRACK_TYPE_VIDEO) { + trackName = joinWithSeparator(buildResolutionString(format), buildBitrateString(format)); + } else if (trackType == C.TRACK_TYPE_AUDIO) { + trackName = + joinWithSeparator( + buildLanguageString(format), + buildAudioChannelString(format), + buildBitrateString(format)); + } else { + trackName = buildLanguageString(format); + } + return trackName.length() == 0 ? resources.getString(R.string.exo_track_unknown) : trackName; + } + + private String buildResolutionString(Format format) { + int width = format.width; + int height = format.height; + return width == Format.NO_VALUE || height == Format.NO_VALUE + ? "" + : resources.getString(R.string.exo_track_resolution, width, height); + } + + private String buildBitrateString(Format format) { + int bitrate = format.bitrate; + return bitrate == Format.NO_VALUE + ? "" + : resources.getString(R.string.exo_track_bitrate, bitrate / 1000000f); + } + + private String buildAudioChannelString(Format format) { + int channelCount = format.channelCount; + if (channelCount == Format.NO_VALUE || channelCount < 1) { + return ""; + } + switch (channelCount) { + case 1: + return resources.getString(R.string.exo_track_mono); + case 2: + return resources.getString(R.string.exo_track_stereo); + case 6: + case 7: + return resources.getString(R.string.exo_track_surround_5_point_1); + case 8: + return resources.getString(R.string.exo_track_surround_7_point_1); + default: + return resources.getString(R.string.exo_track_surround); + } + } + + private String buildLanguageString(Format format) { + String language = format.language; + return TextUtils.isEmpty(language) || C.LANGUAGE_UNDETERMINED.equals(language) + ? "" + : buildLanguageString(language); + } + + private String buildLanguageString(String language) { + Locale locale = Util.SDK_INT >= 21 ? Locale.forLanguageTag(language) : new Locale(language); + return locale.getDisplayLanguage(); + } + + private String joinWithSeparator(String... items) { + String itemList = ""; + for (String item : items) { + if (item.length() > 0) { + if (TextUtils.isEmpty(itemList)) { + itemList = item; + } else { + itemList = resources.getString(R.string.exo_item_list, itemList, item); + } + } + } + return itemList; + } + + private static int inferPrimaryTrackType(Format format) { + int trackType = MimeTypes.getTrackType(format.sampleMimeType); + if (trackType != C.TRACK_TYPE_UNKNOWN) { + return trackType; + } + if (MimeTypes.getVideoMediaMimeType(format.codecs) != null) { + return C.TRACK_TYPE_VIDEO; + } + if (MimeTypes.getAudioMediaMimeType(format.codecs) != null) { + return C.TRACK_TYPE_AUDIO; + } + if (format.width != Format.NO_VALUE || format.height != Format.NO_VALUE) { + return C.TRACK_TYPE_VIDEO; + } + if (format.channelCount != Format.NO_VALUE || format.sampleRate != Format.NO_VALUE) { + return C.TRACK_TYPE_AUDIO; + } + return C.TRACK_TYPE_UNKNOWN; + } +} diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DownloadNotificationUtil.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DownloadNotificationUtil.java index b4f69809a5..0a841fa38f 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DownloadNotificationUtil.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DownloadNotificationUtil.java @@ -16,101 +16,139 @@ package com.google.android.exoplayer2.ui; import android.app.Notification; -import android.app.Notification.BigTextStyle; -import android.app.Notification.Builder; +import android.app.PendingIntent; import android.content.Context; +import android.support.annotation.DrawableRes; import android.support.annotation.Nullable; -import com.google.android.exoplayer2.offline.DownloadManager; -import com.google.android.exoplayer2.offline.DownloadManager.DownloadState; -import com.google.android.exoplayer2.util.ErrorMessageProvider; -import com.google.android.exoplayer2.util.Util; +import android.support.annotation.StringRes; +import android.support.v4.app.NotificationCompat; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.offline.DownloadManager.TaskState; -/** Helper class to create notifications for downloads using {@link DownloadManager}. */ +/** Helper for creating download notifications. */ public final class DownloadNotificationUtil { + private static final @StringRes int NULL_STRING_ID = 0; + private DownloadNotificationUtil() {} /** - * Returns a notification for the given {@link DownloadState}, or null if no notification should - * be displayed. + * Returns a progress notification for the given task states. * - * @param downloadState State of the download. - * @param context Used to access resources. + * @param context A context for accessing resources. + * @param smallIcon A small icon for the notification. + * @param channelId The id of the notification channel to use. Only required for API level 26 and + * above. + * @param contentIntent An optional content intent to send when the notification is clicked. + * @param message An optional message to display on the notification. + * @param taskStates The task states. + * @return The notification. + */ + public static Notification buildProgressNotification( + Context context, + @DrawableRes int smallIcon, + String channelId, + @Nullable PendingIntent contentIntent, + @Nullable String message, + TaskState[] taskStates) { + float totalPercentage = 0; + int downloadTaskCount = 0; + boolean allDownloadPercentagesUnknown = true; + boolean haveDownloadedBytes = false; + for (TaskState taskState : taskStates) { + if (taskState.action.isRemoveAction || taskState.state != TaskState.STATE_STARTED) { + continue; + } + if (taskState.downloadPercentage != C.PERCENTAGE_UNSET) { + allDownloadPercentagesUnknown = false; + totalPercentage += taskState.downloadPercentage; + } + haveDownloadedBytes |= taskState.downloadedBytes > 0; + downloadTaskCount++; + } + + boolean haveDownloadTasks = downloadTaskCount > 0; + int titleStringId = + haveDownloadTasks + ? R.string.exo_download_downloading + : (taskStates.length > 0 ? R.string.exo_download_removing : NULL_STRING_ID); + NotificationCompat.Builder notificationBuilder = + newNotificationBuilder( + context, smallIcon, channelId, contentIntent, message, titleStringId); + + int progress = haveDownloadTasks ? (int) (totalPercentage / downloadTaskCount) : 0; + boolean indeterminate = + !haveDownloadTasks || (allDownloadPercentagesUnknown && haveDownloadedBytes); + notificationBuilder.setProgress(/* max= */ 100, progress, indeterminate); + notificationBuilder.setOngoing(true); + notificationBuilder.setShowWhen(false); + return notificationBuilder.build(); + } + + /** + * Returns a notification for a completed download. + * + * @param context A context for accessing resources. * @param smallIcon A small icon for the notifications. * @param channelId The id of the notification channel to use. Only required for API level 26 and * above. + * @param contentIntent An optional content intent to send when the notification is clicked. * @param message An optional message to display on the notification. - * @param errorMessageProvider An optional {@link ErrorMessageProvider} for translating download - * errors into readable error messages. If not null and there is a download error then the - * error message is displayed instead of {@code message}. - * @return A notification for the given {@link DownloadState}, or null if no notification should - * be displayed. + * @return The notification. */ - public static @Nullable Notification createNotification( - DownloadState downloadState, + public static Notification buildDownloadCompletedNotification( Context context, - int smallIcon, + @DrawableRes int smallIcon, String channelId, + @Nullable PendingIntent contentIntent, + @Nullable String message) { + int titleStringId = R.string.exo_download_completed; + return newNotificationBuilder( + context, smallIcon, channelId, contentIntent, message, titleStringId) + .build(); + } + + /** + * Returns a notification for a failed download. + * + * @param context A context for accessing resources. + * @param smallIcon A small icon for the notifications. + * @param channelId The id of the notification channel to use. Only required for API level 26 and + * above. + * @param contentIntent An optional content intent to send when the notification is clicked. + * @param message An optional message to display on the notification. + * @return The notification. + */ + public static Notification buildDownloadFailedNotification( + Context context, + @DrawableRes int smallIcon, + String channelId, + @Nullable PendingIntent contentIntent, + @Nullable String message) { + @StringRes int titleStringId = R.string.exo_download_failed; + return newNotificationBuilder( + context, smallIcon, channelId, contentIntent, message, titleStringId) + .build(); + } + + private static NotificationCompat.Builder newNotificationBuilder( + Context context, + @DrawableRes int smallIcon, + String channelId, + @Nullable PendingIntent contentIntent, @Nullable String message, - @Nullable ErrorMessageProvider errorMessageProvider) { - if (downloadState.downloadAction.isRemoveAction() - || downloadState.state == DownloadState.STATE_CANCELED) { - return null; + @StringRes int titleStringId) { + NotificationCompat.Builder notificationBuilder = + new NotificationCompat.Builder(context, channelId).setSmallIcon(smallIcon); + if (titleStringId != NULL_STRING_ID) { + notificationBuilder.setContentTitle(context.getResources().getString(titleStringId)); } - - Builder notificationBuilder = new Builder(context); - if (Util.SDK_INT >= 26) { - notificationBuilder.setChannelId(channelId); - } - notificationBuilder.setSmallIcon(smallIcon); - - int titleStringId = getTitleStringId(downloadState); - notificationBuilder.setContentTitle(context.getResources().getString(titleStringId)); - - if (downloadState.state == DownloadState.STATE_STARTED) { - notificationBuilder.setOngoing(true); - float percentage = downloadState.downloadPercentage; - boolean indeterminate = Float.isNaN(percentage); - notificationBuilder.setProgress(100, indeterminate ? 0 : (int) percentage, indeterminate); - } - if (Util.SDK_INT >= 17) { - // Hide timestamp on the notification while download progresses. - notificationBuilder.setShowWhen(downloadState.state != DownloadState.STATE_STARTED); - } - - if (downloadState.error != null && errorMessageProvider != null) { - message = errorMessageProvider.getErrorMessage(downloadState.error).second; + if (contentIntent != null) { + notificationBuilder.setContentIntent(contentIntent); } if (message != null) { - if (Util.SDK_INT >= 16) { - notificationBuilder.setStyle(new BigTextStyle().bigText(message)); - } else { - notificationBuilder.setContentText(message); - } + notificationBuilder.setStyle(new NotificationCompat.BigTextStyle().bigText(message)); } - return notificationBuilder.getNotification(); - } - - private static int getTitleStringId(DownloadState downloadState) { - int titleStringId; - switch (downloadState.state) { - case DownloadState.STATE_QUEUED: - titleStringId = R.string.exo_download_queued; - break; - case DownloadState.STATE_STARTED: - titleStringId = R.string.exo_downloading; - break; - case DownloadState.STATE_ENDED: - titleStringId = R.string.exo_download_completed; - break; - case DownloadState.STATE_ERROR: - titleStringId = R.string.exo_download_failed; - break; - case DownloadState.STATE_CANCELED: - default: - // Never happens. - throw new IllegalStateException(); - } - return titleStringId; + return notificationBuilder; } } diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java index fd33fb8d21..4c258c748f 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java @@ -27,19 +27,22 @@ import android.graphics.Bitmap; import android.graphics.Color; import android.os.Handler; import android.os.Looper; +import android.support.annotation.DrawableRes; import android.support.annotation.IntDef; import android.support.annotation.Nullable; +import android.support.annotation.StringRes; import android.support.v4.app.NotificationCompat; import android.support.v4.app.NotificationManagerCompat; import android.support.v4.media.app.NotificationCompat.MediaStyle; import android.support.v4.media.session.MediaSessionCompat; -import android.support.v4.util.Pair; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ControlDispatcher; import com.google.android.exoplayer2.DefaultControlDispatcher; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.NotificationUtil; import com.google.android.exoplayer2.util.Util; import java.lang.annotation.Retention; import java.util.ArrayList; @@ -235,6 +238,17 @@ public class PlayerNotificationManager { }) public @interface Visibility {} + /** Priority of the notification (required for API 25 and lower). */ + @Retention(SOURCE) + @IntDef({ + NotificationCompat.PRIORITY_DEFAULT, + NotificationCompat.PRIORITY_MAX, + NotificationCompat.PRIORITY_HIGH, + NotificationCompat.PRIORITY_LOW, + NotificationCompat.PRIORITY_MIN + }) + public @interface Priority {} + /** The default fast forward increment, in milliseconds. */ public static final int DEFAULT_FAST_FORWARD_MS = 15000; /** The default rewind increment, in milliseconds. */ @@ -243,17 +257,17 @@ public class PlayerNotificationManager { private static final long MAX_POSITION_FOR_SEEK_TO_PREVIOUS = 3000; private final Context context; + private final String channelId; + private final int notificationId; + private final MediaDescriptionAdapter mediaDescriptionAdapter; + private final CustomActionReceiver customActionReceiver; private final Handler mainHandler; private final NotificationManagerCompat notificationManager; private final IntentFilter intentFilter; private final Player.EventListener playerListener; private final NotificationBroadcastReceiver notificationBroadcastReceiver; - private final MediaDescriptionAdapter mediaDescriptionAdapter; - private final int notificationId; - private final String channelId; private final Map playbackActions; private final Map customActions; - private final CustomActionReceiver customActionReceiver; private Player player; private ControlDispatcher controlDispatcher; @@ -262,56 +276,89 @@ public class PlayerNotificationManager { private NotificationListener notificationListener; private MediaSessionCompat.Token mediaSessionToken; private boolean useNavigationActions; - private Pair stopAction; + private boolean usePlayPauseActions; + private @Nullable String stopAction; + private @Nullable PendingIntent stopPendingIntent; private long fastForwardMs; private long rewindMs; private int badgeIconType; private boolean colorized; private int defaults; private int color; - private int smallIconResourceId; + private @DrawableRes int smallIconResourceId; private int visibility; + private @Priority int priority; private boolean ongoing; private boolean useChronometer; private boolean wasPlayWhenReady; private int lastPlaybackState; /** - * Creates the manager. + * Creates a notification manager and a low-priority notification channel with the specified + * {@code channelId} and {@code channelName}. * * @param context The {@link Context}. - * @param mediaDescriptionAdapter The {@link MediaDescriptionAdapter}. * @param channelId The id of the notification channel. + * @param channelName A string resource identifier for the user visible name of the channel. The + * recommended maximum length is 40 characters; the value may be truncated if it is too long. * @param notificationId The id of the notification. + * @param mediaDescriptionAdapter The {@link MediaDescriptionAdapter}. */ - public PlayerNotificationManager( + public static PlayerNotificationManager createWithNotificationChannel( Context context, - MediaDescriptionAdapter mediaDescriptionAdapter, String channelId, - int notificationId) { - this(context, mediaDescriptionAdapter, channelId, notificationId, null); + @StringRes int channelName, + int notificationId, + MediaDescriptionAdapter mediaDescriptionAdapter) { + NotificationUtil.createNotificationChannel( + context, channelId, channelName, NotificationUtil.IMPORTANCE_LOW); + return new PlayerNotificationManager( + context, channelId, notificationId, mediaDescriptionAdapter); } /** - * Creates the manager with a {@link CustomActionReceiver}. + * Creates a notification manager using the specified notification {@code channelId}. The caller + * is responsible for creating the notification channel. * * @param context The {@link Context}. - * @param mediaDescriptionAdapter The {@link MediaDescriptionAdapter}. * @param channelId The id of the notification channel. * @param notificationId The id of the notification. + * @param mediaDescriptionAdapter The {@link MediaDescriptionAdapter}. + */ + public PlayerNotificationManager( + Context context, + String channelId, + int notificationId, + MediaDescriptionAdapter mediaDescriptionAdapter) { + this( + context, + channelId, + notificationId, + mediaDescriptionAdapter, + /* customActionReceiver= */ null); + } + + /** + * Creates a notification manager using the specified notification {@code channelId} and {@link + * CustomActionReceiver}. The caller is responsible for creating the notification channel. + * + * @param context The {@link Context}. + * @param channelId The id of the notification channel. + * @param notificationId The id of the notification. + * @param mediaDescriptionAdapter The {@link MediaDescriptionAdapter}. * @param customActionReceiver The {@link CustomActionReceiver}. */ public PlayerNotificationManager( Context context, - MediaDescriptionAdapter mediaDescriptionAdapter, String channelId, int notificationId, + MediaDescriptionAdapter mediaDescriptionAdapter, @Nullable CustomActionReceiver customActionReceiver) { this.context = context.getApplicationContext(); - this.mediaDescriptionAdapter = mediaDescriptionAdapter; this.channelId = channelId; - this.customActionReceiver = customActionReceiver; this.notificationId = notificationId; + this.mediaDescriptionAdapter = mediaDescriptionAdapter; + this.customActionReceiver = customActionReceiver; this.controlDispatcher = new DefaultControlDispatcher(); mainHandler = new Handler(Looper.getMainLooper()); notificationManager = NotificationManagerCompat.from(context); @@ -335,12 +382,14 @@ public class PlayerNotificationManager { setStopAction(ACTION_STOP); useNavigationActions = true; + usePlayPauseActions = true; ongoing = true; colorized = true; useChronometer = true; color = Color.TRANSPARENT; smallIconResourceId = R.drawable.exo_notification_small_icon; defaults = 0; + priority = NotificationCompat.PRIORITY_LOW; fastForwardMs = DEFAULT_FAST_FORWARD_MS; rewindMs = DEFAULT_REWIND_MS; setBadgeIconType(NotificationCompat.BADGE_ICON_SMALL); @@ -357,7 +406,7 @@ public class PlayerNotificationManager { *

    If the player is released it must be removed from the manager by calling {@code * setPlayer(null)}. This will cancel the notification. */ - public final void setPlayer(Player player) { + public final void setPlayer(@Nullable Player player) { if (this.player == player) { return; } @@ -438,6 +487,18 @@ public class PlayerNotificationManager { } } + /** + * Sets whether the play and pause actions should be used. + * + * @param usePlayPauseActions Whether to use play and pause actions. + */ + public final void setUsePlayPauseActions(boolean usePlayPauseActions) { + if (this.usePlayPauseActions != usePlayPauseActions) { + this.usePlayPauseActions = usePlayPauseActions; + maybeUpdateNotification(); + } + } + /** * Sets the name of the action to be used as stop action to cancel the notification. If {@code * null} is passed the stop action is not displayed. @@ -446,18 +507,17 @@ public class PlayerNotificationManager { * provided by the {@link CustomActionReceiver}. {@code null} to omit the stop action. */ public final void setStopAction(@Nullable String stopAction) { - if ((this.stopAction == null && stopAction == null) - || (this.stopAction != null && this.stopAction.first.equals(stopAction))) { + if (Util.areEqual(stopAction, this.stopAction)) { return; } - if (stopAction == null) { - this.stopAction = null; - } else if (ACTION_STOP.equals(stopAction)) { - this.stopAction = new Pair<>(stopAction, playbackActions.get(ACTION_STOP)); - } else if (customActions.containsKey(stopAction)) { - this.stopAction = new Pair<>(stopAction, customActions.get(stopAction)); + this.stopAction = stopAction; + if (ACTION_STOP.equals(stopAction)) { + stopPendingIntent = playbackActions.get(ACTION_STOP).actionIntent; + } else if (stopAction != null) { + Assertions.checkArgument(customActions.containsKey(stopAction)); + stopPendingIntent = customActions.get(stopAction).actionIntent; } else { - throw new IllegalArgumentException(); + stopPendingIntent = null; } maybeUpdateNotification(); } @@ -499,7 +559,7 @@ public class PlayerNotificationManager { /** * Sets whether the notification should be colorized. When set, the color set with {@link - * setColor(int)} will be used as the background color for the notification. + * #setColor(int)} will be used as the background color for the notification. * *

    See {@link NotificationCompat.Builder#setColorized(boolean)}. * @@ -556,6 +616,34 @@ public class PlayerNotificationManager { } } + /** + * Sets the priority of the notification required for API 25 and lower. + * + *

    See {@link NotificationCompat.Builder#setPriority(int)}. + * + * @param priority The priority which can be one of {@link NotificationCompat#PRIORITY_DEFAULT}, + * {@link NotificationCompat#PRIORITY_MAX}, {@link NotificationCompat#PRIORITY_HIGH}, {@link + * NotificationCompat#PRIORITY_LOW} or {@link NotificationCompat#PRIORITY_MIN}. If not set + * {@link NotificationCompat#PRIORITY_LOW} is used by default. + */ + public final void setPriority(@Priority int priority) { + if (this.priority == priority) { + return; + } + switch (priority) { + case NotificationCompat.PRIORITY_DEFAULT: + case NotificationCompat.PRIORITY_MAX: + case NotificationCompat.PRIORITY_HIGH: + case NotificationCompat.PRIORITY_LOW: + case NotificationCompat.PRIORITY_MIN: + this.priority = priority; + break; + default: + throw new IllegalArgumentException(); + } + maybeUpdateNotification(); + } + /** * Sets the small icon of the notification which is also shown in the system status bar. * @@ -563,7 +651,7 @@ public class PlayerNotificationManager { * * @param smallIconResourceId The resource id of the small icon. */ - public final void setSmallIcon(int smallIconResourceId) { + public final void setSmallIcon(@DrawableRes int smallIconResourceId) { if (this.smallIconResourceId != smallIconResourceId) { this.smallIconResourceId = smallIconResourceId; maybeUpdateNotification(); @@ -734,9 +822,8 @@ public class PlayerNotificationManager { boolean useStopAction = stopAction != null && !isPlayingAd; mediaStyle.setShowCancelButton(useStopAction); if (useStopAction) { - PendingIntent stopIntent = stopAction.second.actionIntent; - builder.setDeleteIntent(stopIntent); - mediaStyle.setCancelButtonIntent(stopIntent); + builder.setDeleteIntent(stopPendingIntent); + mediaStyle.setCancelButtonIntent(stopPendingIntent); } // Set notification properties from getters. builder @@ -746,6 +833,7 @@ public class PlayerNotificationManager { .setColorized(colorized) .setSmallIcon(smallIconResourceId) .setVisibility(visibility) + .setPriority(priority) .setDefaults(defaults); if (useChronometer && !player.isCurrentWindowDynamic() @@ -804,10 +892,12 @@ public class PlayerNotificationManager { if (rewindMs > 0) { stringActions.add(ACTION_REWIND); } - if (player.getPlayWhenReady()) { - stringActions.add(ACTION_PAUSE); - } else if (!player.getPlayWhenReady()) { - stringActions.add(ACTION_PLAY); + if (usePlayPauseActions) { + if (player.getPlayWhenReady()) { + stringActions.add(ACTION_PAUSE); + } else { + stringActions.add(ACTION_PLAY); + } } if (fastForwardMs > 0) { stringActions.add(ACTION_FAST_FORWARD); @@ -818,8 +908,8 @@ public class PlayerNotificationManager { if (!customActions.isEmpty()) { stringActions.addAll(customActionReceiver.getCustomActions(player)); } - if (stopAction != null) { - stringActions.add(stopAction.first); + if (ACTION_STOP.equals(stopAction)) { + stringActions.add(stopAction); } } return stringActions; @@ -834,6 +924,9 @@ public class PlayerNotificationManager { * @param player The player for which state to build a notification. */ protected int[] getActionIndicesForCompactView(Player player) { + if (!usePlayPauseActions) { + return new int[0]; + } int actionIndex = useNavigationActions ? 1 : 0; actionIndex += fastForwardMs > 0 ? 1 : 0; return new int[] {actionIndex}; diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java index 6732f755d6..25c4318768 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java @@ -36,9 +36,11 @@ import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; import android.widget.ImageView; +import android.widget.TextView; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ControlDispatcher; import com.google.android.exoplayer2.DefaultControlDispatcher; +import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.PlaybackPreparer; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player.DiscontinuityReason; @@ -51,6 +53,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.ui.AspectRatioFrameLayout.ResizeMode; import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.ErrorMessageProvider; import com.google.android.exoplayer2.util.RepeatModeUtil; import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.video.VideoListener; @@ -102,6 +105,12 @@ import java.util.List; *

  • Corresponding method: {@link #setControllerHideDuringAds(boolean)} *
  • Default: {@code true} * + *
  • {@code show_buffering} - Whether the buffering spinner is displayed when the player + * is buffering. + *
      + *
    • Corresponding method: {@link #setShowBuffering(boolean)} + *
    • Default: {@code false} + *
    *
  • {@code resize_mode} - Controls how video and album art is resized within the view. * Valid values are {@code fit}, {@code fixed_width}, {@code fixed_height} and {@code fill}. *
      @@ -164,6 +173,11 @@ import java.util.List; *
        *
      • Type: {@link View} *
      + *
    • {@code exo_buffering} - A view that's made visible when the player is buffering. + * This view typically displays a buffering spinner or animation. + *
        + *
      • Type: {@link View} + *
      *
    • {@code exo_subtitles} - Displays subtitles. *
        *
      • Type: {@link SubtitleView} @@ -172,6 +186,10 @@ import java.util.List; *
          *
        • Type: {@link ImageView} *
        + *
      • {@code exo_error_message} - Displays an error message to the user if playback fails. + *
          + *
        • Type: {@link TextView} + *
        *
      • {@code exo_controller_placeholder} - A placeholder that's replaced with the inflated * {@link PlayerControlView}. Ignored if an {@code exo_controller} view exists. *
          @@ -213,6 +231,8 @@ public class PlayerView extends FrameLayout { private final View surfaceView; private final ImageView artworkView; private final SubtitleView subtitleView; + private final @Nullable View bufferingView; + private final @Nullable TextView errorMessageView; private final PlayerControlView controller; private final ComponentListener componentListener; private final FrameLayout overlayFrameLayout; @@ -221,6 +241,9 @@ public class PlayerView extends FrameLayout { private boolean useController; private boolean useArtwork; private Bitmap defaultArtwork; + private boolean showBuffering; + private @Nullable ErrorMessageProvider errorMessageProvider; + private @Nullable CharSequence customErrorMessage; private int controllerShowTimeoutMs; private boolean controllerAutoShow; private boolean controllerHideDuringAds; @@ -244,6 +267,8 @@ public class PlayerView extends FrameLayout { surfaceView = null; artworkView = null; subtitleView = null; + bufferingView = null; + errorMessageView = null; controller = null; componentListener = null; overlayFrameLayout = null; @@ -269,6 +294,7 @@ public class PlayerView extends FrameLayout { boolean controllerHideOnTouch = true; boolean controllerAutoShow = true; boolean controllerHideDuringAds = true; + boolean showBuffering = false; if (attrs != null) { TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.PlayerView, 0, 0); try { @@ -286,6 +312,7 @@ public class PlayerView extends FrameLayout { controllerHideOnTouch = a.getBoolean(R.styleable.PlayerView_hide_on_touch, controllerHideOnTouch); controllerAutoShow = a.getBoolean(R.styleable.PlayerView_auto_show, controllerAutoShow); + showBuffering = a.getBoolean(R.styleable.PlayerView_show_buffering, showBuffering); controllerHideDuringAds = a.getBoolean(R.styleable.PlayerView_hide_during_ads, controllerHideDuringAds); } finally { @@ -341,6 +368,19 @@ public class PlayerView extends FrameLayout { subtitleView.setUserDefaultTextSize(); } + // Buffering view. + bufferingView = findViewById(R.id.exo_buffering); + if (bufferingView != null) { + bufferingView.setVisibility(View.GONE); + } + this.showBuffering = showBuffering; + + // Error message view. + errorMessageView = findViewById(R.id.exo_error_message); + if (errorMessageView != null) { + errorMessageView.setVisibility(View.GONE); + } + // Playback control view. PlayerControlView customController = findViewById(R.id.exo_controller); View controllerPlaceholder = findViewById(R.id.exo_controller_placeholder); @@ -438,6 +478,8 @@ public class PlayerView extends FrameLayout { if (subtitleView != null) { subtitleView.setCues(null); } + updateBuffering(); + updateErrorMessage(); if (player != null) { Player.VideoComponent newVideoComponent = player.getVideoComponent(); if (newVideoComponent != null) { @@ -558,6 +600,44 @@ public class PlayerView extends FrameLayout { } } + /** + * Sets whether a buffering spinner is displayed when the player is in the buffering state. The + * buffering spinner is not displayed by default. + * + * @param showBuffering Whether the buffering icon is displayer + */ + public void setShowBuffering(boolean showBuffering) { + if (this.showBuffering != showBuffering) { + this.showBuffering = showBuffering; + updateBuffering(); + } + } + + /** + * Sets the optional {@link ErrorMessageProvider}. + * + * @param errorMessageProvider The error message provider. + */ + public void setErrorMessageProvider( + @Nullable ErrorMessageProvider errorMessageProvider) { + if (this.errorMessageProvider != errorMessageProvider) { + this.errorMessageProvider = errorMessageProvider; + updateErrorMessage(); + } + } + + /** + * Sets a custom error message to be displayed by the view. The error message will be displayed + * permanently, unless it is cleared by passing {@code null} to this method. + * + * @param message The message to display, or {@code null} to clear a previously set message. + */ + public void setCustomErrorMessage(@Nullable CharSequence message) { + Assertions.checkState(errorMessageView != null); + customErrorMessage = message; + updateErrorMessage(); + } + @Override public boolean dispatchKeyEvent(KeyEvent event) { if (player != null && player.isPlayingAd()) { @@ -954,6 +1034,40 @@ public class PlayerView extends FrameLayout { } } + private void updateBuffering() { + if (bufferingView != null) { + boolean showBufferingSpinner = + showBuffering + && player != null + && player.getPlaybackState() == Player.STATE_BUFFERING + && player.getPlayWhenReady(); + bufferingView.setVisibility(showBufferingSpinner ? View.VISIBLE : View.GONE); + } + } + + private void updateErrorMessage() { + if (errorMessageView != null) { + if (customErrorMessage != null) { + errorMessageView.setText(customErrorMessage); + errorMessageView.setVisibility(View.VISIBLE); + return; + } + ExoPlaybackException error = null; + if (player != null + && player.getPlaybackState() == Player.STATE_IDLE + && errorMessageProvider != null) { + error = player.getPlaybackError(); + } + if (error != null) { + CharSequence errorMessage = errorMessageProvider.getErrorMessage(error).second; + errorMessageView.setText(errorMessage); + errorMessageView.setVisibility(View.VISIBLE); + } else { + errorMessageView.setVisibility(View.GONE); + } + } + } + @TargetApi(23) private static void configureEditModeLogoV23(Resources resources, ImageView logo) { logo.setImageDrawable(resources.getDrawable(R.drawable.exo_edit_mode_logo, null)); @@ -1070,6 +1184,8 @@ public class PlayerView extends FrameLayout { @Override public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { + updateBuffering(); + updateErrorMessage(); if (isPlayingAd() && controllerHideDuringAds) { hideController(); } else { diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/TrackNameProvider.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/TrackNameProvider.java new file mode 100644 index 0000000000..1b2b66010c --- /dev/null +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/TrackNameProvider.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.ui; + +import com.google.android.exoplayer2.Format; + +/** Converts {@link Format}s to user readable track names. */ +public interface TrackNameProvider { + + /** Returns a user readable track name for the given {@link Format}. */ + String getTrackName(Format format); +} diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/TrackSelectionView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/TrackSelectionView.java new file mode 100644 index 0000000000..45ccd783e7 --- /dev/null +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/TrackSelectionView.java @@ -0,0 +1,354 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.ui; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.util.Pair; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.CheckedTextView; +import android.widget.LinearLayout; +import com.google.android.exoplayer2.RendererCapabilities; +import com.google.android.exoplayer2.source.TrackGroup; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; +import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.SelectionOverride; +import com.google.android.exoplayer2.trackselection.MappingTrackSelector; +import com.google.android.exoplayer2.util.Assertions; +import java.util.Arrays; + +/** A view for making track selections. */ +public class TrackSelectionView extends LinearLayout { + + private final int selectableItemBackgroundResourceId; + private final LayoutInflater inflater; + private final CheckedTextView disableView; + private final CheckedTextView defaultView; + private final ComponentListener componentListener; + + private boolean allowAdaptiveSelections; + + private TrackNameProvider trackNameProvider; + private CheckedTextView[][] trackViews; + + private DefaultTrackSelector trackSelector; + private int rendererIndex; + private TrackGroupArray trackGroups; + private boolean isDisabled; + private SelectionOverride override; + + /** + * Gets a pair consisting of a dialog and the {@link TrackSelectionView} that will be shown by it. + * + * @param activity The parent activity. + * @param title The dialog's title. + * @param trackSelector The track selector. + * @param rendererIndex The index of the renderer. + * @return The dialog and the {@link TrackSelectionView} that will be shown by it. + */ + public static Pair getDialog( + Activity activity, + CharSequence title, + DefaultTrackSelector trackSelector, + int rendererIndex) { + AlertDialog.Builder builder = new AlertDialog.Builder(activity); + + // Inflate with the builder's context to ensure the correct style is used. + LayoutInflater dialogInflater = LayoutInflater.from(builder.getContext()); + View dialogView = dialogInflater.inflate(R.layout.exo_track_selection_dialog, null); + + final TrackSelectionView selectionView = dialogView.findViewById(R.id.exo_track_selection_view); + selectionView.init(trackSelector, rendererIndex); + Dialog.OnClickListener okClickListener = + new Dialog.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + selectionView.applySelection(); + } + }; + + AlertDialog dialog = + builder + .setTitle(title) + .setView(dialogView) + .setPositiveButton(android.R.string.ok, okClickListener) + .setNegativeButton(android.R.string.cancel, null) + .create(); + return Pair.create(dialog, selectionView); + } + + public TrackSelectionView(Context context) { + this(context, null); + } + + public TrackSelectionView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public TrackSelectionView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + TypedArray attributeArray = + context + .getTheme() + .obtainStyledAttributes(new int[] {android.R.attr.selectableItemBackground}); + selectableItemBackgroundResourceId = attributeArray.getResourceId(0, 0); + attributeArray.recycle(); + + inflater = LayoutInflater.from(context); + componentListener = new ComponentListener(); + trackNameProvider = new DefaultTrackNameProvider(getResources()); + + // View for disabling the renderer. + disableView = + (CheckedTextView) + inflater.inflate(android.R.layout.simple_list_item_single_choice, this, false); + disableView.setBackgroundResource(selectableItemBackgroundResourceId); + disableView.setText(R.string.exo_track_selection_none); + disableView.setEnabled(false); + disableView.setFocusable(true); + disableView.setOnClickListener(componentListener); + disableView.setVisibility(View.GONE); + addView(disableView); + // Divider view. + addView(inflater.inflate(R.layout.exo_list_divider, this, false)); + // View for clearing the override to allow the selector to use its default selection logic. + defaultView = + (CheckedTextView) + inflater.inflate(android.R.layout.simple_list_item_single_choice, this, false); + defaultView.setBackgroundResource(selectableItemBackgroundResourceId); + defaultView.setText(R.string.exo_track_selection_auto); + defaultView.setEnabled(false); + defaultView.setFocusable(true); + defaultView.setOnClickListener(componentListener); + addView(defaultView); + } + + /** + * Sets whether adaptive selections (consisting of more than one track) can be made using this + * selection view. + * + *

          For the view to enable adaptive selection it is necessary both for this feature to be + * enabled, and for the target renderer to support adaptation between the available tracks. + * + * @param allowAdaptiveSelections Whether adaptive selection is enabled. + */ + public void setAllowAdaptiveSelections(boolean allowAdaptiveSelections) { + if (!this.allowAdaptiveSelections == allowAdaptiveSelections) { + this.allowAdaptiveSelections = allowAdaptiveSelections; + updateViews(); + } + } + + /** + * Sets whether an option is available for disabling the renderer. + * + * @param showDisableOption Whether the disable option is shown. + */ + public void setShowDisableOption(boolean showDisableOption) { + disableView.setVisibility(showDisableOption ? View.VISIBLE : View.GONE); + } + + /** + * Sets the {@link TrackNameProvider} used to generate the user visible name of each track. + * + * @param trackNameProvider The {@link TrackNameProvider} to use. + */ + public void setTrackNameProvider(TrackNameProvider trackNameProvider) { + this.trackNameProvider = Assertions.checkNotNull(trackNameProvider); + } + + /** + * Initialize the view to select tracks for a specified renderer using a {@link + * DefaultTrackSelector}. + * + * @param trackSelector The {@link DefaultTrackSelector}. + * @param rendererIndex The index of the renderer. + */ + public void init(DefaultTrackSelector trackSelector, int rendererIndex) { + this.trackSelector = trackSelector; + this.rendererIndex = rendererIndex; + updateViews(); + } + + // Private methods. + + private void updateViews() { + // Remove previous per-track views. + for (int i = getChildCount() - 1; i >= 3; i--) { + removeViewAt(i); + } + + if (trackSelector == null) { + // The view is not initialized. + disableView.setEnabled(false); + defaultView.setEnabled(false); + return; + } + disableView.setEnabled(true); + defaultView.setEnabled(true); + + MappingTrackSelector.MappedTrackInfo trackInfo = trackSelector.getCurrentMappedTrackInfo(); + trackGroups = trackInfo.getTrackGroups(rendererIndex); + + DefaultTrackSelector.Parameters parameters = trackSelector.getParameters(); + isDisabled = parameters.getRendererDisabled(rendererIndex); + override = parameters.getSelectionOverride(rendererIndex, trackGroups); + + // Add per-track views. + trackViews = new CheckedTextView[trackGroups.length][]; + for (int groupIndex = 0; groupIndex < trackGroups.length; groupIndex++) { + TrackGroup group = trackGroups.get(groupIndex); + boolean enableAdaptiveSelections = + allowAdaptiveSelections + && trackGroups.get(groupIndex).length > 1 + && trackInfo.getAdaptiveSupport(rendererIndex, groupIndex, false) + != RendererCapabilities.ADAPTIVE_NOT_SUPPORTED; + trackViews[groupIndex] = new CheckedTextView[group.length]; + for (int trackIndex = 0; trackIndex < group.length; trackIndex++) { + if (trackIndex == 0) { + addView(inflater.inflate(R.layout.exo_list_divider, this, false)); + } + int trackViewLayoutId = + enableAdaptiveSelections + ? android.R.layout.simple_list_item_multiple_choice + : android.R.layout.simple_list_item_single_choice; + CheckedTextView trackView = + (CheckedTextView) inflater.inflate(trackViewLayoutId, this, false); + trackView.setBackgroundResource(selectableItemBackgroundResourceId); + trackView.setText(trackNameProvider.getTrackName(group.getFormat(trackIndex))); + if (trackInfo.getTrackSupport(rendererIndex, groupIndex, trackIndex) + == RendererCapabilities.FORMAT_HANDLED) { + trackView.setFocusable(true); + trackView.setTag(Pair.create(groupIndex, trackIndex)); + trackView.setOnClickListener(componentListener); + } else { + trackView.setFocusable(false); + trackView.setEnabled(false); + } + trackViews[groupIndex][trackIndex] = trackView; + addView(trackView); + } + } + + updateViewStates(); + } + + private void updateViewStates() { + disableView.setChecked(isDisabled); + defaultView.setChecked(!isDisabled && override == null); + for (int i = 0; i < trackViews.length; i++) { + for (int j = 0; j < trackViews[i].length; j++) { + trackViews[i][j].setChecked( + override != null && override.groupIndex == i && override.containsTrack(j)); + } + } + } + + private void applySelection() { + DefaultTrackSelector.ParametersBuilder parametersBuilder = trackSelector.buildUponParameters(); + parametersBuilder.setRendererDisabled(rendererIndex, isDisabled); + if (override != null) { + parametersBuilder.setSelectionOverride(rendererIndex, trackGroups, override); + } else { + parametersBuilder.clearSelectionOverrides(rendererIndex); + } + trackSelector.setParameters(parametersBuilder); + } + + private void onClick(View view) { + if (view == disableView) { + onDisableViewClicked(); + } else if (view == defaultView) { + onDefaultViewClicked(); + } else { + onTrackViewClicked(view); + } + updateViewStates(); + } + + private void onDisableViewClicked() { + isDisabled = true; + override = null; + } + + private void onDefaultViewClicked() { + isDisabled = false; + override = null; + } + + private void onTrackViewClicked(View view) { + isDisabled = false; + @SuppressWarnings("unchecked") + Pair tag = (Pair) view.getTag(); + int groupIndex = tag.first; + int trackIndex = tag.second; + if (override == null || override.groupIndex != groupIndex || !allowAdaptiveSelections) { + // A new override is being started. + override = new SelectionOverride(groupIndex, trackIndex); + } else { + // An existing override is being modified. + boolean isEnabled = ((CheckedTextView) view).isChecked(); + int overrideLength = override.length; + if (isEnabled) { + // Remove the track from the override. + if (overrideLength == 1) { + // The last track is being removed, so the override becomes empty. + override = null; + isDisabled = true; + } else { + int[] tracks = getTracksRemoving(override.tracks, trackIndex); + override = new SelectionOverride(groupIndex, tracks); + } + } else { + int[] tracks = getTracksAdding(override.tracks, trackIndex); + override = new SelectionOverride(groupIndex, tracks); + } + } + } + + private static int[] getTracksAdding(int[] tracks, int addedTrack) { + tracks = Arrays.copyOf(tracks, tracks.length + 1); + tracks[tracks.length - 1] = addedTrack; + return tracks; + } + + private static int[] getTracksRemoving(int[] tracks, int removedTrack) { + int[] newTracks = new int[tracks.length - 1]; + int trackCount = 0; + for (int track : tracks) { + if (track != removedTrack) { + newTracks[trackCount++] = track; + } + } + return newTracks; + } + + // Internal classes. + + private class ComponentListener implements OnClickListener { + + @Override + public void onClick(View view) { + TrackSelectionView.this.onClick(view); + } + } +} diff --git a/demos/main/src/main/res/layout/list_divider.xml b/library/ui/src/main/res/layout/exo_list_divider.xml similarity index 100% rename from demos/main/src/main/res/layout/list_divider.xml rename to library/ui/src/main/res/layout/exo_list_divider.xml diff --git a/library/ui/src/main/res/layout/exo_simple_player_view.xml b/library/ui/src/main/res/layout/exo_simple_player_view.xml index 340113da6c..167ac96222 100644 --- a/library/ui/src/main/res/layout/exo_simple_player_view.xml +++ b/library/ui/src/main/res/layout/exo_simple_player_view.xml @@ -36,6 +36,20 @@ android:layout_width="match_parent" android:layout_height="match_parent"/> + + + + - diff --git a/library/ui/src/main/res/values-af/strings.xml b/library/ui/src/main/res/values-af/strings.xml index f1c45cd7f7..4d525836a0 100644 --- a/library/ui/src/main/res/values-af/strings.xml +++ b/library/ui/src/main/res/values-af/strings.xml @@ -1,30 +1,35 @@ - - - "Vorige snit" - "Volgende snit" - "Wag" - "Speel" - "Stop" - "Spoel terug" - "Vinnig vorentoe" - "Herhaal alles" - "Herhaal niks" - "Herhaal een" - "Skommel" + + Vorige snit + Volgende snit + Onderbreek + Speel + Stop + Spoel terug + Spoel vorentoe + Herhaal niks + Herhaal een + Herhaal alles + Skommel Volskermmodus + Aflaai + Aflaaie + Laai tans af + Aflaai is voltooi + Kon nie aflaai nie + Verwyder tans aflaaie + Video + Oudio + SMS + Geen + Outo + Onbekend + %1$d × %2$d + Mono + Stereo + Omringklank + 5.1-omringklank + 7.1-omringklank + %1$.2f Mbps + %1$s, %2$s diff --git a/library/ui/src/main/res/values-am/strings.xml b/library/ui/src/main/res/values-am/strings.xml index 14d3ff0242..08c7a3180a 100644 --- a/library/ui/src/main/res/values-am/strings.xml +++ b/library/ui/src/main/res/values-am/strings.xml @@ -1,30 +1,35 @@ - - - "ቀዳሚ ትራክ" - "ቀጣይ ትራክ" - "ለአፍታ አቁም" - "አጫውት" - "አቁም" - "ወደኋላ አጠንጥን" - "በፍጥነት አሳልፍ" - "ሁሉንም ድገም" - "ምንም አትድገም" - "አንዱን ድገም" - "በው" - ባለ ሙሉ ማያ ገጽ ሁኔታ + + ቀዳሚ ትራክ + ቀጣይ ትራክ + ላፍታ አቁም + አጫውት + አቁም + ወደኋላ መልስ + በፍጥነት አሳልፍ + ምንም አትድገም + አንድ ድገም + ሁሉንም ድገም + በውዝ + የሙሉ ማያ ሁነታ + አውርድ + የወረዱ + በማውረድ ላይ + ማውረድ ተጠናቋል + ማውረድ አልተሳካም + ውርዶችን በማስወገድ ላይ + ቪዲዮ + ኦዲዮ + ጽሑፍ + ምንም + ራስ-ሰር + ያልታወቀ + %1$d × %2$d + ሞኖ + ስቲሪዮ + የዙሪያ ድምፅ + 5.1 የዙሪያ ድምፅ + 7.1 የዙሪያ ድምፅ + %1$.2f ሜብስ + %1$s፣ %2$s diff --git a/library/ui/src/main/res/values-ar/strings.xml b/library/ui/src/main/res/values-ar/strings.xml index 2cc56abbfa..f79a1cf5db 100644 --- a/library/ui/src/main/res/values-ar/strings.xml +++ b/library/ui/src/main/res/values-ar/strings.xml @@ -1,30 +1,35 @@ - - - "المقطع الصوتي السابق" - "المقطع الصوتي التالي" - "إيقاف مؤقت" - "تشغيل" - "إيقاف" - "إرجاع" - "تقديم سريع" - "تكرار الكل" - "عدم التكرار" - "تكرار مقطع واحد" - "ترتيب عشوائي" + + المقطع الصوتي السابق + المقطع الصوتي التالي + إيقاف مؤقت + تشغيل + إيقاف + إرجاع + تقديم سريع + عدم التكرار + تكرار مقطع صوتي واحد + تكرار الكل + ترتيب عشوائي وضع ملء الشاشة + تنزيل + التنزيلات + جارٍ التنزيل. + اكتمل التنزيل + تعذّر التنزيل + جارٍ إزالة التنزيلات + فيديو + صوت + نص + بدون اختيار + تلقائي + غير معروف + %1$d × %2$d + قناة أحادية + استريو + صوت مجسّم + صوت مجسّم 5.1 + صوت مجسّم 7.1 + %1$.2f ميغابت في الثانية + %1$s، %2$s diff --git a/library/ui/src/main/res/values-az-rAZ/strings.xml b/library/ui/src/main/res/values-az-rAZ/strings.xml deleted file mode 100644 index 1071cd5542..0000000000 --- a/library/ui/src/main/res/values-az-rAZ/strings.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - "Öncəki trek" - "Növbəti trek" - "Pauza" - "Oyun" - "Dayandır" - "Geri sarıma" - "Sürətlə irəli" - "Bütün təkrarlayın" - "Təkrar bir" - "Heç bir təkrar" - "Qarışdır" - diff --git a/library/ui/src/main/res/values-az/strings.xml b/library/ui/src/main/res/values-az/strings.xml new file mode 100644 index 0000000000..4191095b4d --- /dev/null +++ b/library/ui/src/main/res/values-az/strings.xml @@ -0,0 +1,35 @@ + + + Əvvəlki trek + Növbəti trek + Pauza + Oxudun + Dayandırın + Geri çəkin + İrəli çəkin + Heç biri təkrarlanmasın + Biri təkrarlansın + Hamısı təkrarlansın + Qarışdırın + Tam ekran rejimi + Endirin + Endirmələr + Endirilir + Endirmə tamamlandı + Endirmə alınmadı + Endirilənlər silinir + Video + Audio + Mətn + Yoxdur + Avtomatik + Naməlum + %1$d × %2$d + Mono + Stereo + Yüksək səs + 5.1 yüksək səs + 7.1 yüksək səs + %1$.2f Mbps + %1$s, %2$s + diff --git a/library/ui/src/main/res/values-b+sr+Latn/strings.xml b/library/ui/src/main/res/values-b+sr+Latn/strings.xml index a9d35e5cb6..6acedb205f 100644 --- a/library/ui/src/main/res/values-b+sr+Latn/strings.xml +++ b/library/ui/src/main/res/values-b+sr+Latn/strings.xml @@ -1,29 +1,35 @@ - - - - "Prethodna pesma" - "Sledeća pesma" - "Pauza" - "Pusti" - "Zaustavi" - "Premotaj unazad" - "Premotaj unapred" - "Ponovi sve" - "Ne ponavljaj nijednu" - "Ponovi jednu" - "Pusti nasumično" + + + Prethodna pesma + Sledeća pesma + Pauziraj + Pusti + Zaustavi + Premotaj unazad + Premotaj unapred + Ne ponavljaj nijednu + Ponovi jednu + Ponovi sve + Pusti nasumično + Režim celog ekrana + Preuzmi + Preuzimanja + Preuzima se + Preuzimanje je završeno + Preuzimanje nije uspelo + Preuzimanja se uklanjaju + Video + Audio + Tekst + Nijedna + Automatski + Nepoznato + %1$d × %2$d + Mono + Stereo + Zvučni sistem + Zvučni sistem 5.1 + Zvučni sistem 7.1 + %1$.2f Mb/s + %1$s, %2$s diff --git a/library/ui/src/main/res/values-be-rBY/strings.xml b/library/ui/src/main/res/values-be-rBY/strings.xml deleted file mode 100644 index 69b24ad5e9..0000000000 --- a/library/ui/src/main/res/values-be-rBY/strings.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - "Папярэдні трэк" - "Наступны трэк" - "Прыпыніць" - "Прайграць" - "Спыніць" - "Перамотка назад" - "Перамотка ўперад" - "Паўтарыць усё" - "Паўтараць ні" - "Паўтарыць адзін" - "Перамяшаць" - diff --git a/library/ui/src/main/res/values-be/strings.xml b/library/ui/src/main/res/values-be/strings.xml new file mode 100644 index 0000000000..63704e66ca --- /dev/null +++ b/library/ui/src/main/res/values-be/strings.xml @@ -0,0 +1,35 @@ + + + Папярэдні трэк + Наступны трэк + Паўза + Гуляць + Спыніць + Пераматаць назад + Пераматаць уперад + Не паўтараць нічога + Паўтарыць адзін элемент + Паўтарыць усе + Перамяшаць + Поўнаэкранны рэжым + Спампаваць + Спампоўкі + Спампоўваецца + Спампоўка завершана + Збой спампоўкі + Выдаленне спамповак + Відэа + Аўдыя + Тэкст + Няма + Аўтаматычна + Невядома + %1$d × %2$d + Мона + Стэрэа + Аб\'ёмны гук + Аб\'ёмны гук 5.1 + Аб\'ёмны гук 7.1 + %1$.2f Мбіт/с + %1$s, %2$s + diff --git a/library/ui/src/main/res/values-bg/strings.xml b/library/ui/src/main/res/values-bg/strings.xml index e350479788..74bc85313d 100644 --- a/library/ui/src/main/res/values-bg/strings.xml +++ b/library/ui/src/main/res/values-bg/strings.xml @@ -1,29 +1,35 @@ - - - - "Предишен запис" - "Следващ запис" - "Пауза" - "Пускане" - "Спиране" - "Превъртане назад" - "Превъртане напред" - "Повтаряне на всички" - "Без повтаряне" - "Повтаряне на един елемент" - "Разбъркване" + + + Предишен запис + Следващ запис + Поставяне на пауза + Възпроизвеждане + Спиране + Превъртане назад + Превъртане напред + Без повтаряне + Повтаряне на един елемент + Повтаряне на всички + Разбъркване + Режим на цял екран + Изтегляне + Изтегляния + Изтегля се + Изтеглянето завърши + Изтеглянето не бе успешно + Изтеглянията се премахват + Видеозапис + Аудиозапис + Текст + Нищо + Автоматично + Неизвестно + %1$d × %2$d + Моно + Стерео + Обемен звук + 5,1-канален обемен звук + 7,1-канален обемен звук + %1$.2f Мб/сек + %1$s – %2$s diff --git a/library/ui/src/main/res/values-bn-rBD/strings.xml b/library/ui/src/main/res/values-bn-rBD/strings.xml deleted file mode 100644 index 446ef982a3..0000000000 --- a/library/ui/src/main/res/values-bn-rBD/strings.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - "পূর্ববর্তী ট্র্যাক" - "পরবর্তী ট্র্যাক" - "বিরাম দিন" - "প্লে করুন" - "থামান" - "গুটিয়ে নিন" - "দ্রুত সামনে এগোন" - "সবগুলির পুনরাবৃত্তি করুন" - "একটিরও পুনরাবৃত্তি করবেন না" - "একটির পুনরাবৃত্তি করুন" - "অদলবদল" - diff --git a/library/ui/src/main/res/values-bn/strings.xml b/library/ui/src/main/res/values-bn/strings.xml new file mode 100644 index 0000000000..4e3b00113f --- /dev/null +++ b/library/ui/src/main/res/values-bn/strings.xml @@ -0,0 +1,35 @@ + + + আগের ট্র্যাক + পরবর্তী ট্র্যাক + পজ করুন + চালান + থামান + পিছিয়ে যান + দ্রুত এগিয়ে যান + কোনও আইটেম আবার চালাবেন না + একটি আইটেম আবার চালান + সবগুলি আইটেম আবার চালান + শাফেল করুন + পূর্ণ স্ক্রিন মোড + ডাউনলোড করুন + ডাউনলোড + ডাউনলোড হচ্ছে + ডাউনলোড হয়ে গেছে + ডাউনলোড করা যায়নি + ডাউনলোড করা কন্টেন্ট সরিয়ে দেওয়া হচ্ছে + ভিডিও + অডিও + টেক্সট + কোনওটিই নয় + অটো + অজানা + %1$d × %2$d + মোনো + স্টিরিও + সারাউন্ড সাউন্ড + 5.1 সারাউন্ড সাউন্ড + 7.1 সারাউন্ড সাউন্ড + %1$.2f এমবিপিএস + %1$s, %2$s + diff --git a/library/ui/src/main/res/values-bs-rBA/strings.xml b/library/ui/src/main/res/values-bs-rBA/strings.xml deleted file mode 100644 index 186b1058d9..0000000000 --- a/library/ui/src/main/res/values-bs-rBA/strings.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - "Prethodna numera" - "Sljedeća numera" - "Pauziraj" - "Reproduciraj" - "Zaustavi" - "Premotaj" - "Ubrzaj" - "Ponovite sve" - "Ne ponavljaju" - "Ponovite jedan" - "Izmiješaj" - diff --git a/library/ui/src/main/res/values-bs/strings.xml b/library/ui/src/main/res/values-bs/strings.xml new file mode 100644 index 0000000000..be2f6459f5 --- /dev/null +++ b/library/ui/src/main/res/values-bs/strings.xml @@ -0,0 +1,35 @@ + + + Prethodna numera + Sljedeća numera + Pauza + Reproduciranje + Zaustavljanje + Premotavanje unazad + Premotavanje unaprijed + Ne ponavljaj + Ponovi jedno + Ponovi sve + Izmiješaj + Način rada preko cijelog ekrana + Preuzmi + Preuzimanja + Preuzimanje + Preuzimanje je završeno + Preuzimanje nije uspjelo + Uklanjanje preuzimanja + Videozapis + Zvuk + Tekst + Ništa + Automatski + Nepoznato + %1$d × %2$d + Mono + Stereo + Prostorno ozvučenje + Prostorno ozvučenje 5.1 + Prostorno ozvučenje 7.1 + %1$.2f Mbps + %1$s, %2$s + diff --git a/library/ui/src/main/res/values-ca/strings.xml b/library/ui/src/main/res/values-ca/strings.xml index fd76a8e08e..10bf259418 100644 --- a/library/ui/src/main/res/values-ca/strings.xml +++ b/library/ui/src/main/res/values-ca/strings.xml @@ -1,29 +1,35 @@ - - - - "Ruta anterior" - "Ruta següent" - "Posa en pausa" - "Reprodueix" - "Atura" - "Rebobina" - "Avança ràpidament" - "Repeteix-ho tot" - "No en repeteixis cap" - "Repeteix-ne un" - "Reprodueix aleatòriament" + + + Pista anterior + Pista següent + Posa en pausa + Reprodueix + Atura + Rebobina + Avança ràpidament + No en repeteixis cap + Repeteix una + Repeteix tot + Reprodueix aleatòriament + Mode de pantalla completa + Baixa + Baixades + S\'està baixant + S\'ha completat la baixada + No s\'ha pogut baixar + S\'estan suprimint les baixades + Vídeo + Àudio + Text + Cap + Automàtica + Desconegut + %1$d × %2$d + Mono + Estèreo + So envoltant + So envoltant 5.1 + So envoltant 7.1 + %1$.2f Mbps + %1$s, %2$s diff --git a/library/ui/src/main/res/values-cs/strings.xml b/library/ui/src/main/res/values-cs/strings.xml index 087ab79c25..c910fd3483 100644 --- a/library/ui/src/main/res/values-cs/strings.xml +++ b/library/ui/src/main/res/values-cs/strings.xml @@ -1,29 +1,35 @@ - - - - "Předchozí skladba" - "Další skladba" - "Pozastavit" - "Přehrát" - "Zastavit" - "Přetočit zpět" - "Přetočit vpřed" - "Opakovat vše" - "Neopakovat" - "Opakovat jednu položku" - "Náhodně" + + + Předchozí skladba + Další skladba + Pozastavit + Přehrát + Zastavit + Přetočit zpět + Rychle vpřed + Neopakovat + Opakovat jednu + Opakovat vše + Náhodně + Režim celé obrazovky + Stáhnout + Stahování + Stahování + Stahování bylo dokončeno + Stažení se nezdařilo + Odstraňování staženého obsahu + Videa + Zvuk + SMS + Žádné + Automaticky + Neznámé + %1$d × %2$d + Mono + Stereo + Prostorový zvuk + Prostorový zvuk 5.1 + Prostorový zvuk 7.1 + %1$.2f Mb/s + %1$s, %2$s diff --git a/library/ui/src/main/res/values-da/strings.xml b/library/ui/src/main/res/values-da/strings.xml index 0ae23ee288..6a25bbf395 100644 --- a/library/ui/src/main/res/values-da/strings.xml +++ b/library/ui/src/main/res/values-da/strings.xml @@ -1,29 +1,35 @@ - - - - "Forrige nummer" - "Næste nummer" - "Pause" - "Afspil" - "Stop" - "Spol tilbage" - "Spol frem" - "Gentag alle" - "Gentag ingen" - "Gentag en" - "Bland" + + + Afspil forrige + Afspil næste + Sæt på pause + Afspil + Stop + Spol tilbage + Spol frem + Gentag ingen + Gentag én + Gentag alle + Bland + Fuld skærm + Download + Downloads + Downloader + Downloaden er udført + Download mislykkedes + Fjerner downloads + Video + Lyd + Undertekst + Ingen + Automatisk + Ukendt + %1$d × %2$d + Mono + Stereo + Surroundsound + 5.1 surround sound + 7.1 surroundsound + %1$.2f Mbps + %1$s, %2$s diff --git a/library/ui/src/main/res/values-de/strings.xml b/library/ui/src/main/res/values-de/strings.xml index b31ecc93e8..3e83396678 100644 --- a/library/ui/src/main/res/values-de/strings.xml +++ b/library/ui/src/main/res/values-de/strings.xml @@ -1,30 +1,35 @@ - - - "Vorheriger Titel" - "Nächster Titel" - "Pausieren" - "Wiedergabe" - "Beenden" - "Zurückspulen" - "Vorspulen" - "Alle wiederholen" - "Keinen Titel wiederholen" - "Einen Titel wiederholen" - "Zufallsmix" + + Vorheriger Titel + Nächster Titel + Pausieren + Wiedergeben + Beenden + Zurückspulen + Vorspulen + Keinen wiederholen + Einen wiederholen + Alle wiederholen + Zufallsmix Vollbildmodus + Herunterladen + Downloads + Wird heruntergeladen + Download abgeschlossen + Download fehlgeschlagen + Downloads werden entfernt + Video + Audio + Text + Keiner + Automatisch + Unbekannt + %1$d × %2$d + Mono + Stereo + Surround-Sound + 5.1-Surround-Sound + 7.1-Surround-Sound + %1$.2f Mbit/s + %1$s und %2$s diff --git a/library/ui/src/main/res/values-el/strings.xml b/library/ui/src/main/res/values-el/strings.xml index 9bc6a87889..699650d31e 100644 --- a/library/ui/src/main/res/values-el/strings.xml +++ b/library/ui/src/main/res/values-el/strings.xml @@ -1,30 +1,35 @@ - - - "Προηγούμενο κομμάτι" - "Επόμενο κομμάτι" - "Παύση" - "Αναπαραγωγή" - "Διακοπή" - "Επαναφορά" - "Γρήγορη προώθηση" - "Επανάληψη όλων" - "Καμία επανάληψη" - "Επανάληψη ενός στοιχείου" - "Τυχαία αναπαραγωγή" + + Προηγούμενο κομμάτι + Επόμενο κομμάτι + Παύση + Αναπαραγωγή + Διακοπή + Επαναφορά + Γρήγορη προώθηση + Καμία επανάληψη + Επανάληψη ενός κομματιού + Επανάληψη όλων + Τυχαία αναπαραγωγή Λειτουργία πλήρους οθόνης + Λήψη + Λήψεις + Λήψη + Η λήψη ολοκληρώθηκε + Η λήψη απέτυχε + Κατάργηση λήψεων + Βίντεο + Ήχος + Κείμενο + Κανένα + Αυτόματο + Άγνωστο + %1$d × %2$d + Μονοφωνικό + Στερεοφωνικό + Περιφερειακός ήχος + Περιφερειακός ήχος 5.1 + Περιφερειακός ήχος 7.1 + %1$.2f Mbps + %1$s, %2$s diff --git a/library/ui/src/main/res/values-en-rAU/strings.xml b/library/ui/src/main/res/values-en-rAU/strings.xml index 0b4c465853..2356f0cd94 100644 --- a/library/ui/src/main/res/values-en-rAU/strings.xml +++ b/library/ui/src/main/res/values-en-rAU/strings.xml @@ -1,29 +1,35 @@ - - - - "Previous track" - "Next track" - "Pause" - "Play" - "Stop" - "Rewind" - "Fast-forward" - "Repeat all" - "Repeat none" - "Repeat one" - "Shuffle" + + + Previous track + Next track + Pause + Play + Stop + Rewind + Fast-forward + Repeat none + Repeat one + Repeat all + Shuffle + Full-screen mode + Download + Downloads + Downloading + Download completed + Download failed + Removing downloads + Video + Audio + Text + None + Auto + Unknown + %1$d × %2$d + Mono + Stereo + Surround sound + 5.1 surround sound + 7.1 surround sound + %1$.2f Mbps + %1$s, %2$s diff --git a/library/ui/src/main/res/values-en-rGB/strings.xml b/library/ui/src/main/res/values-en-rGB/strings.xml index e80b2c70c6..2356f0cd94 100644 --- a/library/ui/src/main/res/values-en-rGB/strings.xml +++ b/library/ui/src/main/res/values-en-rGB/strings.xml @@ -1,30 +1,35 @@ - - - "Previous track" - "Next track" - "Pause" - "Play" - "Stop" - "Rewind" - "Fast-forward" - "Repeat all" - "Repeat none" - "Repeat one" - "Shuffle" - Fullscreen mode + + Previous track + Next track + Pause + Play + Stop + Rewind + Fast-forward + Repeat none + Repeat one + Repeat all + Shuffle + Full-screen mode + Download + Downloads + Downloading + Download completed + Download failed + Removing downloads + Video + Audio + Text + None + Auto + Unknown + %1$d × %2$d + Mono + Stereo + Surround sound + 5.1 surround sound + 7.1 surround sound + %1$.2f Mbps + %1$s, %2$s diff --git a/library/ui/src/main/res/values-en-rIN/strings.xml b/library/ui/src/main/res/values-en-rIN/strings.xml index e80b2c70c6..2356f0cd94 100644 --- a/library/ui/src/main/res/values-en-rIN/strings.xml +++ b/library/ui/src/main/res/values-en-rIN/strings.xml @@ -1,30 +1,35 @@ - - - "Previous track" - "Next track" - "Pause" - "Play" - "Stop" - "Rewind" - "Fast-forward" - "Repeat all" - "Repeat none" - "Repeat one" - "Shuffle" - Fullscreen mode + + Previous track + Next track + Pause + Play + Stop + Rewind + Fast-forward + Repeat none + Repeat one + Repeat all + Shuffle + Full-screen mode + Download + Downloads + Downloading + Download completed + Download failed + Removing downloads + Video + Audio + Text + None + Auto + Unknown + %1$d × %2$d + Mono + Stereo + Surround sound + 5.1 surround sound + 7.1 surround sound + %1$.2f Mbps + %1$s, %2$s diff --git a/library/ui/src/main/res/values-es-rUS/strings.xml b/library/ui/src/main/res/values-es-rUS/strings.xml index e6cf3fc6f2..b7d8facf17 100644 --- a/library/ui/src/main/res/values-es-rUS/strings.xml +++ b/library/ui/src/main/res/values-es-rUS/strings.xml @@ -1,29 +1,35 @@ - - - - "Pista anterior" - "Siguiente pista" - "Pausar" - "Reproducir" - "Detener" - "Retroceder" - "Avanzar" - "Repetir todo" - "No repetir" - "Repetir uno" - "Reproducir aleatoriamente" + + + Pista anterior + Pista siguiente + Pausar + Reproducir + Detener + Retroceder + Avanzar + No repetir + Repetir uno + Repetir todo + Reproducir aleatoriamente + Modo de pantalla completa + Descargar + Descargas + Descargando + Se completó la descarga + No se pudo descargar + Quitando descargas + Video + Audio + Texto + Ninguna + Automática + Desconocido + %1$d × %2$d + Mono + Estéreo + Sonido envolvente + Sonido envolvente 5.1 + Sonido envolvente 7.1 + %1$.2f Mbps + %1$s, %2$s diff --git a/library/ui/src/main/res/values-es/strings.xml b/library/ui/src/main/res/values-es/strings.xml index 2029ab833e..7a48245abb 100644 --- a/library/ui/src/main/res/values-es/strings.xml +++ b/library/ui/src/main/res/values-es/strings.xml @@ -1,30 +1,35 @@ - - - "Canción anterior" - "Siguiente canción" - "Pausar" - "Reproducir" - "Detener" - "Rebobinar" - "Avance rápido" - "Repetir todo" - "No repetir" - "Repetir uno" - "Reproducción aleatoria" + + Pista anterior + Siguiente pista + Pausar + Reproducir + Detener + Rebobinar + Avanzar rápidamente + No repetir + Repetir uno + Repetir todo + Reproducir aleatoriamente Modo de pantalla completa + Descargar + Descargas + Descargando + Descarga de archivos completado + No se ha podido descargar + Quitando descargas + Vídeo + Audio + Texto + Ninguna + Automático + Desconocido + %1$d×%2$d + Mono + Estéreo + Sonido envolvente + Sonido envolvente 5.1 + Sonido envolvente 7.1 + %1$.2f Mbps + %1$s, %2$s diff --git a/library/ui/src/main/res/values-et-rEE/strings.xml b/library/ui/src/main/res/values-et-rEE/strings.xml deleted file mode 100644 index 004ec7e6c3..0000000000 --- a/library/ui/src/main/res/values-et-rEE/strings.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - "Eelmine lugu" - "Järgmine lugu" - "Peata" - "Esita" - "Peata" - "Keri tagasi" - "Keri edasi" - "Korda kõike" - "Ära korda midagi" - "Korda ühte" - "Esita juhuslikus järjekorras" - diff --git a/library/ui/src/main/res/values-et/strings.xml b/library/ui/src/main/res/values-et/strings.xml new file mode 100644 index 0000000000..0ed5389da7 --- /dev/null +++ b/library/ui/src/main/res/values-et/strings.xml @@ -0,0 +1,35 @@ + + + Eelmine lugu + Järgmine lugu + Peata + Esita + Lõpeta + Keri tagasi + Keri edasi + Ära korda ühtegi + Korda ühte + Korda kõiki + Esita juhuslikus järjekorras + Täisekraani režiim + Allalaadimine + Allalaadimised + Allalaadimine + Allalaadimine lõpetati + Allalaadimine ebaõnnestus + Allalaadimiste eemaldamine + Video + Heli + Tekst + Ühtegi + Automaatne + Teadmata + %1$d × %2$d + Mono + Stereo + Ruumiline heli + Ruumiline heli 5.1 + Ruumiline heli 7.1 + %1$.2f Mbit/s + %1$s, %2$s + diff --git a/library/ui/src/main/res/values-eu-rES/strings.xml b/library/ui/src/main/res/values-eu-rES/strings.xml deleted file mode 100644 index 6a3345303a..0000000000 --- a/library/ui/src/main/res/values-eu-rES/strings.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - "Aurreko pista" - "Hurrengo pista" - "Pausatu" - "Erreproduzitu" - "Gelditu" - "Atzeratu" - "Aurreratu" - "Errepikatu guztiak" - "Ez errepikatu" - "Errepikatu bat" - "Erreproduzitu ausaz" - diff --git a/library/ui/src/main/res/values-eu/strings.xml b/library/ui/src/main/res/values-eu/strings.xml new file mode 100644 index 0000000000..31ec286cfb --- /dev/null +++ b/library/ui/src/main/res/values-eu/strings.xml @@ -0,0 +1,35 @@ + + + Aurreko pista + Hurrengo pista + Pausatu + Erreproduzitu + Gelditu + Atzeratu + Aurreratu + Ez errepikatu + Errepikatu bat + Errepikatu guztiak + Erreproduzitu ausaz + Pantaila osoko modua + Deskargak + Deskargak + Deskargatzen + Osatu da deskarga + Ezin izan da deskargatu + Deskargak kentzen + Bideoa + Audioa + Testua + Bat ere ez + Automatikoa + Ezezaguna + %1$d × %2$d + Monoa + Estereoa + Soinu inguratzailea + 5.1 soinu inguratzailea + 7.1 soinu inguratzailea + %1$.2f Mb/s + %1$s, %2$s + diff --git a/library/ui/src/main/res/values-fa/strings.xml b/library/ui/src/main/res/values-fa/strings.xml index c2303a6e62..9b0853cee5 100644 --- a/library/ui/src/main/res/values-fa/strings.xml +++ b/library/ui/src/main/res/values-fa/strings.xml @@ -1,30 +1,35 @@ - - - "آهنگ قبلی" - "آهنگ بعدی" - "مکث" - "پخش" - "توقف" - "عقب بردن" - "جلو بردن سریع" - "تکرار همه" - "تکرار هیچ‌کدام" - "یک‌بار تکرار" - "پخش درهم" - حالت تمام صفحه + + آهنگ قبلی + آهنگ بعدی + مکث + پخش + توقف + عقب بردن + جلو بردن سریع + تکرار هیچ‌کدام + یکبار تکرار + تکرار همه + درهم + حالت تمام‌صفحه + بارگیری + بارگیری‌ها + درحال بارگیری + بارگیری کامل شد + بارگیری نشد + حذف بارگیری‌ها + ویدیو + صوتی + نوشتار + هیچ‌کدام + خودکار + نامشخص + %1$d × %2$d + مونو + استریو + صدای فراگیر + صدای فراگیر ۵.۱ + صدای فراگیر ۷٫۱ + %1$.2f مگابیت در ثانیه + %1$s،‏ %2$s diff --git a/library/ui/src/main/res/values-fi/strings.xml b/library/ui/src/main/res/values-fi/strings.xml index 92feb86683..d9b33f0977 100644 --- a/library/ui/src/main/res/values-fi/strings.xml +++ b/library/ui/src/main/res/values-fi/strings.xml @@ -1,29 +1,35 @@ - - - - "Edellinen raita" - "Seuraava raita" - "Tauko" - "Toista" - "Seis" - "Kelaa taakse" - "Kelaa eteen" - "Toista kaikki" - "Toista ei mitään" - "Toista yksi" - "Toista satunnaisesti" + + + Edellinen kappale + Seuraava kappale + Keskeytä + Toista + Lopeta + Kelaa taaksepäin + Kelaa eteenpäin + Ei uudelleentoistoa + Toista yksi uudelleen + Toista kaikki uudelleen + Satunnaistoisto + Koko näytön tila + Lataa + Lataukset + Ladataan + Lataus valmis + Lataus epäonnistui + Poistetaan latauksia + Video + Ääni + Teksti + + Automaattinen + Tuntematon + %1$d × %2$d + Mono + Stereo + Surround-ääni + 5.1-surround-ääni + 7.1-surround-ääni + %1$.2f Mt/s + %1$s, %2$s diff --git a/library/ui/src/main/res/values-fr-rCA/strings.xml b/library/ui/src/main/res/values-fr-rCA/strings.xml index 45fc0a86f9..b68fab04ed 100644 --- a/library/ui/src/main/res/values-fr-rCA/strings.xml +++ b/library/ui/src/main/res/values-fr-rCA/strings.xml @@ -1,29 +1,35 @@ - - - - "Chanson précédente" - "Chanson suivante" - "Pause" - "Lecture" - "Arrêter" - "Reculer" - "Avance rapide" - "Tout lire en boucle" - "Aucune répétition" - "Répéter un élément" - "Lecture aléatoire" + + + Chanson précédente + Chanson suivante + Pause + Lire + Arrêter + Retour arrière + Avance rapide + Ne rien lire en boucle + Lire une chanson en boucle + Tout lire en boucle + Lecture aléatoire + Mode Plein écran + Télécharger + Téléchargements + Téléchargement en cours… + Téléchargement terminé + Échec du téléchargement + Suppression des téléchargements en cours… + Vidéo + Audio + Texte + Aucun + Auto + Inconnu + %1$d × %2$d + Mono + Stéréo + Son ambiophonique + Son ambiophonique 5.1 + Son ambiophonique 7.1 + %1$.2f Mb/s + %1$s, %2$s diff --git a/library/ui/src/main/res/values-fr/strings.xml b/library/ui/src/main/res/values-fr/strings.xml index 6617fd6e6a..48c19e30d9 100644 --- a/library/ui/src/main/res/values-fr/strings.xml +++ b/library/ui/src/main/res/values-fr/strings.xml @@ -1,30 +1,35 @@ - - - "Piste précédente" - "Piste suivante" - "Interrompre" - "Lire" - "Arrêter" - "Retour arrière" - "Avance rapide" - "Tout lire en boucle" - "Ne rien lire en boucle" - "Lire en boucle un élément" - "Lire en mode aléatoire" + + Titre précédent + Titre suivant + Pause + Lecture + Arrêter + Retour arrière + Avance rapide + Ne rien lire en boucle + Lire un titre en boucle + Tout lire en boucle + Aléatoire Mode plein écran + Télécharger + Téléchargements + Téléchargement… + Téléchargement terminé + Échec du téléchargement + Suppression des téléchargements… + Vidéo + Audio + Texte + Aucun + Automatique + Inconnu + %1$d × %2$d + Mono + Stéréo + Son surround + Son surround 5.1 + Son surround 7.1 + %1$.2f Mbit/s + %1$s, %2$s diff --git a/library/ui/src/main/res/values-gl-rES/strings.xml b/library/ui/src/main/res/values-gl-rES/strings.xml deleted file mode 100644 index 7062d8d023..0000000000 --- a/library/ui/src/main/res/values-gl-rES/strings.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - "Pista anterior" - "Seguinte pista" - "Pausar" - "Reproducir" - "Deter" - "Rebobinar" - "Avance rápido" - "Repetir todo" - "Non repetir" - "Repetir un" - "Reprodución aleatoria" - diff --git a/library/ui/src/main/res/values-gl/strings.xml b/library/ui/src/main/res/values-gl/strings.xml new file mode 100644 index 0000000000..e41b1d1445 --- /dev/null +++ b/library/ui/src/main/res/values-gl/strings.xml @@ -0,0 +1,35 @@ + + + Pista anterior + Pista seguinte + Pausar + Reproducir + Deter + Rebobinar + Avance rápido + Non repetir + Repetir unha pista + Repetir todas as pistas + Reprodución aleatoria + Modo de pantalla completa + Descargar + Descargas + Descargando + Completouse a descarga + Produciuse un erro na descarga + Quitando descargas + Vídeo + Audio + Texto + Ningunha pista + Pista automática + Descoñecido + %1$d × %2$d + Mono + Estéreo + Son envolvente + Son envolvente 5.1 + Son envolvente 7.1 + %1$.2f Mbps + %1$s, %2$s + diff --git a/library/ui/src/main/res/values-gu-rIN/strings.xml b/library/ui/src/main/res/values-gu-rIN/strings.xml deleted file mode 100644 index ed78b1ee30..0000000000 --- a/library/ui/src/main/res/values-gu-rIN/strings.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - "પહેલાનો ટ્રૅક" - "આગલો ટ્રૅક" - "થોભો" - "ચલાવો" - "રોકો" - "રીવાઇન્ડ કરો" - "ઝડપી ફોરવર્ડ કરો" - "બધા પુનરાવર્તન કરો" - "કંઈ પુનરાવર્તન કરો" - "એક પુનરાવર્તન કરો" - "શફલ કરો" - diff --git a/library/ui/src/main/res/values-gu/strings.xml b/library/ui/src/main/res/values-gu/strings.xml new file mode 100644 index 0000000000..5d8dd31cc5 --- /dev/null +++ b/library/ui/src/main/res/values-gu/strings.xml @@ -0,0 +1,35 @@ + + + પહેલાંનો ટ્રૅક + આગલો ટ્રૅક + થોભો + ચલાવો + રોકો + રિવાઇન્ડ કરો + ફાસ્ટ ફૉરવર્ડ કરો + કોઈ રિપીટ કરતા નહીં + એક રિપીટ કરો + બધાને રિપીટ કરો + શફલ કરો + પૂર્ણસ્ક્રીન મોડ + ડાઉનલોડ કરો + ડાઉનલોડ + ડાઉનલોડ કરી રહ્યાં છીએ + ડાઉનલોડ પૂર્ણ થયું + ડાઉનલોડ નિષ્ફળ થયું + ડાઉનલોડ કાઢી નાખી રહ્યાં છીએ + વીડિઓ + ઑડિઓ + ટેક્સ્ટ + એકપણ નહીં + આપમેળે + અજાણ્યો + %1$d × %2$d + મૉનો + સ્ટીરિઓ + સરાઉન્ડ સાઉન્ડ + 5.1 સરાઉન્ડ સાઉન્ડ + 7.1 સરાઉન્ડ સાઉન્ડ + %1$.2f Mbps + %1$s, %2$s + diff --git a/library/ui/src/main/res/values-hi/strings.xml b/library/ui/src/main/res/values-hi/strings.xml index 6545307e8c..9df542ef52 100644 --- a/library/ui/src/main/res/values-hi/strings.xml +++ b/library/ui/src/main/res/values-hi/strings.xml @@ -1,30 +1,35 @@ - - - "पिछला ट्रैक" - "अगला ट्रैक" - "रोकें" - "चलाएं" - "बंद करें" - "रिवाइंड करें" - "फ़ास्ट फ़ॉरवर्ड" - "सभी को दोहराएं" - "कुछ भी न दोहराएं" - "एक दोहराएं" - "शफ़ल करें" - पूर्ण-स्क्रीन मोड + + पिछला ट्रैक + अगला ट्रैक + रोकें + चलाएं + बंद करें + पीछे ले जाएं + तेज़ी से आगे बढ़ाएं + किसी को न दोहराएं + एक को दोहराएं + सभी को दोहराएं + शफ़ल करें + फ़ुलस्क्रीन मोड + डाउनलोड करें + डाउनलोड की गई मीडिया फ़ाइलें + डाउनलोड हो रहा है + डाउनलोड पूरा हुआ + डाउनलोड नहीं हो सका + डाउनलोड की गई फ़ाइलें हटाई जा रही हैं + वीडियो + ऑडियो + लेख + कोई नहीं + अपने आप + अज्ञात + %1$d × %2$d + मोनो साउंड + स्टीरियो साउंड + सराउंड साउंड + 5.1 सराउंड साउंड + 7.1 सराउंड साउंड + %1$.2f एमबीपीएस + %1$s, %2$s diff --git a/library/ui/src/main/res/values-hr/strings.xml b/library/ui/src/main/res/values-hr/strings.xml index dd7423032b..7d5de9b189 100644 --- a/library/ui/src/main/res/values-hr/strings.xml +++ b/library/ui/src/main/res/values-hr/strings.xml @@ -1,30 +1,35 @@ - - - "Prethodna pjesma" - "Sljedeća pjesma" - "Pauziraj" - "Reproduciraj" - "Zaustavi" - "Unatrag" - "Brzo unaprijed" - "Ponovi sve" - "Bez ponavljanja" - "Ponovi jedno" - "Reproduciraj nasumično" + + Prethodni zapis + Sljedeći zapis + Pauza + Reproduciraj + Zaustavi + Unatrag + Brzo unaprijed + Bez ponavljanja + Ponovi jedno + Ponovi sve + Reproduciraj nasumično Prikaz na cijelom zaslonu + Preuzmi + Preuzimanja + Preuzimanje + Preuzimanje je dovršeno + Preuzimanje nije uspjelo + Uklanjanje preuzimanja + Videozapis + Audiozapis + Tekst + Ništa + Automatski + Nepoznato + %1$d × %2$d + Mono + Stereo + Okružujući zvuk + 5.1-kanalni okružujući zvuk + 7.1-kanalni okružujući zvuk + %1$.2f Mbps + %1$s, %2$s diff --git a/library/ui/src/main/res/values-hu/strings.xml b/library/ui/src/main/res/values-hu/strings.xml index abd9f9cfac..bb7d5950e6 100644 --- a/library/ui/src/main/res/values-hu/strings.xml +++ b/library/ui/src/main/res/values-hu/strings.xml @@ -1,30 +1,35 @@ - - - "Előző szám" - "Következő szám" - "Szünet" - "Lejátszás" - "Leállítás" - "Visszatekerés" - "Előretekerés" - "Összes ismétlése" - "Nincs ismétlés" - "Egy ismétlése" - "Véletlenszerű lejátszás" + + Előző szám + Következő szám + Szüneteltetés + Lejátszás + Leállítás + Visszatekerés + Előretekerés + Nincs ismétlés + Egy szám ismétlése + Összes szám ismétlése + Keverés Teljes képernyős mód + Letöltés + Letöltések + Letöltés folyamatban + A letöltés befejeződött + Nem sikerült a letöltés + Letöltések törlése folyamatban + Videó + Hang + Szöveg + Nincs + Automatikus + Ismeretlen + %1$d × %2$d + Monó + Sztereó + Térhatású hangzás + 5.1-es térhatású hangzás + 7.1-es térhatású hangzás + %1$.2f Mbps + %1$s, %2$s diff --git a/library/ui/src/main/res/values-hy-rAM/strings.xml b/library/ui/src/main/res/values-hy-rAM/strings.xml deleted file mode 100644 index 13a489baf5..0000000000 --- a/library/ui/src/main/res/values-hy-rAM/strings.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - "Նախորդը" - "Հաջորդը" - "Դադարեցնել" - "Նվագարկել" - "Դադարեցնել" - "Հետ փաթաթել" - "Արագ առաջ անցնել" - "կրկնել այն ամենը" - "Չկրկնել" - "Կրկնել մեկը" - "Խառնել" - diff --git a/library/ui/src/main/res/values-hy/strings.xml b/library/ui/src/main/res/values-hy/strings.xml new file mode 100644 index 0000000000..38468b892d --- /dev/null +++ b/library/ui/src/main/res/values-hy/strings.xml @@ -0,0 +1,35 @@ + + + Նախորդը + Հաջորդը + Դադարեցնել + Նվագարկել + Կանգնեցնել + Հետ + Առաջ + Չկրկնել + Կրկնել մեկը + Կրկնել բոլորը + Խառնել + Լիաէկրան ռեժիմ + Ներբեռնել + Ներբեռնումներ + Ներբեռնում + Ներբեռնումն ավարտվեց + Չհաջողվեց ներբեռնել + Ներբեռնումները հեռացվում են + Տեսանյութ + Աուդիո + Տեքստ + Ոչ մեկը + Ավտոմատ + Անհայտ + %1$d × %2$d + Մոնո + Ստերեո + Ծավալային ձայն + 5․1 ծավալային ձայն + 7․1 ծավալային ձայն + %1$.2f մբ/վ + %1$s, %2$s + diff --git a/library/ui/src/main/res/values-in/strings.xml b/library/ui/src/main/res/values-in/strings.xml index 09b05815e6..6cbd6a703a 100644 --- a/library/ui/src/main/res/values-in/strings.xml +++ b/library/ui/src/main/res/values-in/strings.xml @@ -1,29 +1,35 @@ - - - - "Lagu sebelumnya" - "Lagu berikutnya" - "Jeda" - "Putar" - "Berhenti" - "Putar Ulang" - "Maju cepat" - "Ulangi Semua" - "Jangan Ulangi" - "Ulangi Satu" - "Acak" + + + Lagu sebelumnya + Lagu berikutnya + Jeda + Putar + Berhenti + Putar Ulang + Maju cepat + Jangan ulangi + Ulangi 1 + Ulangi semua + Acak + Mode layar penuh + Download + Download + Mendownload + Download selesai + Download gagal + Menghapus download + Video + Audio + Teks + Tidak ada + Otomatis + Tidak diketahui + %1$d × %2$d + Mono + Stereo + Suara surround + 5.1 surround sound + 7.1 surround sound + %1$.2f Mbps + %1$s, %2$s diff --git a/library/ui/src/main/res/values-is-rIS/strings.xml b/library/ui/src/main/res/values-is-rIS/strings.xml deleted file mode 100644 index 12c4632cdf..0000000000 --- a/library/ui/src/main/res/values-is-rIS/strings.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - "Fyrra lag" - "Næsta lag" - "Hlé" - "Spila" - "Stöðva" - "Spóla til baka" - "Spóla áfram" - "Endurtaka allt" - "Endurtaka ekkert" - "Endurtaka eitt" - "Stokka" - diff --git a/library/ui/src/main/res/values-is/strings.xml b/library/ui/src/main/res/values-is/strings.xml new file mode 100644 index 0000000000..cb5f40ef51 --- /dev/null +++ b/library/ui/src/main/res/values-is/strings.xml @@ -0,0 +1,35 @@ + + + Fyrra lag + Næsta lag + Hlé + Spila + Stöðva + Spóla til baka + Spóla áfram + Endurtaka ekkert + Endurtaka eitt + Endurtaka allt + Stokka + Allur skjárinn + Sækja + Niðurhal + Sækir + Niðurhali lokið + Niðurhal mistókst + Fjarlægir niðurhal + Myndskeið + Hljóð + Texti + Ekkert + Sjálfvirkt + Óþekkt + %1$d × %2$d + Einóma + Víðóma + Víðóma hljóð + 5.1 víðóma hljóð + 7.1 víðóma hljóð + %1$.2f Mb/sek. + %1$s, %2$s + diff --git a/library/ui/src/main/res/values-it/strings.xml b/library/ui/src/main/res/values-it/strings.xml index 3300224fbb..7e058b4a49 100644 --- a/library/ui/src/main/res/values-it/strings.xml +++ b/library/ui/src/main/res/values-it/strings.xml @@ -1,30 +1,35 @@ - - - "Traccia precedente" - "Traccia successiva" - "Metti in pausa" - "Riproduci" - "Interrompi" - "Riavvolgi" - "Avanti veloce" - "Ripeti tutti" - "Non ripetere nessuno" - "Ripeti uno" - "Riproduci casualmente" + + Traccia precedente + Traccia successiva + Pausa + Riproduci + Interrompi + Riavvolgi + Avanti veloce + Non ripetere nulla + Ripeti uno + Ripeti tutto + Riproduzione casuale Modalità a schermo intero + Scarica + Download + Download… + Download completato + Download non riuscito + Rimozione dei download… + Video + Audio + Testo + Nessuno + Auto + Sconosciuta + %1$d × %2$d + Mono + Stereo + Audio surround + Audio surround 5.1 + Audio surround 7.1 + %1$.2f Mbps + %1$s, %2$s diff --git a/library/ui/src/main/res/values-iw/strings.xml b/library/ui/src/main/res/values-iw/strings.xml index dd973af50b..9aec8e5834 100644 --- a/library/ui/src/main/res/values-iw/strings.xml +++ b/library/ui/src/main/res/values-iw/strings.xml @@ -1,29 +1,35 @@ - - - - "הרצועה הקודמת" - "הרצועה הבאה" - "השהה" - "הפעל" - "הפסק" - "הרץ אחורה" - "הרץ קדימה" - "חזור על הכל" - "אל תחזור על כלום" - "חזור על פריט אחד" - "ערבב" + + + הרצועה הקודמת + הרצועה הבאה + השהיה + הפעלה + הפסקה + הרצה אחורה + הרצה קדימה + אל תחזור על אף פריט + חזור על פריט אחד + חזור על הכול + ערבוב + מצב מסך מלא + הורדה + הורדות + ההורדה מתבצעת + ההורדה הושלמה + ההורדה לא הושלמה + מסיר הורדות + סרטון + אודיו + טקסט + ללא + אוטומטי + לא ידוע + %1$d × %2$d + מונו + סטריאו + סראונד + סראונד 5.1 + סראונד 7.1 + %1$.2f מגה סיביות לשנייה + %1$s‏, %2$s diff --git a/library/ui/src/main/res/values-ja/strings.xml b/library/ui/src/main/res/values-ja/strings.xml index 2e0f23a78f..86f204e572 100644 --- a/library/ui/src/main/res/values-ja/strings.xml +++ b/library/ui/src/main/res/values-ja/strings.xml @@ -1,30 +1,35 @@ - - - "前のトラック" - "次のトラック" - "一時停止" - "再生" - "停止" - "巻き戻し" - "早送り" - "全曲を繰り返し" - "繰り返しなし" - "1曲を繰り返し" - "シャッフル" - フルスクリーンモード + + 前のトラック + 次のトラック + 一時停止 + 再生 + 停止 + 巻き戻し + 早送り + リピートなし + 1 曲をリピート + 全曲をリピート + シャッフル + 全画面モード + ダウンロード + ダウンロード + ダウンロードしています + ダウンロードが完了しました + ダウンロードに失敗しました + ダウンロードを削除しています + 動画 + 音声 + SMS + なし + 自動 + 不明 + %1$d × %2$d + モノラル + ステレオ + サラウンド サウンド + 5.1 サラウンド サウンド + 7.1 サラウンド サウンド + %1$.2f Mbps + %1$s、%2$s diff --git a/library/ui/src/main/res/values-ka-rGE/strings.xml b/library/ui/src/main/res/values-ka-rGE/strings.xml deleted file mode 100644 index 252e52f151..0000000000 --- a/library/ui/src/main/res/values-ka-rGE/strings.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - "წინა ჩანაწერი" - "შემდეგი ჩანაწერი" - "პაუზა" - "დაკვრა" - "შეწყვეტა" - "უკან გადახვევა" - "წინ გადახვევა" - "გამეორება ყველა" - "გაიმეორეთ არცერთი" - "გაიმეორეთ ერთი" - "არეულად დაკვრა" - diff --git a/library/ui/src/main/res/values-ka/strings.xml b/library/ui/src/main/res/values-ka/strings.xml new file mode 100644 index 0000000000..5b8c08065e --- /dev/null +++ b/library/ui/src/main/res/values-ka/strings.xml @@ -0,0 +1,35 @@ + + + წინა ჩანაწერი + შემდეგი ჩანაწერი + პაუზა + დაკვრა + შეწყვეტა + უკან გადახვევა + წინ გადახვევა + არცერთის გამეორება + ერთის გამეორება + ყველას გამეორება + არეულად დაკვრა + სრულეკრანიანი რეჟიმი + ჩამოტვირთვა + ჩამოტვირთვები + მიმდინარეობს ჩამოტვირთვა + ჩამოტვირთვა დასრულდა + ჩამოტვირთვა ვერ მოხერხდა + მიმდინარეობს ჩამოტვირთვების ამოშლა + ვიდეო + აუდიო + SMS + არცერთი + ავტომატური + უცნობი + %1$d × %2$d + მონო + სტერეო + მოცულობითი ხმა + 5.1 მოცულობითი ხმა + 7.1 მოცულობითი ხმა + %1$.2f მბიტ/წმ + %1$s, %2$s + diff --git a/library/ui/src/main/res/values-kk-rKZ/strings.xml b/library/ui/src/main/res/values-kk-rKZ/strings.xml deleted file mode 100644 index 43eb3dd030..0000000000 --- a/library/ui/src/main/res/values-kk-rKZ/strings.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - "Алдыңғы трек" - "Келесі трек" - "Кідірту" - "Ойнату" - "Тоқтату" - "Кері айналдыру" - "Жылдам алға айналдыру" - "Барлығын қайталау" - "Ешқайсысын қайталамау" - "Біреуін қайталау" - "Кездейсоқ ретпен ойнату" - diff --git a/library/ui/src/main/res/values-kk/strings.xml b/library/ui/src/main/res/values-kk/strings.xml new file mode 100644 index 0000000000..3842263348 --- /dev/null +++ b/library/ui/src/main/res/values-kk/strings.xml @@ -0,0 +1,35 @@ + + + Алдыңғы аудиотрек + Келесі аудиотрек + Кідірту + Ойнату + Тоқтату + Артқа айналдыру + Жылдам алға айналдыру + Ешқайсысын қайталамау + Біреуін қайталау + Барлығын қайталау + Араластыру + Толық экран режимі + Жүктеп алу + Жүктеп алынғандар + Жүктеп алынуда + Жүктеп алынды + Жүктеп алынбады + Жүктеп алынғандар өшірілуде + Бейне + Aудиомазмұн + Мәтін + Ешқайсысы + Автоматты + Белгісіз + %1$d × %2$d + Моно + Стерео + Көлемді дыбыс + 5.1 көлемді дыбыс жүйесі + 7.1 көлемді дыбыс жүйесі + %1$.2f МБ/сек + %1$s, %2$s + diff --git a/library/ui/src/main/res/values-km-rKH/strings.xml b/library/ui/src/main/res/values-km-rKH/strings.xml deleted file mode 100644 index 653c9f051d..0000000000 --- a/library/ui/src/main/res/values-km-rKH/strings.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - "បទ​មុន" - "បទ​បន្ទាប់" - "ផ្អាក" - "ចាក់" - "បញ្ឈប់" - "ខា​ថយក្រោយ" - "ទៅ​មុខ​​​រហ័ស" - "ធ្វើ​ម្ដង​ទៀត​ទាំងអស់" - "មិន​ធ្វើ​ឡើង​វិញ" - "ធ្វើ​​ឡើងវិញ​ម្ដង" - "ច្របល់" - diff --git a/library/ui/src/main/res/values-km/strings.xml b/library/ui/src/main/res/values-km/strings.xml new file mode 100644 index 0000000000..89d73605a5 --- /dev/null +++ b/library/ui/src/main/res/values-km/strings.xml @@ -0,0 +1,35 @@ + + + សំនៀង​​មុន + សំនៀង​បន្ទាប់ + ផ្អាក + លេង + ឈប់ + ខា​ថយ​ក្រោយ + ទៅ​មុខ​​​រហ័ស + មិន​លេង​ឡើងវិញ + លេង​ឡើង​វិញ​ម្ដង + លេង​ឡើងវិញ​ទាំងអស់ + ច្របល់ + មុខងារពេញ​អេក្រង់ + ទាញយក + ទាញយក + កំពុង​ទាញ​យក + បាន​បញ្ចប់​ការទាញយក + មិន​អាច​ទាញយក​បាន​ទេ + កំពុង​លុប​ការទាញយក + វីដេអូ + សំឡេង + អក្សរ + គ្មាន + ស្វ័យប្រវត្តិ + មិនស្គាល់ + %1$d × %2$d + ម៉ូ​ណូ + ស្តេរ៉េអូ + សំឡេង​រងំ + សំឡេង​រងំ​ខ្នាត 5.1 + សំឡេង​រងំ​ខ្នាត 7.1 + %1$.2f Mbps + %1$s, %2$s + diff --git a/library/ui/src/main/res/values-kn-rIN/strings.xml b/library/ui/src/main/res/values-kn-rIN/strings.xml deleted file mode 100644 index 7368fc8ad3..0000000000 --- a/library/ui/src/main/res/values-kn-rIN/strings.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - "ಹಿಂದಿನ ಟ್ರ್ಯಾಕ್" - "ಮುಂದಿನ ಟ್ರ್ಯಾಕ್" - "ವಿರಾಮಗೊಳಿಸು" - "ಪ್ಲೇ ಮಾಡು" - "ನಿಲ್ಲಿಸು" - "ರಿವೈಂಡ್ ಮಾಡು" - "ವೇಗವಾಗಿ ಮುಂದಕ್ಕೆ" - "ಎಲ್ಲವನ್ನು ಪುನರಾವರ್ತಿಸಿ" - "ಯಾವುದನ್ನೂ ಪುನರಾವರ್ತಿಸಬೇಡಿ" - "ಒಂದನ್ನು ಪುನರಾವರ್ತಿಸಿ" - "ಬೆರೆಸು" - diff --git a/library/ui/src/main/res/values-kn/strings.xml b/library/ui/src/main/res/values-kn/strings.xml new file mode 100644 index 0000000000..65d65b1d23 --- /dev/null +++ b/library/ui/src/main/res/values-kn/strings.xml @@ -0,0 +1,35 @@ + + + ಹಿಂದಿನ ಟ್ರ್ಯಾಕ್ + ಮುಂದಿನ ಟ್ರ್ಯಾಕ್ + ವಿರಾಮ + ಪ್ಲೇ + ನಿಲ್ಲಿಸಿ + ರಿವೈಂಡ್ + ಫಾಸ್ಟ್ ಫಾರ್ವರ್ಡ್ + ಯಾವುದನ್ನೂ ಪುನರಾವರ್ತಿಸಬೇಡಿ + ಒಂದನ್ನು ಪುನರಾವರ್ತಿಸಿ + ಎಲ್ಲವನ್ನು ಪುನರಾವರ್ತಿಸಿ + ಶಫಲ್‌ + ಪೂರ್ಣ ಪರದೆ ಮೋಡ್ + ಡೌನ್‌ಲೋಡ್‌ + ಡೌನ್‌ಲೋಡ್‌ಗಳು + ಡೌನ್‌ಲೋಡ್ ಮಾಡಲಾಗುತ್ತಿದೆ + ಡೌನ್‌ಲೋಡ್ ಪೂರ್ಣಗೊಂಡಿದೆ + ಡೌನ್‌ಲೋಡ್‌ ವಿಫಲಗೊಂಡಿದೆ + ಡೌನ್ಲೋಡ್‌ಗಳನ್ನು ತೆಗೆದುಹಾಕಲಾಗುತ್ತಿದೆ + ವೀಡಿಯೊ + ಆಡಿಯೊ + ಪಠ್ಯ + ಯಾವುದೂ ಅಲ್ಲ + ಸ್ವಯಂ + ಅಪರಿಚಿತ + %1$d × %2$d + ಮೊನೊ + ಸ್ಟೀರಿಯೊ + ಸರೌಂಡ್ ಶಬ್ದ + 5.1 ಸರೌಂಡ್ ಶಬ್ದ + 7.1 ಸರೌಂಡ್ ಶಬ್ದ + %1$.2f Mbps + %1$s, %2$s + diff --git a/library/ui/src/main/res/values-ko/strings.xml b/library/ui/src/main/res/values-ko/strings.xml index 32d3deeb9e..7714203e5e 100644 --- a/library/ui/src/main/res/values-ko/strings.xml +++ b/library/ui/src/main/res/values-ko/strings.xml @@ -1,30 +1,35 @@ - - - "이전 트랙" - "다음 트랙" - "일시중지" - "재생" - "중지" - "되감기" - "빨리 감기" - "전체 반복" - "반복 안함" - "한 항목 반복" - "셔플" - 전체 화면 모드 + + 이전 트랙 + 다음 트랙 + 일시중지 + 재생 + 중지 + 되감기 + 빨리 감기 + 반복 안함 + 현재 미디어 반복 + 모두 반복 + 셔플 + 전체화면 모드 + 다운로드 + 다운로드 + 다운로드 중 + 다운로드 완료 + 다운로드 실패 + 다운로드 항목 삭제 중 + 동영상 + 오디오 + 문자 메시지 + 없음 + 자동 + 알 수 없음 + %1$d×%2$d + 모노 + 스테레오 + 서라운드 사운드 + 5.1 서라운드 사운드 + 7.1 서라운드 사운드 + %1$.2fMbps + %1$s, %2$s diff --git a/library/ui/src/main/res/values-ky-rKG/strings.xml b/library/ui/src/main/res/values-ky-rKG/strings.xml deleted file mode 100644 index 9b903a124e..0000000000 --- a/library/ui/src/main/res/values-ky-rKG/strings.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - "Мурунку трек" - "Кийинки трек" - "Тындыруу" - "Ойнотуу" - "Токтотуу" - "Артка түрүү" - "Алдыга түрүү" - "Баарын кайталоо" - "Эч бирин кайталабоо" - "Бирөөнү кайталоо" - "Аралаштыруу" - diff --git a/library/ui/src/main/res/values-ky/strings.xml b/library/ui/src/main/res/values-ky/strings.xml new file mode 100644 index 0000000000..20a4739a2b --- /dev/null +++ b/library/ui/src/main/res/values-ky/strings.xml @@ -0,0 +1,35 @@ + + + Мурунку трек + Кийинки трек + Тындыруу + Ойнотуу + Токтотуу + Артка түрүү + Алдыга түрүү + Кайталанбасын + Бирөөнү кайталоо + Баарын кайталоо + Аралаштыруу + Толук экран режими + Жүктөп алуу + Жүктөлүп алынгандар + Жүктөлүп алынууда + Жүктөп алуу аяктады + Жүктөлүп алынбай калды + Жүктөлүп алынгандар өчүрүлүүдө + Видео + Аудио + Текст + Жок + Авто + Белгисиз + %1$d × %2$d + Моно + Стерео + Көлөмдүү добуш + 5.1 көлөмдүү добуш + 7.1 көлөмдүү добуш + %1$.2f Мб/сек. + %1$s, %2$s + diff --git a/library/ui/src/main/res/values-lo-rLA/strings.xml b/library/ui/src/main/res/values-lo-rLA/strings.xml deleted file mode 100644 index 702cd54396..0000000000 --- a/library/ui/src/main/res/values-lo-rLA/strings.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - "​ເພງ​ກ່ອນ​ໜ້າ" - "​ເພງ​ຕໍ່​ໄປ" - "ຢຸດຊົ່ວຄາວ" - "ຫຼິ້ນ" - "ຢຸດ" - "​ຣີ​​ວາຍກັບ" - "ເລື່ອນ​ໄປ​ໜ້າ" - "ຫຼິ້ນ​ຊ້ຳ​ທັງ​ໝົດ" - "​ບໍ່ຫຼິ້ນ​ຊ້ຳ" - "ຫຼິ້ນ​ຊ້ຳ" - "ຫຼີ້ນແບບສຸ່ມ" - diff --git a/library/ui/src/main/res/values-lo/strings.xml b/library/ui/src/main/res/values-lo/strings.xml new file mode 100644 index 0000000000..3de5962a42 --- /dev/null +++ b/library/ui/src/main/res/values-lo/strings.xml @@ -0,0 +1,35 @@ + + + ເພງກ່ອນໜ້າ + ເພງຕໍ່ໄປ + ຢຸດຊົ່ວຄາວ + ຫຼິ້ນ + ຢຸດ + ຍ້ອນກັບ + ເລື່ອນໄປໜ້າ + ບໍ່ຫຼິ້ນຊ້ຳ + ຫຼິ້ນຊໍ້າ + ຫຼິ້ນຊ້ຳທັງໝົດ + ຫຼີ້ນແບບສຸ່ມ + ໂໝດເຕັມຈໍ + ດາວໂຫລດ + ດາວໂຫລດ + ກຳລັງດາວໂຫລດ + ດາວໂຫລດສຳເລັດແລ້ວ + ດາວໂຫຼດບໍ່ສຳເລັດ + ກຳລັງລຶບການດາວໂຫລດອອກ + ວິດີໂອ + ສຽງ + ຂໍ້ຄວາມ + ບໍ່ມີ + ອັດຕະໂນມັດ + ບໍ່ຮູ້ຈັກ + %1$d × %2$d + ໂມໂນ + ສະເຕຣິໂອ + ສຽງຮອບທິດທາງ + ສຽງຮອບທິດທາງ 5.1 + ສຽງຮອບທິດທາງ 7.1 + %1$.2f Mbps + %1$s, %2$s + diff --git a/library/ui/src/main/res/values-lt/strings.xml b/library/ui/src/main/res/values-lt/strings.xml index 091f2384b2..eaf26aafb6 100644 --- a/library/ui/src/main/res/values-lt/strings.xml +++ b/library/ui/src/main/res/values-lt/strings.xml @@ -1,30 +1,35 @@ - - - "Ankstesnis takelis" - "Kitas takelis" - "Pristabdyti" - "Leisti" - "Stabdyti" - "Sukti atgal" - "Sukti pirmyn" - "Kartoti viską" - "Nekartoti nieko" - "Kartoti vieną" - "Maišyti" + + Ankstesnis takelis + Kitas takelis + Pristabdyti + Leisti + Sustabdyti + Sukti atgal + Sukti pirmyn + Nekartoti nieko + Kartoti vieną + Kartoti viską + Maišyti Viso ekrano režimas + Atsisiųsti + Atsisiuntimai + Atsisiunčiama + Atsisiuntimo procesas baigtas + Nepavyko atsisiųsti + Pašalinami atsisiuntimai + Vaizdo įrašas + Garso įrašas + Tekstas + Nėra + Automatinė + Nežinomas + %1$d × %2$d + Monofoninis + Stereofoninis + Erdvinis garsas + 5.1 erdvinis garsas + 7.1 erdvinis garsas + %1$.2f Mb/s + %1$s, %2$s diff --git a/library/ui/src/main/res/values-lv/strings.xml b/library/ui/src/main/res/values-lv/strings.xml index af982587bf..708a2143c7 100644 --- a/library/ui/src/main/res/values-lv/strings.xml +++ b/library/ui/src/main/res/values-lv/strings.xml @@ -1,30 +1,35 @@ - - - "Iepriekšējais ieraksts" - "Nākamais ieraksts" - "Pārtraukt" - "Atskaņot" - "Apturēt" - "Attīt atpakaļ" - "Ātri patīt" - "Atkārtot visu" - "Neatkārtot nevienu" - "Atkārtot vienu" - "Atskaņot jauktā secībā" + + Iepriekšējais ieraksts + Nākamais ieraksts + Pauzēt + Atskaņot + Apturēt + Attīt atpakaļ + Pārtīt uz priekšu + Neatkārtot nevienu + Atkārtot vienu + Atkārtot visu + Atskaņot jauktā secībā Pilnekrāna režīms + Lejupielādēt + Lejupielādes + Notiek lejupielāde + Lejupielāde ir pabeigta + Lejupielāde neizdevās + Notiek lejupielāžu noņemšana + Video + Audio + Teksts + Nav + Automātisks + Nezināms + %1$d × %2$d + Mono + Stereo + Ieskaujošā skaņa + 5.1 ieskaujošā skaņa + 7.1 ieskaujošā skaņa + %1$.2f Mb/s + %1$s, %2$s diff --git a/library/ui/src/main/res/values-mk-rMK/strings.xml b/library/ui/src/main/res/values-mk-rMK/strings.xml deleted file mode 100644 index 60858df8b1..0000000000 --- a/library/ui/src/main/res/values-mk-rMK/strings.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - "Претходна песна" - "Следна песна" - "Пауза" - "Пушти" - "Запри" - "Премотај назад" - "Брзо премотај напред" - "Повтори ги сите" - "Не повторувај ниту една" - "Повтори една" - "По случаен избор" - diff --git a/library/ui/src/main/res/values-mk/strings.xml b/library/ui/src/main/res/values-mk/strings.xml new file mode 100644 index 0000000000..3e6ae777cc --- /dev/null +++ b/library/ui/src/main/res/values-mk/strings.xml @@ -0,0 +1,35 @@ + + + Претходна песна + Следна песна + Пауза + Пушти + Сопри + Премотај наназад + Премотај напред + Не повторувај ниту една + Повтори една + Повтори ги сите + Измешај + Режим на цел екран + Преземи + Преземања + Се презема + Преземањето заврши + Неуспешно преземање + Се отстрануваат преземањата + Видео + Аудио + Текст + Нема + Автоматскa + Непозната + %1$d × %2$d + Моно + Стерео + Опкружувачки звук + 5.1 опкружувачки звук + 7.1 опкружувачки звук + %1$.2f Mб/с + %1$s, %2$s + diff --git a/library/ui/src/main/res/values-ml-rIN/strings.xml b/library/ui/src/main/res/values-ml-rIN/strings.xml deleted file mode 100644 index 4e5eddb93e..0000000000 --- a/library/ui/src/main/res/values-ml-rIN/strings.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - "മുമ്പത്തെ ട്രാക്ക്" - "അടുത്ത ട്രാക്ക്" - "താൽക്കാലികമായി നിർത്തുക" - "പ്ലേ ചെയ്യുക" - "നിര്‍ത്തുക" - "റിവൈൻഡുചെയ്യുക" - "വേഗത്തിലുള്ള കൈമാറൽ" - "എല്ലാം ആവർത്തിക്കുക" - "ഒന്നും ആവർത്തിക്കരുത്" - "ഒന്ന് ആവർത്തിക്കുക" - "ഷഫിൾ ചെയ്യുക" - diff --git a/library/ui/src/main/res/values-ml/strings.xml b/library/ui/src/main/res/values-ml/strings.xml new file mode 100644 index 0000000000..acd3828fe2 --- /dev/null +++ b/library/ui/src/main/res/values-ml/strings.xml @@ -0,0 +1,35 @@ + + + മുമ്പത്തെ ട്രാക്ക് + അടുത്ത ട്രാക്ക് + തൽക്കാലം നിർത്തുക + പ്ലേ ചെയ്യുക + നിര്‍ത്തുക + പിന്നിലേക്ക് പോവുക + വേഗത്തിൽ മുന്നോട്ട് പോവുക + ഒന്നും ആവർത്തിക്കരുത് + ഒരെണ്ണം ആവർത്തിക്കുക + എല്ലാം ആവർത്തിക്കുക + ഇടകലര്‍ത്തുക + പൂർണ്ണ സ്‌ക്രീൻ മോഡ് + ഡൗൺലോഡ് + ഡൗൺലോഡുകൾ + ഡൗൺലോഡ് ചെയ്യുന്നു + ഡൗൺലോഡ് പൂർത്തിയായി + ഡൗൺലോഡ് പരാജയപ്പെട്ടു + ഡൗൺലോഡുകൾ നീക്കം ചെയ്യുന്നു + വീഡിയോ + ഓഡിയോ + ടെക്‌സ്റ്റ് + ഒന്നുമില്ല + സ്വമേധയാ + അജ്ഞാതം + %1$d × %2$d + മോണോ + സ്റ്റീരിയോ + സറൗണ്ട് സൗണ്ട് + 5.1 സറൗണ്ട് സൗണ്ട് + 7.1 സറൗണ്ട് സൗണ്ട് + %1$.2f Mbps + %1$s, %2$s + diff --git a/library/ui/src/main/res/values-mn-rMN/strings.xml b/library/ui/src/main/res/values-mn-rMN/strings.xml deleted file mode 100644 index 4ab26a7f62..0000000000 --- a/library/ui/src/main/res/values-mn-rMN/strings.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - "Өмнөх трек" - "Дараагийн трек" - "Түр зогсоох" - "Тоглуулах" - "Зогсоох" - "Буцааж хураах" - "Хурдан урагшлуулах" - "Бүгдийг давтах" - "Алийг нь ч давтахгүй" - "Нэгийг давтах" - "Холих" - diff --git a/library/ui/src/main/res/values-mn/strings.xml b/library/ui/src/main/res/values-mn/strings.xml new file mode 100644 index 0000000000..328827f87f --- /dev/null +++ b/library/ui/src/main/res/values-mn/strings.xml @@ -0,0 +1,35 @@ + + + Өмнөх бичлэг + Дараагийн бичлэг + Түр зогсоох + Тоглуулах + Зогсоох + Ухраах + Хурдан урагшлуулах + Алийг нь ч дахин тоглуулахгүй + Одоогийн тоглуулж буй медиаг дахин тоглуулах + Бүгдийг нь дахин тоглуулах + Холих + Бүтэн дэлгэцийн горим + Татах + Татaлт + Татаж байна + Татаж дууссан + Татаж чадсангүй + Татаж авсан файлыг хасаж байна + Видео + Дуу + Текст + Байхгүй + Автомат + Үл мэдэгдэх + %1$d × %2$d + Моно + Стерео + Орчин тойрны дуу + 5.1 орчин тойрны дуу + 7.1 орчин тойрны дуу + %1$.2f Mbps + %1$s, %2$s + diff --git a/library/ui/src/main/res/values-mr-rIN/strings.xml b/library/ui/src/main/res/values-mr-rIN/strings.xml deleted file mode 100644 index 7869355b59..0000000000 --- a/library/ui/src/main/res/values-mr-rIN/strings.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - "मागील ट्रॅक" - "पुढील ट्रॅक" - "विराम द्या" - "प्ले करा" - "थांबा" - "रिवाईँड करा" - "फास्ट फॉरवर्ड करा" - "सर्व पुनरावृत्ती करा" - "काहीही पुनरावृत्ती करू नका" - "एक पुनरावृत्ती करा" - "शफल करा" - diff --git a/library/ui/src/main/res/values-mr/strings.xml b/library/ui/src/main/res/values-mr/strings.xml new file mode 100644 index 0000000000..753e6ba8d8 --- /dev/null +++ b/library/ui/src/main/res/values-mr/strings.xml @@ -0,0 +1,35 @@ + + + मागील ट्रॅक + पुढील ट्रॅक + विराम + प्‍ले करा + थांबा + रीवाइंड करा + फास्ट फॉरवर्ड करा + रीपीट करू नका + एक रीपीट करा + सर्व रीपीट करा + शफल करा + पूर्ण स्क्रीन मोड + डाउनलोड करा + डाउनलोड + डाउनलोड होत आहे + डाउनलोड पूर्ण झाले + डाउनलोड अयशस्वी झाले + डाउनलोड काढून टाकत आहे + व्हिडिओ + ऑडिओ + मजकूर + काहीही नाही + आपोआप + अज्ञात + %1$d × %2$d + मोनो + स्टिरिओ + सराउंड साउंड + ५.१ सराउंड साउंड + ७.१ सराउंड साउंड + %1$.2f Mbps + %1$s, %2$s + diff --git a/library/ui/src/main/res/values-ms-rMY/strings.xml b/library/ui/src/main/res/values-ms-rMY/strings.xml deleted file mode 100644 index fdde3de079..0000000000 --- a/library/ui/src/main/res/values-ms-rMY/strings.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - "Lagu sebelumnya" - "Lagu seterusnya" - "Jeda" - "Main" - "Berhenti" - "Gulung semula" - "Mara laju" - "Ulang semua" - "Tiada ulangan" - "Ulangan" - "Rombak" - diff --git a/library/ui/src/main/res/values-ms/strings.xml b/library/ui/src/main/res/values-ms/strings.xml new file mode 100644 index 0000000000..c4a437da3b --- /dev/null +++ b/library/ui/src/main/res/values-ms/strings.xml @@ -0,0 +1,35 @@ + + + Lagu sebelumnya + Lagu seterusnya + Jeda + Main + Berhenti + Mandir + Mundar laju + Jangan ulang + Ulang satu + Ulang semua + Rombak + Mod skrin penuh + Muat turun + Muat turun + Memuat turun + Muat turun selesai + Muat turun gagal + Mengalih keluar muat turun + Video + Audio + Teks + Tiada + Automatik + Tidak diketahui + %1$d × %2$d + Mono + Stereo + Bunyi keliling + Bunyi keliling 5.1 + Bunyi keliling 7.1 + %1$.2f Mbps + %1$s, %2$s + diff --git a/library/ui/src/main/res/values-my-rMM/strings.xml b/library/ui/src/main/res/values-my-rMM/strings.xml deleted file mode 100644 index 3d7918d953..0000000000 --- a/library/ui/src/main/res/values-my-rMM/strings.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - "ယခင် တစ်ပုဒ်" - "နောက် တစ်ပုဒ်" - "ခဏရပ်ရန်" - "ဖွင့်ရန်" - "ရပ်ရန်" - "ပြန်ရစ်ရန်" - "ရှေ့သို့ သွားရန်" - "အားလုံး ထပ်တလဲလဲဖွင့်ရန်" - "ထပ်တလဲလဲမဖွင့်ရန်" - "တစ်ခုအား ထပ်တလဲလဲဖွင့်ရန်" - "မွှေနှောက်ဖွင့်ရန်" - diff --git a/library/ui/src/main/res/values-my/strings.xml b/library/ui/src/main/res/values-my/strings.xml new file mode 100644 index 0000000000..497ff50416 --- /dev/null +++ b/library/ui/src/main/res/values-my/strings.xml @@ -0,0 +1,35 @@ + + + ယခင် တစ်ပုဒ် + နောက် တစ်ပုဒ် + ခဏရပ်ရန် + ဖွင့်ရန် + ရပ်ရန် + ပြန်ရစ်ရန် + ရှေ့သို့ အမြန်သွားရန် + မည်သည်ကိုမျှ ပြန်မကျော့ရန် + တစ်ခုကို ပြန်ကျော့ရန် + အားလုံး ပြန်ကျော့ရန် + ရောသမမွှေ + မျက်နှာပြင်အပြည့် မုဒ် + ဒေါင်းလုဒ် လုပ်ရန် + ဒေါင်းလုဒ်များ + ဒေါင်းလုဒ်လုပ်နေသည် + ဒေါင်းလုဒ်လုပ်ပြီးပါပြီ + ဒေါင်းလုဒ်လုပ်၍ မရပါ + ဒေါင်းလုဒ်များ ဖယ်ရှားနေသည် + ဗီဒီယို + အသံ + စာသား + မရှိ + အလိုအလျောက် + အမည်မသိ + %1$d × %2$d + မိုနို + စတီရီယို + ပတ်လည် အသံစနစ် + 5.1 ပတ်လည် အသံစနစ် + 7.1 ပတ်လည် အသံစနစ် + %1$.2f Mbps + %1$s၊ %2$s + diff --git a/library/ui/src/main/res/values-nb/strings.xml b/library/ui/src/main/res/values-nb/strings.xml index 370c759b84..7e48146084 100644 --- a/library/ui/src/main/res/values-nb/strings.xml +++ b/library/ui/src/main/res/values-nb/strings.xml @@ -1,29 +1,35 @@ - - - - "Forrige spor" - "Neste spor" - "Sett på pause" - "Spill av" - "Stopp" - "Tilbakespoling" - "Fremoverspoling" - "Gjenta alle" - "Ikke gjenta noen" - "Gjenta én" - "Spill av i tilfeldig rekkefølge" + + + Forrige spor + Neste spor + Sett på pause + Spill av + Stopp + Spol tilbake + Spol forover + Ikke gjenta noen + Gjenta én + Gjenta alle + Tilfeldig rekkefølge + Fullskjermmodus + Last ned + Nedlastinger + Laster ned + Nedlastingen er fullført + Nedlastingen mislyktes + Fjerner nedlastinger + Video + Lyd + Tekst + Ingen + Automatisk + Ukjent + %1$d × %2$d + Mono + Stereo + Surround-lyd + 5.1 surround-lyd + 7.1 surround-lyd + %1$.2f Mbps + %1$s, %2$s diff --git a/library/ui/src/main/res/values-ne-rNP/strings.xml b/library/ui/src/main/res/values-ne-rNP/strings.xml deleted file mode 100644 index 19f43d0392..0000000000 --- a/library/ui/src/main/res/values-ne-rNP/strings.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - "अघिल्लो ट्रयाक" - "अर्को ट्रयाक" - "रोक्नुहोस्" - "चलाउनुहोस्" - "रोक्नुहोस्" - "दोहोर्याउनुहोस्" - "फास्ट फर्वार्ड" - "सबै दोहोर्याउनुहोस्" - "कुनै पनि नदोहोर्याउनुहोस्" - "एउटा दोहोर्याउनुहोस्" - "मिसाउनुहोस्" - diff --git a/library/ui/src/main/res/values-ne/strings.xml b/library/ui/src/main/res/values-ne/strings.xml new file mode 100644 index 0000000000..5011998b87 --- /dev/null +++ b/library/ui/src/main/res/values-ne/strings.xml @@ -0,0 +1,35 @@ + + + अघिल्लो ट्रयाक + अर्को ट्र्याक + पज गर्नुहोस् + प्ले गर्नुहोस् + रोक्नुहोस् + रिवाइन्ड गर्नुहोस् + फास्ट फर्वार्ड गर्नुहोस् + कुनै पनि नदोहोर्‍याउनुहोस् + एउटा दोहोर्‍याउनुहोस् + सबै दोहोर्‍याउनुहोस् + मिसाउनुहोस् + पूर्ण स्क्रिन मोड + डाउनलोड गर्नुहोस् + डाउनलोडहरू + डाउनलोड गरिँदै छ + डाउनलोड सम्पन्न भयो + डाउनलोड गर्न सकिएन + डाउनलोडहरू हटाउँदै + भिडियो + अडियो + पाठ + कुनै पनि होइन + स्वतः + अज्ञात + %1$d × %2$d + मोनो + स्टेरियो + सराउन्ड साउन्ड + 5.1 सराउन्ड साउन्ड + 7.1 सराउन्ड साउन्ड + %1$.2f Mbps + %1$s, %2$s + diff --git a/library/ui/src/main/res/values-nl/strings.xml b/library/ui/src/main/res/values-nl/strings.xml index a67ab2968c..1a2880ae1f 100644 --- a/library/ui/src/main/res/values-nl/strings.xml +++ b/library/ui/src/main/res/values-nl/strings.xml @@ -1,29 +1,35 @@ - - - - "Vorig nummer" - "Volgend nummer" - "Onderbreken" - "Afspelen" - "Stoppen" - "Terugspoelen" - "Vooruitspoelen" - "Alles herhalen" - "Niet herhalen" - "Eén herhalen" - "Shuffle" + + + Vorige track + Volgende track + Pauzeren + Afspelen + Stoppen + Terugspoelen + Vooruitspoelen + Niets herhalen + Eén herhalen + Alles herhalen + Shuffle + Modus \'Volledig scherm\' + Downloaden + Downloads + Downloaden + Downloaden voltooid + Downloaden mislukt + Downloads verwijderen + Video + Audio + Tekst + Geen + Auto + Onbekend + %1$d × %2$d + Mono + Stereo + Surround sound + 5.1 surroundgeluid + 7.1 surroundgeluid + %1$.2f Mbps + %1$s, %2$s diff --git a/library/ui/src/main/res/values-pa-rIN/strings.xml b/library/ui/src/main/res/values-pa-rIN/strings.xml deleted file mode 100644 index 6250b90514..0000000000 --- a/library/ui/src/main/res/values-pa-rIN/strings.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - "ਪਿਛਲਾ ਟਰੈਕ" - "ਅਗਲਾ ਟਰੈਕ" - "ਰੋਕੋ" - "ਪਲੇ ਕਰੋ" - "ਰੋਕੋ" - "ਰੀਵਾਈਂਡ ਕਰੋ" - "ਅੱਗੇ ਭੇਜੋ" - "ਸਭ ਨੂੰ ਦੁਹਰਾਓ" - "ਕੋਈ ਵੀ ਨਹੀਂ ਦੁਹਰਾਓ" - "ਇੱਕ ਦੁਹਰਾਓ" - "ਸ਼ੱਫਲ" - diff --git a/library/ui/src/main/res/values-pa/strings.xml b/library/ui/src/main/res/values-pa/strings.xml new file mode 100644 index 0000000000..effc9250ed --- /dev/null +++ b/library/ui/src/main/res/values-pa/strings.xml @@ -0,0 +1,35 @@ + + + ਪਿਛਲਾ ਟਰੈਕ + ਅਗਲਾ ਟਰੈਕ + ਰੋਕੋ + ਚਲਾਓ + ਬੰਦ ਕਰੋ + ਪਿੱਛੇ ਕਰੋ + ਤੇਜ਼ੀ ਨਾਲ ਅੱਗੇ ਕਰੋ + ਕਿਸੇ ਨੂੰ ਨਾ ਦੁਹਰਾਓ + ਇੱਕ ਵਾਰ ਦੁਹਰਾਓ + ਸਾਰਿਆਂ ਨੂੰ ਦੁਹਰਾਓ + ਬੇਤਰਤੀਬ ਕਰੋ + ਪੂਰੀ-ਸਕ੍ਰੀਨ ਮੋਡ + ਡਾਊਨਲੋਡ ਕਰੋ + ਡਾਊਨਲੋਡ + ਡਾਊਨਲੋਡ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ + ਡਾਊਨਲੋਡ ਮੁਕੰਮਲ ਹੋਇਆ + ਡਾਊਨਲੋਡ ਅਸਫਲ ਰਿਹਾ + ਡਾਊਨਲੋਡ ਕੀਤੀ ਸਮੱਗਰੀ ਹਟਾਈ ਜਾ ਰਹੀ ਹੈ + ਵੀਡੀਓ + ਆਡੀਓ + ਲਿਖਤ + ਕੋਈ ਨਹੀਂ + ਸਵੈਚਲਿਤ + ਅਗਿਆਤ + %1$d × %2$d + ਮੋਨੋ + ਸਟੀਰਿਓ + ਸਰਾਊਂਡ ਸਾਊਂਡ + 5.1 ਸਰਾਊਂਡ ਸਾਊਂਡ + 7.1 ਸਰਾਊਂਡ ਸਾਊਂਡ + %1$.2f Mbps + %1$s, %2$s + diff --git a/library/ui/src/main/res/values-pl/strings.xml b/library/ui/src/main/res/values-pl/strings.xml index 981aa17543..312481fa2b 100644 --- a/library/ui/src/main/res/values-pl/strings.xml +++ b/library/ui/src/main/res/values-pl/strings.xml @@ -1,30 +1,35 @@ - - - "Poprzedni utwór" - "Następny utwór" - "Wstrzymaj" - "Odtwórz" - "Zatrzymaj" - "Przewiń do tyłu" - "Przewiń do przodu" - "Powtórz wszystkie" - "Nie powtarzaj" - "Powtórz jeden" - "Odtwarzaj losowo" + + Poprzedni utwór + Następny utwór + Wstrzymaj + Odtwórz + Zatrzymaj + Przewiń do tyłu + Przewiń do przodu + Nie powtarzaj + Powtórz jeden + Powtórz wszystkie + Odtwarzanie losowe Tryb pełnoekranowy + Pobierz + Pobieranie + Pobieram + Zakończono pobieranie + Nie udało się pobrać + Usuwam pobrane + Film + Dźwięk + Tekst + Brak + Automatycznie + Nieznany + %1$d × %2$d + Mono + Stereo + Dźwięk przestrzenny + System dźwięku przestrzennego 5.1 + System dźwięku przestrzennego 7.1 + %1$.2f Mb/s + %1$s, %2$s diff --git a/library/ui/src/main/res/values-pt-rBR/strings.xml b/library/ui/src/main/res/values-pt-rBR/strings.xml deleted file mode 100644 index 86a91b0677..0000000000 --- a/library/ui/src/main/res/values-pt-rBR/strings.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - "Faixa anterior" - "Próxima faixa" - "Pausar" - "Reproduzir" - "Parar" - "Retroceder" - "Avançar" - "Repetir tudo" - "Não repetir" - "Repetir um" - "Reproduzir aleatoriamente" - diff --git a/library/ui/src/main/res/values-pt-rPT/strings.xml b/library/ui/src/main/res/values-pt-rPT/strings.xml index f0c3770c51..52eb01e219 100644 --- a/library/ui/src/main/res/values-pt-rPT/strings.xml +++ b/library/ui/src/main/res/values-pt-rPT/strings.xml @@ -1,30 +1,35 @@ - - - "Faixa anterior" - "Faixa seguinte" - "Interromper" - "Reproduzir" - "Parar" - "Rebobinar" - "Avançar" - "Repetir tudo" - "Não repetir" - "Repetir um" - "Reproduzir aleatoriamente" + + Faixa anterior + Faixa seguinte + Colocar em pausa + Reproduzir + Parar + Recuar + Avançar + Não repetir nenhum + Repetir um + Repetir tudo + Reproduzir aleatoriamente Modo de ecrã inteiro + Transferir + Transferências + A transferir… + Transferência concluída + Falha na transferência + A remover as transferências… + Vídeo + Áudio + Texto + Nenhuma + Automático + Desconhecida + %1$d × %2$d + Mono + Estéreo + Som surround + Som surround 5.1 + Som surround 7.1 + %1$.2f Mbps + %1$s, %2$s diff --git a/library/ui/src/main/res/values-pt/strings.xml b/library/ui/src/main/res/values-pt/strings.xml index 8441e4e1cc..6ea00fed8d 100644 --- a/library/ui/src/main/res/values-pt/strings.xml +++ b/library/ui/src/main/res/values-pt/strings.xml @@ -1,29 +1,35 @@ - - - - "Faixa anterior" - "Próxima faixa" - "Pausar" - "Reproduzir" - "Parar" - "Retroceder" - "Avançar" - "Repetir tudo" - "Não repetir" - "Repetir uma" - "Reproduzir aleatoriamente" + + + Faixa anterior + Próxima faixa + Pausar + Reproduzir + Parar + Retroceder + Avançar + Não repetir + Repetir uma + Repetir tudo + Aleatório + Modo de tela cheia + Fazer o download + Downloads + Fazendo o download + Download concluído + Falha no download + Removendo downloads + Vídeo + Áudio + Texto + Nenhuma + Automática + Desconhecido + %1$d × %2$d + Mono + Estéreo + Sistema surround + Sistema surround 5.1 + Sistema surround 7.1 + %1$.2f Mbps + %1$s, %2$s diff --git a/library/ui/src/main/res/values-ro/strings.xml b/library/ui/src/main/res/values-ro/strings.xml index 6b8644e30a..4ea18d4a58 100644 --- a/library/ui/src/main/res/values-ro/strings.xml +++ b/library/ui/src/main/res/values-ro/strings.xml @@ -1,29 +1,35 @@ - - - - "Melodia anterioară" - "Melodia următoare" - "Pauză" - "Redați" - "Opriți" - "Derulați" - "Derulați rapid înainte" - "Repetați toate" - "Repetați niciuna" - "Repetați unul" - "Redați aleatoriu" + + + Melodia anterioară + Următoarea înregistrare + Întrerupeți + Redați + Opriți + Derulați înapoi + Derulați rapid înainte + Nu repetați niciunul + Repetați unul + Repetați-le pe toate + Redați aleatoriu + Modul Ecran complet + Descărcați + Descărcări + Se descarcă + Descărcarea a fost finalizată + Descărcarea nu a reușit + Se elimină descărcările + Video + Audio + Text + Fără + Automat + Necunoscut + %1$d × %2$d + Mono + Stereo + Sunet surround + Sunet surround 5.1 + Sunet surround 7.1 + %1$.2f Mbps + %1$s, %2$s diff --git a/library/ui/src/main/res/values-ru/strings.xml b/library/ui/src/main/res/values-ru/strings.xml index 51d11d6371..14c8badf99 100644 --- a/library/ui/src/main/res/values-ru/strings.xml +++ b/library/ui/src/main/res/values-ru/strings.xml @@ -1,29 +1,35 @@ - - - - "Предыдущий трек" - "Следующий трек" - "Приостановить" - "Воспроизвести" - "Остановить" - "Перемотать назад" - "Перемотать вперед" - "Повторять все" - "Не повторять" - "Повторять один элемент" - "Перемешать" + + + Предыдущий трек + Следующий трек + Приостановить + Воспроизвести + Остановить + Перемотать назад + Перемотать вперед + Не повторять + Повторять трек + Повторять все + Перемешать + Полноэкранный режим + Скачать + Скачивания + Скачивание… + Скачивание завершено + Ошибка скачивания + Удаление скачанных файлов… + Видео + Аудио + Текст + Нет + Авто + Неизвестный трек + %1$d × %2$d + Моно + Стерео + Объемный звук + Система объемного звука 5.1 + Система объемного звука 7.1 + %1$.2f Мбит/сек + %1$s, %2$s diff --git a/library/ui/src/main/res/values-si-rLK/strings.xml b/library/ui/src/main/res/values-si-rLK/strings.xml deleted file mode 100644 index eb8453b156..0000000000 --- a/library/ui/src/main/res/values-si-rLK/strings.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - "පෙර ගීතය" - "ඊළඟ ගීතය" - "විරාමය" - "ධාවනය කරන්න" - "නතර කරන්න" - "නැවත ඔතන්න" - "වේගයෙන් ඉදිරියට යන" - "සියලු නැවත" - "කිසිවක් නැවත" - "නැවත නැවත එක්" - "කලවම් කරන්න" - diff --git a/library/ui/src/main/res/values-si/strings.xml b/library/ui/src/main/res/values-si/strings.xml new file mode 100644 index 0000000000..92ae038c4b --- /dev/null +++ b/library/ui/src/main/res/values-si/strings.xml @@ -0,0 +1,35 @@ + + + පෙර ඛණ්ඩය + ඊළඟ ඛණ්ඩය + විරාම කරන්න + ධාවනය කරන්න + නවත්වන්න + නැවත ඔතන්න + වේගයෙන් ඉදිරියට + කිසිවක් පුනරාවර්තනය නොකරන්න + එකක් පුනරාවර්තනය කරන්න + සියල්ල පුනරාවර්තනය කරන්න + කලවම් කරන්න + සම්පූර්ණ තිර ප්‍රකාරය + බාගන්න + බාගැනීම් + බාගනිමින් + බාගැනීම සම්පූර්ණ කරන ලදී + බාගැනීම අසමත් විය + බාගැනීම් ඉවත් කිරීම + වීඩියෝ + ශ්‍රව්‍ය + පෙළ + කිසිවක් නැත + ස්වයං + නොදනී + %1$d × %2$d + ඒකල + ස්ටීරියෝ + අවට ශබ්දය + 5.1 අවට ශබ්දය + 7.1 අවට ශබ්දය + %1$.2f Mbps + %1$s, %2$s + diff --git a/library/ui/src/main/res/values-sk/strings.xml b/library/ui/src/main/res/values-sk/strings.xml index a289e89d34..f4f997b207 100644 --- a/library/ui/src/main/res/values-sk/strings.xml +++ b/library/ui/src/main/res/values-sk/strings.xml @@ -1,30 +1,35 @@ - - - "Predchádzajúca stopa" - "Ďalšia stopa" - "Pozastaviť" - "Prehrať" - "Zastaviť" - "Pretočiť späť" - "Pretočiť dopredu" - "Opakovať všetko" - "Neopakovať" - "Opakovať jednu položku" - "Náhodne prehrávať" + + Predchádzajúca skladba + Ďalšia skladba + Pozastaviť + Prehrať + Zastaviť + Pretočiť späť + Pretočiť dopredu + Neopakovať + Opakovať jednu + Opakovať všetko + Náhodne prehrávať Režim celej obrazovky + Stiahnuť + Stiahnuté + Sťahuje sa + Sťahovanie bolo dokončené + Nepodarilo sa stiahnuť + Odstraňuje sa stiahnutý obsah + Video + Zvuk + Text + Žiadne + Automaticky + Neznáme + %1$d × %2$d + Mono + Stereo + Priestorový zvuk + Priestorový zvuk 5.1 + Priestorový zvuk 7.1 + %1$.2f MB/s + %1$s, %2$s diff --git a/library/ui/src/main/res/values-sl/strings.xml b/library/ui/src/main/res/values-sl/strings.xml index 8ed731b0d3..83d332103e 100644 --- a/library/ui/src/main/res/values-sl/strings.xml +++ b/library/ui/src/main/res/values-sl/strings.xml @@ -1,29 +1,35 @@ - - - - "Prejšnja skladba" - "Naslednja skladba" - "Zaustavi" - "Predvajaj" - "Ustavi" - "Previj nazaj" - "Previj naprej" - "Ponovi vse" - "Ne ponovi" - "Ponovi eno" - "Naključno predvajaj" + + + Prejšnja skladba + Naslednja skladba + Zaustavitev + Predvajanje + Ustavitev + Previjanje nazaj + Previjanje naprej + Brez ponavljanja + Ponavljanje ene + Ponavljanje vseh + Naključno predvajanje + Celozaslonski način + Prenos + Prenosi + Prenašanje + Prenos je končan + Prenos ni uspel + Odstranjevanje prenosov + Videoposnetki + Zvočni posnetki + Podnapisi + Nič + Samodejno + Neznano + %1$d × %2$d + Mono + Stereo + Prostorski zvok + Prostorski zvok 5.1 + Prostorski zvok 7.1 + %1$.2f Mb/s + %1$s, %2$s diff --git a/library/ui/src/main/res/values-sq-rAL/strings.xml b/library/ui/src/main/res/values-sq-rAL/strings.xml deleted file mode 100644 index e2d209e10b..0000000000 --- a/library/ui/src/main/res/values-sq-rAL/strings.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - "Kënga e mëparshme" - "Kënga tjetër" - "Pauzë" - "Luaj" - "Ndalo" - "Kthehu pas" - "Përparo me shpejtësi" - "Përsërit të gjithë" - "Përsëritni asnjë" - "Përsëritni një" - "Përziej" - diff --git a/library/ui/src/main/res/values-sq/strings.xml b/library/ui/src/main/res/values-sq/strings.xml new file mode 100644 index 0000000000..c46524762d --- /dev/null +++ b/library/ui/src/main/res/values-sq/strings.xml @@ -0,0 +1,35 @@ + + + Kënga e mëparshme + Kënga tjetër + Pauzë + Luaj + Ndalo + Rikthe + Përparo me shpejtësi + Mos përsërit asnjë + Përsërit një + Përsërit të gjitha + Përziej + Modaliteti me ekran të plotë + Shkarko + Shkarkimet + Po shkarkohet + Shkarkimi përfundoi + Shkarkimi dështoi + Shkarkimet po hiqen + Video + Audio + Tekst + Asnjë + Automatike + E panjohur + %1$d × %2$d + Mono + Stereo + Tingulli rrethues + Tingull rrethues 5.1 + Tingull rrethues 7.1 + %1$.2f Mbps + %1$s, %2$s + diff --git a/library/ui/src/main/res/values-sr/strings.xml b/library/ui/src/main/res/values-sr/strings.xml index 9cff134a61..394ab8d36e 100644 --- a/library/ui/src/main/res/values-sr/strings.xml +++ b/library/ui/src/main/res/values-sr/strings.xml @@ -1,30 +1,35 @@ - - - "Претходна песма" - "Следећа песма" - "Пауза" - "Пусти" - "Заустави" - "Премотај уназад" - "Премотај унапред" - "Понови све" - "Понављање је искључено" - "Понови једну" - "Пусти насумично" + + Претходна песма + Следећа песма + Паузирај + Пусти + Заустави + Премотај уназад + Премотај унапред + Не понављај ниједну + Понови једну + Понови све + Пусти насумично Режим целог екрана + Преузми + Преузимања + Преузима се + Преузимање је завршено + Преузимање није успело + Преузимања се уклањају + Видео + Аудио + Текст + Ниједна + Аутоматски + Непознато + %1$d × %2$d + Моно + Стерео + Звучни систем + Звучни систем 5.1 + Звучни систем 7.1 + %1$.2f Mb/s + %1$s, %2$s diff --git a/library/ui/src/main/res/values-sv/strings.xml b/library/ui/src/main/res/values-sv/strings.xml index b8fc7a1fff..38daddb507 100644 --- a/library/ui/src/main/res/values-sv/strings.xml +++ b/library/ui/src/main/res/values-sv/strings.xml @@ -1,30 +1,35 @@ - - - "Föregående spår" - "Nästa spår" - "Pausa" - "Spela upp" - "Avbryt" - "Spola tillbaka" - "Snabbspola framåt" - "Upprepa alla" - "Upprepa inga" - "Upprepa en" - "Blanda" + + Föregående spår + Nästa spår + Pausa + Spela upp + Stoppa + Spola tillbaka + Snabbspola framåt + Upprepa inga + Upprepa en + Upprepa alla + Blanda spår Helskärmsläge + Ladda ned + Nedladdningar + Laddar ned + Nedladdningen är klar + Nedladdningen misslyckades + Nedladdningar tas bort + Video + Ljud + Text + Ingen + Automatiskt + Okänt + %1$d × %2$d + Mono + Stereo + Surroundljud + 5.1-kanaligt surroundljud + 7.1-kanaligt surroundljud + %1$.2f Mbit/s + %1$s, %2$s diff --git a/library/ui/src/main/res/values-sw/strings.xml b/library/ui/src/main/res/values-sw/strings.xml index 4451ad3c2b..2c7268626f 100644 --- a/library/ui/src/main/res/values-sw/strings.xml +++ b/library/ui/src/main/res/values-sw/strings.xml @@ -1,30 +1,35 @@ - - - "Wimbo uliotangulia" - "Wimbo unaofuata" - "Sitisha" - "Cheza" - "Simamisha" - "Rudisha nyuma" - "Peleka mbele kwa kasi" - "Rudia zote" - "Usirudie Yoyote" - "Rudia Moja" - "Changanya" - Hali ya skrini kamili + + Wimbo uliotangulia + Wimbo unaofuata + Sitisha + Cheza + Simamisha + Rudisha nyuma + Sogeza mbele haraka + Usirudie yoyote + Rudia moja + Rudia zote + Changanya + Hali ya skrini nzima + Pakua + Vipakuliwa + Inapakua + Imepakuliwa + Imeshindwa kupakua + Inaondoa vipakuliwa + Video + Sauti + SMS + Hamna + Otomatiki + Haijulikani + %1$d × %2$d + Mono + Stereo + Sauti ya mzunguko + Sauti ya mzunguko ya 5.1 + Sauti ya mzunguko ya 7.1 + Mbps %1$.2f + %1$s, %2$s diff --git a/library/ui/src/main/res/values-ta-rIN/strings.xml b/library/ui/src/main/res/values-ta-rIN/strings.xml deleted file mode 100644 index 43a925aa2e..0000000000 --- a/library/ui/src/main/res/values-ta-rIN/strings.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - "முந்தைய ட்ராக்" - "அடுத்த ட்ராக்" - "இடைநிறுத்து" - "இயக்கு" - "நிறுத்து" - "மீண்டும் காட்டு" - "வேகமாக முன்செல்" - "அனைத்தையும் மீண்டும் இயக்கு" - "எதையும் மீண்டும் இயக்காதே" - "ஒன்றை மட்டும் மீண்டும் இயக்கு" - "குலை" - diff --git a/library/ui/src/main/res/values-ta/strings.xml b/library/ui/src/main/res/values-ta/strings.xml new file mode 100644 index 0000000000..14a0203e06 --- /dev/null +++ b/library/ui/src/main/res/values-ta/strings.xml @@ -0,0 +1,35 @@ + + + முந்தைய டிராக் + அடுத்த டிராக் + இடைநிறுத்து + இயக்கு + நிறுத்து + பின்செல் + வேகமாக முன்செல் + எதையும் மீண்டும் இயக்காதே + இதை மட்டும் மீண்டும் இயக்கு + அனைத்தையும் மீண்டும் இயக்கு + கலைத்துப் போடு + முழுத்திரைப் பயன்முறை + பதிவிறக்கும் பட்டன் + பதிவிறக்கங்கள் + பதிவிறக்குகிறது + பதிவிறக்கப்பட்டது + பதிவிறக்க முடியவில்லை + பதிவிறக்கங்கள் அகற்றப்படுகின்றன + வீடியோ + ஆடியோ + உரை + ஏதுமில்லை + தானியங்கு + தெரியாதவை + %1$d × %2$d + மோனோ + ஸ்டீரியோ + சரவுண்ட் சவுண்ட் + 5.1 சரவுண்ட் சவுண்ட் + 7.1 சரவுண்ட் சவுண்ட் + %1$.2f மெ.பை./வி + %1$s, %2$s + diff --git a/library/ui/src/main/res/values-te-rIN/strings.xml b/library/ui/src/main/res/values-te-rIN/strings.xml deleted file mode 100644 index 8541a44553..0000000000 --- a/library/ui/src/main/res/values-te-rIN/strings.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - "మునుపటి ట్రాక్" - "తదుపరి ట్రాక్" - "పాజ్ చేయి" - "ప్లే చేయి" - "ఆపివేయి" - "రివైండ్ చేయి" - "వేగంగా ఫార్వార్డ్ చేయి" - "అన్నీ పునరావృతం చేయి" - "ఏదీ పునరావృతం చేయవద్దు" - "ఒకదాన్ని పునరావృతం చేయి" - "షఫుల్ చేయి" - diff --git a/library/ui/src/main/res/values-te/strings.xml b/library/ui/src/main/res/values-te/strings.xml new file mode 100644 index 0000000000..7e3f32a039 --- /dev/null +++ b/library/ui/src/main/res/values-te/strings.xml @@ -0,0 +1,35 @@ + + + మునుపటి ట్రాక్ + తదుపరి ట్రాక్ + పాజ్ చేయండి + ప్లే చేయండి + ఆపండి + రివైండ్ చేయండి + వేగంగా ఫార్వార్డ్ చేయండి + దేన్నీ పునరావృతం చేయకండి + ఒకదాన్ని పునరావృతం చేయండి + అన్నింటినీ పునరావృతం చేయండి + షఫుల్ చేయండి + పూర్తి స్క్రీన్ మోడ్ + డౌన్‌లోడ్ చేయి + డౌన్‌లోడ్‌లు + డౌన్‌లోడ్ చేస్తోంది + డౌన్‌లోడ్ పూర్తయింది + డౌన్‌లోడ్ విఫలమైంది + డౌన్‌లోడ్‌లను తీసివేస్తోంది + వీడియో + ఆడియో + వచనం + ఏదీ కాదు + స్వీయ + తెలియదు + %1$d × %2$d + మోనో + స్టీరియో + సరౌండ్ ధ్వని + 5.1 సరౌండ్ ధ్వని + 7.1 సరౌండ్ ధ్వని + %1$.2f Mbps + %1$s, %2$s + diff --git a/library/ui/src/main/res/values-th/strings.xml b/library/ui/src/main/res/values-th/strings.xml index 664900e7da..85c4f8cf92 100644 --- a/library/ui/src/main/res/values-th/strings.xml +++ b/library/ui/src/main/res/values-th/strings.xml @@ -1,30 +1,35 @@ - - - "แทร็กก่อนหน้า" - "แทร็กถัดไป" - "หยุดชั่วคราว" - "เล่น" - "หยุด" - "กรอกลับ" - "กรอไปข้างหน้า" - "เล่นซ้ำทั้งหมด" - "ไม่เล่นซ้ำ" - "เล่นซ้ำรายการเดียว" - "สุ่มเพลง" + + แทร็กก่อนหน้า + แทร็กถัดไป + หยุด + เล่น + หยุด + กรอกลับ + กรอไปข้างหน้า + ไม่เล่นซ้ำ + เล่นซ้ำเพลงเดียว + เล่นซ้ำทั้งหมด + สุ่ม โหมดเต็มหน้าจอ + ดาวน์โหลด + ดาวน์โหลด + กำลังดาวน์โหลด + การดาวน์โหลดเสร็จสมบูรณ์ + การดาวน์โหลดล้มเหลว + กำลังนำรายการที่ดาวน์โหลดออก + วิดีโอ + เสียง + ข้อความ + ไม่มี + ยานยนต์ + ไม่ทราบ + %1$d × %2$d + โมโน + สเตอริโอ + เสียงเซอร์ราวด์ + ระบบเสียง 5.1 เซอร์ราวด์ + 7.1 เสียงเซอร์ราวด์ + %1$.2f Mbps + %1$s, %2$s diff --git a/library/ui/src/main/res/values-tl/strings.xml b/library/ui/src/main/res/values-tl/strings.xml index 471191a81a..dfad2c1b08 100644 --- a/library/ui/src/main/res/values-tl/strings.xml +++ b/library/ui/src/main/res/values-tl/strings.xml @@ -1,30 +1,35 @@ - - - "Nakaraang track" - "Susunod na track" - "I-pause" - "I-play" - "Ihinto" - "I-rewind" - "I-fast forward" - "Ulitin Lahat" - "Walang Uulitin" - "Ulitin ang Isa" - "I-shuffle" + + Nakaraang track + Susunod na track + I-pause + I-play + Ihinto + I-rewind + I-fast forward + Walang uulitin + Mag-ulit ng isa + Ulitin lahat + I-shuffle Fullscreen mode + I-download + Mga Download + Nagda-download + Tapos na ang pag-download + Hindi na-download + Inaalis ang mga na-download + Video + Audio + Text + Wala + Awtomatiko + Hindi Alam + %1$d × %2$d + Mono + Stereo + Surround sound + 5.1 na surround sound + 7.1 na surround sound + %1$.2f Mbps + %1$s, %2$s diff --git a/library/ui/src/main/res/values-tr/strings.xml b/library/ui/src/main/res/values-tr/strings.xml index cd1bfc5444..cacb60c5a5 100644 --- a/library/ui/src/main/res/values-tr/strings.xml +++ b/library/ui/src/main/res/values-tr/strings.xml @@ -1,29 +1,35 @@ - - - - "Önceki parça" - "Sonraki parça" - "Duraklat" - "Çal" - "Durdur" - "Geri sar" - "İleri sar" - "Tümünü Tekrarla" - "Hiçbirini Tekrarlama" - "Birini Tekrarla" - "Karıştır" + + + Önceki parça + Sonraki parça + Duraklat + Çal + Durdur + Geri sar + İleri sar + Hiçbirini tekrarlama + Birini tekrarla + Tümünü tekrarla + Karıştır + Tam ekran modu + İndir + İndirilenler + İndiriliyor + İndirme işlemi tamamlandı + İndirilemedi + İndirilenler kaldırılıyor + Video + Ses + Metin + Yok + Otomatik + Bilinmiyor + %1$d × %2$d + Mono + Stereo + Surround ses + 5.1 surround ses + 7.1 surround ses + %1$.2f Mbps + %1$s, %2$s diff --git a/library/ui/src/main/res/values-uk/strings.xml b/library/ui/src/main/res/values-uk/strings.xml index 36bfca2a34..ecf6c8745e 100644 --- a/library/ui/src/main/res/values-uk/strings.xml +++ b/library/ui/src/main/res/values-uk/strings.xml @@ -1,30 +1,35 @@ - - - "Попередня композиція" - "Наступна композиція" - "Пауза" - "Відтворити" - "Зупинити" - "Перемотати назад" - "Перемотати вперед" - "Повторити все" - "Не повторювати" - "Повторити один елемент" - "Перемішати" + + Попередня композиція + Наступна композиція + Призупинити + Відтворити + Припинити + Перемотати назад + Перемотати вперед + Не повторювати + Повторити 1 + Повторити всі + Перемішати Повноекранний режим + Завантажити + Завантаження + Завантажується + Завантаження завершено + Не вдалося завантажити + Завантаження видаляються + Відео + Аудіо + Текст + Нічого + Автоматично + Невідомо + %1$d × %2$d + Моно + Стерео + Об’ємний звук + Об’ємний звук у форматі 5.1 + Об’ємний звук у форматі 7.1 + %1$.2f Мбіт/с + %1$s, %2$s diff --git a/library/ui/src/main/res/values-ur-rPK/strings.xml b/library/ui/src/main/res/values-ur-rPK/strings.xml deleted file mode 100644 index f253e56c00..0000000000 --- a/library/ui/src/main/res/values-ur-rPK/strings.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - "پچھلا ٹریک" - "اگلا ٹریک" - "موقوف کریں" - "چلائیں" - "روکیں" - "ریوائینڈ کریں" - "تیزی سے فارورڈ کریں" - "سبھی کو دہرائیں" - "کسی کو نہ دہرائیں" - "ایک کو دہرائیں" - "شفل کریں" - diff --git a/library/ui/src/main/res/values-ur/strings.xml b/library/ui/src/main/res/values-ur/strings.xml new file mode 100644 index 0000000000..fbc18fa347 --- /dev/null +++ b/library/ui/src/main/res/values-ur/strings.xml @@ -0,0 +1,35 @@ + + + پچھلا ٹریک + اگلا ٹریک + موقوف کریں + چلائیں + روکیں + ریوائینڈ کریں + تیزی سے فارورڈ کریں + کسی کو نہ دہرائیں + ایک کو دہرائیں + سبھی کو دہرائیں + شفل کریں + پوری اسکرین والی وضع + ڈاؤن لوڈ کریں + ڈاؤن لوڈز + ڈاؤن لوڈ ہو رہا ہے + ڈاؤن لوڈ مکمل ہو گیا + ڈاؤن لوڈ ناکام ہو گیا + ڈاؤن لوڈز کو ہٹایا جا رہا ہے + ویڈیو + آڈیو + متن + کوئی نہیں + خودکار + نامعلوم + %1$d × %2$d + مونو + اسٹیریو + محیط آواز + 5.1 محیط آواز + 7.1 محیط آواز + %1$.2f Mbps + %1$s، %2$s + diff --git a/library/ui/src/main/res/values-uz-rUZ/strings.xml b/library/ui/src/main/res/values-uz-rUZ/strings.xml deleted file mode 100644 index a322690b2d..0000000000 --- a/library/ui/src/main/res/values-uz-rUZ/strings.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - "Avvalgi musiqa" - "Keyingi musiqa" - "To‘xtatib turish" - "Ijro qilish" - "To‘xtatish" - "Orqaga o‘tkazish" - "Oldinga o‘tkazish" - "Barchasini takrorlash" - "Takrorlamaslik" - "Bir marta takrorlash" - "Tasodifiy tartibda" - diff --git a/library/ui/src/main/res/values-uz/strings.xml b/library/ui/src/main/res/values-uz/strings.xml new file mode 100644 index 0000000000..5c9a05d259 --- /dev/null +++ b/library/ui/src/main/res/values-uz/strings.xml @@ -0,0 +1,35 @@ + + + Avvalgi trek + Keyingi trek + Pauza + Ijro + To‘xtatish + Orqaga qaytarish + Oldinga o‘tkazish + Takrorlanmasin + Bittasini takrorlash + Hammasini takrorlash + Aralash + Butun ekran rejimi + Yuklab olish + Yuklanmalar + Yuklab olinmoqda + Yuklab olindi + Yuklab olinmadi + Yuklanmalar olib tashlanmoqda + Video + Audio + Matn + Hech qanday + Avtomatik + Notanish + %1$d × %2$d + Mono + Stereo + Qamrovli ovoz + 5.1 qamrovli ovoz + 7.1 qamrovli ovoz + %1$.2f Mbit/s + %1$s, %2$s + diff --git a/library/ui/src/main/res/values-vi/strings.xml b/library/ui/src/main/res/values-vi/strings.xml index 748de96949..65c9cb52a3 100644 --- a/library/ui/src/main/res/values-vi/strings.xml +++ b/library/ui/src/main/res/values-vi/strings.xml @@ -1,30 +1,35 @@ - - - "Bản nhạc trước" - "Bản nhạc tiếp theo" - "Tạm dừng" - "Phát" - "Ngừng" - "Tua lại" - "Tua đi" - "Lặp lại tất cả" - "Không lặp lại" - "Lặp lại một mục" - "Trộn bài" + + Bản nhạc trước + Bản nhạc tiếp theo + Tạm dừng + Phát + Dừng + Tua lại + Tua đi + Không lặp lại + Lặp lại một + Lặp lại tất cả + Phát ngẫu nhiên Chế độ toàn màn hình + Tải xuống + Tài nguyên đã tải xuống + Đang tải xuống + Đã hoàn tất tải xuống + Không tải xuống được + Đang xóa các mục đã tải xuống + Video + Âm thanh + Văn bản + Không + Tự động + Không xác định + %1$d × %2$d + Đơn âm + Âm thanh nổi + Âm thanh vòm + Âm thanh vòm 5.1 + Âm thanh vòm 7.1 + %1$.2f Mb/giây + %1$s, %2$s diff --git a/library/ui/src/main/res/values-zh-rCN/strings.xml b/library/ui/src/main/res/values-zh-rCN/strings.xml index d357152a64..e75697621c 100644 --- a/library/ui/src/main/res/values-zh-rCN/strings.xml +++ b/library/ui/src/main/res/values-zh-rCN/strings.xml @@ -1,30 +1,35 @@ - - - "上一曲" - "下一曲" - "暂停" - "播放" - "停止" - "快退" - "快进" - "重复播放全部" - "不重复播放" - "重复播放单个视频" - "随机播放" + + 上一曲 + 下一曲 + 暂停 + 播放 + 停止 + 快退 + 快进 + 不重复播放 + 重复播放一项 + 全部重复播放 + 随机播放 全屏模式 + 下载 + 下载内容 + 正在下载 + 下载完毕 + 下载失败 + 正在移除下载内容 + 视频 + 音频 + 文字 + + 自动 + 未知 + %1$d × %2$d + 单声道 + 立体声 + 环绕声 + 5.1 环绕声 + 7.1 环绕声 + %1$.2f Mbps + %1$s,%2$s diff --git a/library/ui/src/main/res/values-zh-rHK/strings.xml b/library/ui/src/main/res/values-zh-rHK/strings.xml index 3a26b8b5f0..e65831ad9f 100644 --- a/library/ui/src/main/res/values-zh-rHK/strings.xml +++ b/library/ui/src/main/res/values-zh-rHK/strings.xml @@ -1,30 +1,35 @@ - - - "上一首曲目" - "下一首曲目" - "暫停" - "播放" - "停止" - "倒帶" - "向前快轉" - "重複播放所有媒體項目" - "不重複播放任何媒體項目" - "重複播放一個媒體項目" - "隨機播放" + + 上一首曲目 + 下一首曲目 + 暫停 + 播放 + 停止 + 倒轉 + 向前快轉 + 不重複播放 + 重複播放單一項目 + 全部重複播放 + 隨機播放 全螢幕模式 + 下載 + 下載內容 + 正在下載 + 下載完畢 + 下載失敗 + 正在移除下載內容 + 影片 + 音訊 + 文字 + + 自動 + 不明 + %1$d × %2$d + 單聲道 + 立體聲 + 環迴立體聲 + 5.1 環迴立體聲 + 7.1 環迴立體聲 + %1$.2f Mbps + %1$s、%2$s diff --git a/library/ui/src/main/res/values-zh-rTW/strings.xml b/library/ui/src/main/res/values-zh-rTW/strings.xml index 6f87d143ad..b817f189fb 100644 --- a/library/ui/src/main/res/values-zh-rTW/strings.xml +++ b/library/ui/src/main/res/values-zh-rTW/strings.xml @@ -1,30 +1,35 @@ - - - "上一首曲目" - "下一首曲目" - "暫停" - "播放" - "停止" - "倒轉" - "快轉" - "重複播放所有媒體項目" - "不重複播放" - "重複播放單一媒體項目" - "隨機播放" + + 上一首曲目 + 下一首曲目 + 暫停 + 播放 + 停止 + 倒轉 + 快轉 + 不重複播放 + 重複播放單一項目 + 重複播放所有項目 + 隨機播放 全螢幕模式 + 下載 + 下載 + 下載中 + 下載完成 + 無法下載 + 正在移除下載內容 + 影片 + 音訊 + 文字 + + 自動 + 不明 + %1$d × %2$d + 單聲道 + 立體聲 + 環繞音效 + 5.1 環繞音效 + 7.1 環繞音效 + %1$.2f Mbps + %1$s、%2$s diff --git a/library/ui/src/main/res/values-zu/strings.xml b/library/ui/src/main/res/values-zu/strings.xml index aff66ba0cf..0b78b7d1fa 100644 --- a/library/ui/src/main/res/values-zu/strings.xml +++ b/library/ui/src/main/res/values-zu/strings.xml @@ -1,30 +1,35 @@ - - - "Ithrekhi yangaphambilini" - "Ithrekhi elandelayo" - "Misa isikhashana" - "Dlala" - "Misa" - "Buyisela emumva" - "Ukudlulisa ngokushesha" - "Phinda konke" - "Ungaphindi lutho" - "Phida okukodwa" - "Shova" - Imodi yesikrini esiphelele + + Ithrekhi yangaphambilini + Ithrekhi elandelayo + Phumula + Dlala + Misa + Buyisela emuva + Dlulisela phambili + Phinda okungekho + Phinda okukodwa + Phinda konke + Shova + Imodi yesikrini esigcwele + Landa + Ukulandwa + Iyalanda + Ukulanda kuqedile + Ukulanda kuhlulekile + Kususwa okulandiwe + Ividiyo + Umsindo + Umbhalo + Lutho + Okuzenzakalelayo + Akwaziwa + %1$d × %2$d + Okukodwa + I-Stereo + Umsindo ozungelezile + Umsindo ozungelezile ongu-5.1 + Umsindo ozungelezile ongu-7.1 + %1$.2f Mbps + %1$s, %2$s diff --git a/library/ui/src/main/res/values/attrs.xml b/library/ui/src/main/res/values/attrs.xml index 40f0db0ac9..9eefc027ed 100644 --- a/library/ui/src/main/res/values/attrs.xml +++ b/library/ui/src/main/res/values/attrs.xml @@ -50,6 +50,7 @@ + diff --git a/library/ui/src/main/res/values/constants.xml b/library/ui/src/main/res/values/constants.xml index eb94cacadd..9b374d8382 100644 --- a/library/ui/src/main/res/values/constants.xml +++ b/library/ui/src/main/res/values/constants.xml @@ -18,6 +18,7 @@ 71dp 52dp + #AA000000 #FFF4F3F0 diff --git a/library/ui/src/main/res/values/ids.xml b/library/ui/src/main/res/values/ids.xml index b90d2329b3..184e51ac58 100644 --- a/library/ui/src/main/res/values/ids.xml +++ b/library/ui/src/main/res/values/ids.xml @@ -33,5 +33,7 @@ + + diff --git a/library/ui/src/main/res/values/strings.xml b/library/ui/src/main/res/values/strings.xml index c5967a260a..d3befa2f43 100644 --- a/library/ui/src/main/res/values/strings.xml +++ b/library/ui/src/main/res/values/strings.xml @@ -13,7 +13,7 @@ See the License for the specific language governing permissions and limitations under the License. --> - + Previous track @@ -38,12 +38,44 @@ Shuffle Fullscreen mode - - Download queued + + Download + + Downloads - Downloading + Downloading Download completed Download failed + + Removing downloads + + Video + + Audio + + Text + + None + + Auto + + Unknown + + %1$d × %2$d + + Mono + + Stereo + + Surround sound + + 5.1 surround sound + + 7.1 surround sound + + %1$.2f Mbps + + %1$s, %2$s diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashDownloadTest.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashDownloadTest.java index dd5369d64d..5267d54bef 100644 --- a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashDownloadTest.java +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashDownloadTest.java @@ -15,15 +15,12 @@ */ package com.google.android.exoplayer2.playbacktests.gts; -import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; import android.net.Uri; import android.test.ActivityInstrumentationTestCase2; -import android.util.Log; -import com.google.android.exoplayer2.offline.Downloader; -import com.google.android.exoplayer2.offline.Downloader.ProgressListener; import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; +import com.google.android.exoplayer2.source.dash.DashUtil; import com.google.android.exoplayer2.source.dash.manifest.AdaptationSet; import com.google.android.exoplayer2.source.dash.manifest.DashManifest; import com.google.android.exoplayer2.source.dash.manifest.Representation; @@ -38,8 +35,6 @@ import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor; import com.google.android.exoplayer2.upstream.cache.SimpleCache; import com.google.android.exoplayer2.util.Util; import java.io.File; -import java.io.IOException; -import java.io.InterruptedIOException; import java.util.ArrayList; import java.util.List; @@ -50,9 +45,13 @@ public final class DashDownloadTest extends ActivityInstrumentationTestCase2 keys = new ArrayList<>(); - for (int pIndex = 0; pIndex < dashManifest.getPeriodCount(); pIndex++) { - List adaptationSets = dashManifest.getPeriod(pIndex).adaptationSets; - for (int aIndex = 0; aIndex < adaptationSets.size(); aIndex++) { - AdaptationSet adaptationSet = adaptationSets.get(aIndex); - List representations = adaptationSet.representations; - for (int rIndex = 0; rIndex < representations.size(); rIndex++) { - String id = representations.get(rIndex).format.id; - if (DashTestData.AAC_AUDIO_REPRESENTATION_ID.equals(id) - || DashTestData.H264_CDD_FIXED.equals(id)) { - keys.add(new RepresentationKey(pIndex, aIndex, rIndex)); - } + private DashDownloader downloadContent() throws Exception { + DashManifest dashManifest = + DashUtil.loadManifest(httpDataSourceFactory.createDataSource(), MANIFEST_URI); + ArrayList keys = new ArrayList<>(); + for (int pIndex = 0; pIndex < dashManifest.getPeriodCount(); pIndex++) { + List adaptationSets = dashManifest.getPeriod(pIndex).adaptationSets; + for (int aIndex = 0; aIndex < adaptationSets.size(); aIndex++) { + AdaptationSet adaptationSet = adaptationSets.get(aIndex); + List representations = adaptationSet.representations; + for (int rIndex = 0; rIndex < representations.size(); rIndex++) { + String id = representations.get(rIndex).format.id; + if (DashTestData.AAC_AUDIO_REPRESENTATION_ID.equals(id) + || DashTestData.H264_CDD_FIXED.equals(id)) { + keys.add(new RepresentationKey(pIndex, aIndex, rIndex)); } } - dashDownloader.selectRepresentations(keys.toArray(new RepresentationKey[keys.size()])); - TestProgressListener listener = new TestProgressListener(stopAt); - dashDownloader.download(listener); - } - } catch (InterruptedException e) { - // do nothing - } catch (IOException e) { - Throwable exception = e; - while (!(exception instanceof InterruptedIOException)) { - if (exception == null) { - throw e; - } - exception = exception.getCause(); - } - // else do nothing - } - return dashDownloader; - } - - private DashDownloader createDashDownloader(boolean offline) { - DownloaderConstructorHelper constructorHelper = new DownloaderConstructorHelper(cache, - offline ? DummyDataSource.FACTORY : new DefaultHttpDataSourceFactory("ExoPlayer", null)); - return new DashDownloader(Uri.parse(DashTestData.H264_MANIFEST), constructorHelper); - } - - private CacheDataSourceFactory newOfflineCacheDataSourceFactory() { - return new CacheDataSourceFactory(cache, DummyDataSource.FACTORY, - CacheDataSource.FLAG_BLOCK_ON_CACHE); - } - - private static class TestProgressListener implements ProgressListener { - - private final float stopAt; - - private TestProgressListener(float stopAt) { - this.stopAt = stopAt; - } - - @Override - public void onDownloadProgress(Downloader downloader, float downloadPercentage, - long downloadedBytes) { - Log.d("DashDownloadTest", - String.format("onDownloadProgress downloadPercentage = [%g], downloadedData = [%d]%n", - downloadPercentage, downloadedBytes)); - if (downloadPercentage >= stopAt) { - Thread.currentThread().interrupt(); } } - + DownloaderConstructorHelper constructorHelper = + new DownloaderConstructorHelper(cache, httpDataSourceFactory); + return new DashDownloader(MANIFEST_URI, keys, constructorHelper); } } diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestRunner.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestRunner.java index 0c0a5879ca..e9d8acb031 100644 --- a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestRunner.java +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestRunner.java @@ -395,23 +395,26 @@ public final class DashTestRunner { @Override protected TrackSelection[] selectAllTracks( - RendererCapabilities[] rendererCapabilities, MappedTrackInfo mappedTrackInfo) + MappedTrackInfo mappedTrackInfo, + int[][][] rendererFormatSupports, + int[] rendererMixedMimeTypeAdaptationSupports, + Parameters parameters) throws ExoPlaybackException { - Assertions.checkState(rendererCapabilities[VIDEO_RENDERER_INDEX].getTrackType() - == C.TRACK_TYPE_VIDEO); - Assertions.checkState(rendererCapabilities[AUDIO_RENDERER_INDEX].getTrackType() - == C.TRACK_TYPE_AUDIO); + Assertions.checkState( + mappedTrackInfo.getRendererType(VIDEO_RENDERER_INDEX) == C.TRACK_TYPE_VIDEO); + Assertions.checkState( + mappedTrackInfo.getRendererType(AUDIO_RENDERER_INDEX) == C.TRACK_TYPE_AUDIO); TrackGroupArray videoTrackGroups = mappedTrackInfo.getTrackGroups(VIDEO_RENDERER_INDEX); TrackGroupArray audioTrackGroups = mappedTrackInfo.getTrackGroups(AUDIO_RENDERER_INDEX); Assertions.checkState(videoTrackGroups.length == 1); Assertions.checkState(audioTrackGroups.length == 1); - TrackSelection[] selections = new TrackSelection[rendererCapabilities.length]; + TrackSelection[] selections = new TrackSelection[mappedTrackInfo.getRendererCount()]; selections[VIDEO_RENDERER_INDEX] = new RandomTrackSelection( videoTrackGroups.get(0), getVideoTrackIndices( videoTrackGroups.get(0), - mappedTrackInfo.getRendererTrackSupport(VIDEO_RENDERER_INDEX)[0], + rendererFormatSupports[VIDEO_RENDERER_INDEX][0], videoFormatIds, canIncludeAdditionalVideoFormats), 0 /* seed */); @@ -423,8 +426,11 @@ public final class DashTestRunner { return selections; } - private int[] getVideoTrackIndices(TrackGroup trackGroup, int[] formatSupport, - String[] formatIds, boolean canIncludeAdditionalFormats) { + private int[] getVideoTrackIndices( + TrackGroup trackGroup, + int[] formatSupports, + String[] formatIds, + boolean canIncludeAdditionalFormats) { List trackIndices = new ArrayList<>(); // Always select explicitly listed representations. @@ -438,7 +444,7 @@ public final class DashTestRunner { // Select additional video representations, if supported by the device. if (canIncludeAdditionalFormats) { for (int i = 0; i < trackGroup.length; i++) { - if (!trackIndices.contains(i) && isFormatHandled(formatSupport[i])) { + if (!trackIndices.contains(i) && isFormatHandled(formatSupports[i])) { Log.d(tag, "Adding extra video format: " + Format.toLogString(trackGroup.getFormat(i))); trackIndices.add(i); diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java index d969d799f6..a6c3438a52 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java @@ -33,6 +33,7 @@ import com.google.android.exoplayer2.testutil.ActionSchedule.ActionNode; import com.google.android.exoplayer2.testutil.ActionSchedule.PlayerRunnable; import com.google.android.exoplayer2.testutil.ActionSchedule.PlayerTarget; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; +import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.Parameters; import com.google.android.exoplayer2.util.HandlerWrapper; /** @@ -219,7 +220,10 @@ public abstract class Action { } - /** Calls {@link DefaultTrackSelector#setRendererDisabled(int, boolean)}. */ + /** + * Updates the {@link Parameters} of a {@link DefaultTrackSelector} to specify whether the + * renderer at a given index should be disabled. + */ public static final class SetRendererDisabled extends Action { private final int rendererIndex; @@ -239,7 +243,8 @@ public abstract class Action { @Override protected void doActionImpl( SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) { - trackSelector.setRendererDisabled(rendererIndex, disabled); + trackSelector.setParameters( + trackSelector.buildUponParameters().setRendererDisabled(rendererIndex, disabled)); } } diff --git a/testutils_robolectric/build.gradle b/testutils_robolectric/build.gradle index c221149c29..1fd745c676 100644 --- a/testutils_robolectric/build.gradle +++ b/testutils_robolectric/build.gradle @@ -24,9 +24,8 @@ android { } lintOptions { - // Truth depends on JUnit, which depends on java.lang.management, which - // is not part of Android. Remove this when JUnit 4.13 or later is used. - // See: https://github.com/junit-team/junit4/pull/1187. + // Robolectric depends on BouncyCastle, which depends on javax.naming, + // which is not part of Android. disable 'InvalidPackage' } } diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelector.java b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelector.java index 0e8045d489..4d4a53bcdd 100644 --- a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelector.java +++ b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelector.java @@ -17,7 +17,6 @@ package com.google.android.exoplayer2.testutil; import android.support.annotation.NonNull; import com.google.android.exoplayer2.ExoPlaybackException; -import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; @@ -47,7 +46,10 @@ public class FakeTrackSelector extends DefaultTrackSelector { @Override protected TrackSelection[] selectAllTracks( - RendererCapabilities[] rendererCapabilities, MappedTrackInfo mappedTrackInfo) + MappedTrackInfo mappedTrackInfo, + int[][][] rendererFormatSupports, + int[] rendererMixedMimeTypeAdaptationSupports, + Parameters params) throws ExoPlaybackException { TrackSelection[] selections = new TrackSelection[mappedTrackInfo.length]; for (int i = 0; i < mappedTrackInfo.length; i++) { diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/RobolectricUtil.java b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/RobolectricUtil.java index e606fd104b..0d5d4e4437 100644 --- a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/RobolectricUtil.java +++ b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/RobolectricUtil.java @@ -156,8 +156,10 @@ public final class RobolectricUtil { @Override public boolean enqueueMessage(Message msg, long when) { ShadowLooper looper = shadowOf(ShadowLooper.getLooperForThread(looperThread)); - if (looper instanceof CustomLooper) { + if (looper instanceof CustomLooper && looper != ShadowLooper.getShadowMainLooper()) { ((CustomLooper) looper).addPendingMessage(msg, when); + } else { + super.enqueueMessage(msg, when); } return true; } @@ -165,7 +167,7 @@ public final class RobolectricUtil { @Implementation public void removeMessages(Handler handler, int what, Object object) { ShadowLooper looper = shadowOf(ShadowLooper.getLooperForThread(looperThread)); - if (looper instanceof CustomLooper) { + if (looper instanceof CustomLooper && looper != ShadowLooper.getShadowMainLooper()) { ((CustomLooper) looper).removeMessages(handler, what, object); } } diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java index 8de92ddfa9..d81cef9d8a 100644 --- a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java +++ b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.testutil; import android.os.Looper; import android.support.annotation.Nullable; +import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; @@ -63,6 +64,11 @@ public abstract class StubExoPlayer implements ExoPlayer { throw new UnsupportedOperationException(); } + @Override + public ExoPlaybackException getPlaybackError() { + throw new UnsupportedOperationException(); + } + @Override public void prepare(MediaSource mediaSource) { throw new UnsupportedOperationException(); diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/TestDownloadListener.java b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/TestDownloadManagerListener.java similarity index 58% rename from library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/TestDownloadListener.java rename to testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/TestDownloadManagerListener.java index 21af7e42c1..b624c49350 100644 --- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/TestDownloadListener.java +++ b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/TestDownloadManagerListener.java @@ -13,37 +13,56 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer2.source.dash.offline; +package com.google.android.exoplayer2.testutil; import static com.google.common.truth.Truth.assertThat; +import com.google.android.exoplayer2.offline.DownloadAction; import com.google.android.exoplayer2.offline.DownloadManager; -import com.google.android.exoplayer2.offline.DownloadManager.DownloadListener; -import com.google.android.exoplayer2.offline.DownloadManager.DownloadState; -import com.google.android.exoplayer2.testutil.DummyMainThread; +import java.util.HashMap; +import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; -/** A {@link DownloadListener} for testing. */ -/*package*/ final class TestDownloadListener implements DownloadListener { +/** A {@link DownloadManager.Listener} for testing. */ +public final class TestDownloadManagerListener implements DownloadManager.Listener { private static final int TIMEOUT = 1000; private final DownloadManager downloadManager; private final DummyMainThread dummyMainThread; + private final HashMap> actionStates; + private CountDownLatch downloadFinishedCondition; private Throwable downloadError; - public TestDownloadListener(DownloadManager downloadManager, DummyMainThread dummyMainThread) { + public TestDownloadManagerListener( + DownloadManager downloadManager, DummyMainThread dummyMainThread) { this.downloadManager = downloadManager; this.dummyMainThread = dummyMainThread; + actionStates = new HashMap<>(); + } + + public int pollStateChange(DownloadAction action, long timeoutMs) throws InterruptedException { + return getStateQueue(action).poll(timeoutMs, TimeUnit.MILLISECONDS); + } + + public void clearDownloadError() { + this.downloadError = null; } @Override - public void onStateChange(DownloadManager downloadManager, DownloadState downloadState) { - if (downloadState.state == DownloadState.STATE_ERROR && downloadError == null) { - downloadError = downloadState.error; + public void onInitialized(DownloadManager downloadManager) { + // Do nothing. + } + + @Override + public void onTaskStateChanged( + DownloadManager downloadManager, DownloadManager.TaskState taskState) { + if (taskState.state == DownloadManager.TaskState.STATE_FAILED && downloadError == null) { + downloadError = taskState.error; } + getStateQueue(taskState.action).add(taskState.state); } @Override @@ -75,4 +94,13 @@ import java.util.concurrent.TimeUnit; throw new Exception(downloadError); } } + + private ArrayBlockingQueue getStateQueue(DownloadAction action) { + synchronized (actionStates) { + if (!actionStates.containsKey(action)) { + actionStates.put(action, new ArrayBlockingQueue(10)); + } + return actionStates.get(action); + } + } }