diff --git a/.gitignore b/.gitignore index 1a946e2ade..db5a8c4305 100644 --- a/.gitignore +++ b/.gitignore @@ -43,6 +43,9 @@ cmake-build-debug dist tmp +# External native builds +.externalNativeBuild + # VP9 extension extensions/vp9/src/main/jni/libvpx extensions/vp9/src/main/jni/libvpx_android_configs @@ -62,3 +65,4 @@ extensions/cronet/jniLibs/* !extensions/cronet/jniLibs/README.md extensions/cronet/libs/* !extensions/cronet/libs/README.md + diff --git a/ISSUE_TEMPLATE b/ISSUE_TEMPLATE index 807888a447..8d2f66093d 100644 --- a/ISSUE_TEMPLATE +++ b/ISSUE_TEMPLATE @@ -38,4 +38,6 @@ devices and Android versions. Capture a full bug report using "adb bugreport". Output from "adb logcat" or a log snippet is NOT sufficient. Please attach the captured bug report as a file. If you don't wish to post it publicly, please submit the issue, then email the -bug report to dev.exoplayer@gmail.com using a subject in the format "Issue #1234". +bug report to dev.exoplayer@gmail.com using a subject in the format +"Issue #1234". + diff --git a/README.md b/README.md index 8755ac588d..13dfaddab3 100644 --- a/README.md +++ b/README.md @@ -38,8 +38,8 @@ repositories { } ``` -Next add a gradle compile dependency to the `build.gradle` file of your app -module. The following will add a dependency to the full library: +Next add a dependency in the `build.gradle` file of your app module. The +following will add a dependency to the full library: ```gradle implementation 'com.google.android.exoplayer:exoplayer:2.X.X' diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 884cfe6bc2..5efb382bba 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,5 +1,119 @@ # Release notes # +### 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. + ([#3498](https://github.com/google/ExoPlayer/issues/3498)). + * Merged `DynamicConcatenatingMediaSource` into `ConcatenatingMediaSource` and + deprecated `DynamicConcatenatingMediaSource`. + * Allow clipping of child media sources where the period and window have a + non-zero offset with `ClippingMediaSource`. + * Allow adding and removing `MediaSourceEventListener`s to MediaSources after + they have been created. Listening to events is now supported for all + media sources including composite sources. + * Added callbacks to `MediaSourceEventListener` to get notified when media + periods are created, released and being read from. + * 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`. +* 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)). + * Supports FLAC files with high sample rate (176400 and 192000) + ([#3769](https://github.com/google/ExoPlayer/issues/3769)). + * Factor out `AudioTrack` position tracking from `DefaultAudioSink`. + * Fix an issue where the playback position would pause just after playback + begins, and poll the audio timestamp less frequently once it starts + advancing ([#3841](https://github.com/google/ExoPlayer/issues/3841)). + * Add an option to skip silent audio in `PlaybackParameters` + ((#2635)[https://github.com/google/ExoPlayer/issues/2635]). + * 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 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: + * Allow multiple listeners for `DefaultDrmSessionManager`. + * Pass `DrmSessionManager` to `ExoPlayerFactory` instead of `RendererFactory`. + * Change minimum API requirement for CBC and pattern encryption from 24 to 25 + ([#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. + * 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 ### * Fix ProGuard configuration for Cast, IMA and OkHttp extensions. @@ -93,7 +207,7 @@ ([#3630](https://github.com/google/ExoPlayer/issues/3630)). * DASH: * Support in-band Emsg events targeting the player with scheme id - "urn:mpeg:dash:event:2012" and scheme values "1", "2" and "3". + `urn:mpeg:dash:event:2012` and scheme values "1", "2" and "3". * Support EventStream elements in DASH manifests. * HLS: * Add opt-in support for chunkless preparation in HLS. This allows an @@ -163,6 +277,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 32210bc95c..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 @@ -25,12 +25,14 @@ project.ext { buildToolsVersion = '27.0.3' testSupportLibraryVersion = '0.5' supportLibraryVersion = '27.0.0' - playServicesLibraryVersion = '11.4.2' + playServicesLibraryVersion = '12.0.0' dexmakerVersion = '1.2' mockitoVersion = '1.9.5' 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/core_settings.gradle b/core_settings.gradle index c4914e3040..fc738c8476 100644 --- a/core_settings.gradle +++ b/core_settings.gradle @@ -36,6 +36,7 @@ include modulePrefix + 'extension-opus' include modulePrefix + 'extension-vp9' include modulePrefix + 'extension-rtmp' include modulePrefix + 'extension-leanback' +include modulePrefix + 'extension-jobdispatcher' project(modulePrefix + 'library').projectDir = new File(rootDir, 'library/all') project(modulePrefix + 'library-core').projectDir = new File(rootDir, 'library/core') @@ -56,6 +57,7 @@ project(modulePrefix + 'extension-opus').projectDir = new File(rootDir, 'extensi project(modulePrefix + 'extension-vp9').projectDir = new File(rootDir, 'extensions/vp9') project(modulePrefix + 'extension-rtmp').projectDir = new File(rootDir, 'extensions/rtmp') project(modulePrefix + 'extension-leanback').projectDir = new File(rootDir, 'extensions/leanback') +project(modulePrefix + 'extension-jobdispatcher').projectDir = new File(rootDir, 'extensions/jobdispatcher') if (gradle.ext.has('exoplayerIncludeCronetExtension') && gradle.ext.exoplayerIncludeCronetExtension) { diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java index a14978a46a..63b18b0aa7 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java @@ -32,7 +32,7 @@ import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline.Period; import com.google.android.exoplayer2.castdemo.DemoUtil.Sample; import com.google.android.exoplayer2.ext.cast.CastPlayer; -import com.google.android.exoplayer2.source.DynamicConcatenatingMediaSource; +import com.google.android.exoplayer2.source.ConcatenatingMediaSource; import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.dash.DashMediaSource; @@ -80,8 +80,8 @@ import java.util.ArrayList; private final CastPlayer castPlayer; private final ArrayList mediaQueue; private final QueuePositionListener queuePositionListener; + private final ConcatenatingMediaSource concatenatingMediaSource; - private DynamicConcatenatingMediaSource dynamicConcatenatingMediaSource; private boolean castMediaQueueCreationPending; private int currentItemIndex; private Player currentPlayer; @@ -117,9 +117,10 @@ import java.util.ArrayList; this.castControlView = castControlView; mediaQueue = new ArrayList<>(); currentItemIndex = C.INDEX_UNSET; + concatenatingMediaSource = new ConcatenatingMediaSource(); DefaultTrackSelector trackSelector = new DefaultTrackSelector(BANDWIDTH_METER); - RenderersFactory renderersFactory = new DefaultRenderersFactory(context, null); + RenderersFactory renderersFactory = new DefaultRenderersFactory(context); exoPlayer = ExoPlayerFactory.newSimpleInstance(renderersFactory, trackSelector); exoPlayer.addListener(this); localPlayerView.setPlayer(exoPlayer); @@ -155,9 +156,8 @@ import java.util.ArrayList; */ public void addItem(Sample sample) { mediaQueue.add(sample); - if (currentPlayer == exoPlayer) { - dynamicConcatenatingMediaSource.addMediaSource(buildMediaSource(sample)); - } else { + concatenatingMediaSource.addMediaSource(buildMediaSource(sample)); + if (currentPlayer == castPlayer) { castPlayer.addItems(buildMediaQueueItem(sample)); } } @@ -186,9 +186,8 @@ import java.util.ArrayList; * @return Whether the removal was successful. */ public boolean removeItem(int itemIndex) { - if (currentPlayer == exoPlayer) { - dynamicConcatenatingMediaSource.removeMediaSource(itemIndex); - } else { + concatenatingMediaSource.removeMediaSource(itemIndex); + if (currentPlayer == castPlayer) { if (castPlayer.getPlaybackState() != Player.STATE_IDLE) { Timeline castTimeline = castPlayer.getCurrentTimeline(); if (castTimeline.getPeriodCount() <= itemIndex) { @@ -215,9 +214,8 @@ import java.util.ArrayList; */ public boolean moveItem(int fromIndex, int toIndex) { // Player update. - if (currentPlayer == exoPlayer) { - dynamicConcatenatingMediaSource.moveMediaSource(fromIndex, toIndex); - } else if (castPlayer.getPlaybackState() != Player.STATE_IDLE) { + concatenatingMediaSource.moveMediaSource(fromIndex, toIndex); + if (currentPlayer == castPlayer && castPlayer.getPlaybackState() != Player.STATE_IDLE) { Timeline castTimeline = castPlayer.getCurrentTimeline(); int periodCount = castTimeline.getPeriodCount(); if (periodCount <= fromIndex || periodCount <= toIndex) { @@ -263,6 +261,7 @@ import java.util.ArrayList; public void release() { currentItemIndex = C.INDEX_UNSET; mediaQueue.clear(); + concatenatingMediaSource.clear(); castPlayer.setSessionAvailabilityListener(null); castPlayer.release(); localPlayerView.setPlayer(null); @@ -354,11 +353,7 @@ import java.util.ArrayList; // Media queue management. castMediaQueueCreationPending = currentPlayer == castPlayer; if (currentPlayer == exoPlayer) { - dynamicConcatenatingMediaSource = new DynamicConcatenatingMediaSource(); - for (int i = 0; i < mediaQueue.size(); i++) { - dynamicConcatenatingMediaSource.addMediaSource(buildMediaSource(mediaQueue.get(i))); - } - exoPlayer.prepare(dynamicConcatenatingMediaSource); + exoPlayer.prepare(concatenatingMediaSource); } // Playback transition. diff --git a/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java b/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java index e51c5e89b7..4fab1966fe 100644 --- a/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java +++ b/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java @@ -17,8 +17,6 @@ package com.google.android.exoplayer2.imademo; import android.content.Context; import android.net.Uri; -import android.os.Handler; -import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C.ContentType; import com.google.android.exoplayer2.ExoPlayer; @@ -27,7 +25,6 @@ import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.ext.ima.ImaAdsLoader; import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.source.MediaSourceEventListener; import com.google.android.exoplayer2.source.ads.AdsMediaSource; import com.google.android.exoplayer2.source.dash.DashMediaSource; import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource; @@ -83,8 +80,7 @@ import com.google.android.exoplayer2.util.Util; // This is the MediaSource representing the content media (i.e. not the ad). String contentUrl = context.getString(R.string.content_url); - MediaSource contentMediaSource = - buildMediaSource(Uri.parse(contentUrl), /* handler= */ null, /* listener= */ null); + MediaSource contentMediaSource = buildMediaSource(Uri.parse(contentUrl)); // Compose the content media source into a new AdsMediaSource with both ads and content. MediaSource mediaSourceWithAds = @@ -121,9 +117,8 @@ import com.google.android.exoplayer2.util.Util; // AdsMediaSource.MediaSourceFactory implementation. @Override - public MediaSource createMediaSource( - Uri uri, @Nullable Handler handler, @Nullable MediaSourceEventListener listener) { - return buildMediaSource(uri, handler, listener); + public MediaSource createMediaSource(Uri uri) { + return buildMediaSource(uri); } @Override @@ -134,25 +129,22 @@ import com.google.android.exoplayer2.util.Util; // Internal methods. - private MediaSource buildMediaSource( - Uri uri, @Nullable Handler handler, @Nullable MediaSourceEventListener listener) { + private MediaSource buildMediaSource(Uri uri) { @ContentType int type = Util.inferContentType(uri); switch (type) { case C.TYPE_DASH: return new DashMediaSource.Factory( new DefaultDashChunkSource.Factory(mediaDataSourceFactory), manifestDataSourceFactory) - .createMediaSource(uri, handler, listener); + .createMediaSource(uri); case C.TYPE_SS: return new SsMediaSource.Factory( new DefaultSsChunkSource.Factory(mediaDataSourceFactory), manifestDataSourceFactory) - .createMediaSource(uri, handler, listener); + .createMediaSource(uri); case C.TYPE_HLS: - return new HlsMediaSource.Factory(mediaDataSourceFactory) - .createMediaSource(uri, handler, listener); + return new HlsMediaSource.Factory(mediaDataSourceFactory).createMediaSource(uri); case C.TYPE_OTHER: - return new ExtractorMediaSource.Factory(mediaDataSourceFactory) - .createMediaSource(uri, handler, listener); + return new ExtractorMediaSource.Factory(mediaDataSourceFactory).createMediaSource(uri); default: throw new IllegalStateException("Unsupported type: " + type); } 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/assets/media.exolist.json b/demos/main/src/main/assets/media.exolist.json index 7052e7c436..0d26f196c1 100644 --- a/demos/main/src/main/assets/media.exolist.json +++ b/demos/main/src/main/assets/media.exolist.json @@ -578,5 +578,16 @@ "ad_tag_uri": "http://vastsynthesizer.appspot.com/empty-midroll-2" } ] + }, + { + "name": "ABR", + "samples": [ + { + "name": "Random ABR - Google Glass (MP4,H264)", + "uri": "http://www.youtube.com/api/manifest/dash/id/bf5bb2419360daf1/source/youtube?as=fmp4_audio_clear,fmp4_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=51AF5F39AB0CEC3E5497CD9C900EBFEAECCCB5C7.8506521BFC350652163895D4C26DEE124209AA9E&key=ik0", + "extension": "mpd", + "abr_algorithm": "random" + } + ] } ] 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 5d019e4c53..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,20 +16,51 @@ 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; +import com.google.android.exoplayer2.upstream.FileDataSourceFactory; import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.upstream.TransferListener; +import com.google.android.exoplayer2.upstream.cache.Cache; +import com.google.android.exoplayer2.upstream.cache.CacheDataSource; +import com.google.android.exoplayer2.upstream.cache.CacheDataSourceFactory; +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; /** * Placeholder application to facilitate overriding Application methods for debugging and testing. */ public class DemoApplication extends Application { + 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() { super.onCreate(); @@ -38,7 +69,9 @@ public class DemoApplication extends Application { /** Returns a {@link DataSource.Factory}. */ public DataSource.Factory buildDataSourceFactory(TransferListener listener) { - return new DefaultDataSourceFactory(this, listener, buildHttpDataSourceFactory(listener)); + DefaultDataSourceFactory upstreamFactory = + new DefaultDataSourceFactory(this, listener, buildHttpDataSourceFactory(listener)); + return buildReadOnlyCacheDataSource(upstreamFactory, getDownloadCache()); } /** Returns a {@link HttpDataSource.Factory}. */ @@ -47,8 +80,69 @@ public class DemoApplication extends Application { return new DefaultHttpDataSourceFactory(userAgent, listener); } + /** Returns whether extension renderers should be used. */ public boolean useExtensionRenderers() { - return BuildConfig.FLAVOR.equals("withExtensions"); + return "withExtensions".equals(BuildConfig.FLAVOR); } + 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, + CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR, + /* 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 058133895e..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.support.annotation.NonNull; import android.support.annotation.Nullable; -import android.text.TextUtils; +import android.util.Pair; import android.view.KeyEvent; import android.view.View; import android.view.View.OnClickListener; @@ -42,43 +42,52 @@ import com.google.android.exoplayer2.PlaybackPreparer; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.drm.DefaultDrmSessionManager; -import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.google.android.exoplayer2.drm.FrameworkMediaDrm; 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; import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.source.MediaSourceEventListener; import com.google.android.exoplayer2.source.TrackGroupArray; 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.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.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.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; +import com.google.android.exoplayer2.trackselection.RandomTrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelection; 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.Util; import java.lang.reflect.Constructor; import java.net.CookieHandler; import java.net.CookieManager; import java.net.CookiePolicy; +import java.util.List; import java.util.UUID; /** An activity that plays media using {@link SimpleExoPlayer}. */ @@ -86,10 +95,10 @@ public class PlayerActivity extends Activity implements OnClickListener, PlaybackPreparer, PlayerControlView.VisibilityListener { public static final String DRM_SCHEME_EXTRA = "drm_scheme"; - public static final String DRM_LICENSE_URL = "drm_license_url"; - public static final String DRM_KEY_REQUEST_PROPERTIES = "drm_key_request_properties"; - public static final String DRM_MULTI_SESSION = "drm_multi_session"; - public static final String PREFER_EXTENSION_DECODERS = "prefer_extension_decoders"; + public static final String DRM_LICENSE_URL_EXTRA = "drm_license_url"; + public static final String DRM_KEY_REQUEST_PROPERTIES_EXTRA = "drm_key_request_properties"; + public static final String DRM_MULTI_SESSION_EXTRA = "drm_multi_session"; + public static final String PREFER_EXTENSION_DECODERS_EXTRA = "prefer_extension_decoders"; public static final String ACTION_VIEW = "com.google.android.exoplayer.demo.action.VIEW"; public static final String EXTENSION_EXTRA = "extension"; @@ -98,11 +107,22 @@ public class PlayerActivity extends Activity "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 AD_TAG_URI_EXTRA = "ad_tag_uri"; - // For backwards compatibility. + public static final String ABR_ALGORITHM_EXTRA = "abr_algorithm"; + private static final String ABR_ALGORITHM_DEFAULT = "default"; + private static final String ABR_ALGORITHM_RANDOM = "random"; + + // 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 { @@ -110,23 +130,21 @@ public class PlayerActivity extends Activity DEFAULT_COOKIE_MANAGER.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER); } - private Handler mainHandler; - private EventLogger eventLogger; private PlayerView playerView; private LinearLayout debugRootView; private TextView debugTextView; private DataSource.Factory mediaDataSourceFactory; 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. @@ -139,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); } @@ -155,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); } @@ -207,7 +232,12 @@ public class PlayerActivity extends Activity @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { - if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + 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); @@ -215,6 +245,16 @@ public class PlayerActivity extends Activity } } + @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 @Override @@ -230,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(); } } } @@ -253,21 +304,40 @@ public class PlayerActivity extends Activity // Internal methods private void initializePlayer() { - Intent intent = getIntent(); - boolean needNewPlayer = player == null; - if (needNewPlayer) { - TrackSelection.Factory adaptiveTrackSelectionFactory = - new AdaptiveTrackSelection.Factory(BANDWIDTH_METER); - trackSelector = new DefaultTrackSelector(adaptiveTrackSelectionFactory); - trackSelectionHelper = new TrackSelectionHelper(trackSelector, adaptiveTrackSelectionFactory); - lastSeenTrackGroupArray = null; - eventLogger = new EventLogger(trackSelector); + if (player == null) { + Intent intent = getIntent(); + String action = intent.getAction(); + Uri[] uris; + String[] extensions; + if (ACTION_VIEW.equals(action)) { + uris = new Uri[] {intent.getData()}; + extensions = new String[] {intent.getStringExtra(EXTENSION_EXTRA)}; + } else if (ACTION_VIEW_LIST.equals(action)) { + String[] uriStrings = intent.getStringArrayExtra(URI_LIST_EXTRA); + uris = new Uri[uriStrings.length]; + for (int i = 0; i < uriStrings.length; i++) { + uris[i] = Uri.parse(uriStrings[i]); + } + extensions = intent.getStringArrayExtra(EXTENSION_LIST_EXTRA); + if (extensions == null) { + extensions = new String[uriStrings.length]; + } + } else { + showToast(getString(R.string.unexpected_intent_action, action)); + finish(); + return; + } + if (Util.maybeRequestReadExternalStoragePermission(this, uris)) { + // The player will be reinitialized if the permission is granted. + return; + } - DrmSessionManager drmSessionManager = null; + DefaultDrmSessionManager drmSessionManager = null; if (intent.hasExtra(DRM_SCHEME_EXTRA) || intent.hasExtra(DRM_SCHEME_UUID_EXTRA)) { - String drmLicenseUrl = intent.getStringExtra(DRM_LICENSE_URL); - String[] keyRequestPropertiesArray = intent.getStringArrayExtra(DRM_KEY_REQUEST_PROPERTIES); - boolean multiSession = intent.getBooleanExtra(DRM_MULTI_SESSION, false); + String drmLicenseUrl = intent.getStringExtra(DRM_LICENSE_URL_EXTRA); + String[] keyRequestPropertiesArray = + intent.getStringArrayExtra(DRM_KEY_REQUEST_PROPERTIES_EXTRA); + boolean multiSession = intent.getBooleanExtra(DRM_MULTI_SESSION_EXTRA, false); int errorStringId = R.string.error_drm_unknown; if (Util.SDK_INT < 18) { errorStringId = R.string.error_drm_not_supported; @@ -290,154 +360,168 @@ public class PlayerActivity extends Activity } if (drmSessionManager == null) { showToast(errorStringId); + finish(); return; } } - boolean preferExtensionDecoders = intent.getBooleanExtra(PREFER_EXTENSION_DECODERS, false); + TrackSelection.Factory trackSelectionFactory; + String abrAlgorithm = intent.getStringExtra(ABR_ALGORITHM_EXTRA); + if (abrAlgorithm == null || ABR_ALGORITHM_DEFAULT.equals(abrAlgorithm)) { + trackSelectionFactory = new AdaptiveTrackSelection.Factory(BANDWIDTH_METER); + } else if (ABR_ALGORITHM_RANDOM.equals(abrAlgorithm)) { + trackSelectionFactory = new RandomTrackSelection.Factory(); + } else { + showToast(R.string.error_unrecognized_abr_algorithm); + finish(); + return; + } + + boolean preferExtensionDecoders = + intent.getBooleanExtra(PREFER_EXTENSION_DECODERS_EXTRA, false); @DefaultRenderersFactory.ExtensionRendererMode int extensionRendererMode = ((DemoApplication) getApplication()).useExtensionRenderers() ? (preferExtensionDecoders ? DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER : DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON) : DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF; - DefaultRenderersFactory renderersFactory = new DefaultRenderersFactory(this, - drmSessionManager, extensionRendererMode); + DefaultRenderersFactory renderersFactory = + new DefaultRenderersFactory(this, extensionRendererMode); - player = ExoPlayerFactory.newSimpleInstance(renderersFactory, trackSelector); + trackSelector = new DefaultTrackSelector(trackSelectionFactory); + trackSelector.setParameters(trackSelectorParameters); + lastSeenTrackGroupArray = null; + + player = + ExoPlayerFactory.newSimpleInstance(renderersFactory, trackSelector, drmSessionManager); player.addListener(new PlayerEventListener()); - player.addListener(eventLogger); - player.addMetadataOutput(eventLogger); - player.addAudioDebugListener(eventLogger); - player.addVideoDebugListener(eventLogger); - player.setPlayWhenReady(shouldAutoPlay); - + player.setPlayWhenReady(startAutoPlay); + player.addAnalyticsListener(new EventLogger(trackSelector)); playerView.setPlayer(player); playerView.setPlaybackPreparer(this); debugViewHelper = new DebugTextViewHelper(player, debugTextView); debugViewHelper.start(); - } - String action = intent.getAction(); - Uri[] uris; - String[] extensions; - if (ACTION_VIEW.equals(action)) { - uris = new Uri[]{intent.getData()}; - extensions = new String[]{intent.getStringExtra(EXTENSION_EXTRA)}; - } else if (ACTION_VIEW_LIST.equals(action)) { - String[] uriStrings = intent.getStringArrayExtra(URI_LIST_EXTRA); - uris = new Uri[uriStrings.length]; - for (int i = 0; i < uriStrings.length; i++) { - uris[i] = Uri.parse(uriStrings[i]); + + MediaSource[] mediaSources = new MediaSource[uris.length]; + for (int i = 0; i < uris.length; i++) { + mediaSources[i] = buildMediaSource(uris[i], extensions[i]); } - extensions = intent.getStringArrayExtra(EXTENSION_LIST_EXTRA); - if (extensions == null) { - extensions = new String[uriStrings.length]; - } - } else { - showToast(getString(R.string.unexpected_intent_action, action)); - return; - } - if (Util.maybeRequestReadExternalStoragePermission(this, uris)) { - // The player will be reinitialized if the permission is granted. - return; - } - MediaSource[] mediaSources = new MediaSource[uris.length]; - for (int i = 0; i < uris.length; i++) { - mediaSources[i] = buildMediaSource(uris[i], extensions[i], mainHandler, eventLogger); - } - MediaSource mediaSource = mediaSources.length == 1 ? mediaSources[0] - : new ConcatenatingMediaSource(mediaSources); - String adTagUriString = intent.getStringExtra(AD_TAG_URI_EXTRA); - if (adTagUriString != null) { - Uri adTagUri = Uri.parse(adTagUriString); - if (!adTagUri.equals(loadedAdTagUri)) { - releaseAdsLoader(); - loadedAdTagUri = adTagUri; - } - MediaSource adsMediaSource = createAdsMediaSource(mediaSource, Uri.parse(adTagUriString)); - if (adsMediaSource != null) { - mediaSource = adsMediaSource; + mediaSource = + mediaSources.length == 1 ? mediaSources[0] : new ConcatenatingMediaSource(mediaSources); + String adTagUriString = intent.getStringExtra(AD_TAG_URI_EXTRA); + if (adTagUriString != null) { + Uri adTagUri = Uri.parse(adTagUriString); + if (!adTagUri.equals(loadedAdTagUri)) { + releaseAdsLoader(); + loadedAdTagUri = adTagUri; + } + MediaSource adsMediaSource = createAdsMediaSource(mediaSource, Uri.parse(adTagUriString)); + if (adsMediaSource != null) { + mediaSource = adsMediaSource; + } else { + showToast(R.string.ima_not_loaded); + } } else { - showToast(R.string.ima_not_loaded); + releaseAdsLoader(); } - } else { - releaseAdsLoader(); } - 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, - String overrideExtension, - @Nullable Handler handler, - @Nullable MediaSourceEventListener listener) { - @ContentType int type = TextUtils.isEmpty(overrideExtension) ? Util.inferContentType(uri) - : Util.inferContentType("." + overrideExtension); + private MediaSource buildMediaSource(Uri uri) { + return buildMediaSource(uri, null); + } + + @SuppressWarnings("unchecked") + private MediaSource buildMediaSource(Uri uri, @Nullable String overrideExtension) { + @ContentType int type = Util.inferContentType(uri, overrideExtension); switch (type) { case C.TYPE_DASH: return new DashMediaSource.Factory( new DefaultDashChunkSource.Factory(mediaDataSourceFactory), buildDataSourceFactory(false)) - .createMediaSource(uri, handler, listener); + .setManifestParser( + new FilteringManifestParser<>( + new DashManifestParser(), (List) getOfflineStreamKeys(uri))) + .createMediaSource(uri); case C.TYPE_SS: return new SsMediaSource.Factory( new DefaultSsChunkSource.Factory(mediaDataSourceFactory), buildDataSourceFactory(false)) - .createMediaSource(uri, handler, listener); + .setManifestParser( + new FilteringManifestParser<>( + new SsManifestParser(), (List) getOfflineStreamKeys(uri))) + .createMediaSource(uri); case C.TYPE_HLS: return new HlsMediaSource.Factory(mediaDataSourceFactory) - .createMediaSource(uri, handler, listener); + .setPlaylistParser( + new FilteringManifestParser<>( + new HlsPlaylistParser(), (List) getOfflineStreamKeys(uri))) + .createMediaSource(uri); case C.TYPE_OTHER: - return new ExtractorMediaSource.Factory(mediaDataSourceFactory) - .createMediaSource(uri, handler, listener); + return new ExtractorMediaSource.Factory(mediaDataSourceFactory).createMediaSource(uri); default: { throw new IllegalStateException("Unsupported type: " + type); } } } - private DrmSessionManager buildDrmSessionManagerV18(UUID uuid, - String licenseUrl, String[] keyRequestPropertiesArray, boolean multiSession) + 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]); } } - return new DefaultDrmSessionManager<>(uuid, FrameworkMediaDrm.newInstance(uuid), drmCallback, - null, mainHandler, eventLogger, multiSession); + 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; - eventLogger = 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; } /** @@ -452,18 +536,6 @@ 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) { // Load the extension source using reflection so the demo app doesn't have to depend on it. @@ -486,10 +558,8 @@ public class PlayerActivity extends Activity AdsMediaSource.MediaSourceFactory adMediaSourceFactory = new AdsMediaSource.MediaSourceFactory() { @Override - public MediaSource createMediaSource( - Uri uri, @Nullable Handler handler, @Nullable MediaSourceEventListener listener) { - return PlayerActivity.this.buildMediaSource( - uri, /* overrideExtension= */ null, handler, listener); + public MediaSource createMediaSource(Uri uri) { + return PlayerActivity.this.buildMediaSource(uri); } @Override @@ -497,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; @@ -529,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; @@ -593,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(); } @@ -647,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); } @@ -659,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 3895ad8e84..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,20 +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; -import java.util.UUID; -/** - * 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; @@ -80,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) { @@ -89,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> { @@ -176,15 +233,16 @@ 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; - UUID drmUuid = null; + String drmScheme = null; String drmLicenseUrl = null; String[] drmKeyRequestProperties = null; boolean drmMultiSession = false; boolean preferExtensionDecoders = false; ArrayList playlistSamples = null; String adTagUri = null; + String abrAlgorithm = null; reader.beginObject(); while (reader.hasNext()) { @@ -194,16 +252,14 @@ 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(); break; case "drm_scheme": Assertions.checkState(!insidePlaylist, "Invalid attribute on nested item: drm_scheme"); - String drmScheme = reader.nextString(); - drmUuid = Util.getDrmUuid(drmScheme); - Assertions.checkState(drmUuid != null, "Invalid drm_scheme: " + drmScheme); + drmScheme = reader.nextString(); break; case "drm_license_url": Assertions.checkState(!insidePlaylist, @@ -242,21 +298,28 @@ public class SampleChooserActivity extends Activity { case "ad_tag_uri": adTagUri = reader.nextString(); break; + case "abr_algorithm": + Assertions.checkState( + !insidePlaylist, "Invalid attribute on nested item: abr_algorithm"); + abrAlgorithm = reader.nextString(); + break; default: throw new ParserException("Unsupported attribute name: " + name); } } reader.endObject(); - DrmInfo drmInfo = drmUuid == null ? null : new DrmInfo(drmUuid, drmLicenseUrl, - drmKeyRequestProperties, drmMultiSession); + DrmInfo drmInfo = + drmScheme == null + ? null + : new DrmInfo(drmScheme, drmLicenseUrl, drmKeyRequestProperties, drmMultiSession); if (playlistSamples != null) { UriSample[] playlistSamplesArray = playlistSamples.toArray( new UriSample[playlistSamples.size()]); - return new PlaylistSample(sampleName, preferExtensionDecoders, drmInfo, - playlistSamplesArray); + return new PlaylistSample( + sampleName, preferExtensionDecoders, abrAlgorithm, drmInfo, playlistSamplesArray); } else { - return new UriSample(sampleName, preferExtensionDecoders, drmInfo, uri, extension, - adTagUri); + return new UriSample( + sampleName, preferExtensionDecoders, abrAlgorithm, drmInfo, uri, extension, adTagUri); } } @@ -273,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 @@ -298,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; } @@ -325,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; @@ -347,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 { @@ -362,14 +450,17 @@ public class SampleChooserActivity extends Activity { } private static final class DrmInfo { - public final UUID drmSchemeUuid; + public final String drmScheme; public final String drmLicenseUrl; public final String[] drmKeyRequestProperties; public final boolean drmMultiSession; - public DrmInfo(UUID drmSchemeUuid, String drmLicenseUrl, - String[] drmKeyRequestProperties, boolean drmMultiSession) { - this.drmSchemeUuid = drmSchemeUuid; + public DrmInfo( + String drmScheme, + String drmLicenseUrl, + String[] drmKeyRequestProperties, + boolean drmMultiSession) { + this.drmScheme = drmScheme; this.drmLicenseUrl = drmLicenseUrl; this.drmKeyRequestProperties = drmKeyRequestProperties; this.drmMultiSession = drmMultiSession; @@ -377,31 +468,34 @@ public class SampleChooserActivity extends Activity { public void updateIntent(Intent intent) { Assertions.checkNotNull(intent); - intent.putExtra(PlayerActivity.DRM_SCHEME_EXTRA, drmSchemeUuid.toString()); - intent.putExtra(PlayerActivity.DRM_LICENSE_URL, drmLicenseUrl); - intent.putExtra(PlayerActivity.DRM_KEY_REQUEST_PROPERTIES, drmKeyRequestProperties); - intent.putExtra(PlayerActivity.DRM_MULTI_SESSION, drmMultiSession); + intent.putExtra(PlayerActivity.DRM_SCHEME_EXTRA, drmScheme); + intent.putExtra(PlayerActivity.DRM_LICENSE_URL_EXTRA, drmLicenseUrl); + intent.putExtra(PlayerActivity.DRM_KEY_REQUEST_PROPERTIES_EXTRA, drmKeyRequestProperties); + intent.putExtra(PlayerActivity.DRM_MULTI_SESSION_EXTRA, drmMultiSession); } } private abstract static class Sample { public final String name; public final boolean preferExtensionDecoders; + public final String abrAlgorithm; public final DrmInfo drmInfo; - public Sample(String name, boolean preferExtensionDecoders, DrmInfo drmInfo) { + public Sample( + String name, boolean preferExtensionDecoders, String abrAlgorithm, DrmInfo drmInfo) { this.name = name; this.preferExtensionDecoders = preferExtensionDecoders; + this.abrAlgorithm = abrAlgorithm; this.drmInfo = drmInfo; } public Intent buildIntent(Context context) { Intent intent = new Intent(context, PlayerActivity.class); - intent.putExtra(PlayerActivity.PREFER_EXTENSION_DECODERS, preferExtensionDecoders); + intent.putExtra(PlayerActivity.PREFER_EXTENSION_DECODERS_EXTRA, preferExtensionDecoders); + intent.putExtra(PlayerActivity.ABR_ALGORITHM_EXTRA, abrAlgorithm); if (drmInfo != null) { drmInfo.updateIntent(intent); } - return intent; } @@ -409,13 +503,19 @@ 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; - public UriSample(String name, boolean preferExtensionDecoders, DrmInfo drmInfo, String uri, - String extension, String adTagUri) { - super(name, preferExtensionDecoders, drmInfo); + public UriSample( + String name, + boolean preferExtensionDecoders, + String abrAlgorithm, + DrmInfo drmInfo, + Uri uri, + String extension, + String adTagUri) { + super(name, preferExtensionDecoders, abrAlgorithm, drmInfo); this.uri = uri; this.extension = extension; this.adTagUri = adTagUri; @@ -424,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); @@ -436,9 +536,13 @@ public class SampleChooserActivity extends Activity { public final UriSample[] children; - public PlaylistSample(String name, boolean preferExtensionDecoders, DrmInfo drmInfo, + public PlaylistSample( + String name, + boolean preferExtensionDecoders, + String abrAlgorithm, + DrmInfo drmInfo, UriSample... children) { - super(name, preferExtensionDecoders, drmInfo); + super(name, preferExtensionDecoders, abrAlgorithm, drmInfo); this.children = children; } @@ -447,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 e033b91eef..0000000000 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionHelper.java +++ /dev/null @@ -1,290 +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.FixedTrackSelection; -import com.google.android.exoplayer2.trackselection.MappingTrackSelector; -import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo; -import com.google.android.exoplayer2.trackselection.MappingTrackSelector.SelectionOverride; -import com.google.android.exoplayer2.trackselection.RandomTrackSelection; -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 static final TrackSelection.Factory FIXED_FACTORY = new FixedTrackSelection.Factory(); - private static final TrackSelection.Factory RANDOM_FACTORY = new RandomTrackSelection.Factory(); - - private final MappingTrackSelector selector; - private final TrackSelection.Factory adaptiveTrackSelectionFactory; - - 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 enableRandomAdaptationView; - private CheckedTextView[][] trackViews; - - /** - * @param selector The track selector. - * @param adaptiveTrackSelectionFactory A factory for adaptive {@link TrackSelection}s, or null - * if the selection helper should not support adaptive tracks. - */ - public TrackSelectionHelper(MappingTrackSelector selector, - TrackSelection.Factory adaptiveTrackSelectionFactory) { - this.selector = selector; - this.adaptiveTrackSelectionFactory = adaptiveTrackSelectionFactory; - } - - /** - * 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] = adaptiveTrackSelectionFactory != null - && 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. - boolean haveAdaptiveTracks = false; - trackViews = new CheckedTextView[trackGroups.length][]; - for (int groupIndex = 0; groupIndex < trackGroups.length; groupIndex++) { - TrackGroup group = trackGroups.get(groupIndex); - boolean groupIsAdaptive = trackGroupsAdaptive[groupIndex]; - haveAdaptiveTracks |= groupIsAdaptive; - 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); - } - } - - if (haveAdaptiveTracks) { - // View for using random adaptation. - enableRandomAdaptationView = (CheckedTextView) inflater.inflate( - android.R.layout.simple_list_item_multiple_choice, root, false); - enableRandomAdaptationView.setBackgroundResource(selectableItemBackgroundResourceId); - enableRandomAdaptationView.setText(R.string.enable_random_adaptation); - enableRandomAdaptationView.setOnClickListener(this); - root.addView(inflater.inflate(R.layout.list_divider, root, false)); - root.addView(enableRandomAdaptationView); - } - - 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)); - } - } - if (enableRandomAdaptationView != null) { - boolean enableView = !isDisabled && override != null && override.length > 1; - enableRandomAdaptationView.setEnabled(enableView); - enableRandomAdaptationView.setFocusable(enableView); - if (enableView) { - enableRandomAdaptationView.setChecked(!isDisabled - && override.factory instanceof RandomTrackSelection.Factory); - } - } - } - - // 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 if (view == enableRandomAdaptationView) { - setOverride(override.groupIndex, override.tracks, !enableRandomAdaptationView.isChecked()); - } 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) { - override = new SelectionOverride(FIXED_FACTORY, 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), - enableRandomAdaptationView.isChecked()); - } - } else { - // Add the track to the override. - setOverride(groupIndex, getTracksAdding(override, trackIndex), - enableRandomAdaptationView.isChecked()); - } - } - } - // Update the views with the new state. - updateViews(); - } - - private void setOverride(int group, int[] tracks, boolean enableRandomAdaptation) { - TrackSelection.Factory factory = tracks.length == 1 ? FIXED_FACTORY - : (enableRandomAdaptation ? RANDOM_FACTORY : adaptiveTrackSelectionFactory); - override = new SelectionOverride(factory, 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/proguard-rules.txt b/demos/main/src/main/proguard-rules.txt deleted file mode 100644 index cd201892ab..0000000000 --- a/demos/main/src/main/proguard-rules.txt +++ /dev/null @@ -1,7 +0,0 @@ -# Proguard rules specific to the main demo app. - -# Constructor accessed via reflection in PlayerActivity --dontnote com.google.android.exoplayer2.ext.ima.ImaAdsLoader --keepclassmembers class com.google.android.exoplayer2.ext.ima.ImaAdsLoader { - (android.content.Context, android.net.Uri); -} 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 43b17052fb..eb260e6ffc 100644 --- a/demos/main/src/main/res/values/strings.xml +++ b/demos/main/src/main/res/values/strings.xml @@ -17,19 +17,11 @@ ExoPlayer - Video - - Audio - - Text - - Disabled - - Default - Unexpected intent action: %1$s - Enable random adaptation + 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/build.gradle b/extensions/cast/build.gradle index d7e99573cb..ded92000d3 100644 --- a/extensions/cast/build.gradle +++ b/extensions/cast/build.gradle @@ -30,9 +30,9 @@ dependencies { // com.android.support:support-v4, com.android.support:appcompat-v7 and // com.android.support:mediarouter-v7 to be used. Else older versions are // used, for example: - // com.google.android.gms:play-services-cast-framework:11.4.2 - // |-- com.google.android.gms:play-services-basement:11.4.2 - // |-- com.android.support:support-v4:25.2.0 + // com.google.android.gms:play-services-cast-framework:12.0.0 + // |-- com.google.android.gms:play-services-basement:12.0.0 + // |-- com.android.support:support-v4:26.1.0 api 'com.android.support:support-v4:' + supportLibraryVersion api 'com.android.support:appcompat-v7:' + supportLibraryVersion api 'com.android.support:mediarouter-v7:' + supportLibraryVersion 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 50c883c3f6..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) { @@ -481,6 +487,14 @@ public final class CastPlayer implements Player { : currentTimeline.getPreviousWindowIndex(getCurrentWindowIndex(), repeatMode, false); } + @Override + public @Nullable Object getCurrentTag() { + int windowIndex = getCurrentWindowIndex(); + return windowIndex > currentTimeline.getWindowCount() + ? null + : currentTimeline.getWindow(windowIndex, window, /* setTag= */ true).tag; + } + // TODO: Fill the cast timeline information with ProgressListener's duration updates. // See [Internal: b/65152553]. @Override diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastTimeline.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastTimeline.java index a0be844439..24d815bae2 100644 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastTimeline.java +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastTimeline.java @@ -73,12 +73,22 @@ import java.util.Map; } @Override - public Window getWindow(int windowIndex, Window window, boolean setIds, - long defaultPositionProjectionUs) { + public Window getWindow( + int windowIndex, Window window, boolean setTag, long defaultPositionProjectionUs) { long durationUs = durationsUs[windowIndex]; boolean isDynamic = durationUs == C.TIME_UNSET; - return window.set(ids[windowIndex], C.TIME_UNSET, C.TIME_UNSET, !isDynamic, isDynamic, - defaultPositionsUs[windowIndex], durationUs, windowIndex, windowIndex, 0); + Object tag = setTag ? ids[windowIndex] : null; + return window.set( + tag, + /* presentationStartTimeMs= */ C.TIME_UNSET, + /* windowStartTimeMs= */ C.TIME_UNSET, + /* isSeekable= */ !isDynamic, + isDynamic, + defaultPositionsUs[windowIndex], + durationUs, + /* firstPeriodIndex= */ windowIndex, + /* lastPeriodIndex= */ windowIndex, + /* positionInFirstPeriodUs= */ 0); } @Override diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastUtils.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastUtils.java index f17c39bdbf..d2154eec1b 100644 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastUtils.java +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastUtils.java @@ -89,7 +89,7 @@ import com.google.android.gms.cast.MediaTrack; case CastStatusCodes.UNKNOWN_ERROR: return "An unknown, unexpected error has occurred."; default: - return "Unknown: " + statusCode; + return CastStatusCodes.getStatusCodeString(statusCode); } } diff --git a/extensions/cast/src/test/AndroidManifest.xml b/extensions/cast/src/test/AndroidManifest.xml index 3f34bbb1f5..aea8bda663 100644 --- a/extensions/cast/src/test/AndroidManifest.xml +++ b/extensions/cast/src/test/AndroidManifest.xml @@ -14,9 +14,4 @@ limitations under the License. --> - - - - - + 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/cronet/src/test/AndroidManifest.xml b/extensions/cronet/src/test/AndroidManifest.xml index a1512ae605..82cffe17c2 100644 --- a/extensions/cronet/src/test/AndroidManifest.xml +++ b/extensions/cronet/src/test/AndroidManifest.xml @@ -14,9 +14,4 @@ limitations under the License. --> - - - - - + 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 f617064ce5..609953130b 100644 --- a/extensions/flac/build.gradle +++ b/extensions/flac/build.gradle @@ -31,8 +31,10 @@ android { } 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/androidTest/AndroidManifest.xml b/extensions/flac/src/androidTest/AndroidManifest.xml index 38a6bfc927..4e3925d8e3 100644 --- a/extensions/flac/src/androidTest/AndroidManifest.xml +++ b/extensions/flac/src/androidTest/AndroidManifest.xml @@ -18,8 +18,6 @@ xmlns:tools="http://schemas.android.com/tools" package="com.google.android.exoplayer2.ext.flac.test"> - - diff --git a/extensions/flac/src/androidTest/assets/bear.flac.0.dump b/extensions/flac/src/androidTest/assets/bear.flac.0.dump index ad88981718..71359322b0 100644 --- a/extensions/flac/src/androidTest/assets/bear.flac.0.dump +++ b/extensions/flac/src/androidTest/assets/bear.flac.0.dump @@ -13,13 +13,13 @@ track 0: width = -1 height = -1 frameRate = -1.0 - rotationDegrees = -1 - pixelWidthHeightRatio = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 channelCount = 2 sampleRate = 48000 pcmEncoding = 2 - encoderDelay = -1 - encoderPadding = -1 + encoderDelay = 0 + encoderPadding = 0 subsampleOffsetUs = 9223372036854775807 selectionFlags = 0 language = null diff --git a/extensions/flac/src/androidTest/assets/bear.flac.1.dump b/extensions/flac/src/androidTest/assets/bear.flac.1.dump index 22f30e9db2..820b9eed10 100644 --- a/extensions/flac/src/androidTest/assets/bear.flac.1.dump +++ b/extensions/flac/src/androidTest/assets/bear.flac.1.dump @@ -13,13 +13,13 @@ track 0: width = -1 height = -1 frameRate = -1.0 - rotationDegrees = -1 - pixelWidthHeightRatio = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 channelCount = 2 sampleRate = 48000 pcmEncoding = 2 - encoderDelay = -1 - encoderPadding = -1 + encoderDelay = 0 + encoderPadding = 0 subsampleOffsetUs = 9223372036854775807 selectionFlags = 0 language = null diff --git a/extensions/flac/src/androidTest/assets/bear.flac.2.dump b/extensions/flac/src/androidTest/assets/bear.flac.2.dump index c52a74cbfb..c2d58347eb 100644 --- a/extensions/flac/src/androidTest/assets/bear.flac.2.dump +++ b/extensions/flac/src/androidTest/assets/bear.flac.2.dump @@ -13,13 +13,13 @@ track 0: width = -1 height = -1 frameRate = -1.0 - rotationDegrees = -1 - pixelWidthHeightRatio = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 channelCount = 2 sampleRate = 48000 pcmEncoding = 2 - encoderDelay = -1 - encoderPadding = -1 + encoderDelay = 0 + encoderPadding = 0 subsampleOffsetUs = 9223372036854775807 selectionFlags = 0 language = null diff --git a/extensions/flac/src/androidTest/assets/bear.flac.3.dump b/extensions/flac/src/androidTest/assets/bear.flac.3.dump index 760f369597..8c1115f1ec 100644 --- a/extensions/flac/src/androidTest/assets/bear.flac.3.dump +++ b/extensions/flac/src/androidTest/assets/bear.flac.3.dump @@ -13,13 +13,13 @@ track 0: width = -1 height = -1 frameRate = -1.0 - rotationDegrees = -1 - pixelWidthHeightRatio = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 channelCount = 2 sampleRate = 48000 pcmEncoding = 2 - encoderDelay = -1 - encoderPadding = -1 + encoderDelay = 0 + encoderPadding = 0 subsampleOffsetUs = 9223372036854775807 selectionFlags = 0 language = null diff --git a/extensions/flac/src/androidTest/assets/bear_with_id3.flac b/extensions/flac/src/androidTest/assets/bear_with_id3.flac new file mode 100644 index 0000000000..fc945f14ad Binary files /dev/null and b/extensions/flac/src/androidTest/assets/bear_with_id3.flac differ diff --git a/extensions/flac/src/androidTest/assets/bear_with_id3.flac.0.dump b/extensions/flac/src/androidTest/assets/bear_with_id3.flac.0.dump new file mode 100644 index 0000000000..d8903fcade --- /dev/null +++ b/extensions/flac/src/androidTest/assets/bear_with_id3.flac.0.dump @@ -0,0 +1,162 @@ +seekMap: + isSeekable = true + duration = 2741000 + getPosition(0) = [[timeUs=0, position=55284]] +numberOfTracks = 1 +track 0: + format: + bitrate = 768000 + id = null + containerMimeType = null + sampleMimeType = audio/raw + maxInputSize = 16384 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = 2 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = null + drmInitData = - + initializationData: + total output bytes = 526272 + sample count = 33 + sample 0: + time = 0 + flags = 1 + data = length 16384, hash 61D2C5C2 + sample 1: + time = 85333 + flags = 1 + data = length 16384, hash E6D7F214 + sample 2: + time = 170666 + flags = 1 + data = length 16384, hash 59BF0D5D + sample 3: + time = 256000 + flags = 1 + data = length 16384, hash 3625F468 + sample 4: + time = 341333 + flags = 1 + data = length 16384, hash F66A323 + sample 5: + time = 426666 + flags = 1 + data = length 16384, hash CDBAE629 + sample 6: + time = 512000 + flags = 1 + data = length 16384, hash 536F3A91 + sample 7: + time = 597333 + flags = 1 + data = length 16384, hash D4F35C9C + sample 8: + time = 682666 + flags = 1 + data = length 16384, hash EE04CEBF + sample 9: + time = 768000 + flags = 1 + data = length 16384, hash 647E2A67 + sample 10: + time = 853333 + flags = 1 + data = length 16384, hash 31583F2C + sample 11: + time = 938666 + flags = 1 + data = length 16384, hash E433A93D + sample 12: + time = 1024000 + flags = 1 + data = length 16384, hash 5E1C7051 + sample 13: + time = 1109333 + flags = 1 + data = length 16384, hash 43E6E358 + sample 14: + time = 1194666 + flags = 1 + data = length 16384, hash 5DC1B256 + sample 15: + time = 1280000 + flags = 1 + data = length 16384, hash 3D9D95CF + sample 16: + time = 1365333 + flags = 1 + data = length 16384, hash 2A5BD2C0 + sample 17: + time = 1450666 + flags = 1 + data = length 16384, hash 93E25061 + sample 18: + time = 1536000 + flags = 1 + data = length 16384, hash B81793D8 + sample 19: + time = 1621333 + flags = 1 + data = length 16384, hash 1A3BD49F + sample 20: + time = 1706666 + flags = 1 + data = length 16384, hash FB672FF1 + sample 21: + time = 1792000 + flags = 1 + data = length 16384, hash 48AB8B45 + sample 22: + time = 1877333 + flags = 1 + data = length 16384, hash 13C9640A + sample 23: + time = 1962666 + flags = 1 + data = length 16384, hash 499E4A0B + sample 24: + time = 2048000 + flags = 1 + data = length 16384, hash F9A783E6 + sample 25: + time = 2133333 + flags = 1 + data = length 16384, hash D2B77598 + sample 26: + time = 2218666 + flags = 1 + data = length 16384, hash CE5B826C + sample 27: + time = 2304000 + flags = 1 + data = length 16384, hash E99EE956 + sample 28: + time = 2389333 + flags = 1 + data = length 16384, hash F2DB1486 + sample 29: + time = 2474666 + flags = 1 + data = length 16384, hash 1636EAB + sample 30: + time = 2560000 + flags = 1 + data = length 16384, hash 23457C08 + sample 31: + time = 2645333 + flags = 1 + data = length 16384, hash 30EB8381 + sample 32: + time = 2730666 + flags = 1 + data = length 1984, hash 59CFDE1B +tracksEnded = true diff --git a/extensions/flac/src/androidTest/assets/bear_with_id3.flac.1.dump b/extensions/flac/src/androidTest/assets/bear_with_id3.flac.1.dump new file mode 100644 index 0000000000..100fdd1eaf --- /dev/null +++ b/extensions/flac/src/androidTest/assets/bear_with_id3.flac.1.dump @@ -0,0 +1,122 @@ +seekMap: + isSeekable = true + duration = 2741000 + getPosition(0) = [[timeUs=0, position=55284]] +numberOfTracks = 1 +track 0: + format: + bitrate = 768000 + id = null + containerMimeType = null + sampleMimeType = audio/raw + maxInputSize = 16384 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = 2 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = null + drmInitData = - + initializationData: + total output bytes = 362432 + sample count = 23 + sample 0: + time = 853333 + flags = 1 + data = length 16384, hash 31583F2C + sample 1: + time = 938666 + flags = 1 + data = length 16384, hash E433A93D + sample 2: + time = 1024000 + flags = 1 + data = length 16384, hash 5E1C7051 + sample 3: + time = 1109333 + flags = 1 + data = length 16384, hash 43E6E358 + sample 4: + time = 1194666 + flags = 1 + data = length 16384, hash 5DC1B256 + sample 5: + time = 1280000 + flags = 1 + data = length 16384, hash 3D9D95CF + sample 6: + time = 1365333 + flags = 1 + data = length 16384, hash 2A5BD2C0 + sample 7: + time = 1450666 + flags = 1 + data = length 16384, hash 93E25061 + sample 8: + time = 1536000 + flags = 1 + data = length 16384, hash B81793D8 + sample 9: + time = 1621333 + flags = 1 + data = length 16384, hash 1A3BD49F + sample 10: + time = 1706666 + flags = 1 + data = length 16384, hash FB672FF1 + sample 11: + time = 1792000 + flags = 1 + data = length 16384, hash 48AB8B45 + sample 12: + time = 1877333 + flags = 1 + data = length 16384, hash 13C9640A + sample 13: + time = 1962666 + flags = 1 + data = length 16384, hash 499E4A0B + sample 14: + time = 2048000 + flags = 1 + data = length 16384, hash F9A783E6 + sample 15: + time = 2133333 + flags = 1 + data = length 16384, hash D2B77598 + sample 16: + time = 2218666 + flags = 1 + data = length 16384, hash CE5B826C + sample 17: + time = 2304000 + flags = 1 + data = length 16384, hash E99EE956 + sample 18: + time = 2389333 + flags = 1 + data = length 16384, hash F2DB1486 + sample 19: + time = 2474666 + flags = 1 + data = length 16384, hash 1636EAB + sample 20: + time = 2560000 + flags = 1 + data = length 16384, hash 23457C08 + sample 21: + time = 2645333 + flags = 1 + data = length 16384, hash 30EB8381 + sample 22: + time = 2730666 + flags = 1 + data = length 1984, hash 59CFDE1B +tracksEnded = true diff --git a/extensions/flac/src/androidTest/assets/bear_with_id3.flac.2.dump b/extensions/flac/src/androidTest/assets/bear_with_id3.flac.2.dump new file mode 100644 index 0000000000..6c3cd731b3 --- /dev/null +++ b/extensions/flac/src/androidTest/assets/bear_with_id3.flac.2.dump @@ -0,0 +1,78 @@ +seekMap: + isSeekable = true + duration = 2741000 + getPosition(0) = [[timeUs=0, position=55284]] +numberOfTracks = 1 +track 0: + format: + bitrate = 768000 + id = null + containerMimeType = null + sampleMimeType = audio/raw + maxInputSize = 16384 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = 2 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = null + drmInitData = - + initializationData: + total output bytes = 182208 + sample count = 12 + sample 0: + time = 1792000 + flags = 1 + data = length 16384, hash 48AB8B45 + sample 1: + time = 1877333 + flags = 1 + data = length 16384, hash 13C9640A + sample 2: + time = 1962666 + flags = 1 + data = length 16384, hash 499E4A0B + sample 3: + time = 2048000 + flags = 1 + data = length 16384, hash F9A783E6 + sample 4: + time = 2133333 + flags = 1 + data = length 16384, hash D2B77598 + sample 5: + time = 2218666 + flags = 1 + data = length 16384, hash CE5B826C + sample 6: + time = 2304000 + flags = 1 + data = length 16384, hash E99EE956 + sample 7: + time = 2389333 + flags = 1 + data = length 16384, hash F2DB1486 + sample 8: + time = 2474666 + flags = 1 + data = length 16384, hash 1636EAB + sample 9: + time = 2560000 + flags = 1 + data = length 16384, hash 23457C08 + sample 10: + time = 2645333 + flags = 1 + data = length 16384, hash 30EB8381 + sample 11: + time = 2730666 + flags = 1 + data = length 1984, hash 59CFDE1B +tracksEnded = true diff --git a/extensions/flac/src/androidTest/assets/bear_with_id3.flac.3.dump b/extensions/flac/src/androidTest/assets/bear_with_id3.flac.3.dump new file mode 100644 index 0000000000..decf9c6af3 --- /dev/null +++ b/extensions/flac/src/androidTest/assets/bear_with_id3.flac.3.dump @@ -0,0 +1,38 @@ +seekMap: + isSeekable = true + duration = 2741000 + getPosition(0) = [[timeUs=0, position=55284]] +numberOfTracks = 1 +track 0: + format: + bitrate = 768000 + id = null + containerMimeType = null + sampleMimeType = audio/raw + maxInputSize = 16384 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = 2 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = null + drmInitData = - + initializationData: + total output bytes = 18368 + sample count = 2 + sample 0: + time = 2645333 + flags = 1 + data = length 16384, hash 30EB8381 + sample 1: + time = 2730666 + flags = 1 + data = length 1984, hash 59CFDE1B +tracksEnded = true diff --git a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorTest.java b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorTest.java index c5f1f5c146..fc9bdac2ea 100644 --- a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorTest.java +++ b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorTest.java @@ -33,7 +33,7 @@ public class FlacExtractorTest extends InstrumentationTestCase { } } - public void testSample() throws Exception { + public void testExtractFlacSample() throws Exception { ExtractorAsserts.assertBehavior( new ExtractorFactory() { @Override @@ -44,4 +44,16 @@ public class FlacExtractorTest extends InstrumentationTestCase { "bear.flac", getInstrumentation().getContext()); } + + public void testExtractFlacSampleWithId3Header() throws Exception { + ExtractorAsserts.assertBehavior( + new ExtractorFactory() { + @Override + public Extractor create() { + return new FlacExtractor(); + } + }, + "bear_with_id3.flac", + getInstrumentation().getContext()); + } } 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 6859b44877..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 @@ -17,20 +17,27 @@ package com.google.android.exoplayer2.ext.flac; import static com.google.android.exoplayer2.util.Util.getPcmEncoding; +import android.support.annotation.IntDef; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; 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.Id3Peeker; import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.SeekPoint; import com.google.android.exoplayer2.extractor.TrackOutput; +import com.google.android.exoplayer2.metadata.Metadata; +import com.google.android.exoplayer2.metadata.id3.Id3Decoder; import com.google.android.exoplayer2.util.FlacStreamInfo; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableByteArray; import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.nio.ByteBuffer; import java.util.Arrays; @@ -51,22 +58,56 @@ public final class FlacExtractor implements Extractor { }; + /** Flags controlling the behavior of the extractor. */ + @Retention(RetentionPolicy.SOURCE) + @IntDef( + flag = true, + value = {FLAG_DISABLE_ID3_METADATA} + ) + public @interface Flags {} + + /** + * Flag to disable parsing of ID3 metadata. Can be set to save memory if ID3 metadata is not + * required. + */ + public static final int FLAG_DISABLE_ID3_METADATA = 1; + /** * FLAC signature: first 4 is the signature word, second 4 is the sizeof STREAMINFO. 0x22 is the * mandatory STREAMINFO. */ private static final byte[] FLAC_SIGNATURE = {'f', 'L', 'a', 'C', 0, 0, 0, 0x22}; - private ExtractorOutput extractorOutput; - private TrackOutput trackOutput; + private final Id3Peeker id3Peeker; + private final boolean isId3MetadataDisabled; private FlacDecoderJni decoderJni; - private boolean metadataParsed; + private ExtractorOutput extractorOutput; + private TrackOutput trackOutput; private ParsableByteArray outputBuffer; private ByteBuffer outputByteBuffer; + private Metadata id3Metadata; + + private boolean metadataParsed; + + /** Constructs an instance with flags = 0. */ + public FlacExtractor() { + this(0); + } + + /** + * Constructs an instance. + * + * @param flags Flags that control the extractor's behavior. + */ + public FlacExtractor(int flags) { + id3Peeker = new Id3Peeker(); + isId3MetadataDisabled = (flags & FLAG_DISABLE_ID3_METADATA) != 0; + } + @Override public void init(ExtractorOutput output) { extractorOutput = output; @@ -81,14 +122,19 @@ public final class FlacExtractor implements Extractor { @Override public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { - byte[] header = new byte[FLAC_SIGNATURE.length]; - input.peekFully(header, 0, FLAC_SIGNATURE.length); - return Arrays.equals(header, FLAC_SIGNATURE); + if (input.getPosition() == 0) { + id3Metadata = peekId3Data(input); + } + return peekFlacSignature(input); } @Override public int read(final ExtractorInput input, PositionHolder seekPosition) throws IOException, InterruptedException { + if (input.getPosition() == 0 && !isId3MetadataDisabled && id3Metadata == null) { + id3Metadata = peekId3Data(input); + } + decoderJni.setData(input); if (!metadataParsed) { @@ -112,18 +158,21 @@ public final class FlacExtractor implements Extractor { : new SeekMap.Unseekable(streamInfo.durationUs(), 0)); Format mediaFormat = Format.createAudioSampleFormat( - null, + /* id= */ null, MimeTypes.AUDIO_RAW, - null, + /* codecs= */ null, streamInfo.bitRate(), streamInfo.maxDecodedFrameSize(), streamInfo.channels, streamInfo.sampleRate, getPcmEncoding(streamInfo.bitsPerSample), - null, - null, - 0, - null); + /* encoderDelay= */ 0, + /* encoderPadding= */ 0, + /* initializationData= */ null, + /* drmInitData= */ null, + /* selectionFlags= */ 0, + /* language= */ null, + isId3MetadataDisabled ? null : id3Metadata); trackOutput.format(mediaFormat); outputBuffer = new ParsableByteArray(streamInfo.maxDecodedFrameSize()); @@ -170,6 +219,31 @@ public final class FlacExtractor implements Extractor { } } + /** + * Peeks ID3 tag data (if present) at the beginning of the input. + * + * @return The first ID3 tag decoded into a {@link Metadata} object. May be null if ID3 tag is not + * present in the input. + */ + @Nullable + private Metadata peekId3Data(ExtractorInput input) throws IOException, InterruptedException { + input.resetPeekPosition(); + Id3Decoder.FramePredicate id3FramePredicate = + isId3MetadataDisabled ? Id3Decoder.NO_FRAMES_PREDICATE : null; + return id3Peeker.peekId3Data(input, id3FramePredicate); + } + + /** + * Peeks from the beginning of the input to see if {@link #FLAC_SIGNATURE} is present. + * + * @return Whether the input begins with {@link #FLAC_SIGNATURE}. + */ + private boolean peekFlacSignature(ExtractorInput input) throws IOException, InterruptedException { + byte[] header = new byte[FLAC_SIGNATURE.length]; + input.peekFully(header, 0, FLAC_SIGNATURE.length); + return Arrays.equals(header, FLAC_SIGNATURE); + } + private static final class FlacSeekMap implements SeekMap { private final long durationUs; diff --git a/extensions/flac/src/main/jni/flac_parser.cc b/extensions/flac/src/main/jni/flac_parser.cc index b9918e7871..83d3367415 100644 --- a/extensions/flac/src/main/jni/flac_parser.cc +++ b/extensions/flac/src/main/jni/flac_parser.cc @@ -319,6 +319,8 @@ bool FLACParser::decodeMetadata() { case 48000: case 88200: case 96000: + case 176400: + case 192000: break; default: ALOGE("unsupported sample rate %u", getSampleRate()); diff --git a/extensions/gvr/build.gradle b/extensions/gvr/build.gradle index f146ba4df6..87e72939c5 100644 --- a/extensions/gvr/build.gradle +++ b/extensions/gvr/build.gradle @@ -26,6 +26,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') + implementation 'com.android.support:support-annotations:' + supportLibraryVersion implementation 'com.google.vr:sdk-audio:1.80.0' } diff --git a/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrAudioProcessor.java b/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrAudioProcessor.java index 8d71f551cd..1b595d6886 100644 --- a/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrAudioProcessor.java +++ b/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrAudioProcessor.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.ext.gvr; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.Format; @@ -39,7 +40,7 @@ public final class GvrAudioProcessor implements AudioProcessor { private int sampleRateHz; private int channelCount; - private GvrAudioSurround gvrAudioSurround; + @Nullable private GvrAudioSurround gvrAudioSurround; private ByteBuffer buffer; private boolean inputEnded; @@ -48,14 +49,13 @@ public final class GvrAudioProcessor implements AudioProcessor { private float y; private float z; - /** - * Creates a new GVR audio processor. - */ + /** Creates a new GVR audio processor. */ public GvrAudioProcessor() { // Use the identity for the initial orientation. w = 1f; sampleRateHz = Format.NO_VALUE; channelCount = Format.NO_VALUE; + buffer = EMPTY_BUFFER; } /** @@ -77,9 +77,11 @@ public final class GvrAudioProcessor implements AudioProcessor { } } + @SuppressWarnings("ReferenceEquality") @Override - public synchronized boolean configure(int sampleRateHz, int channelCount, - @C.Encoding int encoding) throws UnhandledFormatException { + public synchronized boolean configure( + int sampleRateHz, int channelCount, @C.Encoding int encoding) + throws UnhandledFormatException { if (encoding != C.ENCODING_PCM_16BIT) { maybeReleaseGvrAudioSurround(); throw new UnhandledFormatException(sampleRateHz, channelCount, encoding); @@ -116,7 +118,7 @@ public final class GvrAudioProcessor implements AudioProcessor { gvrAudioSurround = new GvrAudioSurround(surroundFormat, sampleRateHz, channelCount, FRAMES_PER_OUTPUT_BUFFER); gvrAudioSurround.updateNativeOrientation(w, x, y, z); - if (buffer == null) { + if (buffer == EMPTY_BUFFER) { buffer = ByteBuffer.allocateDirect(FRAMES_PER_OUTPUT_BUFFER * OUTPUT_FRAME_SIZE) .order(ByteOrder.nativeOrder()); } @@ -179,10 +181,11 @@ public final class GvrAudioProcessor implements AudioProcessor { @Override public synchronized void reset() { maybeReleaseGvrAudioSurround(); + updateOrientation(/* w= */ 1f, /* x= */ 0f, /* y= */ 0f, /* z= */ 0f); inputEnded = false; - buffer = null; sampleRateHz = Format.NO_VALUE; channelCount = Format.NO_VALUE; + buffer = EMPTY_BUFFER; } private void maybeReleaseGvrAudioSurround() { diff --git a/extensions/ima/build.gradle b/extensions/ima/build.gradle index 1a35ad3450..3529e05380 100644 --- a/extensions/ima/build.gradle +++ b/extensions/ima/build.gradle @@ -29,12 +29,12 @@ dependencies { // This dependency is necessary to force the supportLibraryVersion of // com.android.support:support-v4 to be used. Else an older version (25.2.0) // is included via: - // com.google.android.gms:play-services-ads:11.4.2 - // |-- com.google.android.gms:play-services-ads-lite:11.4.2 - // |-- com.google.android.gms:play-services-basement:11.4.2 - // |-- com.android.support:support-v4:25.2.0 + // com.google.android.gms:play-services-ads:12.0.0 + // |-- com.google.android.gms:play-services-ads-lite:12.0.0 + // |-- com.google.android.gms:play-services-basement:12.0.0 + // |-- com.android.support:support-v4:26.1.0 api 'com.android.support:support-v4:' + supportLibraryVersion - api 'com.google.ads.interactivemedia.v3:interactivemedia:3.7.4' + api 'com.google.ads.interactivemedia.v3:interactivemedia:3.8.5' implementation project(modulePrefix + 'library-core') implementation 'com.google.android.gms:play-services-ads:' + playServicesLibraryVersion } diff --git a/extensions/ima/src/main/AndroidManifest.xml b/extensions/ima/src/main/AndroidManifest.xml index 22fb518c58..1bb79ff21d 100644 --- a/extensions/ima/src/main/AndroidManifest.xml +++ b/extensions/ima/src/main/AndroidManifest.xml @@ -1,3 +1,18 @@ + + = 0); this.vastLoadTimeoutMs = vastLoadTimeoutMs; return this; } + /** + * Sets the ad media load timeout, in milliseconds. + * + * @param mediaLoadTimeoutMs The ad media load timeout, in milliseconds. + * @return This builder, for convenience. + * @see AdsRenderingSettings#setLoadVideoTimeout(int) + */ + public Builder setMediaLoadTimeoutMs(int mediaLoadTimeoutMs) { + Assertions.checkArgument(mediaLoadTimeoutMs >= 0); + this.mediaLoadTimeoutMs = mediaLoadTimeoutMs; + return this; + } + /** * Returns a new {@link ImaAdsLoader} for the specified ad tag. * @@ -128,7 +158,14 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A * @return The new {@link ImaAdsLoader}. */ public ImaAdsLoader buildForAdTag(Uri adTagUri) { - return new ImaAdsLoader(context, adTagUri, imaSdkSettings, null, vastLoadTimeoutMs); + return new ImaAdsLoader( + context, + adTagUri, + imaSdkSettings, + null, + vastLoadTimeoutMs, + mediaLoadTimeoutMs, + adEventListener); } /** @@ -139,7 +176,14 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A * @return The new {@link ImaAdsLoader}. */ public ImaAdsLoader buildForAdsResponse(String adsResponse) { - return new ImaAdsLoader(context, null, imaSdkSettings, adsResponse, vastLoadTimeoutMs); + return new ImaAdsLoader( + context, + null, + imaSdkSettings, + adsResponse, + vastLoadTimeoutMs, + mediaLoadTimeoutMs, + adEventListener); } } @@ -174,6 +218,8 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A private static final String FOCUS_SKIP_BUTTON_WORKAROUND_JS = "javascript:" + "try{ document.getElementsByClassName(\"videoAdUiSkipButton\")[0].focus(); } catch (e) {}"; + private static final int TIMEOUT_UNSET = -1; + /** The state of ad playback. */ @Retention(RetentionPolicy.SOURCE) @IntDef({IMA_AD_STATE_NONE, IMA_AD_STATE_PLAYING, IMA_AD_STATE_PAUSED}) @@ -193,7 +239,9 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A private final @Nullable Uri adTagUri; private final @Nullable String adsResponse; - private final long vastLoadTimeoutMs; + 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; @@ -209,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; @@ -282,7 +330,14 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A * more information. */ public ImaAdsLoader(Context context, Uri adTagUri) { - this(context, adTagUri, null, null, C.TIME_UNSET); + this( + context, + adTagUri, + /* imaSdkSettings= */ null, + /* adsResponse= */ null, + /* vastLoadTimeoutMs= */ TIMEOUT_UNSET, + /* mediaLoadTimeoutMs= */ TIMEOUT_UNSET, + /* adEventListener= */ null); } /** @@ -298,7 +353,14 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A */ @Deprecated public ImaAdsLoader(Context context, Uri adTagUri, ImaSdkSettings imaSdkSettings) { - this(context, adTagUri, imaSdkSettings, null, C.TIME_UNSET); + this( + context, + adTagUri, + imaSdkSettings, + /* adsResponse= */ null, + /* vastLoadTimeoutMs= */ TIMEOUT_UNSET, + /* mediaLoadTimeoutMs= */ TIMEOUT_UNSET, + /* adEventListener= */ null); } private ImaAdsLoader( @@ -306,11 +368,15 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A @Nullable Uri adTagUri, @Nullable ImaSdkSettings imaSdkSettings, @Nullable String adsResponse, - long vastLoadTimeoutMs) { + int vastLoadTimeoutMs, + 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(); @@ -361,7 +427,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A } else /* adsResponse != null */ { request.setAdsResponse(adsResponse); } - if (vastLoadTimeoutMs != C.TIME_UNSET) { + if (vastLoadTimeoutMs != TIMEOUT_UNSET) { request.setVastLoadTimeout(vastLoadTimeoutMs); } request.setAdDisplayContainer(adDisplayContainer); @@ -466,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 { @@ -510,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(); } @@ -796,6 +865,9 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A AdsRenderingSettings adsRenderingSettings = imaSdkFactory.createAdsRenderingSettings(); adsRenderingSettings.setEnablePreloading(ENABLE_PRELOADING); adsRenderingSettings.setMimeTypes(supportedMimeTypes); + if (mediaLoadTimeoutMs != TIMEOUT_UNSET) { + adsRenderingSettings.setLoadVideoTimeout(mediaLoadTimeoutMs); + } // Set up the ad playback state, skipping ads based on the start position as required. long[] adGroupTimesUs = getAdGroupTimesUs(adsManager.getAdCuePoints()); @@ -900,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: @@ -974,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) { @@ -996,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) { @@ -1074,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(); @@ -1098,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/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java index 1899c815da..d3e1d9725e 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java @@ -20,6 +20,7 @@ import android.support.annotation.Nullable; import android.view.ViewGroup; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.source.BaseMediaSource; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.ads.AdsMediaSource; @@ -33,10 +34,12 @@ import java.io.IOException; * @deprecated Use com.google.android.exoplayer2.source.ads.AdsMediaSource with ImaAdsLoader. */ @Deprecated -public final class ImaAdsMediaSource implements MediaSource { +public final class ImaAdsMediaSource extends BaseMediaSource { private final AdsMediaSource adsMediaSource; + private SourceInfoRefreshListener adsMediaSourceListener; + /** * Constructs a new source that inserts ads linearly with the content specified by * {@code contentMediaSource}. @@ -74,18 +77,16 @@ public final class ImaAdsMediaSource implements MediaSource { } @Override - public void prepareSource( - final ExoPlayer player, boolean isTopLevelSource, final Listener listener) { - adsMediaSource.prepareSource( - player, - isTopLevelSource, - new Listener() { + public void prepareSourceInternal(final ExoPlayer player, boolean isTopLevelSource) { + adsMediaSourceListener = + new SourceInfoRefreshListener() { @Override public void onSourceInfoRefreshed( MediaSource source, Timeline timeline, @Nullable Object manifest) { - listener.onSourceInfoRefreshed(ImaAdsMediaSource.this, timeline, manifest); + refreshSourceInfo(timeline, manifest); } - }); + }; + adsMediaSource.prepareSource(player, isTopLevelSource, adsMediaSourceListener); } @Override @@ -104,7 +105,7 @@ public final class ImaAdsMediaSource implements MediaSource { } @Override - public void releaseSource() { - adsMediaSource.releaseSource(); + public void releaseSourceInternal() { + adsMediaSource.releaseSource(adsMediaSourceListener); } } diff --git a/extensions/jobdispatcher/README.md b/extensions/jobdispatcher/README.md new file mode 100644 index 0000000000..f70125ba38 --- /dev/null +++ b/extensions/jobdispatcher/README.md @@ -0,0 +1,23 @@ +# ExoPlayer Firebase JobDispatcher extension # + +This extension provides a Scheduler implementation which uses [Firebase JobDispatcher][]. + +[Firebase JobDispatcher]: https://github.com/firebase/firebase-jobdispatcher-android + +## Getting the extension ## + +The easiest way to use the extension is to add it as a gradle dependency: + +```gradle +implementation 'com.google.android.exoplayer:extension-jobdispatcher:2.X.X' +``` + +where `2.X.X` is the version, which must match the version of the ExoPlayer +library being used. + +Alternatively, you can clone the ExoPlayer repository and depend on the module +locally. Instructions for doing this can be found in ExoPlayer's +[top level README][]. + +[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md + diff --git a/extensions/jobdispatcher/build.gradle b/extensions/jobdispatcher/build.gradle new file mode 100644 index 0000000000..f4a8751c67 --- /dev/null +++ b/extensions/jobdispatcher/build.gradle @@ -0,0 +1,43 @@ +/* + * 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. + */ +apply from: '../../constants.gradle' +apply plugin: 'com.android.library' + +android { + compileSdkVersion project.ext.compileSdkVersion + buildToolsVersion project.ext.buildToolsVersion + + defaultConfig { + minSdkVersion project.ext.minSdkVersion + targetSdkVersion project.ext.targetSdkVersion + } +} + +dependencies { + implementation project(modulePrefix + 'library-core') + implementation 'com.firebase:firebase-jobdispatcher:0.8.5' +} + +ext { + javadocTitle = 'Firebase JobDispatcher extension' +} +apply from: '../../javadoc_library.gradle' + +ext { + releaseArtifact = 'extension-jobdispatcher' + releaseDescription = 'Firebase JobDispatcher extension for ExoPlayer.' +} +apply from: '../../publish.gradle' diff --git a/extensions/jobdispatcher/src/main/AndroidManifest.xml b/extensions/jobdispatcher/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..306a087e6c --- /dev/null +++ b/extensions/jobdispatcher/src/main/AndroidManifest.xml @@ -0,0 +1,18 @@ + + + + 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 new file mode 100644 index 0000000000..c6701da964 --- /dev/null +++ b/extensions/jobdispatcher/src/main/java/com/google/android/exoplayer2/ext/jobdispatcher/JobDispatcherScheduler.java @@ -0,0 +1,169 @@ +/* + * 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.ext.jobdispatcher; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.util.Log; +import com.firebase.jobdispatcher.Constraint; +import com.firebase.jobdispatcher.FirebaseJobDispatcher; +import com.firebase.jobdispatcher.GooglePlayDriver; +import com.firebase.jobdispatcher.Job; +import com.firebase.jobdispatcher.Job.Builder; +import com.firebase.jobdispatcher.JobParameters; +import com.firebase.jobdispatcher.JobService; +import com.firebase.jobdispatcher.Lifetime; +import com.google.android.exoplayer2.scheduler.Requirements; +import com.google.android.exoplayer2.scheduler.Scheduler; +import com.google.android.exoplayer2.util.Util; + +/** + * A {@link Scheduler} that uses {@link FirebaseJobDispatcher}. To use this scheduler, you must add + * {@link JobDispatcherSchedulerService} to your manifest: + * + *

{@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)} + * + * @see GoogleApiAvailability + */ +public final class JobDispatcherScheduler implements Scheduler { + + private static final String TAG = "JobDispatcherScheduler"; + 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 FirebaseJobDispatcher jobDispatcher; + + /** + * @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, String jobTag) { + this.jobDispatcher = + new FirebaseJobDispatcher(new GooglePlayDriver(context.getApplicationContext())); + this.jobTag = jobTag; + } + + @Override + public boolean schedule(Requirements requirements, String serviceAction, String servicePackage) { + Job job = buildJob(jobDispatcher, requirements, jobTag, serviceAction, servicePackage); + int result = jobDispatcher.schedule(job); + logd("Scheduling job: " + jobTag + " result: " + result); + return result == FirebaseJobDispatcher.SCHEDULE_RESULT_SUCCESS; + } + + @Override + public boolean cancel() { + int result = jobDispatcher.cancel(jobTag); + logd("Canceling job: " + jobTag + " result: " + result); + return result == FirebaseJobDispatcher.CANCEL_RESULT_SUCCESS; + } + + private static Job buildJob( + FirebaseJobDispatcher dispatcher, + Requirements requirements, + String tag, + String serviceAction, + String servicePackage) { + Builder builder = + dispatcher + .newJobBuilder() + .setService(JobDispatcherSchedulerService.class) // the JobService that will be called + .setTag(tag); + + switch (requirements.getRequiredNetworkType()) { + case Requirements.NETWORK_TYPE_NONE: + // do nothing. + break; + case Requirements.NETWORK_TYPE_ANY: + builder.addConstraint(Constraint.ON_ANY_NETWORK); + break; + case Requirements.NETWORK_TYPE_UNMETERED: + builder.addConstraint(Constraint.ON_UNMETERED_NETWORK); + break; + default: + throw new UnsupportedOperationException(); + } + + if (requirements.isIdleRequired()) { + builder.addConstraint(Constraint.DEVICE_IDLE); + } + if (requirements.isChargingRequired()) { + builder.addConstraint(Constraint.DEVICE_CHARGING); + } + builder.setLifetime(Lifetime.FOREVER).setReplaceCurrent(true); + + Bundle extras = new Bundle(); + extras.putString(KEY_SERVICE_ACTION, serviceAction); + extras.putString(KEY_SERVICE_PACKAGE, servicePackage); + extras.putInt(KEY_REQUIREMENTS, requirements.getRequirementsData()); + builder.setExtras(extras); + + return builder.build(); + } + + private static void logd(String message) { + if (DEBUG) { + Log.d(TAG, message); + } + } + + /** 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(KEY_REQUIREMENTS)); + if (requirements.checkRequirements(this)) { + 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); + Util.startForegroundService(this, intent); + } else { + logd("Requirements are not met"); + jobFinished(params, /* needsReschedule */ true); + } + return false; + } + + @Override + public boolean onStopJob(JobParameters params) { + 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 544644d03b..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; @@ -674,12 +684,8 @@ public final class MediaSessionConnector { // active queue item and queue navigation actions may need to be updated updateMediaSessionPlaybackState(); } - if (currentWindowCount != windowCount) { - // active queue item and queue navigation actions may need to be updated - updateMediaSessionPlaybackState(); - } currentWindowCount = windowCount; - currentWindowIndex = player.getCurrentWindowIndex(); + currentWindowIndex = windowIndex; updateMediaSessionMetadata(); } @@ -703,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/TimelineQueueEditor.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueEditor.java index 402abf7c70..853750077d 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueEditor.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueEditor.java @@ -24,21 +24,21 @@ import android.support.v4.media.session.MediaControllerCompat; import android.support.v4.media.session.MediaSessionCompat; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.source.DynamicConcatenatingMediaSource; +import com.google.android.exoplayer2.source.ConcatenatingMediaSource; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.util.Util; import java.util.List; /** - * A {@link MediaSessionConnector.QueueEditor} implementation based on the - * {@link DynamicConcatenatingMediaSource}. - *

- * This class implements the {@link MediaSessionConnector.CommandReceiver} interface and handles + * A {@link MediaSessionConnector.QueueEditor} implementation based on the {@link + * ConcatenatingMediaSource}. + * + *

This class implements the {@link MediaSessionConnector.CommandReceiver} interface and handles * the {@link #COMMAND_MOVE_QUEUE_ITEM} to move a queue item instead of removing and inserting it. * This allows to move the currently playing window without interrupting playback. */ -public final class TimelineQueueEditor implements MediaSessionConnector.QueueEditor, - MediaSessionConnector.CommandReceiver { +public final class TimelineQueueEditor + implements MediaSessionConnector.QueueEditor, MediaSessionConnector.CommandReceiver { public static final String COMMAND_MOVE_QUEUE_ITEM = "exo_move_window"; public static final String EXTRA_FROM_INDEX = "from_index"; @@ -124,20 +124,21 @@ public final class TimelineQueueEditor implements MediaSessionConnector.QueueEdi private final QueueDataAdapter queueDataAdapter; private final MediaSourceFactory sourceFactory; private final MediaDescriptionEqualityChecker equalityChecker; - private final DynamicConcatenatingMediaSource queueMediaSource; + private final ConcatenatingMediaSource queueMediaSource; /** * Creates a new {@link TimelineQueueEditor} with a given mediaSourceFactory. * * @param mediaController A {@link MediaControllerCompat} to read the current queue. - * @param queueMediaSource The {@link DynamicConcatenatingMediaSource} to - * manipulate. + * @param queueMediaSource The {@link ConcatenatingMediaSource} to manipulate. * @param queueDataAdapter A {@link QueueDataAdapter} to change the backing data. * @param sourceFactory The {@link MediaSourceFactory} to build media sources. */ - public TimelineQueueEditor(@NonNull MediaControllerCompat mediaController, - @NonNull DynamicConcatenatingMediaSource queueMediaSource, - @NonNull QueueDataAdapter queueDataAdapter, @NonNull MediaSourceFactory sourceFactory) { + public TimelineQueueEditor( + @NonNull MediaControllerCompat mediaController, + @NonNull ConcatenatingMediaSource queueMediaSource, + @NonNull QueueDataAdapter queueDataAdapter, + @NonNull MediaSourceFactory sourceFactory) { this(mediaController, queueMediaSource, queueDataAdapter, sourceFactory, new MediaIdEqualityChecker()); } @@ -146,15 +147,16 @@ public final class TimelineQueueEditor implements MediaSessionConnector.QueueEdi * Creates a new {@link TimelineQueueEditor} with a given mediaSourceFactory. * * @param mediaController A {@link MediaControllerCompat} to read the current queue. - * @param queueMediaSource The {@link DynamicConcatenatingMediaSource} to - * manipulate. + * @param queueMediaSource The {@link ConcatenatingMediaSource} to manipulate. * @param queueDataAdapter A {@link QueueDataAdapter} to change the backing data. * @param sourceFactory The {@link MediaSourceFactory} to build media sources. * @param equalityChecker The {@link MediaDescriptionEqualityChecker} to match queue items. */ - public TimelineQueueEditor(@NonNull MediaControllerCompat mediaController, - @NonNull DynamicConcatenatingMediaSource queueMediaSource, - @NonNull QueueDataAdapter queueDataAdapter, @NonNull MediaSourceFactory sourceFactory, + public TimelineQueueEditor( + @NonNull MediaControllerCompat mediaController, + @NonNull ConcatenatingMediaSource queueMediaSource, + @NonNull QueueDataAdapter queueDataAdapter, + @NonNull MediaSourceFactory sourceFactory, @NonNull MediaDescriptionEqualityChecker equalityChecker) { this.mediaController = mediaController; this.queueMediaSource = queueMediaSource; 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 65bc1e89d8..92d171cfdc 100644 --- a/extensions/mediasession/src/main/res/values-af/strings.xml +++ b/extensions/mediasession/src/main/res/values-af/strings.xml @@ -1,22 +1,6 @@ - - - - - "Herhaal niks" - "Herhaal een" - "Herhaal alles" + + + 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 0dc20aaa04..54509a65ab 100644 --- a/extensions/mediasession/src/main/res/values-am/strings.xml +++ b/extensions/mediasession/src/main/res/values-am/strings.xml @@ -1,22 +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 2776e28356..707ad41a16 100644 --- a/extensions/mediasession/src/main/res/values-ar/strings.xml +++ b/extensions/mediasession/src/main/res/values-ar/strings.xml @@ -1,22 +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 d20b16531a..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,22 +1,6 @@ - - - - - "Ne ponavljaj nijednu" - "Ponovi jednu" - "Ponovi sve" + + + 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 087eaee8c2..8a639c6cff 100644 --- a/extensions/mediasession/src/main/res/values-bg/strings.xml +++ b/extensions/mediasession/src/main/res/values-bg/strings.xml @@ -1,22 +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 4a4d8646a2..cdb41b2b0a 100644 --- a/extensions/mediasession/src/main/res/values-ca/strings.xml +++ b/extensions/mediasession/src/main/res/values-ca/strings.xml @@ -1,22 +1,6 @@ - - - - - "No en repeteixis cap" - "Repeteix una" - "Repeteix tot" + + + 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 c59dcfc874..4d25b3a3ba 100644 --- a/extensions/mediasession/src/main/res/values-cs/strings.xml +++ b/extensions/mediasession/src/main/res/values-cs/strings.xml @@ -1,22 +1,6 @@ - - - - - "Neopakovat" - "Opakovat jednu" - "Opakovat vše" + + + 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 0d31261f3d..f74409a50b 100644 --- a/extensions/mediasession/src/main/res/values-da/strings.xml +++ b/extensions/mediasession/src/main/res/values-da/strings.xml @@ -1,22 +1,6 @@ - - - - - "Gentag ingen" - "Gentag én" - "Gentag alle" + + + 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 dfa86a54d4..af3564cb41 100644 --- a/extensions/mediasession/src/main/res/values-de/strings.xml +++ b/extensions/mediasession/src/main/res/values-de/strings.xml @@ -1,22 +1,6 @@ - - - - - "Keinen wiederholen" - "Einen wiederholen" - "Alle 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 e73b24592e..e4f6666622 100644 --- a/extensions/mediasession/src/main/res/values-el/strings.xml +++ b/extensions/mediasession/src/main/res/values-el/strings.xml @@ -1,22 +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 197222473d..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,22 +1,6 @@ - - - - - "Repeat none" - "Repeat one" - "Repeat all" + + + 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 197222473d..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,22 +1,6 @@ - - - - - "Repeat none" - "Repeat one" - "Repeat all" + + + 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 197222473d..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,22 +1,6 @@ - - - - - "Repeat none" - "Repeat one" - "Repeat all" + + + 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 192ad2f2ef..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,22 +1,6 @@ - - - - - "No repetir" - "Repetir uno" - "Repetir todo" + + + 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 192ad2f2ef..700e6de4e2 100644 --- a/extensions/mediasession/src/main/res/values-es/strings.xml +++ b/extensions/mediasession/src/main/res/values-es/strings.xml @@ -1,22 +1,6 @@ - - - - - "No repetir" - "Repetir uno" - "Repetir todo" + + + 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 42b1b14c90..96e8a1e819 100644 --- a/extensions/mediasession/src/main/res/values-fa/strings.xml +++ b/extensions/mediasession/src/main/res/values-fa/strings.xml @@ -1,22 +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 68f1b6c93b..db1aca3f5c 100644 --- a/extensions/mediasession/src/main/res/values-fi/strings.xml +++ b/extensions/mediasession/src/main/res/values-fi/strings.xml @@ -1,22 +1,6 @@ - - - - - "Ei uudelleentoistoa" - "Toista yksi uudelleen" - "Toista kaikki uudelleen" + + + 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 62edf759bb..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,22 +1,6 @@ - - - - - "Ne rien lire en boucle" - "Lire une chanson en boucle" - "Tout lire en boucle" + + + 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 2ea8653e93..9e35e35a0c 100644 --- a/extensions/mediasession/src/main/res/values-fr/strings.xml +++ b/extensions/mediasession/src/main/res/values-fr/strings.xml @@ -1,22 +1,6 @@ - - - - - "Ne rien lire en boucle" - "Lire un titre en boucle" - "Tout lire en boucle" + + + 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 79261e4e59..66415ed45d 100644 --- a/extensions/mediasession/src/main/res/values-hi/strings.xml +++ b/extensions/mediasession/src/main/res/values-hi/strings.xml @@ -1,22 +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 81bb428528..3b3f8170db 100644 --- a/extensions/mediasession/src/main/res/values-hr/strings.xml +++ b/extensions/mediasession/src/main/res/values-hr/strings.xml @@ -1,22 +1,6 @@ - - - - - "Bez ponavljanja" - "Ponovi jedno" - "Ponovi sve" + + + 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 8e8369a61f..392959a462 100644 --- a/extensions/mediasession/src/main/res/values-hu/strings.xml +++ b/extensions/mediasession/src/main/res/values-hu/strings.xml @@ -1,22 +1,6 @@ - - - - - "Nincs ismétlés" - "Egy szám ismétlése" - "Összes szám 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 a20a6362c8..1388877293 100644 --- a/extensions/mediasession/src/main/res/values-in/strings.xml +++ b/extensions/mediasession/src/main/res/values-in/strings.xml @@ -1,22 +1,6 @@ - - - - - "Jangan ulangi" - "Ulangi 1" - "Ulangi semua" + + + 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 3a59bb5804..8922453204 100644 --- a/extensions/mediasession/src/main/res/values-it/strings.xml +++ b/extensions/mediasession/src/main/res/values-it/strings.xml @@ -1,22 +1,6 @@ - - - - - "Non ripetere nulla" - "Ripeti uno" - "Ripeti tutto" + + + 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 f9eac73e59..193a3ac606 100644 --- a/extensions/mediasession/src/main/res/values-iw/strings.xml +++ b/extensions/mediasession/src/main/res/values-iw/strings.xml @@ -1,22 +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 bcfb6eb7c2..d1cd378d53 100644 --- a/extensions/mediasession/src/main/res/values-ja/strings.xml +++ b/extensions/mediasession/src/main/res/values-ja/strings.xml @@ -1,22 +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 7be13b133a..b561abc1d7 100644 --- a/extensions/mediasession/src/main/res/values-ko/strings.xml +++ b/extensions/mediasession/src/main/res/values-ko/strings.xml @@ -1,22 +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 78d1753ed0..20eb0e9b1f 100644 --- a/extensions/mediasession/src/main/res/values-lt/strings.xml +++ b/extensions/mediasession/src/main/res/values-lt/strings.xml @@ -1,22 +1,6 @@ - - - - - "Nekartoti nieko" - "Kartoti vieną" - "Kartoti viską" + + + 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 085723a271..44cddec124 100644 --- a/extensions/mediasession/src/main/res/values-lv/strings.xml +++ b/extensions/mediasession/src/main/res/values-lv/strings.xml @@ -1,22 +1,6 @@ - - - - - "Neatkārtot nevienu" - "Atkārtot vienu" - "Atkārtot visu" + + + 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 2e986733fc..eab972792f 100644 --- a/extensions/mediasession/src/main/res/values-nb/strings.xml +++ b/extensions/mediasession/src/main/res/values-nb/strings.xml @@ -1,22 +1,6 @@ - - - - - "Ikke gjenta noen" - "Gjenta én" - "Gjenta alle" + + + 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 4dfc31bb98..b1309f40d6 100644 --- a/extensions/mediasession/src/main/res/values-nl/strings.xml +++ b/extensions/mediasession/src/main/res/values-nl/strings.xml @@ -1,22 +1,6 @@ - - - - - "Niets herhalen" - "Eén herhalen" - "Alles 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 37af4c1616..5654c0f095 100644 --- a/extensions/mediasession/src/main/res/values-pl/strings.xml +++ b/extensions/mediasession/src/main/res/values-pl/strings.xml @@ -1,22 +1,6 @@ - - - - - "Nie powtarzaj" - "Powtórz jeden" - "Powtórz wszystkie" + + + 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 43a4cd9e6a..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,22 +1,6 @@ - - - - - "Não repetir nenhum" - "Repetir um" - "Repetir tudo" + + + 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 4e7ce248cc..a858ea4fc6 100644 --- a/extensions/mediasession/src/main/res/values-pt/strings.xml +++ b/extensions/mediasession/src/main/res/values-pt/strings.xml @@ -1,22 +1,6 @@ - - - - - "Não repetir" - "Repetir uma" - "Repetir tudo" + + + 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 9345a5df35..a88088fb0c 100644 --- a/extensions/mediasession/src/main/res/values-ro/strings.xml +++ b/extensions/mediasession/src/main/res/values-ro/strings.xml @@ -1,22 +1,6 @@ - - - - - "Nu repetați niciunul" - "Repetați unul" - "Repetați-le pe toate" + + + 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 8c52ea8395..f350724813 100644 --- a/extensions/mediasession/src/main/res/values-ru/strings.xml +++ b/extensions/mediasession/src/main/res/values-ru/strings.xml @@ -1,22 +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 9a7cccd096..9c0235daec 100644 --- a/extensions/mediasession/src/main/res/values-sk/strings.xml +++ b/extensions/mediasession/src/main/res/values-sk/strings.xml @@ -1,22 +1,6 @@ - - - - - "Neopakovať" - "Opakovať jednu" - "Opakovať všetko" + + + 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 7bf20baa19..9ee3add8bc 100644 --- a/extensions/mediasession/src/main/res/values-sl/strings.xml +++ b/extensions/mediasession/src/main/res/values-sl/strings.xml @@ -1,22 +1,6 @@ - - - - - "Brez ponavljanja" - "Ponavljanje ene" - "Ponavljanje vseh" + + + 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 b82940da2e..71edd5c341 100644 --- a/extensions/mediasession/src/main/res/values-sr/strings.xml +++ b/extensions/mediasession/src/main/res/values-sr/strings.xml @@ -1,22 +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 13edc46d1f..0956ac9fc7 100644 --- a/extensions/mediasession/src/main/res/values-sv/strings.xml +++ b/extensions/mediasession/src/main/res/values-sv/strings.xml @@ -1,22 +1,6 @@ - - - - - "Upprepa inga" - "Upprepa en" - "Upprepa alla" + + + 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 b40ce1a727..0010774a6f 100644 --- a/extensions/mediasession/src/main/res/values-sw/strings.xml +++ b/extensions/mediasession/src/main/res/values-sw/strings.xml @@ -1,22 +1,6 @@ - - - - - "Usirudie yoyote" - "Rudia moja" - "Rudia zote" + + + 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 4e40f559d0..bec0410a44 100644 --- a/extensions/mediasession/src/main/res/values-th/strings.xml +++ b/extensions/mediasession/src/main/res/values-th/strings.xml @@ -1,22 +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 4fff164f9f..6f8d8f4f88 100644 --- a/extensions/mediasession/src/main/res/values-tl/strings.xml +++ b/extensions/mediasession/src/main/res/values-tl/strings.xml @@ -1,22 +1,6 @@ - - - - - "Walang uulitin" - "Mag-ulit ng isa" - "Ulitin lahat" + + + 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 f93fd7fc80..20c05d9fa6 100644 --- a/extensions/mediasession/src/main/res/values-tr/strings.xml +++ b/extensions/mediasession/src/main/res/values-tr/strings.xml @@ -1,22 +1,6 @@ - - - - - "Hiçbirini tekrarlama" - "Bir şarkıyı tekrarla" - "Tümünü 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 fb9d000474..44db07ef9c 100644 --- a/extensions/mediasession/src/main/res/values-uk/strings.xml +++ b/extensions/mediasession/src/main/res/values-uk/strings.xml @@ -1,22 +1,6 @@ - - - - - "Не повторювати" - "Повторити 1" - "Повторити всі" + + + Не повторювати + Повторити 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 379dc36ee6..9de007cdb9 100644 --- a/extensions/mediasession/src/main/res/values-vi/strings.xml +++ b/extensions/mediasession/src/main/res/values-vi/strings.xml @@ -1,22 +1,6 @@ - - - - - "Không lặp lại" - "Lặp lại một" - "Lặp lại tất 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 6917f75bf9..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,22 +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 b63f103e2a..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,22 +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 0a460b9e08..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,22 +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 ccf8452d69..a6299ba987 100644 --- a/extensions/mediasession/src/main/res/values-zu/strings.xml +++ b/extensions/mediasession/src/main/res/values-zu/strings.xml @@ -1,22 +1,6 @@ - - - - - "Phinda okungekho" - "Phinda okukodwa" - "Phinda konke" + + + 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 0519673e50..172159b7af 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/opus/src/androidTest/AndroidManifest.xml b/extensions/opus/src/androidTest/AndroidManifest.xml index 2d56e8d1a7..9e7f05051e 100644 --- a/extensions/opus/src/androidTest/AndroidManifest.xml +++ b/extensions/opus/src/androidTest/AndroidManifest.xml @@ -18,8 +18,6 @@ xmlns:tools="http://schemas.android.com/tools" package="com.google.android.exoplayer2.ext.opus.test"> - - diff --git a/extensions/vp9/src/androidTest/AndroidManifest.xml b/extensions/vp9/src/androidTest/AndroidManifest.xml index 152ce2f533..c7ed3d7fb2 100644 --- a/extensions/vp9/src/androidTest/AndroidManifest.xml +++ b/extensions/vp9/src/androidTest/AndroidManifest.xml @@ -18,8 +18,6 @@ xmlns:tools="http://schemas.android.com/tools" package="com.google.android.exoplayer2.ext.vp9.test"> - - 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 4d75f6076b..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 @@ -127,6 +127,7 @@ public class LibvpxVideoRenderer extends BaseRenderer { private Bitmap bitmap; private boolean renderedFirstFrame; + private long initialPositionUs; private long joiningDeadlineMs; private Surface surface; private VpxOutputBufferRenderer outputBufferRenderer; @@ -168,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); } /** @@ -303,6 +311,7 @@ public class LibvpxVideoRenderer extends BaseRenderer { inputStreamEnded = false; outputStreamEnded = false; clearRenderedFirstFrame(); + initialPositionUs = C.TIME_UNSET; consecutiveDroppedFrameCount = 0; if (decoder != null) { flushDecoder(); @@ -809,6 +818,10 @@ public class LibvpxVideoRenderer extends BaseRenderer { */ private boolean processOutputBuffer(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { + if (initialPositionUs == C.TIME_UNSET) { + initialPositionUs = positionUs; + } + long earlyUs = outputBuffer.timeUs - positionUs; if (outputMode == VpxDecoder.OUTPUT_MODE_NONE) { // Skip frames in sync with playback, so we'll be at the right frame if the mode changes. @@ -828,7 +841,7 @@ public class LibvpxVideoRenderer extends BaseRenderer { return true; } - if (!isStarted) { + if (!isStarted || positionUs == initialPositionUs) { return false; } diff --git a/extensions/vp9/src/main/jni/vpx_jni.cc b/extensions/vp9/src/main/jni/vpx_jni.cc index 421b16d26d..12bc30112d 100644 --- a/extensions/vp9/src/main/jni/vpx_jni.cc +++ b/extensions/vp9/src/main/jni/vpx_jni.cc @@ -295,7 +295,11 @@ DECODER_FUNC(jlong, vpxInit, jboolean disableLoopFilter) { return 0; } if (disableLoopFilter) { - vpx_codec_control_(context, VP9_SET_SKIP_LOOP_FILTER, true); + // TODO(b/71930387): Use vpx_codec_control(), not vpx_codec_control_(). + err = vpx_codec_control_(context, VP9_SET_SKIP_LOOP_FILTER, true); + if (err) { + LOGE("ERROR: Failed to shut off libvpx loop filter, error = %d.", err); + } } // Populate JNI References. 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/androidTest/AndroidManifest.xml b/library/core/src/androidTest/AndroidManifest.xml index 38ae6b0b2d..1aa47c10f6 100644 --- a/library/core/src/androidTest/AndroidManifest.xml +++ b/library/core/src/androidTest/AndroidManifest.xml @@ -18,8 +18,6 @@ xmlns:tools="http://schemas.android.com/tools" package="com.google.android.exoplayer2.core.test"> - - diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java index 9791fdb46f..58531346ab 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java @@ -1,8 +1,24 @@ +/* + * 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.upstream.cache; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; +import android.net.Uri; import android.test.InstrumentationTestCase; import android.util.SparseArray; import com.google.android.exoplayer2.C; @@ -14,23 +30,43 @@ import java.io.IOException; import java.util.Collection; import java.util.Set; -/** - * Tests {@link CachedContentIndex}. - */ +/** Tests {@link CachedContentIndex}. */ public class CachedContentIndexTest extends InstrumentationTestCase { private final byte[] testIndexV1File = { 0, 0, 0, 1, // version 0, 0, 0, 0, // flags 0, 0, 0, 2, // number_of_CachedContent - 0, 0, 0, 5, // cache_id - 0, 5, 65, 66, 67, 68, 69, // cache_key + 0, 0, 0, 5, // cache_id 5 + 0, 5, 65, 66, 67, 68, 69, // cache_key "ABCDE" 0, 0, 0, 0, 0, 0, 0, 10, // original_content_length - 0, 0, 0, 2, // cache_id - 0, 5, 75, 76, 77, 78, 79, // cache_key + 0, 0, 0, 2, // cache_id 2 + 0, 5, 75, 76, 77, 78, 79, // cache_key "KLMNO" 0, 0, 0, 0, 0, 0, 10, 0, // original_content_length (byte) 0xF6, (byte) 0xFB, 0x50, 0x41 // hashcode_of_CachedContent_array }; + + private final byte[] testIndexV2File = { + 0, 0, 0, 2, // version + 0, 0, 0, 0, // flags + 0, 0, 0, 2, // number_of_CachedContent + 0, 0, 0, 5, // cache_id 5 + 0, 5, 65, 66, 67, 68, 69, // cache_key "ABCDE" + 0, 0, 0, 2, // metadata count + 0, 9, 101, 120, 111, 95, 114, 101, 100, 105, 114, // "exo_redir" + 0, 0, 0, 5, // value length + 97, 98, 99, 100, 101, // Redirected Uri "abcde" + 0, 7, 101, 120, 111, 95, 108, 101, 110, // "exo_len" + 0, 0, 0, 8, // value length + 0, 0, 0, 0, 0, 0, 0, 10, // original_content_length + 0, 0, 0, 2, // cache_id 2 + 0, 5, 75, 76, 77, 78, 79, // cache_key "KLMNO" + 0, 0, 0, 1, // metadata count + 0, 7, 101, 120, 111, 95, 108, 101, 110, // "exo_len" + 0, 0, 0, 8, // value length + 0, 0, 0, 0, 0, 0, 10, 0, // original_content_length + 0x12, 0x15, 0x66, (byte) 0x8A // hashcode_of_CachedContent_array + }; private CachedContentIndex index; private File cacheDir; @@ -53,14 +89,13 @@ public class CachedContentIndexTest extends InstrumentationTestCase { final String key3 = "key3"; // Add two CachedContents with add methods - CachedContent cachedContent1 = new CachedContent(5, key1, 10); - index.addNew(cachedContent1); + CachedContent cachedContent1 = index.getOrAdd(key1); CachedContent cachedContent2 = index.getOrAdd(key2); assertThat(cachedContent1.id != cachedContent2.id).isTrue(); // add a span - File cacheSpanFile = SimpleCacheSpanTest - .createCacheSpanFile(cacheDir, cachedContent1.id, 10, 20, 30); + File cacheSpanFile = + SimpleCacheSpanTest.createCacheSpanFile(cacheDir, cachedContent1.id, 10, 20, 30); SimpleCacheSpan span = SimpleCacheSpan.createCacheEntry(cacheSpanFile, index); assertThat(span).isNotNull(); cachedContent1.addSpan(span); @@ -90,7 +125,7 @@ public class CachedContentIndexTest extends InstrumentationTestCase { assertThat(cacheSpanFile.exists()).isTrue(); // test removeEmpty() - index.addNew(cachedContent2); + index.getOrAdd(key2); index.removeEmpty(); assertThat(index.get(key1)).isEqualTo(cachedContent1); assertThat(index.get(key2)).isNull(); @@ -108,27 +143,32 @@ public class CachedContentIndexTest extends InstrumentationTestCase { index.load(); assertThat(index.getAll()).hasSize(2); + assertThat(index.assignIdForKey("ABCDE")).isEqualTo(5); - assertThat(index.getContentLength("ABCDE")).isEqualTo(10); + ContentMetadata metadata = index.get("ABCDE").getMetadata(); + assertThat(ContentMetadataInternal.getContentLength(metadata)).isEqualTo(10); + assertThat(index.assignIdForKey("KLMNO")).isEqualTo(2); - assertThat(index.getContentLength("KLMNO")).isEqualTo(2560); + ContentMetadata metadata2 = index.get("KLMNO").getMetadata(); + assertThat(ContentMetadataInternal.getContentLength(metadata2)).isEqualTo(2560); } - public void testStoreV1() throws Exception { - index.addNew(new CachedContent(2, "KLMNO", 2560)); - index.addNew(new CachedContent(5, "ABCDE", 10)); - - index.store(); - - byte[] buffer = new byte[testIndexV1File.length]; - FileInputStream fos = new FileInputStream(new File(cacheDir, CachedContentIndex.FILE_NAME)); - assertThat(fos.read(buffer)).isEqualTo(testIndexV1File.length); - assertThat(fos.read()).isEqualTo(-1); + public void testLoadV2() throws Exception { + FileOutputStream fos = new FileOutputStream(new File(cacheDir, CachedContentIndex.FILE_NAME)); + fos.write(testIndexV2File); fos.close(); - // TODO: The order of the CachedContent stored in index file isn't defined so this test may fail - // on a different implementation of the underlying set - assertThat(buffer).isEqualTo(testIndexV1File); + index.load(); + assertThat(index.getAll()).hasSize(2); + + assertThat(index.assignIdForKey("ABCDE")).isEqualTo(5); + ContentMetadata metadata = index.get("ABCDE").getMetadata(); + assertThat(ContentMetadataInternal.getContentLength(metadata)).isEqualTo(10); + assertThat(ContentMetadataInternal.getRedirectedUri(metadata)).isEqualTo(Uri.parse("abcde")); + + assertThat(index.assignIdForKey("KLMNO")).isEqualTo(2); + ContentMetadata metadata2 = index.get("KLMNO").getMetadata(); + assertThat(ContentMetadataInternal.getContentLength(metadata2)).isEqualTo(2560); } public void testAssignIdForKeyAndGetKeyForId() throws Exception { @@ -143,13 +183,6 @@ public class CachedContentIndexTest extends InstrumentationTestCase { assertThat(index.assignIdForKey(key2)).isEqualTo(id2); } - public void testSetGetContentLength() throws Exception { - final String key1 = "key1"; - assertThat(index.getContentLength(key1)).isEqualTo(C.LENGTH_UNSET); - index.setContentLength(key1, 10); - assertThat(index.getContentLength(key1)).isEqualTo(10); - } - public void testGetNewId() throws Exception { SparseArray idToKey = new SparseArray<>(); assertThat(CachedContentIndex.getNewId(idToKey)).isEqualTo(0); @@ -165,8 +198,8 @@ public class CachedContentIndexTest extends InstrumentationTestCase { byte[] key = "Bar12345Bar12345".getBytes(C.UTF8_NAME); // 128 bit key byte[] key2 = "Foo12345Foo12345".getBytes(C.UTF8_NAME); // 128 bit key - assertStoredAndLoadedEqual(new CachedContentIndex(cacheDir, key), - new CachedContentIndex(cacheDir, key)); + assertStoredAndLoadedEqual( + new CachedContentIndex(cacheDir, key), new CachedContentIndex(cacheDir, key)); // Rename the index file from the test above File file1 = new File(cacheDir, CachedContentIndex.FILE_NAME); @@ -174,8 +207,8 @@ public class CachedContentIndexTest extends InstrumentationTestCase { assertThat(file1.renameTo(file2)).isTrue(); // Write a new index file - assertStoredAndLoadedEqual(new CachedContentIndex(cacheDir, key), - new CachedContentIndex(cacheDir, key)); + assertStoredAndLoadedEqual( + new CachedContentIndex(cacheDir, key), new CachedContentIndex(cacheDir, key)); assertThat(file1.length()).isEqualTo(file2.length()); // Assert file content is different @@ -187,8 +220,8 @@ public class CachedContentIndexTest extends InstrumentationTestCase { boolean threw = false; try { - assertStoredAndLoadedEqual(new CachedContentIndex(cacheDir, key), - new CachedContentIndex(cacheDir, key2)); + assertStoredAndLoadedEqual( + new CachedContentIndex(cacheDir, key), new CachedContentIndex(cacheDir, key2)); } catch (AssertionError e) { threw = true; } @@ -197,8 +230,8 @@ public class CachedContentIndexTest extends InstrumentationTestCase { .isTrue(); try { - assertStoredAndLoadedEqual(new CachedContentIndex(cacheDir, key), - new CachedContentIndex(cacheDir)); + assertStoredAndLoadedEqual( + new CachedContentIndex(cacheDir, key), new CachedContentIndex(cacheDir)); } catch (AssertionError e) { threw = true; } @@ -207,19 +240,18 @@ public class CachedContentIndexTest extends InstrumentationTestCase { .isTrue(); // Non encrypted index file can be read even when encryption key provided. - assertStoredAndLoadedEqual(new CachedContentIndex(cacheDir), - new CachedContentIndex(cacheDir, key)); + assertStoredAndLoadedEqual( + new CachedContentIndex(cacheDir), new CachedContentIndex(cacheDir, key)); // Test multiple store() calls CachedContentIndex index = new CachedContentIndex(cacheDir, key); - index.addNew(new CachedContent(15, "key3", 110)); + index.getOrAdd("key3"); index.store(); assertStoredAndLoadedEqual(index, new CachedContentIndex(cacheDir, key)); } public void testRemoveEmptyNotLockedCachedContent() throws Exception { - CachedContent cachedContent = new CachedContent(5, "key1", 10); - index.addNew(cachedContent); + CachedContent cachedContent = index.getOrAdd("key1"); index.maybeRemove(cachedContent.key); @@ -227,8 +259,7 @@ public class CachedContentIndexTest extends InstrumentationTestCase { } public void testCantRemoveNotEmptyCachedContent() throws Exception { - CachedContent cachedContent = new CachedContent(5, "key1", 10); - index.addNew(cachedContent); + CachedContent cachedContent = index.getOrAdd("key1"); File cacheSpanFile = SimpleCacheSpanTest.createCacheSpanFile(cacheDir, cachedContent.id, 10, 20, 30); SimpleCacheSpan span = SimpleCacheSpan.createCacheEntry(cacheSpanFile, index); @@ -240,9 +271,8 @@ public class CachedContentIndexTest extends InstrumentationTestCase { } public void testCantRemoveLockedCachedContent() throws Exception { - CachedContent cachedContent = new CachedContent(5, "key1", 10); + CachedContent cachedContent = index.getOrAdd("key1"); cachedContent.setLocked(true); - index.addNew(cachedContent); index.maybeRemove(cachedContent.key); @@ -251,8 +281,13 @@ public class CachedContentIndexTest extends InstrumentationTestCase { private void assertStoredAndLoadedEqual(CachedContentIndex index, CachedContentIndex index2) throws IOException { - index.addNew(new CachedContent(5, "key1", 10)); - index.getOrAdd("key2"); + ContentMetadataMutations mutations1 = new ContentMetadataMutations(); + ContentMetadataInternal.setContentLength(mutations1, 2560); + index.getOrAdd("KLMNO").applyMetadataMutations(mutations1); + ContentMetadataMutations mutations2 = new ContentMetadataMutations(); + ContentMetadataInternal.setContentLength(mutations2, 10); + ContentMetadataInternal.setRedirectedUri(mutations2, Uri.parse("abcde")); + index.getOrAdd("ABCDE").applyMetadataMutations(mutations2); index.store(); index2.load(); @@ -260,9 +295,7 @@ public class CachedContentIndexTest extends InstrumentationTestCase { Set keys2 = index2.getKeys(); assertThat(keys2).isEqualTo(keys); for (String key : keys) { - assertThat(index2.getContentLength(key)).isEqualTo(index.getContentLength(key)); - assertThat(index2.get(key).getSpans()).isEqualTo(index.get(key).getSpans()); + assertThat(index2.get(key)).isEqualTo(index.get(key)); } } - } 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/BaseRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java index 8ee9a13c55..cb917b9b79 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java @@ -35,6 +35,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { private int index; private int state; private SampleStream stream; + private Format[] streamFormats; private long streamOffsetUs; private boolean readEndOfStream; private boolean streamIsFinal; @@ -98,6 +99,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { Assertions.checkState(!streamIsFinal); this.stream = stream; readEndOfStream = false; + streamFormats = formats; streamOffsetUs = offsetUs; onStreamChanged(formats, offsetUs); } @@ -146,6 +148,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { Assertions.checkState(state == STATE_ENABLED); state = STATE_DISABLED; stream = null; + streamFormats = null; streamIsFinal = false; onDisabled(); } @@ -246,6 +249,11 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { // Methods to be called by subclasses. + /** Returns the formats of the currently enabled stream. */ + protected final Format[] getStreamFormats() { + return streamFormats; + } + /** * Returns the configuration set when the renderer was most recently enabled. */ 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/ControlDispatcher.java b/library/core/src/main/java/com/google/android/exoplayer2/ControlDispatcher.java index 21c596e6d4..f8749fc1a8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ControlDispatcher.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ControlDispatcher.java @@ -64,4 +64,12 @@ public interface ControlDispatcher { */ boolean dispatchSetShuffleModeEnabled(Player player, boolean shuffleModeEnabled); + /** + * Dispatches a {@link Player#stop()} operation. + * + * @param player The {@link Player} to which the operation should be dispatched. + * @param reset Whether the player should be reset. + * @return True if the operation was dispatched. False if suppressed. + */ + boolean dispatchStop(Player player, boolean reset); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/DefaultControlDispatcher.java b/library/core/src/main/java/com/google/android/exoplayer2/DefaultControlDispatcher.java index 84711d752a..df3ef36b88 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/DefaultControlDispatcher.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/DefaultControlDispatcher.java @@ -47,4 +47,9 @@ public class DefaultControlDispatcher implements ControlDispatcher { return true; } + @Override + public boolean dispatchStop(Player player, boolean reset) { + player.stop(reset); + return true; + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java b/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java index e8ea2f1621..b5b364a327 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java @@ -19,6 +19,7 @@ import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DefaultAllocator; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.PriorityTaskManager; import com.google.android.exoplayer2.util.Util; @@ -36,7 +37,7 @@ public class DefaultLoadControl implements LoadControl { /** * The default maximum duration of media that the player will attempt to buffer, in milliseconds. */ - public static final int DEFAULT_MAX_BUFFER_MS = 30000; + public static final int DEFAULT_MAX_BUFFER_MS = 50000; /** * The default duration of media that must be buffered for playback to start or resume following a @@ -60,6 +61,116 @@ public class DefaultLoadControl implements LoadControl { /** The default prioritization of buffer time constraints over size constraints. */ public static final boolean DEFAULT_PRIORITIZE_TIME_OVER_SIZE_THRESHOLDS = true; + /** Builder for {@link DefaultLoadControl}. */ + public static final class Builder { + + private DefaultAllocator allocator; + private int minBufferMs; + private int maxBufferMs; + private int bufferForPlaybackMs; + private int bufferForPlaybackAfterRebufferMs; + private int targetBufferBytes; + private boolean prioritizeTimeOverSizeThresholds; + private PriorityTaskManager priorityTaskManager; + + /** Constructs a new instance. */ + public Builder() { + allocator = null; + minBufferMs = DEFAULT_MIN_BUFFER_MS; + maxBufferMs = DEFAULT_MAX_BUFFER_MS; + bufferForPlaybackMs = DEFAULT_BUFFER_FOR_PLAYBACK_MS; + bufferForPlaybackAfterRebufferMs = DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS; + targetBufferBytes = DEFAULT_TARGET_BUFFER_BYTES; + prioritizeTimeOverSizeThresholds = DEFAULT_PRIORITIZE_TIME_OVER_SIZE_THRESHOLDS; + priorityTaskManager = null; + } + + /** + * Sets the {@link DefaultAllocator} used by the loader. + * + * @param allocator The {@link DefaultAllocator}. + * @return This builder, for convenience. + */ + public Builder setAllocator(DefaultAllocator allocator) { + this.allocator = allocator; + return this; + } + + /** + * Sets the buffer duration parameters. + * + * @param minBufferMs The minimum duration of media that the player will attempt to ensure is + * buffered at all times, in milliseconds. + * @param maxBufferMs The maximum duration of media that the player will attempt to buffer, in + * milliseconds. + * @param bufferForPlaybackMs The duration of media that must be buffered for playback to start + * or resume following a user action such as a seek, in milliseconds. + * @param bufferForPlaybackAfterRebufferMs The default duration of media that must be buffered + * for playback to resume after a rebuffer, in milliseconds. A rebuffer is defined to be + * caused by buffer depletion rather than a user action. + * @return This builder, for convenience. + */ + public Builder setBufferDurationsMs( + int minBufferMs, + int maxBufferMs, + int bufferForPlaybackMs, + int bufferForPlaybackAfterRebufferMs) { + this.minBufferMs = minBufferMs; + this.maxBufferMs = maxBufferMs; + this.bufferForPlaybackMs = bufferForPlaybackMs; + this.bufferForPlaybackAfterRebufferMs = bufferForPlaybackAfterRebufferMs; + return this; + } + + /** + * Sets the target buffer size in bytes. If set to {@link C#LENGTH_UNSET}, the target buffer + * size will be calculated using {@link #calculateTargetBufferSize(Renderer[], + * TrackSelectionArray)}. + * + * @param targetBufferBytes The target buffer size in bytes. + * @return This builder, for convenience. + */ + public Builder setTargetBufferBytes(int targetBufferBytes) { + this.targetBufferBytes = targetBufferBytes; + return this; + } + + /** + * Sets whether the load control prioritizes buffer time constraints over buffer size + * constraints. + * + * @param prioritizeTimeOverSizeThresholds Whether the load control prioritizes buffer time + * constraints over buffer size constraints. + * @return This builder, for convenience. + */ + public Builder setPrioritizeTimeOverSizeThresholds(boolean prioritizeTimeOverSizeThresholds) { + this.prioritizeTimeOverSizeThresholds = prioritizeTimeOverSizeThresholds; + return this; + } + + /** Sets the {@link PriorityTaskManager} to use. */ + public Builder setPriorityTaskManager(PriorityTaskManager priorityTaskManager) { + this.priorityTaskManager = priorityTaskManager; + return this; + } + + /** Creates a {@link DefaultLoadControl}. */ + public DefaultLoadControl createDefaultLoadControl() { + if (allocator == null) { + allocator = new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE); + } + return new DefaultLoadControl( + allocator, + minBufferMs, + maxBufferMs, + bufferForPlaybackMs, + bufferForPlaybackAfterRebufferMs, + targetBufferBytes, + prioritizeTimeOverSizeThresholds, + priorityTaskManager); + } + } + private final DefaultAllocator allocator; private final long minBufferUs; @@ -80,11 +191,8 @@ public class DefaultLoadControl implements LoadControl { this(new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE)); } - /** - * Constructs a new instance, using the {@code DEFAULT_*} constants defined in this class. - * - * @param allocator The {@link DefaultAllocator} used by the loader. - */ + /** @deprecated Use {@link Builder} instead. */ + @Deprecated public DefaultLoadControl(DefaultAllocator allocator) { this( allocator, @@ -96,24 +204,8 @@ public class DefaultLoadControl implements LoadControl { DEFAULT_PRIORITIZE_TIME_OVER_SIZE_THRESHOLDS); } - /** - * Constructs a new instance. - * - * @param allocator The {@link DefaultAllocator} used by the loader. - * @param minBufferMs The minimum duration of media that the player will attempt to ensure is - * buffered at all times, in milliseconds. - * @param maxBufferMs The maximum duration of media that the player will attempt buffer, in - * milliseconds. - * @param bufferForPlaybackMs The duration of media that must be buffered for playback to start or - * resume following a user action such as a seek, in milliseconds. - * @param bufferForPlaybackAfterRebufferMs The default duration of media that must be buffered for - * playback to resume after a rebuffer, in milliseconds. A rebuffer is defined to be caused by - * buffer depletion rather than a user action. - * @param targetBufferBytes The target buffer size in bytes. If set to {@link C#LENGTH_UNSET}, the - * target buffer size will be calculated using {@link #calculateTargetBufferSize(Renderer[], - * TrackSelectionArray)}. - * @param prioritizeTimeOverSizeThresholds Whether the load control prioritizes buffer time - */ + /** @deprecated Use {@link Builder} instead. */ + @Deprecated public DefaultLoadControl( DefaultAllocator allocator, int minBufferMs, @@ -133,27 +225,8 @@ public class DefaultLoadControl implements LoadControl { null); } - /** - * Constructs a new instance. - * - * @param allocator The {@link DefaultAllocator} used by the loader. - * @param minBufferMs The minimum duration of media that the player will attempt to ensure is - * buffered at all times, in milliseconds. - * @param maxBufferMs The maximum duration of media that the player will attempt buffer, in - * milliseconds. - * @param bufferForPlaybackMs The duration of media that must be buffered for playback to start or - * resume following a user action such as a seek, in milliseconds. - * @param bufferForPlaybackAfterRebufferMs The default duration of media that must be buffered for - * playback to resume after a rebuffer, in milliseconds. A rebuffer is defined to be caused by - * buffer depletion rather than a user action. - * @param targetBufferBytes The target buffer size in bytes. If set to {@link C#LENGTH_UNSET}, the - * target buffer size will be calculated using {@link #calculateTargetBufferSize(Renderer[], - * TrackSelectionArray)}. - * @param prioritizeTimeOverSizeThresholds Whether the load control prioritizes buffer time - * constraints over buffer size constraints. - * @param priorityTaskManager If not null, registers itself as a task with priority {@link - * C#PRIORITY_PLAYBACK} during loading periods, and unregisters itself during draining - */ + /** @deprecated Use {@link Builder} instead. */ + @Deprecated public DefaultLoadControl( DefaultAllocator allocator, int minBufferMs, @@ -163,6 +236,17 @@ public class DefaultLoadControl implements LoadControl { int targetBufferBytes, boolean prioritizeTimeOverSizeThresholds, PriorityTaskManager priorityTaskManager) { + assertGreaterOrEqual(bufferForPlaybackMs, 0, "bufferForPlaybackMs", "0"); + assertGreaterOrEqual( + bufferForPlaybackAfterRebufferMs, 0, "bufferForPlaybackAfterRebufferMs", "0"); + assertGreaterOrEqual(minBufferMs, bufferForPlaybackMs, "minBufferMs", "bufferForPlaybackMs"); + assertGreaterOrEqual( + minBufferMs, + bufferForPlaybackAfterRebufferMs, + "minBufferMs", + "bufferForPlaybackAfterRebufferMs"); + assertGreaterOrEqual(maxBufferMs, minBufferMs, "maxBufferMs", "minBufferMs"); + this.allocator = allocator; minBufferUs = minBufferMs * 1000L; maxBufferUs = maxBufferMs * 1000L; @@ -217,18 +301,19 @@ public class DefaultLoadControl implements LoadControl { public boolean shouldContinueLoading(long bufferedDurationUs, float playbackSpeed) { boolean targetBufferSizeReached = allocator.getTotalBytesAllocated() >= targetBufferSize; boolean wasBuffering = isBuffering; - if (prioritizeTimeOverSizeThresholds) { - isBuffering = - bufferedDurationUs < minBufferUs // below low watermark - || (bufferedDurationUs <= maxBufferUs // between watermarks - && isBuffering - && !targetBufferSizeReached); - } else { - isBuffering = - !targetBufferSizeReached - && (bufferedDurationUs < minBufferUs // below low watermark - || (bufferedDurationUs <= maxBufferUs && isBuffering)); // between watermarks + long minBufferUs = this.minBufferUs; + if (playbackSpeed > 1) { + // The playback speed is faster than real time, so scale up the minimum required media + // duration to keep enough media buffered for a playout duration of minBufferUs. + long mediaDurationMinBufferUs = + Util.getMediaDurationForPlayoutDuration(minBufferUs, playbackSpeed); + minBufferUs = Math.min(mediaDurationMinBufferUs, maxBufferUs); } + if (bufferedDurationUs < minBufferUs) { + isBuffering = prioritizeTimeOverSizeThresholds || !targetBufferSizeReached; + } else if (bufferedDurationUs > maxBufferUs || targetBufferSizeReached) { + isBuffering = false; + } // Else don't change the buffering state if (priorityTaskManager != null && isBuffering != wasBuffering) { if (isBuffering) { priorityTaskManager.add(C.PRIORITY_PLAYBACK); @@ -280,4 +365,7 @@ public class DefaultLoadControl implements LoadControl { } } + private static void assertGreaterOrEqual(int value1, int value2, String name1, String name2) { + Assertions.checkArgument(value1 >= value2, name1 + " cannot be less than " + name2); + } } 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 16074108b1..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 @@ -90,28 +90,37 @@ public class DefaultRenderersFactory implements RenderersFactory { * @param context A {@link Context}. */ public DefaultRenderersFactory(Context context) { - this(context, null); + this(context, EXTENSION_RENDERER_MODE_OFF); } /** - * @param context A {@link Context}. - * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if DRM protected - * playbacks are not required. + * @deprecated Use {@link #DefaultRenderersFactory(Context)} and pass {@link DrmSessionManager} + * directly to {@link SimpleExoPlayer} or {@link ExoPlayerFactory}. */ - public DefaultRenderersFactory(Context context, - @Nullable DrmSessionManager drmSessionManager) { + @Deprecated + public DefaultRenderersFactory( + Context context, @Nullable DrmSessionManager drmSessionManager) { this(context, drmSessionManager, EXTENSION_RENDERER_MODE_OFF); } /** * @param context A {@link Context}. - * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if DRM protected - * playbacks are not required. - * @param extensionRendererMode The extension renderer mode, which determines if and how - * available extension renderers are used. Note that extensions must be included in the - * application build for them to be considered available. + * @param extensionRendererMode The extension renderer mode, which determines if and how available + * extension renderers are used. Note that extensions must be included in the application + * build for them to be considered available. */ - public DefaultRenderersFactory(Context context, + public DefaultRenderersFactory( + Context context, @ExtensionRendererMode int extensionRendererMode) { + this(context, null, extensionRendererMode, DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS); + } + + /** + * @deprecated Use {@link #DefaultRenderersFactory(Context, int)} and pass {@link + * DrmSessionManager} directly to {@link SimpleExoPlayer} or {@link ExoPlayerFactory}. + */ + @Deprecated + public DefaultRenderersFactory( + Context context, @Nullable DrmSessionManager drmSessionManager, @ExtensionRendererMode int extensionRendererMode) { this(context, drmSessionManager, extensionRendererMode, DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS); @@ -119,28 +128,46 @@ public class DefaultRenderersFactory implements RenderersFactory { /** * @param context A {@link Context}. - * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if DRM protected - * playbacks are not required. - * @param extensionRendererMode The extension renderer mode, which determines if and how - * available extension renderers are used. Note that extensions must be included in the - * application build for them to be considered available. - * @param allowedVideoJoiningTimeMs The maximum duration for which video renderers can attempt - * to seamlessly join an ongoing playback. + * @param extensionRendererMode The extension renderer mode, which determines if and how available + * extension renderers are used. Note that extensions must be included in the application + * build for them to be considered available. + * @param allowedVideoJoiningTimeMs The maximum duration for which video renderers can attempt to + * seamlessly join an ongoing playback. */ - public DefaultRenderersFactory(Context context, + public DefaultRenderersFactory( + Context context, + @ExtensionRendererMode int extensionRendererMode, + long allowedVideoJoiningTimeMs) { + this(context, null, extensionRendererMode, allowedVideoJoiningTimeMs); + } + + /** + * @deprecated Use {@link #DefaultRenderersFactory(Context, int, long)} and pass {@link + * DrmSessionManager} directly to {@link SimpleExoPlayer} or {@link ExoPlayerFactory}. + */ + @Deprecated + public DefaultRenderersFactory( + Context context, @Nullable DrmSessionManager drmSessionManager, - @ExtensionRendererMode int extensionRendererMode, long allowedVideoJoiningTimeMs) { + @ExtensionRendererMode int extensionRendererMode, + long allowedVideoJoiningTimeMs) { this.context = context; - this.drmSessionManager = drmSessionManager; this.extensionRendererMode = extensionRendererMode; this.allowedVideoJoiningTimeMs = allowedVideoJoiningTimeMs; + this.drmSessionManager = drmSessionManager; } @Override - public Renderer[] createRenderers(Handler eventHandler, + public Renderer[] createRenderers( + Handler eventHandler, VideoRendererEventListener videoRendererEventListener, AudioRendererEventListener audioRendererEventListener, - TextOutput textRendererOutput, MetadataOutput metadataRendererOutput) { + TextOutput textRendererOutput, + MetadataOutput metadataRendererOutput, + @Nullable DrmSessionManager drmSessionManager) { + if (drmSessionManager == null) { + drmSessionManager = this.drmSessionManager; + } ArrayList renderersList = new ArrayList<>(); buildVideoRenderers(context, drmSessionManager, allowedVideoJoiningTimeMs, eventHandler, videoRendererEventListener, extensionRendererMode, renderersList); @@ -172,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; @@ -232,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 c13fd6cacd..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 @@ -21,7 +21,6 @@ import com.google.android.exoplayer2.audio.MediaCodecAudioRenderer; import com.google.android.exoplayer2.metadata.MetadataRenderer; import com.google.android.exoplayer2.source.ClippingMediaSource; import com.google.android.exoplayer2.source.ConcatenatingMediaSource; -import com.google.android.exoplayer2.source.DynamicConcatenatingMediaSource; import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.LoopingMediaSource; import com.google.android.exoplayer2.source.MediaSource; @@ -54,8 +53,7 @@ import com.google.android.exoplayer2.video.MediaCodecVideoRenderer; * implementation for loading single media samples ({@link SingleSampleMediaSource}) that's * most often used for side-loaded subtitle files, and implementations for building more * complex MediaSources from simpler ones ({@link MergingMediaSource}, {@link - * ConcatenatingMediaSource}, {@link DynamicConcatenatingMediaSource}, {@link - * LoopingMediaSource} and {@link ClippingMediaSource}). + * ConcatenatingMediaSource}, {@link LoopingMediaSource} and {@link ClippingMediaSource}). *

  • {@link Renderer}s that render individual components of the media. The library * provides default implementations for common media types ({@link MediaCodecVideoRenderer}, * {@link MediaCodecAudioRenderer}, {@link TextRenderer} and {@link MetadataRenderer}). A @@ -187,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/ExoPlayerFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java index 821671e34e..8095ed9c64 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2; import android.content.Context; import android.support.annotation.Nullable; +import com.google.android.exoplayer2.analytics.AnalyticsCollector; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.google.android.exoplayer2.trackselection.TrackSelector; @@ -58,8 +59,8 @@ public final class ExoPlayerFactory { public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector, LoadControl loadControl, @Nullable DrmSessionManager drmSessionManager) { - RenderersFactory renderersFactory = new DefaultRenderersFactory(context, drmSessionManager); - return newSimpleInstance(renderersFactory, trackSelector, loadControl); + RenderersFactory renderersFactory = new DefaultRenderersFactory(context); + return newSimpleInstance(renderersFactory, trackSelector, loadControl, drmSessionManager); } /** @@ -79,9 +80,8 @@ public final class ExoPlayerFactory { public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector, LoadControl loadControl, @Nullable DrmSessionManager drmSessionManager, @DefaultRenderersFactory.ExtensionRendererMode int extensionRendererMode) { - RenderersFactory renderersFactory = new DefaultRenderersFactory(context, drmSessionManager, - extensionRendererMode); - return newSimpleInstance(renderersFactory, trackSelector, loadControl); + RenderersFactory renderersFactory = new DefaultRenderersFactory(context, extensionRendererMode); + return newSimpleInstance(renderersFactory, trackSelector, loadControl, drmSessionManager); } /** @@ -104,9 +104,9 @@ public final class ExoPlayerFactory { LoadControl loadControl, @Nullable DrmSessionManager drmSessionManager, @DefaultRenderersFactory.ExtensionRendererMode int extensionRendererMode, long allowedVideoJoiningTimeMs) { - RenderersFactory renderersFactory = new DefaultRenderersFactory(context, drmSessionManager, - extensionRendererMode, allowedVideoJoiningTimeMs); - return newSimpleInstance(renderersFactory, trackSelector, loadControl); + RenderersFactory renderersFactory = + new DefaultRenderersFactory(context, extensionRendererMode, allowedVideoJoiningTimeMs); + return newSimpleInstance(renderersFactory, trackSelector, loadControl, drmSessionManager); } /** @@ -130,6 +130,22 @@ public final class ExoPlayerFactory { return newSimpleInstance(renderersFactory, trackSelector, new DefaultLoadControl()); } + /** + * Creates a {@link SimpleExoPlayer} instance. + * + * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance. + * @param trackSelector The {@link TrackSelector} that will be used by the instance. + * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance + * will not be used for DRM protected playbacks. + */ + public static SimpleExoPlayer newSimpleInstance( + RenderersFactory renderersFactory, + TrackSelector trackSelector, + @Nullable DrmSessionManager drmSessionManager) { + return newSimpleInstance( + renderersFactory, trackSelector, new DefaultLoadControl(), drmSessionManager); + } + /** * Creates a {@link SimpleExoPlayer} instance. * @@ -139,7 +155,46 @@ public final class ExoPlayerFactory { */ public static SimpleExoPlayer newSimpleInstance(RenderersFactory renderersFactory, TrackSelector trackSelector, LoadControl loadControl) { - return new SimpleExoPlayer(renderersFactory, trackSelector, loadControl); + return new SimpleExoPlayer( + renderersFactory, trackSelector, loadControl, /* drmSessionManager= */ null); + } + + /** + * Creates a {@link SimpleExoPlayer} instance. + * + * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance. + * @param trackSelector The {@link TrackSelector} that will be used by the instance. + * @param loadControl The {@link LoadControl} that will be used by the instance. + * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance + * will not be used for DRM protected playbacks. + */ + public static SimpleExoPlayer newSimpleInstance( + RenderersFactory renderersFactory, + TrackSelector trackSelector, + LoadControl loadControl, + @Nullable DrmSessionManager drmSessionManager) { + return new SimpleExoPlayer(renderersFactory, trackSelector, loadControl, drmSessionManager); + } + + /** + * Creates a {@link SimpleExoPlayer} instance. + * + * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance. + * @param trackSelector The {@link TrackSelector} that will be used by the instance. + * @param loadControl The {@link LoadControl} that will be used by the instance. + * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance + * will not be used for DRM protected playbacks. + * @param analyticsCollectorFactory A factory for creating the {@link AnalyticsCollector} that + * will collect and forward all player events. + */ + public static SimpleExoPlayer newSimpleInstance( + RenderersFactory renderersFactory, + TrackSelector trackSelector, + LoadControl loadControl, + @Nullable DrmSessionManager drmSessionManager, + AnalyticsCollector.Factory analyticsCollectorFactory) { + return new SimpleExoPlayer( + renderersFactory, trackSelector, loadControl, drmSessionManager, analyticsCollectorFactory); } /** 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 0e0a6e3c26..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; @@ -92,11 +93,9 @@ import java.util.concurrent.CopyOnWriteArraySet; this.listeners = new CopyOnWriteArraySet<>(); emptyTrackSelectorResult = new TrackSelectorResult( - TrackGroupArray.EMPTY, - new boolean[renderers.length], - new TrackSelectionArray(new TrackSelection[renderers.length]), - null, - new RendererConfiguration[renderers.length]); + new RendererConfiguration[renderers.length], + new TrackSelection[renderers.length], + null); window = new Timeline.Window(); period = new Timeline.Period(); playbackParameters = PlaybackParameters.DEFAULT; @@ -108,7 +107,11 @@ import java.util.concurrent.CopyOnWriteArraySet; } }; playbackInfo = - new PlaybackInfo(Timeline.EMPTY, /* startPositionUs= */ 0, emptyTrackSelectorResult); + new PlaybackInfo( + Timeline.EMPTY, + /* startPositionUs= */ 0, + TrackGroupArray.EMPTY, + emptyTrackSelectorResult); internalPlayer = new ExoPlayerImplInternal( renderers, @@ -154,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); @@ -161,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); @@ -308,6 +317,14 @@ import java.util.concurrent.CopyOnWriteArraySet; internalPlayer.setSeekParameters(seekParameters); } + @Override + public @Nullable Object getCurrentTag() { + int windowIndex = getCurrentWindowIndex(); + return windowIndex > playbackInfo.timeline.getWindowCount() + ? null + : playbackInfo.timeline.getWindow(windowIndex, window, /* setTag= */ true).tag; + } + @Override public void stop() { stop(/* reset= */ false); @@ -315,6 +332,9 @@ import java.util.concurrent.CopyOnWriteArraySet; @Override public void stop(boolean reset) { + if (reset) { + playbackError = null; + } PlaybackInfo playbackInfo = getResetPlaybackInfo( /* resetPosition= */ reset, @@ -512,7 +532,7 @@ import java.util.concurrent.CopyOnWriteArraySet; @Override public TrackGroupArray getCurrentTrackGroups() { - return playbackInfo.trackSelectorResult.groups; + return playbackInfo.trackGroups; } @Override @@ -550,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: @@ -616,6 +636,7 @@ import java.util.concurrent.CopyOnWriteArraySet; playbackInfo.contentPositionUs, playbackState, /* isLoading= */ false, + resetState ? TrackGroupArray.EMPTY : playbackInfo.trackGroups, resetState ? emptyTrackSelectorResult : playbackInfo.trackSelectorResult); } @@ -648,7 +669,7 @@ import java.util.concurrent.CopyOnWriteArraySet; trackSelector.onSelectionActivated(playbackInfo.trackSelectorResult.info); for (Player.EventListener listener : listeners) { listener.onTracksChanged( - playbackInfo.trackSelectorResult.groups, playbackInfo.trackSelectorResult.selections); + playbackInfo.trackGroups, playbackInfo.trackSelectorResult.selections); } } if (isLoadingChanged) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index 24bd31c62f..ceee25af82 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -31,6 +31,7 @@ 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.SampleStream; +import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelectorResult; @@ -48,7 +49,7 @@ import java.util.Collections; implements Handler.Callback, MediaPeriod.Callback, TrackSelector.InvalidationListener, - MediaSource.Listener, + MediaSource.SourceInfoRefreshListener, PlaybackParameterListener, PlayerMessage.Sender { @@ -81,14 +82,6 @@ import java.util.Collections; private static final int RENDERING_INTERVAL_MS = 10; private static final int IDLE_INTERVAL_MS = 1000; - /** - * Offset added to all sample timestamps read by renderers to make them non-negative. This is - * provided for convenience of sources that may return negative timestamps due to prerolling - * samples from a keyframe before their first sample with timestamp zero, so it must be set to a - * value greater than or equal to the maximum key-frame interval in seekable periods. - */ - private static final int RENDERER_TIMESTAMP_OFFSET_US = 60000000; - private final Renderer[] renderers; private final RendererCapabilities[] rendererCapabilities; private final TrackSelector trackSelector; @@ -154,7 +147,10 @@ import java.util.Collections; seekParameters = SeekParameters.DEFAULT; playbackInfo = new PlaybackInfo( - Timeline.EMPTY, /* startPositionUs= */ C.TIME_UNSET, emptyTrackSelectorResult); + Timeline.EMPTY, + /* startPositionUs= */ C.TIME_UNSET, + TrackGroupArray.EMPTY, + emptyTrackSelectorResult); playbackInfoUpdate = new PlaybackInfoUpdate(); rendererCapabilities = new RendererCapabilities[renderers.length]; for (int i = 0; i < renderers.length; i++) { @@ -244,7 +240,7 @@ import java.util.Collections; return internalPlaybackThread.getLooper(); } - // MediaSource.Listener implementation. + // MediaSource.SourceInfoRefreshListener implementation. @Override public void onSourceInfoRefreshed(MediaSource source, Timeline timeline, Object manifest) { @@ -691,7 +687,7 @@ import java.util.Collections; resetRendererPosition(periodPositionUs); maybeContinueLoading(); } else { - queue.clear(); + queue.clear(/* keepFrontPeriodUid= */ true); resetRendererPosition(periodPositionUs); } @@ -715,7 +711,7 @@ import java.util.Collections; private void resetRendererPosition(long periodPositionUs) throws ExoPlaybackException { rendererPositionUs = !queue.hasPlayingPeriod() - ? periodPositionUs + RENDERER_TIMESTAMP_OFFSET_US + ? periodPositionUs : queue.getPlayingPeriod().toRendererTime(periodPositionUs); mediaClock.resetPosition(rendererPositionUs); for (Renderer renderer : enabledRenderers) { @@ -766,7 +762,7 @@ import java.util.Collections; handler.removeMessages(MSG_DO_SOME_WORK); rebuffering = false; mediaClock.stop(); - rendererPositionUs = RENDERER_TIMESTAMP_OFFSET_US; + rendererPositionUs = 0; for (Renderer renderer : enabledRenderers) { try { disableRenderer(renderer); @@ -776,7 +772,7 @@ import java.util.Collections; } } enabledRenderers = new Renderer[0]; - queue.clear(); + queue.clear(/* keepFrontPeriodUid= */ !resetPosition); setIsLoading(false); if (resetPosition) { pendingInitialSeekPosition = null; @@ -799,10 +795,11 @@ import java.util.Collections; resetPosition ? C.TIME_UNSET : playbackInfo.contentPositionUs, playbackInfo.playbackState, /* isLoading= */ false, + resetState ? TrackGroupArray.EMPTY : playbackInfo.trackGroups, resetState ? emptyTrackSelectorResult : playbackInfo.trackSelectorResult); if (releaseMediaSource) { if (mediaSource != null) { - mediaSource.releaseSource(); + mediaSource.releaseSource(/* listener= */ this); mediaSource = null; } } @@ -1007,7 +1004,8 @@ import java.util.Collections; long periodPositionUs = playingPeriodHolder.applyTrackSelection( playbackInfo.positionUs, recreateStreams, streamResetFlags); - updateLoadControlTrackSelection(playingPeriodHolder.trackSelectorResult); + updateLoadControlTrackSelection( + playingPeriodHolder.trackGroups, playingPeriodHolder.trackSelectorResult); if (playbackInfo.playbackState != Player.STATE_ENDED && periodPositionUs != playbackInfo.positionUs) { playbackInfo = playbackInfo.fromNewPosition(playbackInfo.periodId, periodPositionUs, @@ -1036,7 +1034,8 @@ import java.util.Collections; } } playbackInfo = - playbackInfo.copyWithTrackSelectorResult(playingPeriodHolder.trackSelectorResult); + playbackInfo.copyWithTrackInfo( + playingPeriodHolder.trackGroups, playingPeriodHolder.trackSelectorResult); enableRenderers(rendererWasEnabledFlags, enabledRendererCount); } else { // Release and re-prepare/buffer periods after the one whose selection changed. @@ -1046,7 +1045,7 @@ import java.util.Collections; Math.max( periodHolder.info.startPositionUs, periodHolder.toPeriodTime(rendererPositionUs)); periodHolder.applyTrackSelection(loadingPeriodPositionUs, false); - updateLoadControlTrackSelection(periodHolder.trackSelectorResult); + updateLoadControlTrackSelection(periodHolder.trackGroups, periodHolder.trackSelectorResult); } } if (playbackInfo.playbackState != Player.STATE_ENDED) { @@ -1056,9 +1055,9 @@ import java.util.Collections; } } - private void updateLoadControlTrackSelection(TrackSelectorResult trackSelectorResult) { - loadControl.onTracksSelected( - renderers, trackSelectorResult.groups, trackSelectorResult.selections); + private void updateLoadControlTrackSelection( + TrackGroupArray trackGroups, TrackSelectorResult trackSelectorResult) { + loadControl.onTracksSelected(renderers, trackGroups, trackSelectorResult.selections); } private void updateTrackSelectionPlaybackSpeed(float playbackSpeed) { @@ -1439,7 +1438,7 @@ import java.util.Collections; readingPeriodHolder.mediaPeriod.readDiscontinuity() != C.TIME_UNSET; for (int i = 0; i < renderers.length; i++) { Renderer renderer = renderers[i]; - boolean rendererWasEnabled = oldTrackSelectorResult.renderersEnabled[i]; + boolean rendererWasEnabled = oldTrackSelectorResult.isRendererEnabled(i); if (!rendererWasEnabled) { // The renderer was disabled and will be enabled when we play the next period. } else if (initialDiscontinuity) { @@ -1448,7 +1447,7 @@ import java.util.Collections; renderer.setCurrentStreamFinal(); } else if (!renderer.isCurrentStreamFinal()) { TrackSelection newSelection = newTrackSelectorResult.selections.get(i); - boolean newRendererEnabled = newTrackSelectorResult.renderersEnabled[i]; + boolean newRendererEnabled = newTrackSelectorResult.isRendererEnabled(i); boolean isNoSampleRenderer = rendererCapabilities[i].getTrackType() == C.TRACK_TYPE_NONE; RendererConfiguration oldConfig = oldTrackSelectorResult.rendererConfigurations[i]; RendererConfiguration newConfig = newTrackSelectorResult.rendererConfigurations[i]; @@ -1485,7 +1484,6 @@ import java.util.Collections; MediaPeriod mediaPeriod = queue.enqueueNextMediaPeriod( rendererCapabilities, - RENDERER_TIMESTAMP_OFFSET_US, trackSelector, loadControl.getAllocator(), mediaSource, @@ -1502,9 +1500,10 @@ import java.util.Collections; // Stale event. return; } - TrackSelectorResult trackSelectorResult = - queue.handleLoadingPeriodPrepared(mediaClock.getPlaybackParameters().speed); - updateLoadControlTrackSelection(trackSelectorResult); + MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod(); + loadingPeriodHolder.handlePrepared(mediaClock.getPlaybackParameters().speed); + updateLoadControlTrackSelection( + loadingPeriodHolder.trackGroups, loadingPeriodHolder.trackSelectorResult); if (!queue.hasPlayingPeriod()) { // This is the first prepared period, so start playing it. MediaPeriodHolder playingPeriodHolder = queue.advancePlayingPeriod(); @@ -1552,11 +1551,11 @@ import java.util.Collections; for (int i = 0; i < renderers.length; i++) { Renderer renderer = renderers[i]; rendererWasEnabledFlags[i] = renderer.getState() != Renderer.STATE_DISABLED; - if (newPlayingPeriodHolder.trackSelectorResult.renderersEnabled[i]) { + if (newPlayingPeriodHolder.trackSelectorResult.isRendererEnabled(i)) { enabledRendererCount++; } if (rendererWasEnabledFlags[i] - && (!newPlayingPeriodHolder.trackSelectorResult.renderersEnabled[i] + && (!newPlayingPeriodHolder.trackSelectorResult.isRendererEnabled(i) || (renderer.isCurrentStreamFinal() && renderer.getStream() == oldPlayingPeriodHolder.sampleStreams[i]))) { // The renderer should be disabled before playing the next period, either because it's not @@ -1566,7 +1565,8 @@ import java.util.Collections; } } playbackInfo = - playbackInfo.copyWithTrackSelectorResult(newPlayingPeriodHolder.trackSelectorResult); + playbackInfo.copyWithTrackInfo( + newPlayingPeriodHolder.trackGroups, newPlayingPeriodHolder.trackSelectorResult); enableRenderers(rendererWasEnabledFlags, enabledRendererCount); } @@ -1576,7 +1576,7 @@ import java.util.Collections; int enabledRendererCount = 0; MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod(); for (int i = 0; i < renderers.length; i++) { - if (playingPeriodHolder.trackSelectorResult.renderersEnabled[i]) { + if (playingPeriodHolder.trackSelectorResult.isRendererEnabled(i)) { enableRenderer(i, rendererWasEnabledFlags[i], enabledRendererCount++); } } 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 c830a246ae..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 @@ -15,17 +15,14 @@ */ package com.google.android.exoplayer2; -import android.annotation.SuppressLint; -import android.annotation.TargetApi; -import android.media.MediaFormat; 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; import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.video.ColorInfo; -import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -47,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. @@ -77,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. @@ -88,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. @@ -109,14 +96,10 @@ public final class Format implements Parcelable { public final float frameRate; /** * The clockwise rotation that should be applied to the video for it to be rendered in the correct - * orientation, or {@link #NO_VALUE} if unknown or not applicable. Only 0, 90, 180 and 270 are - * supported. + * orientation, or 0 if unknown or not applicable. Only 0, 90, 180 and 270 are supported. */ public final int rotationDegrees; - /** - * The width to height ratio of pixels in the video, or {@link #NO_VALUE} if unknown or not - * applicable. - */ + /** The width to height ratio of pixels in the video, or 1.0 if unknown or not applicable. */ public final float pixelWidthHeightRatio; /** * The stereo layout for 360/3D/VR video, or {@link #NO_VALUE} if not applicable. Valid stereo @@ -125,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. @@ -153,11 +132,12 @@ public final class Format implements Parcelable { @C.PcmEncoding public final int pcmEncoding; /** - * The number of samples to trim from the start of the decoded audio stream. + * The number of frames to trim from the start of the decoded audio stream, or 0 if not + * applicable. */ public final int encoderDelay; /** - * The number of samples to trim from the end of the decoded audio stream. + * The number of frames to trim from the end of the decoded audio stream, or 0 if not applicable. */ public final int encoderPadding; @@ -178,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. @@ -193,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, @@ -231,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, @@ -270,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, @@ -324,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, @@ -363,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; @@ -402,16 +518,17 @@ public final class Format implements Parcelable { this.width = width; this.height = height; this.frameRate = frameRate; - this.rotationDegrees = rotationDegrees; - this.pixelWidthHeightRatio = pixelWidthHeightRatio; + this.rotationDegrees = rotationDegrees == Format.NO_VALUE ? 0 : rotationDegrees; + this.pixelWidthHeightRatio = + pixelWidthHeightRatio == Format.NO_VALUE ? 1 : pixelWidthHeightRatio; this.projectionData = projectionData; this.stereoMode = stereoMode; this.colorInfo = colorInfo; this.channelCount = channelCount; this.sampleRate = sampleRate; this.pcmEncoding = pcmEncoding; - this.encoderDelay = encoderDelay; - this.encoderPadding = encoderPadding; + this.encoderDelay = encoderDelay == Format.NO_VALUE ? 0 : encoderDelay; + this.encoderPadding = encoderPadding == Format.NO_VALUE ? 0 : encoderPadding; this.selectionFlags = selectionFlags; this.language = language; this.accessibilityChannel = accessibilityChannel; @@ -435,7 +552,7 @@ public final class Format implements Parcelable { frameRate = in.readFloat(); rotationDegrees = in.readInt(); pixelWidthHeightRatio = in.readFloat(); - boolean hasProjectionData = in.readInt() != 0; + boolean hasProjectionData = Util.readBoolean(in); projectionData = hasProjectionData ? in.createByteArray() : null; stereoMode = in.readInt(); colorInfo = in.readParcelable(ColorInfo.class.getClassLoader()); @@ -474,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, @@ -518,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, @@ -526,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, @@ -550,29 +667,6 @@ public final class Format implements Parcelable { return width == NO_VALUE || height == NO_VALUE ? NO_VALUE : (width * height); } - /** - * Returns a {@link MediaFormat} representation of this format. - */ - @SuppressLint("InlinedApi") - @TargetApi(16) - public final MediaFormat getFrameworkMediaFormatV16() { - MediaFormat format = new MediaFormat(); - format.setString(MediaFormat.KEY_MIME, sampleMimeType); - maybeSetStringV16(format, MediaFormat.KEY_LANGUAGE, language); - maybeSetIntegerV16(format, MediaFormat.KEY_MAX_INPUT_SIZE, maxInputSize); - maybeSetIntegerV16(format, MediaFormat.KEY_WIDTH, width); - maybeSetIntegerV16(format, MediaFormat.KEY_HEIGHT, height); - maybeSetFloatV16(format, MediaFormat.KEY_FRAME_RATE, frameRate); - maybeSetIntegerV16(format, "rotation-degrees", rotationDegrees); - maybeSetIntegerV16(format, MediaFormat.KEY_CHANNEL_COUNT, channelCount); - maybeSetIntegerV16(format, MediaFormat.KEY_SAMPLE_RATE, sampleRate); - for (int i = 0; i < initializationData.size(); i++) { - format.setByteBuffer("csd-" + i, ByteBuffer.wrap(initializationData.get(i))); - } - maybeSetColorInfoV24(format, colorInfo); - return format; - } - @Override public String toString() { return "Format(" + id + ", " + containerMimeType + ", " + sampleMimeType + ", " + bitrate + ", " @@ -603,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; } @@ -611,24 +705,44 @@ public final class Format implements Parcelable { return false; } Format other = (Format) obj; - if (bitrate != other.bitrate || maxInputSize != other.maxInputSize - || width != other.width || height != other.height || frameRate != other.frameRate - || rotationDegrees != other.rotationDegrees - || pixelWidthHeightRatio != other.pixelWidthHeightRatio || stereoMode != other.stereoMode - || channelCount != other.channelCount || sampleRate != other.sampleRate - || pcmEncoding != other.pcmEncoding || encoderDelay != other.encoderDelay - || encoderPadding != other.encoderPadding || subsampleOffsetUs != other.subsampleOffsetUs - || selectionFlags != other.selectionFlags || !Util.areEqual(id, other.id) - || !Util.areEqual(language, other.language) - || accessibilityChannel != other.accessibilityChannel - || !Util.areEqual(containerMimeType, other.containerMimeType) - || !Util.areEqual(sampleMimeType, other.sampleMimeType) - || !Util.areEqual(codecs, other.codecs) - || !Util.areEqual(drmInitData, other.drmInitData) - || !Util.areEqual(metadata, other.metadata) - || !Util.areEqual(colorInfo, other.colorInfo) - || !Arrays.equals(projectionData, other.projectionData) - || initializationData.size() != other.initializationData.size()) { + return bitrate == other.bitrate + && maxInputSize == other.maxInputSize + && width == other.width + && height == other.height + && frameRate == other.frameRate + && rotationDegrees == other.rotationDegrees + && pixelWidthHeightRatio == other.pixelWidthHeightRatio + && stereoMode == other.stereoMode + && channelCount == other.channelCount + && sampleRate == other.sampleRate + && pcmEncoding == other.pcmEncoding + && encoderDelay == other.encoderDelay + && encoderPadding == other.encoderPadding + && subsampleOffsetUs == other.subsampleOffsetUs + && selectionFlags == other.selectionFlags + && Util.areEqual(id, other.id) + && Util.areEqual(language, other.language) + && accessibilityChannel == other.accessibilityChannel + && Util.areEqual(containerMimeType, other.containerMimeType) + && Util.areEqual(sampleMimeType, other.sampleMimeType) + && Util.areEqual(codecs, other.codecs) + && Util.areEqual(drmInitData, other.drmInitData) + && Util.areEqual(metadata, other.metadata) + && Util.areEqual(colorInfo, other.colorInfo) + && Arrays.equals(projectionData, other.projectionData) + && initializationDataEquals(other); + } + + /** + * Returns whether the {@link #initializationData}s belonging to this format and {@code other} are + * equal. + * + * @param other The other format whose {@link #initializationData} is being compared. + * @return Whether the {@link #initializationData}s belonging to this format and {@code other} are + * equal. + */ + public boolean initializationDataEquals(Format other) { + if (initializationData.size() != other.initializationData.size()) { return false; } for (int i = 0; i < initializationData.size(); i++) { @@ -639,45 +753,6 @@ public final class Format implements Parcelable { return true; } - @TargetApi(24) - private static void maybeSetColorInfoV24(MediaFormat format, ColorInfo colorInfo) { - if (colorInfo == null) { - return; - } - maybeSetIntegerV16(format, MediaFormat.KEY_COLOR_TRANSFER, colorInfo.colorTransfer); - maybeSetIntegerV16(format, MediaFormat.KEY_COLOR_STANDARD, colorInfo.colorSpace); - maybeSetIntegerV16(format, MediaFormat.KEY_COLOR_RANGE, colorInfo.colorRange); - maybeSetByteBufferV16(format, MediaFormat.KEY_HDR_STATIC_INFO, colorInfo.hdrStaticInfo); - } - - @TargetApi(16) - private static void maybeSetStringV16(MediaFormat format, String key, String value) { - if (value != null) { - format.setString(key, value); - } - } - - @TargetApi(16) - private static void maybeSetIntegerV16(MediaFormat format, String key, int value) { - if (value != NO_VALUE) { - format.setInteger(key, value); - } - } - - @TargetApi(16) - private static void maybeSetFloatV16(MediaFormat format, String key, float value) { - if (value != NO_VALUE) { - format.setFloat(key, value); - } - } - - @TargetApi(16) - private static void maybeSetByteBufferV16(MediaFormat format, String key, byte[] value) { - if (value != null) { - format.setByteBuffer(key, ByteBuffer.wrap(value)); - } - } - // Utility methods /** @@ -730,7 +805,7 @@ public final class Format implements Parcelable { dest.writeFloat(frameRate); dest.writeInt(rotationDegrees); dest.writeFloat(pixelWidthHeightRatio); - dest.writeInt(projectionData != null ? 1 : 0); + Util.writeBoolean(dest, projectionData != null); if (projectionData != null) { dest.writeByteArray(projectionData); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodHolder.java b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodHolder.java index 43036b154b..2f71d0d547 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodHolder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodHolder.java @@ -21,6 +21,7 @@ import com.google.android.exoplayer2.source.EmptySampleStream; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.SampleStream; +import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.trackselection.TrackSelector; @@ -43,6 +44,7 @@ import com.google.android.exoplayer2.util.Assertions; public boolean hasEnabledTracks; public MediaPeriodInfo info; public MediaPeriodHolder next; + public TrackGroupArray trackGroups; public TrackSelectorResult trackSelectorResult; private final RendererCapabilities[] rendererCapabilities; @@ -81,9 +83,12 @@ import com.google.android.exoplayer2.util.Assertions; mayRetainStreamFlags = new boolean[rendererCapabilities.length]; MediaPeriod mediaPeriod = mediaSource.createPeriod(info.id, allocator); if (info.endPositionUs != C.TIME_END_OF_SOURCE) { - ClippingMediaPeriod clippingMediaPeriod = new ClippingMediaPeriod(mediaPeriod, true); - clippingMediaPeriod.setClipping(0, info.endPositionUs); - mediaPeriod = clippingMediaPeriod; + mediaPeriod = + new ClippingMediaPeriod( + mediaPeriod, + /* enableInitialDiscontinuity= */ true, + /* startUs= */ 0, + info.endPositionUs); } this.mediaPeriod = mediaPeriod; } @@ -132,13 +137,13 @@ import com.google.android.exoplayer2.util.Assertions; return !prepared ? 0 : mediaPeriod.getNextLoadPositionUs(); } - public TrackSelectorResult handlePrepared(float playbackSpeed) throws ExoPlaybackException { + public void handlePrepared(float playbackSpeed) throws ExoPlaybackException { prepared = true; + trackGroups = mediaPeriod.getTrackGroups(); selectTracks(playbackSpeed); long newStartPositionUs = applyTrackSelection(info.startPositionUs, false); rendererPositionOffsetUs += info.startPositionUs - newStartPositionUs; info = info.copyWithStartPositionUs(newStartPositionUs); - return trackSelectorResult; } public void reevaluateBuffer(long rendererPositionUs) { @@ -154,7 +159,7 @@ import com.google.android.exoplayer2.util.Assertions; public boolean selectTracks(float playbackSpeed) throws ExoPlaybackException { TrackSelectorResult selectorResult = - trackSelector.selectTracks(rendererCapabilities, mediaPeriod.getTrackGroups()); + trackSelector.selectTracks(rendererCapabilities, trackGroups); if (selectorResult.isEquivalent(periodTrackSelectorResult)) { return false; } @@ -174,8 +179,7 @@ import com.google.android.exoplayer2.util.Assertions; public long applyTrackSelection( long positionUs, boolean forceRecreateStreams, boolean[] streamResetFlags) { - TrackSelectionArray trackSelections = trackSelectorResult.selections; - for (int i = 0; i < trackSelections.length; i++) { + for (int i = 0; i < trackSelectorResult.length; i++) { mayRetainStreamFlags[i] = !forceRecreateStreams && trackSelectorResult.isEquivalent(periodTrackSelectorResult, i); } @@ -185,6 +189,7 @@ import com.google.android.exoplayer2.util.Assertions; disassociateNoSampleRenderersWithEmptySampleStream(sampleStreams); updatePeriodTrackSelectorResult(trackSelectorResult); // Disable streams on the period and get new streams for updated/newly-enabled tracks. + TrackSelectionArray trackSelections = trackSelectorResult.selections; positionUs = mediaPeriod.selectTracks( trackSelections.getAll(), @@ -198,7 +203,7 @@ import com.google.android.exoplayer2.util.Assertions; hasEnabledTracks = false; for (int i = 0; i < sampleStreams.length; i++) { if (sampleStreams[i] != null) { - Assertions.checkState(trackSelectorResult.renderersEnabled[i]); + Assertions.checkState(trackSelectorResult.isRendererEnabled(i)); // hasEnabledTracks should be true only when non-empty streams exists. if (rendererCapabilities[i].getTrackType() != C.TRACK_TYPE_NONE) { hasEnabledTracks = true; @@ -235,8 +240,8 @@ import com.google.android.exoplayer2.util.Assertions; } private void enableTrackSelectionsInResult(TrackSelectorResult trackSelectorResult) { - for (int i = 0; i < trackSelectorResult.renderersEnabled.length; i++) { - boolean rendererEnabled = trackSelectorResult.renderersEnabled[i]; + for (int i = 0; i < trackSelectorResult.length; i++) { + boolean rendererEnabled = trackSelectorResult.isRendererEnabled(i); TrackSelection trackSelection = trackSelectorResult.selections.get(i); if (rendererEnabled && trackSelection != null) { trackSelection.enable(); @@ -245,8 +250,8 @@ import com.google.android.exoplayer2.util.Assertions; } private void disableTrackSelectionsInResult(TrackSelectorResult trackSelectorResult) { - for (int i = 0; i < trackSelectorResult.renderersEnabled.length; i++) { - boolean rendererEnabled = trackSelectorResult.renderersEnabled[i]; + for (int i = 0; i < trackSelectorResult.length; i++) { + boolean rendererEnabled = trackSelectorResult.isRendererEnabled(i); TrackSelection trackSelection = trackSelectorResult.selections.get(i); if (rendererEnabled && trackSelection != null) { trackSelection.disable(); @@ -273,7 +278,7 @@ import com.google.android.exoplayer2.util.Assertions; private void associateNoSampleRenderersWithEmptySampleStream(SampleStream[] sampleStreams) { for (int i = 0; i < rendererCapabilities.length; i++) { if (rendererCapabilities[i].getTrackType() == C.TRACK_TYPE_NONE - && trackSelectorResult.renderersEnabled[i]) { + && trackSelectorResult.isRendererEnabled(i)) { sampleStreams[i] = new EmptySampleStream(); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodQueue.java index 3efff58f5d..717f873622 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodQueue.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodQueue.java @@ -22,7 +22,6 @@ 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.trackselection.TrackSelector; -import com.google.android.exoplayer2.trackselection.TrackSelectorResult; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.util.Assertions; @@ -52,6 +51,8 @@ import com.google.android.exoplayer2.util.Assertions; private MediaPeriodHolder reading; private MediaPeriodHolder loading; private int length; + private Object oldFrontPeriodUid; + private long oldFrontPeriodWindowSequenceNumber; /** Creates a new media period queue. */ public MediaPeriodQueue() { @@ -130,7 +131,6 @@ import com.google.android.exoplayer2.util.Assertions; * and returns it. * * @param rendererCapabilities The renderer capabilities. - * @param rendererTimestampOffsetUs The base time offset added to for renderers. * @param trackSelector The track selector. * @param allocator The allocator. * @param mediaSource The media source that produced the media period. @@ -139,7 +139,6 @@ import com.google.android.exoplayer2.util.Assertions; */ public MediaPeriod enqueueNextMediaPeriod( RendererCapabilities[] rendererCapabilities, - long rendererTimestampOffsetUs, TrackSelector trackSelector, Allocator allocator, MediaSource mediaSource, @@ -147,7 +146,7 @@ import com.google.android.exoplayer2.util.Assertions; MediaPeriodInfo info) { long rendererPositionOffsetUs = loading == null - ? (info.startPositionUs + rendererTimestampOffsetUs) + ? info.startPositionUs : (loading.getRendererOffset() + loading.info.durationUs); MediaPeriodHolder newPeriodHolder = new MediaPeriodHolder( @@ -162,22 +161,12 @@ import com.google.android.exoplayer2.util.Assertions; Assertions.checkState(hasPlayingPeriod()); loading.next = newPeriodHolder; } + oldFrontPeriodUid = null; loading = newPeriodHolder; length++; return newPeriodHolder.mediaPeriod; } - /** - * Handles the loading media period being prepared. - * - * @param playbackSpeed The current playback speed. - * @return The result of selecting tracks on the newly prepared loading media period. - */ - public TrackSelectorResult handleLoadingPeriodPrepared(float playbackSpeed) - throws ExoPlaybackException { - return loading.handlePrepared(playbackSpeed); - } - /** * Returns the loading period holder which is at the end of the queue, or null if the queue is * empty. @@ -276,12 +265,21 @@ import com.google.android.exoplayer2.util.Assertions; return removedReading; } - /** Clears the queue. */ - public void clear() { + /** + * Clears the queue. + * + * @param keepFrontPeriodUid Whether the queue should keep the id of the media period in the front + * of queue (typically the playing one) for later reuse. + */ + public void clear(boolean keepFrontPeriodUid) { MediaPeriodHolder front = getFrontPeriod(); if (front != null) { + oldFrontPeriodUid = keepFrontPeriodUid ? front.uid : null; + oldFrontPeriodWindowSequenceNumber = front.info.id.windowSequenceNumber; front.release(); removeAfter(front); + } else if (!keepFrontPeriodUid) { + oldFrontPeriodUid = null; } playing = null; loading = null; @@ -408,6 +406,17 @@ import com.google.android.exoplayer2.util.Assertions; */ private long resolvePeriodIndexToWindowSequenceNumber(int periodIndex) { Object periodUid = timeline.getPeriod(periodIndex, period, /* setIds= */ true).uid; + int windowIndex = period.windowIndex; + if (oldFrontPeriodUid != null) { + int oldFrontPeriodIndex = timeline.getIndexOfPeriod(oldFrontPeriodUid); + if (oldFrontPeriodIndex != C.INDEX_UNSET) { + int oldFrontWindowIndex = timeline.getPeriod(oldFrontPeriodIndex, period).windowIndex; + if (oldFrontWindowIndex == windowIndex) { + // Try to match old front uid after the queue has been cleared. + return oldFrontPeriodWindowSequenceNumber; + } + } + } MediaPeriodHolder mediaPeriodHolder = getFrontPeriod(); while (mediaPeriodHolder != null) { if (mediaPeriodHolder.uid.equals(periodUid)) { @@ -416,7 +425,6 @@ import com.google.android.exoplayer2.util.Assertions; } mediaPeriodHolder = mediaPeriodHolder.next; } - int windowIndex = period.windowIndex; mediaPeriodHolder = getFrontPeriod(); while (mediaPeriodHolder != null) { int indexOfHolderInTimeline = timeline.getIndexOfPeriod(mediaPeriodHolder.uid); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java index 3a4ee0e501..80de073e2d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java @@ -13,10 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.google.android.exoplayer2; +package com.google.android.exoplayer2; import android.support.annotation.Nullable; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; +import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.TrackSelectorResult; /** @@ -31,13 +32,17 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; public final long contentPositionUs; public final int playbackState; public final boolean isLoading; + public final TrackGroupArray trackGroups; public final TrackSelectorResult trackSelectorResult; public volatile long positionUs; public volatile long bufferedPositionUs; public PlaybackInfo( - Timeline timeline, long startPositionUs, TrackSelectorResult trackSelectorResult) { + Timeline timeline, + long startPositionUs, + TrackGroupArray trackGroups, + TrackSelectorResult trackSelectorResult) { this( timeline, /* manifest= */ null, @@ -46,6 +51,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; /* contentPositionUs =*/ C.TIME_UNSET, Player.STATE_IDLE, /* isLoading= */ false, + trackGroups, trackSelectorResult); } @@ -57,6 +63,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; long contentPositionUs, int playbackState, boolean isLoading, + TrackGroupArray trackGroups, TrackSelectorResult trackSelectorResult) { this.timeline = timeline; this.manifest = manifest; @@ -67,11 +74,12 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; this.bufferedPositionUs = startPositionUs; this.playbackState = playbackState; this.isLoading = isLoading; + this.trackGroups = trackGroups; this.trackSelectorResult = trackSelectorResult; } - public PlaybackInfo fromNewPosition(MediaPeriodId periodId, long startPositionUs, - long contentPositionUs) { + public PlaybackInfo fromNewPosition( + MediaPeriodId periodId, long startPositionUs, long contentPositionUs) { return new PlaybackInfo( timeline, manifest, @@ -80,6 +88,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; periodId.isAd() ? contentPositionUs : C.TIME_UNSET, playbackState, isLoading, + trackGroups, trackSelectorResult); } @@ -93,6 +102,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; contentPositionUs, playbackState, isLoading, + trackGroups, trackSelectorResult); copyMutablePositions(this, playbackInfo); return playbackInfo; @@ -108,6 +118,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; contentPositionUs, playbackState, isLoading, + trackGroups, trackSelectorResult); copyMutablePositions(this, playbackInfo); return playbackInfo; @@ -123,6 +134,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; contentPositionUs, playbackState, isLoading, + trackGroups, trackSelectorResult); copyMutablePositions(this, playbackInfo); return playbackInfo; @@ -138,12 +150,14 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; contentPositionUs, playbackState, isLoading, + trackGroups, trackSelectorResult); copyMutablePositions(this, playbackInfo); return playbackInfo; } - public PlaybackInfo copyWithTrackSelectorResult(TrackSelectorResult trackSelectorResult) { + public PlaybackInfo copyWithTrackInfo( + TrackGroupArray trackGroups, TrackSelectorResult trackSelectorResult) { PlaybackInfo playbackInfo = new PlaybackInfo( timeline, @@ -153,6 +167,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; contentPositionUs, playbackState, isLoading, + trackGroups, trackSelectorResult); copyMutablePositions(this, playbackInfo); return playbackInfo; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/PlaybackParameters.java b/library/core/src/main/java/com/google/android/exoplayer2/PlaybackParameters.java index 47d5bc88b9..a7de96a2de 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/PlaybackParameters.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/PlaybackParameters.java @@ -23,33 +23,55 @@ import com.google.android.exoplayer2.util.Assertions; public final class PlaybackParameters { /** - * The default playback parameters: real-time playback with no pitch modification. + * The default playback parameters: real-time playback with no pitch modification or silence + * skipping. */ - public static final PlaybackParameters DEFAULT = new PlaybackParameters(1f, 1f); + public static final PlaybackParameters DEFAULT = new PlaybackParameters(/* speed= */ 1f); - /** - * The factor by which playback will be sped up. - */ + /** The factor by which playback will be sped up. */ public final float speed; - /** - * The factor by which the audio pitch will be scaled. - */ + /** The factor by which the audio pitch will be scaled. */ public final float pitch; + /** Whether to skip silence in the input. */ + public final boolean skipSilence; + private final int scaledUsPerMs; /** - * Creates new playback parameters. + * Creates new playback parameters that set the playback speed. + * + * @param speed The factor by which playback will be sped up. Must be greater than zero. + */ + public PlaybackParameters(float speed) { + this(speed, /* pitch= */ 1f, /* skipSilence= */ false); + } + + /** + * Creates new playback parameters that set the playback speed and audio pitch scaling factor. * * @param speed The factor by which playback will be sped up. Must be greater than zero. * @param pitch The factor by which the audio pitch will be scaled. Must be greater than zero. */ public PlaybackParameters(float speed, float pitch) { + this(speed, pitch, /* skipSilence= */ false); + } + + /** + * Creates new playback parameters that set the playback speed, audio pitch scaling factor and + * whether to skip silence in the audio stream. + * + * @param speed The factor by which playback will be sped up. Must be greater than zero. + * @param pitch The factor by which the audio pitch will be scaled. Must be greater than zero. + * @param skipSilence Whether to skip silences in the audio stream. + */ + public PlaybackParameters(float speed, float pitch, boolean skipSilence) { Assertions.checkArgument(speed > 0); Assertions.checkArgument(pitch > 0); this.speed = speed; this.pitch = pitch; + this.skipSilence = skipSilence; scaledUsPerMs = Math.round(speed * 1000f); } @@ -73,14 +95,17 @@ public final class PlaybackParameters { return false; } PlaybackParameters other = (PlaybackParameters) obj; - return this.speed == other.speed && this.pitch == other.pitch; + return this.speed == other.speed + && this.pitch == other.pitch + && this.skipSilence == other.skipSilence; } - + @Override public int hashCode() { int result = 17; result = 31 * result + Float.floatToRawIntBits(speed); result = 31 * result + Float.floatToRawIntBits(pitch); + result = 31 * result + (skipSilence ? 1 : 0); return result; } 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 443ff8a2ea..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}. *

    @@ -655,6 +666,12 @@ public interface Player { */ int getPreviousWindowIndex(); + /** + * Returns the tag of the currently playing window in the timeline. May be null if no tag is set + * or the timeline is not yet available. + */ + @Nullable Object getCurrentTag(); + /** * Returns the duration of the current window in milliseconds, or {@link C#TIME_UNSET} if the * duration is not known. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Renderer.java b/library/core/src/main/java/com/google/android/exoplayer2/Renderer.java index d0a07930e0..e53db4568d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Renderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Renderer.java @@ -15,21 +15,29 @@ */ package com.google.android.exoplayer2; +import android.support.annotation.IntDef; import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.util.MediaClock; import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; /** * Renders media read from a {@link SampleStream}. * *

    Internally, a renderer's lifecycle is managed by the owning {@link ExoPlayer}. The renderer is - * transitioned through various states as the overall playback state changes. The valid state - * transitions are shown below, annotated with the methods that are called during each transition. + * transitioned through various states as the overall playback state and enabled tracks change. The + * valid state transitions are shown below, annotated with the methods that are called during each + * transition. * *

    Renderer state transitions */ public interface Renderer extends PlayerMessage.Target { + /** The renderer states. */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({STATE_DISABLED, STATE_ENABLED, STATE_STARTED}) + @interface State {} /** * The renderer is disabled. */ @@ -80,8 +88,10 @@ public interface Renderer extends PlayerMessage.Target { /** * Returns the current state of the renderer. * - * @return The current state (one of the {@code STATE_*} constants). + * @return The current state. One of {@link #STATE_DISABLED}, {@link #STATE_ENABLED} and {@link + * #STATE_STARTED}. */ + @State int getState(); /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/RenderersFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/RenderersFactory.java index 944a6a9e5e..e221898471 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/RenderersFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/RenderersFactory.java @@ -16,7 +16,10 @@ package com.google.android.exoplayer2; import android.os.Handler; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.audio.AudioRendererEventListener; +import com.google.android.exoplayer2.drm.DrmSessionManager; +import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.google.android.exoplayer2.metadata.MetadataOutput; import com.google.android.exoplayer2.text.TextOutput; import com.google.android.exoplayer2.video.VideoRendererEventListener; @@ -34,11 +37,14 @@ public interface RenderersFactory { * @param audioRendererEventListener An event listener for audio renderers. * @param textRendererOutput An output for text renderers. * @param metadataRendererOutput An output for metadata renderers. + * @param drmSessionManager A drm session manager used by renderers. * @return The {@link Renderer instances}. */ - Renderer[] createRenderers(Handler eventHandler, + Renderer[] createRenderers( + Handler eventHandler, VideoRendererEventListener videoRendererEventListener, - AudioRendererEventListener audioRendererEventListener, TextOutput textRendererOutput, - MetadataOutput metadataRendererOutput); - + AudioRendererEventListener audioRendererEventListener, + TextOutput textRendererOutput, + MetadataOutput metadataRendererOutput, + @Nullable DrmSessionManager drmSessionManager); } 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 98ef35d62c..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 @@ -27,9 +27,14 @@ import android.view.Surface; import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.TextureView; +import com.google.android.exoplayer2.analytics.AnalyticsCollector; +import com.google.android.exoplayer2.analytics.AnalyticsListener; import com.google.android.exoplayer2.audio.AudioAttributes; import com.google.android.exoplayer2.audio.AudioRendererEventListener; import com.google.android.exoplayer2.decoder.DecoderCounters; +import com.google.android.exoplayer2.drm.DefaultDrmSessionManager; +import com.google.android.exoplayer2.drm.DrmSessionManager; +import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.MetadataOutput; import com.google.android.exoplayer2.source.MediaSource; @@ -42,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; @@ -61,6 +67,7 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player protected final Renderer[] renderers; private final ExoPlayer player; + private final Handler eventHandler; private final ComponentListener componentListener; private final CopyOnWriteArraySet videoListeners; @@ -68,6 +75,7 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player private final CopyOnWriteArraySet metadataOutputs; private final CopyOnWriteArraySet videoDebugListeners; private final CopyOnWriteArraySet audioDebugListeners; + private final AnalyticsCollector analyticsCollector; private Format videoFormat; private Format audioFormat; @@ -83,21 +91,60 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player private int audioSessionId; private AudioAttributes audioAttributes; private float audioVolume; + private MediaSource mediaSource; /** * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance. * @param trackSelector The {@link TrackSelector} that will be used by the instance. * @param loadControl The {@link LoadControl} that will be used by the instance. + * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance + * will not be used for DRM protected playbacks. */ protected SimpleExoPlayer( - RenderersFactory renderersFactory, TrackSelector trackSelector, LoadControl loadControl) { - this(renderersFactory, trackSelector, loadControl, Clock.DEFAULT); + RenderersFactory renderersFactory, + TrackSelector trackSelector, + LoadControl loadControl, + @Nullable DrmSessionManager drmSessionManager) { + this( + renderersFactory, + trackSelector, + loadControl, + drmSessionManager, + new AnalyticsCollector.Factory()); } /** * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance. * @param trackSelector The {@link TrackSelector} that will be used by the instance. * @param loadControl The {@link LoadControl} that will be used by the instance. + * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance + * will not be used for DRM protected playbacks. + * @param analyticsCollectorFactory A factory for creating the {@link AnalyticsCollector} that + * will collect and forward all player events. + */ + protected SimpleExoPlayer( + RenderersFactory renderersFactory, + TrackSelector trackSelector, + LoadControl loadControl, + @Nullable DrmSessionManager drmSessionManager, + AnalyticsCollector.Factory analyticsCollectorFactory) { + this( + renderersFactory, + trackSelector, + loadControl, + drmSessionManager, + analyticsCollectorFactory, + Clock.DEFAULT); + } + + /** + * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance. + * @param trackSelector The {@link TrackSelector} that will be used by the instance. + * @param loadControl The {@link LoadControl} that will be used by the instance. + * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance + * will not be used for DRM protected playbacks. + * @param analyticsCollectorFactory A factory for creating the {@link AnalyticsCollector} that + * will collect and forward all player events. * @param clock The {@link Clock} that will be used by the instance. Should always be {@link * Clock#DEFAULT}, unless the player is being used from a test. */ @@ -105,6 +152,8 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player RenderersFactory renderersFactory, TrackSelector trackSelector, LoadControl loadControl, + @Nullable DrmSessionManager drmSessionManager, + AnalyticsCollector.Factory analyticsCollectorFactory, Clock clock) { componentListener = new ComponentListener(); videoListeners = new CopyOnWriteArraySet<>(); @@ -113,9 +162,15 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player videoDebugListeners = new CopyOnWriteArraySet<>(); audioDebugListeners = new CopyOnWriteArraySet<>(); Looper eventLooper = Looper.myLooper() != null ? Looper.myLooper() : Looper.getMainLooper(); - Handler eventHandler = new Handler(eventLooper); - renderers = renderersFactory.createRenderers(eventHandler, componentListener, componentListener, - componentListener, componentListener); + eventHandler = new Handler(eventLooper); + renderers = + renderersFactory.createRenderers( + eventHandler, + componentListener, + componentListener, + componentListener, + componentListener, + drmSessionManager); // Set initial values. audioVolume = 1; @@ -125,6 +180,14 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player // Build the player and associated objects. player = createExoPlayerImpl(renderers, trackSelector, loadControl, clock); + analyticsCollector = analyticsCollectorFactory.createAnalyticsCollector(player, clock); + addListener(analyticsCollector); + videoDebugListeners.add(analyticsCollector); + audioDebugListeners.add(analyticsCollector); + addMetadataOutput(analyticsCollector); + if (drmSessionManager instanceof DefaultDrmSessionManager) { + ((DefaultDrmSessionManager) drmSessionManager).addListener(eventHandler, analyticsCollector); + } } @Override @@ -267,6 +330,29 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player return Util.getStreamTypeForAudioUsage(audioAttributes.usage); } + /** Returns the {@link AnalyticsCollector} used for collecting analytics events. */ + public AnalyticsCollector getAnalyticsCollector() { + return analyticsCollector; + } + + /** + * Adds an {@link AnalyticsListener} to receive analytics events. + * + * @param listener The listener to be added. + */ + public void addAnalyticsListener(AnalyticsListener listener) { + analyticsCollector.addListener(listener); + } + + /** + * Removes an {@link AnalyticsListener}. + * + * @param listener The listener to be removed. + */ + public void removeAnalyticsListener(AnalyticsListener listener) { + analyticsCollector.removeListener(listener); + } + /** * Sets the attributes for audio playback, used by the underlying audio track. If not set, the * default audio attributes will be used. They are suitable for general media playback. @@ -449,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); } @@ -465,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); } @@ -483,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); } @@ -568,13 +660,26 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player return player.getPlaybackState(); } + @Override + public ExoPlaybackException getPlaybackError() { + return player.getPlaybackError(); + } + @Override public void prepare(MediaSource mediaSource) { - player.prepare(mediaSource); + prepare(mediaSource, /* resetPosition= */ true, /* resetState= */ true); } @Override public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) { + if (this.mediaSource != mediaSource) { + if (this.mediaSource != null) { + this.mediaSource.removeEventListener(analyticsCollector); + analyticsCollector.resetForNewMediaSource(); + } + mediaSource.addEventListener(eventHandler, analyticsCollector); + this.mediaSource = mediaSource; + } player.prepare(mediaSource, resetPosition, resetState); } @@ -615,21 +720,25 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player @Override public void seekToDefaultPosition() { + analyticsCollector.notifySeekStarted(); player.seekToDefaultPosition(); } @Override public void seekToDefaultPosition(int windowIndex) { + analyticsCollector.notifySeekStarted(); player.seekToDefaultPosition(windowIndex); } @Override public void seekTo(long positionMs) { + analyticsCollector.notifySeekStarted(); player.seekTo(positionMs); } @Override public void seekTo(int windowIndex, long positionMs) { + analyticsCollector.notifySeekStarted(); player.seekTo(windowIndex, positionMs); } @@ -648,14 +757,24 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player player.setSeekParameters(seekParameters); } + @Override + public @Nullable Object getCurrentTag() { + return player.getCurrentTag(); + } + @Override public void stop() { - player.stop(); + stop(/* reset= */ false); } @Override public void stop(boolean reset) { player.stop(reset); + if (mediaSource != null) { + mediaSource.removeEventListener(analyticsCollector); + mediaSource = null; + analyticsCollector.resetForNewMediaSource(); + } } @Override @@ -668,6 +787,9 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player } surface = null; } + if (mediaSource != null) { + mediaSource.removeEventListener(analyticsCollector); + } } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java b/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java index 50a3e66880..600fbc3014 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2; +import android.support.annotation.Nullable; import android.util.Pair; import com.google.android.exoplayer2.source.ads.AdPlaybackState; import com.google.android.exoplayer2.util.Assertions; @@ -118,10 +119,8 @@ public abstract class Timeline { */ public static final class Window { - /** - * An identifier for the window. Not necessarily unique. - */ - public Object id; + /** A tag for the window. Not necessarily unique. */ + public @Nullable Object tag; /** * The start time of the presentation to which this window belongs in milliseconds since the @@ -174,13 +173,19 @@ public abstract class Timeline { */ public long positionInFirstPeriodUs; - /** - * Sets the data held by this window. - */ - public Window set(Object id, long presentationStartTimeMs, long windowStartTimeMs, - boolean isSeekable, boolean isDynamic, long defaultPositionUs, long durationUs, - int firstPeriodIndex, int lastPeriodIndex, long positionInFirstPeriodUs) { - this.id = id; + /** Sets the data held by this window. */ + public Window set( + @Nullable Object tag, + long presentationStartTimeMs, + long windowStartTimeMs, + boolean isSeekable, + boolean isDynamic, + long defaultPositionUs, + long durationUs, + int firstPeriodIndex, + int lastPeriodIndex, + long positionInFirstPeriodUs) { + this.tag = tag; this.presentationStartTimeMs = presentationStartTimeMs; this.windowStartTimeMs = windowStartTimeMs; this.isSeekable = isSeekable; @@ -486,38 +491,36 @@ public abstract class Timeline { } - /** - * An empty timeline. - */ - public static final Timeline EMPTY = new Timeline() { + /** An empty timeline. */ + public static final Timeline EMPTY = + new Timeline() { - @Override - public int getWindowCount() { - return 0; - } + @Override + public int getWindowCount() { + return 0; + } - @Override - public Window getWindow(int windowIndex, Window window, boolean setIds, - long defaultPositionProjectionUs) { - throw new IndexOutOfBoundsException(); - } + @Override + public Window getWindow( + int windowIndex, Window window, boolean setTag, long defaultPositionProjectionUs) { + throw new IndexOutOfBoundsException(); + } - @Override - public int getPeriodCount() { - return 0; - } + @Override + public int getPeriodCount() { + return 0; + } - @Override - public Period getPeriod(int periodIndex, Period period, boolean setIds) { - throw new IndexOutOfBoundsException(); - } + @Override + public Period getPeriod(int periodIndex, Period period, boolean setIds) { + throw new IndexOutOfBoundsException(); + } - @Override - public int getIndexOfPeriod(Object uid) { - return C.INDEX_UNSET; - } - - }; + @Override + public int getIndexOfPeriod(Object uid) { + return C.INDEX_UNSET; + } + }; /** * Returns whether the timeline is empty. @@ -607,7 +610,7 @@ public abstract class Timeline { /** * Populates a {@link Window} with data for the window at the specified index. Does not populate - * {@link Window#id}. + * {@link Window#tag}. * * @param windowIndex The index of the window. * @param window The {@link Window} to populate. Must not be null. @@ -622,12 +625,12 @@ public abstract class Timeline { * * @param windowIndex The index of the window. * @param window The {@link Window} to populate. Must not be null. - * @param setIds Whether {@link Window#id} should be populated. If false, the field will be set to - * null. The caller should pass false for efficiency reasons unless the field is required. + * @param setTag Whether {@link Window#tag} should be populated. If false, the field will be set + * to null. The caller should pass false for efficiency reasons unless the field is required. * @return The populated {@link Window}, for convenience. */ - public final Window getWindow(int windowIndex, Window window, boolean setIds) { - return getWindow(windowIndex, window, setIds, 0); + public final Window getWindow(int windowIndex, Window window, boolean setTag) { + return getWindow(windowIndex, window, setTag, 0); } /** @@ -635,14 +638,14 @@ public abstract class Timeline { * * @param windowIndex The index of the window. * @param window The {@link Window} to populate. Must not be null. - * @param setIds Whether {@link Window#id} should be populated. If false, the field will be set to - * null. The caller should pass false for efficiency reasons unless the field is required. + * @param setTag Whether {@link Window#tag} should be populated. If false, the field will be set + * to null. The caller should pass false for efficiency reasons unless the field is required. * @param defaultPositionProjectionUs A duration into the future that the populated window's * default start position should be projected. * @return The populated {@link Window}, for convenience. */ - public abstract Window getWindow(int windowIndex, Window window, boolean setIds, - long defaultPositionProjectionUs); + public abstract Window getWindow( + int windowIndex, Window window, boolean setTag, long defaultPositionProjectionUs); /** * Returns the number of periods in the timeline. 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 new file mode 100644 index 0000000000..43ef308f27 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java @@ -0,0 +1,798 @@ +/* + * 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.analytics; + +import android.net.NetworkInfo; +import android.support.annotation.Nullable; +import android.view.Surface; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.PlaybackParameters; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.Timeline.Period; +import com.google.android.exoplayer2.Timeline.Window; +import com.google.android.exoplayer2.analytics.AnalyticsListener.EventTime; +import com.google.android.exoplayer2.audio.AudioRendererEventListener; +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.TrackGroupArray; +import com.google.android.exoplayer2.trackselection.TrackSelectionArray; +import com.google.android.exoplayer2.upstream.BandwidthMeter; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Clock; +import com.google.android.exoplayer2.video.VideoRendererEventListener; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; + +/** + * Data collector which is able to forward analytics events to {@link AnalyticsListener}s by + * listening to all available ExoPlayer listeners. + */ +public class AnalyticsCollector + implements Player.EventListener, + MetadataOutput, + AudioRendererEventListener, + VideoRendererEventListener, + MediaSourceEventListener, + BandwidthMeter.EventListener, + DefaultDrmSessionEventListener { + + /** Factory for an analytics collector. */ + public static class Factory { + + /** + * Creates an analytics collector for the specified player. + * + * @param player The {@link Player} for which data will be collected. + * @param clock A {@link Clock} used to generate timestamps. + * @return An analytics collector. + */ + public AnalyticsCollector createAnalyticsCollector(Player player, Clock clock) { + return new AnalyticsCollector(player, clock); + } + } + + private final CopyOnWriteArraySet listeners; + private final Player player; + private final Clock clock; + private final Window window; + private final MediaPeriodQueueTracker mediaPeriodQueueTracker; + + /** + * Creates an analytics collector for the specified player. + * + * @param player The {@link Player} for which data will be collected. + * @param clock A {@link Clock} used to generate timestamps. + */ + protected AnalyticsCollector(Player player, Clock clock) { + this.player = Assertions.checkNotNull(player); + this.clock = Assertions.checkNotNull(clock); + listeners = new CopyOnWriteArraySet<>(); + mediaPeriodQueueTracker = new MediaPeriodQueueTracker(); + window = new Window(); + } + + /** + * Adds a listener for analytics events. + * + * @param listener The listener to add. + */ + public void addListener(AnalyticsListener listener) { + listeners.add(listener); + } + + /** + * Removes a previously added analytics event listener. + * + * @param listener The listener to remove. + */ + public void removeListener(AnalyticsListener listener) { + listeners.remove(listener); + } + + // External events. + + /** + * Notify analytics collector that a seek operation will start. Should be called before the player + * adjusts its state and position to the seek. + */ + public final void notifySeekStarted() { + if (!mediaPeriodQueueTracker.isSeeking()) { + EventTime eventTime = generatePlayingMediaPeriodEventTime(); + mediaPeriodQueueTracker.onSeekStarted(); + for (AnalyticsListener listener : listeners) { + listener.onSeekStarted(eventTime); + } + } + } + + /** + * Notify analytics collector that the viewport size changed. + * + * @param width The new width of the viewport in device-independent pixels (dp). + * @param height The new height of the viewport in device-independent pixels (dp). + */ + public final void notifyViewportSizeChanged(int width, int height) { + EventTime eventTime = generatePlayingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onViewportSizeChange(eventTime, width, height); + } + } + + /** + * Notify analytics collector that the network type or connectivity changed. + * + * @param networkInfo The new network info, or null if no network connection exists. + */ + public final void notifyNetworkTypeChanged(@Nullable NetworkInfo networkInfo) { + EventTime eventTime = generatePlayingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onNetworkTypeChanged(eventTime, networkInfo); + } + } + + /** + * Resets the analytics collector for a new media source. Should be called before the player is + * prepared with a new media source. + */ + public final void resetForNewMediaSource() { + // Copying the list is needed because onMediaPeriodReleased will modify the list. + List activeMediaPeriods = + new ArrayList<>(mediaPeriodQueueTracker.activeMediaPeriods); + for (WindowAndMediaPeriodId mediaPeriod : activeMediaPeriods) { + onMediaPeriodReleased(mediaPeriod.windowIndex, mediaPeriod.mediaPeriodId); + } + } + + // MetadataOutput implementation. + + @Override + public final void onMetadata(Metadata metadata) { + EventTime eventTime = generatePlayingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onMetadata(eventTime, metadata); + } + } + + // AudioRendererEventListener implementation. + + @Override + public final void onAudioEnabled(DecoderCounters counters) { + // The renderers are only enabled after we changed the playing media period. + EventTime eventTime = generatePlayingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onDecoderEnabled(eventTime, C.TRACK_TYPE_AUDIO, counters); + } + } + + @Override + public final void onAudioSessionId(int audioSessionId) { + EventTime eventTime = generateReadingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onAudioSessionId(eventTime, audioSessionId); + } + } + + @Override + public final void onAudioDecoderInitialized( + String decoderName, long initializedTimestampMs, long initializationDurationMs) { + EventTime eventTime = generateReadingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onDecoderInitialized( + eventTime, C.TRACK_TYPE_AUDIO, decoderName, initializationDurationMs); + } + } + + @Override + public final void onAudioInputFormatChanged(Format format) { + EventTime eventTime = generateReadingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onDecoderInputFormatChanged(eventTime, C.TRACK_TYPE_AUDIO, format); + } + } + + @Override + public final void onAudioSinkUnderrun( + int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) { + EventTime eventTime = generateReadingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onAudioUnderrun(eventTime, bufferSize, bufferSizeMs, elapsedSinceLastFeedMs); + } + } + + @Override + public final void onAudioDisabled(DecoderCounters counters) { + // The renderers are disabled after we changed the playing media period on the playback thread + // but before this change is reported to the app thread. + EventTime eventTime = generateLastReportedPlayingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onDecoderDisabled(eventTime, C.TRACK_TYPE_AUDIO, counters); + } + } + + // VideoRendererEventListener implementation. + + @Override + public final void onVideoEnabled(DecoderCounters counters) { + // The renderers are only enabled after we changed the playing media period. + EventTime eventTime = generatePlayingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onDecoderEnabled(eventTime, C.TRACK_TYPE_VIDEO, counters); + } + } + + @Override + public final void onVideoDecoderInitialized( + String decoderName, long initializedTimestampMs, long initializationDurationMs) { + EventTime eventTime = generateReadingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onDecoderInitialized( + eventTime, C.TRACK_TYPE_VIDEO, decoderName, initializationDurationMs); + } + } + + @Override + public final void onVideoInputFormatChanged(Format format) { + EventTime eventTime = generateReadingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onDecoderInputFormatChanged(eventTime, C.TRACK_TYPE_VIDEO, format); + } + } + + @Override + public final void onDroppedFrames(int count, long elapsedMs) { + EventTime eventTime = generateLastReportedPlayingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onDroppedVideoFrames(eventTime, count, elapsedMs); + } + } + + @Override + public final void onVideoSizeChanged( + int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { + EventTime eventTime = generateReadingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onVideoSizeChanged( + eventTime, width, height, unappliedRotationDegrees, pixelWidthHeightRatio); + } + } + + @Override + public final void onRenderedFirstFrame(Surface surface) { + EventTime eventTime = generateReadingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onRenderedFirstFrame(eventTime, surface); + } + } + + @Override + public final void onVideoDisabled(DecoderCounters counters) { + // The renderers are disabled after we changed the playing media period on the playback thread + // but before this change is reported to the app thread. + EventTime eventTime = generateLastReportedPlayingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onDecoderDisabled(eventTime, C.TRACK_TYPE_VIDEO, counters); + } + } + + // MediaSourceEventListener implementation. + + @Override + public final void onMediaPeriodCreated(int windowIndex, MediaPeriodId mediaPeriodId) { + mediaPeriodQueueTracker.onMediaPeriodCreated(windowIndex, mediaPeriodId); + EventTime eventTime = generateEventTime(windowIndex, mediaPeriodId); + for (AnalyticsListener listener : listeners) { + listener.onMediaPeriodCreated(eventTime); + } + } + + @Override + public final void onMediaPeriodReleased(int windowIndex, MediaPeriodId mediaPeriodId) { + mediaPeriodQueueTracker.onMediaPeriodReleased(windowIndex, mediaPeriodId); + EventTime eventTime = generateEventTime(windowIndex, mediaPeriodId); + for (AnalyticsListener listener : listeners) { + listener.onMediaPeriodReleased(eventTime); + } + } + + @Override + public final void onLoadStarted( + int windowIndex, + @Nullable MediaPeriodId mediaPeriodId, + LoadEventInfo loadEventInfo, + MediaLoadData mediaLoadData) { + EventTime eventTime = generateEventTime(windowIndex, mediaPeriodId); + for (AnalyticsListener listener : listeners) { + listener.onLoadStarted(eventTime, loadEventInfo, mediaLoadData); + } + } + + @Override + public final void onLoadCompleted( + int windowIndex, + @Nullable MediaPeriodId mediaPeriodId, + LoadEventInfo loadEventInfo, + MediaLoadData mediaLoadData) { + EventTime eventTime = generateEventTime(windowIndex, mediaPeriodId); + for (AnalyticsListener listener : listeners) { + listener.onLoadCompleted(eventTime, loadEventInfo, mediaLoadData); + } + } + + @Override + public final void onLoadCanceled( + int windowIndex, + @Nullable MediaPeriodId mediaPeriodId, + LoadEventInfo loadEventInfo, + MediaLoadData mediaLoadData) { + EventTime eventTime = generateEventTime(windowIndex, mediaPeriodId); + for (AnalyticsListener listener : listeners) { + listener.onLoadCanceled(eventTime, loadEventInfo, mediaLoadData); + } + } + + @Override + public final void onLoadError( + int windowIndex, + @Nullable MediaPeriodId mediaPeriodId, + LoadEventInfo loadEventInfo, + MediaLoadData mediaLoadData, + IOException error, + boolean wasCanceled) { + EventTime eventTime = generateEventTime(windowIndex, mediaPeriodId); + for (AnalyticsListener listener : listeners) { + listener.onLoadError(eventTime, loadEventInfo, mediaLoadData, error, wasCanceled); + } + } + + @Override + public final void onReadingStarted(int windowIndex, MediaPeriodId mediaPeriodId) { + mediaPeriodQueueTracker.onReadingStarted(windowIndex, mediaPeriodId); + EventTime eventTime = generateEventTime(windowIndex, mediaPeriodId); + for (AnalyticsListener listener : listeners) { + listener.onReadingStarted(eventTime); + } + } + + @Override + public final void onUpstreamDiscarded( + int windowIndex, @Nullable MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData) { + EventTime eventTime = generateEventTime(windowIndex, mediaPeriodId); + for (AnalyticsListener listener : listeners) { + listener.onUpstreamDiscarded(eventTime, mediaLoadData); + } + } + + @Override + public final void onDownstreamFormatChanged( + int windowIndex, @Nullable MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData) { + EventTime eventTime = generateEventTime(windowIndex, mediaPeriodId); + for (AnalyticsListener listener : listeners) { + listener.onDownstreamFormatChanged(eventTime, mediaLoadData); + } + } + + // Player.EventListener implementation. + + // TODO: Add onFinishedReportingChanges to Player.EventListener to know when a set of simultaneous + // callbacks finished. This helps to assign exactly the same EventTime to all of them instead of + // having slightly different real times. + + @Override + public final void onTimelineChanged( + Timeline timeline, Object manifest, @Player.TimelineChangeReason int reason) { + mediaPeriodQueueTracker.onTimelineChanged(timeline); + EventTime eventTime = generatePlayingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onTimelineChanged(eventTime, reason); + } + } + + @Override + public final void onTracksChanged( + TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { + EventTime eventTime = generatePlayingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onTracksChanged(eventTime, trackGroups, trackSelections); + } + } + + @Override + public final void onLoadingChanged(boolean isLoading) { + EventTime eventTime = generatePlayingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onLoadingChanged(eventTime, isLoading); + } + } + + @Override + public final void onPlayerStateChanged(boolean playWhenReady, int playbackState) { + EventTime eventTime = generatePlayingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onPlayerStateChanged(eventTime, playWhenReady, playbackState); + } + } + + @Override + public final void onRepeatModeChanged(@Player.RepeatMode int repeatMode) { + EventTime eventTime = generatePlayingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onRepeatModeChanged(eventTime, repeatMode); + } + } + + @Override + public final void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { + EventTime eventTime = generatePlayingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onShuffleModeChanged(eventTime, shuffleModeEnabled); + } + } + + @Override + public final void onPlayerError(ExoPlaybackException error) { + EventTime eventTime = generatePlayingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onPlayerError(eventTime, error); + } + } + + @Override + public final void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { + mediaPeriodQueueTracker.onPositionDiscontinuity(reason); + EventTime eventTime = generatePlayingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onPositionDiscontinuity(eventTime, reason); + } + } + + @Override + public final void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { + EventTime eventTime = generatePlayingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onPlaybackParametersChanged(eventTime, playbackParameters); + } + } + + @Override + public final void onSeekProcessed() { + if (mediaPeriodQueueTracker.isSeeking()) { + mediaPeriodQueueTracker.onSeekProcessed(); + EventTime eventTime = generatePlayingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onSeekProcessed(eventTime); + } + } + } + + // BandwidthMeter.Listener implementation. + + @Override + public final void onBandwidthSample(int elapsedMs, long bytes, long bitrate) { + EventTime eventTime = generateLoadingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onBandwidthEstimate(eventTime, elapsedMs, bytes, bitrate); + } + } + + // DefaultDrmSessionManager.EventListener implementation. + + @Override + public final void onDrmKeysLoaded() { + EventTime eventTime = generateReadingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onDrmKeysLoaded(eventTime); + } + } + + @Override + public final void onDrmSessionManagerError(Exception error) { + EventTime eventTime = generateReadingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onDrmSessionManagerError(eventTime, error); + } + } + + @Override + public final void onDrmKeysRestored() { + EventTime eventTime = generateReadingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onDrmKeysRestored(eventTime); + } + } + + @Override + public final void onDrmKeysRemoved() { + EventTime eventTime = generateReadingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onDrmKeysRemoved(eventTime); + } + } + + // Internal methods. + + /** Returns read-only set of registered listeners. */ + protected Set getListeners() { + return Collections.unmodifiableSet(listeners); + } + + /** Returns a new {@link EventTime} for the specified window index and media period id. */ + protected EventTime generateEventTime(int windowIndex, @Nullable MediaPeriodId mediaPeriodId) { + long realtimeMs = clock.elapsedRealtime(); + Timeline timeline = player.getCurrentTimeline(); + long eventPositionMs; + if (windowIndex == player.getCurrentWindowIndex()) { + if (mediaPeriodId != null && mediaPeriodId.isAd()) { + // This event is for an ad in the currently playing window. + eventPositionMs = + player.getCurrentAdGroupIndex() == mediaPeriodId.adGroupIndex + && player.getCurrentAdIndexInAdGroup() == mediaPeriodId.adIndexInAdGroup + ? player.getCurrentPosition() + : 0 /* Assume start position of 0 for a future ad. */; + } else { + // This event is for content in the currently playing window. + eventPositionMs = player.getContentPosition(); + } + } 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; + } else { + // This event is for content in a future window. Assume default start position. + eventPositionMs = timeline.getWindow(windowIndex, window).getDefaultPositionMs(); + } + // TODO(b/30792113): implement this properly (player.getTotalBufferedDuration()). + long bufferedDurationMs = player.getBufferedPosition() - player.getContentPosition(); + return new EventTime( + realtimeMs, + timeline, + windowIndex, + mediaPeriodId, + eventPositionMs, + player.getCurrentPosition(), + bufferedDurationMs); + } + + private EventTime generateEventTime(@Nullable WindowAndMediaPeriodId mediaPeriod) { + if (mediaPeriod == null) { + int windowIndex = player.getCurrentWindowIndex(); + MediaPeriodId mediaPeriodId = mediaPeriodQueueTracker.tryResolveWindowIndex(windowIndex); + return generateEventTime(windowIndex, mediaPeriodId); + } + return generateEventTime(mediaPeriod.windowIndex, mediaPeriod.mediaPeriodId); + } + + private EventTime generateLastReportedPlayingMediaPeriodEventTime() { + return generateEventTime(mediaPeriodQueueTracker.getLastReportedPlayingMediaPeriod()); + } + + private EventTime generatePlayingMediaPeriodEventTime() { + return generateEventTime(mediaPeriodQueueTracker.getPlayingMediaPeriod()); + } + + private EventTime generateReadingMediaPeriodEventTime() { + return generateEventTime(mediaPeriodQueueTracker.getReadingMediaPeriod()); + } + + private EventTime generateLoadingMediaPeriodEventTime() { + return generateEventTime(mediaPeriodQueueTracker.getLoadingMediaPeriod()); + } + + /** Keeps track of the active media periods and currently playing and reading media period. */ + private static final class MediaPeriodQueueTracker { + + // 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 Period period; + + private WindowAndMediaPeriodId lastReportedPlayingMediaPeriod; + private WindowAndMediaPeriodId readingMediaPeriod; + private Timeline timeline; + private boolean isSeeking; + + public MediaPeriodQueueTracker() { + activeMediaPeriods = new ArrayList<>(); + period = new Period(); + timeline = Timeline.EMPTY; + } + + /** + * 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 WindowAndMediaPeriodId getPlayingMediaPeriod() { + return activeMediaPeriods.isEmpty() || timeline.isEmpty() || isSeeking + ? null + : activeMediaPeriods.get(0); + } + + /** + * 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 WindowAndMediaPeriodId getLastReportedPlayingMediaPeriod() { + return lastReportedPlayingMediaPeriod; + } + + /** + * 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 WindowAndMediaPeriodId getReadingMediaPeriod() { + return readingMediaPeriod; + } + + /** + * Returns the {@link MediaPeriodId} of the media period at the end of the queue which is + * currently loading or will be the next one loading. May be null, if no media period is active + * yet. + */ + public @Nullable WindowAndMediaPeriodId getLoadingMediaPeriod() { + return activeMediaPeriods.isEmpty() + ? null + : activeMediaPeriods.get(activeMediaPeriods.size() - 1); + } + + /** Returns whether the player is currently seeking. */ + public boolean isSeeking() { + return isSeeking; + } + + /** + * 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) { + int timelinePeriodCount = timeline.getPeriodCount(); + for (int i = 0; i < activeMediaPeriods.size(); i++) { + 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 = mediaPeriod.mediaPeriodId; + } + } + } + return match; + } + + /** Updates the queue with a reported position discontinuity . */ + public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { + updateLastReportedPlayingMediaPeriod(); + } + + /** Updates the queue with a reported timeline change. */ + public void onTimelineChanged(Timeline timeline) { + for (int i = 0; i < activeMediaPeriods.size(); i++) { + activeMediaPeriods.set( + i, updateMediaPeriodToNewTimeline(activeMediaPeriods.get(i), timeline)); + } + if (readingMediaPeriod != null) { + readingMediaPeriod = updateMediaPeriodToNewTimeline(readingMediaPeriod, timeline); + } + this.timeline = timeline; + updateLastReportedPlayingMediaPeriod(); + } + + /** Updates the queue with a reported start of seek. */ + public void onSeekStarted() { + isSeeking = true; + } + + /** Updates the queue with a reported processed seek. */ + public void onSeekProcessed() { + isSeeking = false; + updateLastReportedPlayingMediaPeriod(); + } + + /** Updates the queue with a newly created media period. */ + 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(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(int windowIndex, MediaPeriodId mediaPeriodId) { + readingMediaPeriod = new WindowAndMediaPeriodId(windowIndex, mediaPeriodId); + } + + private void updateLastReportedPlayingMediaPeriod() { + if (!activeMediaPeriods.isEmpty()) { + lastReportedPlayingMediaPeriod = activeMediaPeriods.get(0); + } + } + + private WindowAndMediaPeriodId updateMediaPeriodToNewTimeline( + WindowAndMediaPeriodId mediaPeriod, Timeline newTimeline) { + if (newTimeline.isEmpty() || timeline.isEmpty()) { + return mediaPeriod; + } + 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 new file mode 100644 index 0000000000..48057f2bff --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsListener.java @@ -0,0 +1,465 @@ +/* + * 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.analytics; + +import android.net.NetworkInfo; +import android.support.annotation.Nullable; +import android.view.Surface; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.PlaybackParameters; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Player.DiscontinuityReason; +import com.google.android.exoplayer2.Player.TimelineChangeReason; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.audio.AudioSink; +import com.google.android.exoplayer2.decoder.DecoderCounters; +import com.google.android.exoplayer2.metadata.Metadata; +import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; +import com.google.android.exoplayer2.source.MediaSourceEventListener.LoadEventInfo; +import com.google.android.exoplayer2.source.MediaSourceEventListener.MediaLoadData; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.trackselection.TrackSelectionArray; +import java.io.IOException; + +/** + * A listener for analytics events. + * + *

    All events are recorded with an {@link EventTime} specifying the elapsed real time and media + * time at the time of the event. + */ +public interface AnalyticsListener { + + /** Time information of an event. */ + final class EventTime { + + /** + * Elapsed real-time as returned by {@code SystemClock.elapsedRealtime()} at the time of the + * event, in milliseconds. + */ + public final long realtimeMs; + + /** Timeline at the time of the event. */ + public final Timeline timeline; + + /** + * Window index in the {@code timeline} this event belongs to, or the prospective window index + * if the timeline is not yet known and empty. + */ + public final int windowIndex; + + /** + * Media period identifier for the media period this event belongs to, or {@code null} if the + * event is not associated with a specific media period. + */ + public final @Nullable MediaPeriodId mediaPeriodId; + + /** + * Position in the window or ad this event belongs to at the time of the event, in milliseconds. + */ + public final long eventPlaybackPositionMs; + + /** + * Position in the current timeline window ({@code timeline.getCurrentWindowIndex()} or the + * currently playing ad at the time of the event, in milliseconds. + */ + public final long currentPlaybackPositionMs; + + /** + * Total buffered duration from {@link #currentPlaybackPositionMs} at the time of the event, in + * milliseconds. This includes pre-buffered data for subsequent ads and windows. + */ + public final long totalBufferedDurationMs; + + /** + * @param realtimeMs Elapsed real-time as returned by {@code SystemClock.elapsedRealtime()} at + * the time of the event, in milliseconds. + * @param timeline Timeline at the time of the event. + * @param windowIndex Window index in the {@code timeline} this event belongs to, or the + * prospective window index if the timeline is not yet known and empty. + * @param mediaPeriodId Media period identifier for the media period this event belongs to, or + * {@code null} if the event is not associated with a specific media period. + * @param eventPlaybackPositionMs Position in the window or ad this event belongs to at the time + * of the event, in milliseconds. + * @param currentPlaybackPositionMs Position in the current timeline window ({@code + * timeline.getCurrentWindowIndex()} or the currently playing ad at the time of the event, + * in milliseconds. + * @param totalBufferedDurationMs Total buffered duration from {@link + * #currentPlaybackPositionMs} at the time of the event, in milliseconds. This includes + * pre-buffered data for subsequent ads and windows. + */ + public EventTime( + long realtimeMs, + Timeline timeline, + int windowIndex, + @Nullable MediaPeriodId mediaPeriodId, + long eventPlaybackPositionMs, + long currentPlaybackPositionMs, + long totalBufferedDurationMs) { + this.realtimeMs = realtimeMs; + this.timeline = timeline; + this.windowIndex = windowIndex; + this.mediaPeriodId = mediaPeriodId; + this.eventPlaybackPositionMs = eventPlaybackPositionMs; + this.currentPlaybackPositionMs = currentPlaybackPositionMs; + this.totalBufferedDurationMs = totalBufferedDurationMs; + } + } + + /** + * Called when the player state changed. + * + * @param eventTime The event time. + * @param playWhenReady Whether the playback will proceed when ready. + * @param playbackState One of the {@link Player}.STATE constants. + */ + void onPlayerStateChanged(EventTime eventTime, boolean playWhenReady, int playbackState); + + /** + * Called when the timeline changed. + * + * @param eventTime The event time. + * @param reason The reason for the timeline change. + */ + void onTimelineChanged(EventTime eventTime, @TimelineChangeReason int reason); + + /** + * Called when a position discontinuity occurred. + * + * @param eventTime The event time. + * @param reason The reason for the position discontinuity. + */ + void onPositionDiscontinuity(EventTime eventTime, @DiscontinuityReason int reason); + + /** + * Called when a seek operation started. + * + * @param eventTime The event time. + */ + void onSeekStarted(EventTime eventTime); + + /** + * Called when a seek operation was processed. + * + * @param eventTime The event time. + */ + void onSeekProcessed(EventTime eventTime); + + /** + * Called when the playback parameters changed. + * + * @param eventTime The event time. + * @param playbackParameters The new playback parameters. + */ + void onPlaybackParametersChanged(EventTime eventTime, PlaybackParameters playbackParameters); + + /** + * Called when the repeat mode changed. + * + * @param eventTime The event time. + * @param repeatMode The new repeat mode. + */ + void onRepeatModeChanged(EventTime eventTime, @Player.RepeatMode int repeatMode); + + /** + * Called when the shuffle mode changed. + * + * @param eventTime The event time. + * @param shuffleModeEnabled Whether the shuffle mode is enabled. + */ + void onShuffleModeChanged(EventTime eventTime, boolean shuffleModeEnabled); + + /** + * Called when the player starts or stops loading data from a source. + * + * @param eventTime The event time. + * @param isLoading Whether the player is loading. + */ + void onLoadingChanged(EventTime eventTime, boolean isLoading); + + /** + * Called when a fatal player error occurred. + * + * @param eventTime The event time. + * @param error The error. + */ + void onPlayerError(EventTime eventTime, ExoPlaybackException error); + + /** + * Called when the available or selected tracks for the renderers changed. + * + * @param eventTime The event time. + * @param trackGroups The available tracks. May be empty. + * @param trackSelections The track selections for each renderer. May contain null elements. + */ + void onTracksChanged( + EventTime eventTime, TrackGroupArray trackGroups, TrackSelectionArray trackSelections); + + /** + * Called when a media source started loading data. + * + * @param eventTime The event time. + * @param loadEventInfo The {@link LoadEventInfo} defining the load event. + * @param mediaLoadData The {@link MediaLoadData} defining the data being loaded. + */ + void onLoadStarted(EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData); + + /** + * Called when a media source completed loading data. + * + * @param eventTime The event time. + * @param loadEventInfo The {@link LoadEventInfo} defining the load event. + * @param mediaLoadData The {@link MediaLoadData} defining the data being loaded. + */ + void onLoadCompleted( + EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData); + + /** + * Called when a media source canceled loading data. + * + * @param eventTime The event time. + * @param loadEventInfo The {@link LoadEventInfo} defining the load event. + * @param mediaLoadData The {@link MediaLoadData} defining the data being loaded. + */ + void onLoadCanceled( + EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData); + + /** + * Called when a media source loading error occurred. These errors are just for informational + * purposes and the player may recover. + * + * @param eventTime The event time. + * @param loadEventInfo The {@link LoadEventInfo} defining the load event. + * @param mediaLoadData The {@link MediaLoadData} defining the data being loaded. + * @param error The load error. + * @param wasCanceled Whether the load was canceled as a result of the error. + */ + void onLoadError( + EventTime eventTime, + LoadEventInfo loadEventInfo, + MediaLoadData mediaLoadData, + IOException error, + boolean wasCanceled); + + /** + * Called when the downstream format sent to the renderers changed. + * + * @param eventTime The event time. + * @param mediaLoadData The {@link MediaLoadData} defining the newly selected media data. + */ + void onDownstreamFormatChanged(EventTime eventTime, MediaLoadData mediaLoadData); + + /** + * Called when data is removed from the back of a media buffer, typically so that it can be + * re-buffered in a different format. + * + * @param eventTime The event time. + * @param mediaLoadData The {@link MediaLoadData} defining the media being discarded. + */ + void onUpstreamDiscarded(EventTime eventTime, MediaLoadData mediaLoadData); + + /** + * Called when a media source created a media period. + * + * @param eventTime The event time. + */ + void onMediaPeriodCreated(EventTime eventTime); + + /** + * Called when a media source released a media period. + * + * @param eventTime The event time. + */ + void onMediaPeriodReleased(EventTime eventTime); + + /** + * Called when the player started reading a media period. + * + * @param eventTime The event time. + */ + void onReadingStarted(EventTime eventTime); + + /** + * Called when the bandwidth estimate for the current data source has been updated. + * + * @param eventTime The event time. + * @param totalLoadTimeMs The total time spend loading this update is based on, in milliseconds. + * @param totalBytesLoaded The total bytes loaded this update is based on. + * @param bitrateEstimate The bandwidth estimate, in bits per second. + */ + void onBandwidthEstimate( + EventTime eventTime, int totalLoadTimeMs, long totalBytesLoaded, long bitrateEstimate); + + /** + * Called when the viewport size of the output surface changed. + * + * @param eventTime The event time. + * @param width The width of the viewport in device-independent pixels (dp). + * @param height The height of the viewport in device-independent pixels (dp). + */ + void onViewportSizeChange(EventTime eventTime, int width, int height); + + /** + * Called when the type of the network connection changed. + * + * @param eventTime The event time. + * @param networkInfo The network info for the current connection, or null if disconnected. + */ + void onNetworkTypeChanged(EventTime eventTime, @Nullable NetworkInfo networkInfo); + + /** + * Called when there is {@link Metadata} associated with the current playback time. + * + * @param eventTime The event time. + * @param metadata The metadata. + */ + void onMetadata(EventTime eventTime, Metadata metadata); + + /** + * Called when an audio or video decoder has been enabled. + * + * @param eventTime The event time. + * @param trackType The track type of the enabled decoder. Either {@link C#TRACK_TYPE_AUDIO} or + * {@link C#TRACK_TYPE_VIDEO}. + * @param decoderCounters The accumulated event counters associated with this decoder. + */ + void onDecoderEnabled(EventTime eventTime, int trackType, DecoderCounters decoderCounters); + + /** + * Called when an audio or video decoder has been initialized. + * + * @param eventTime The event time. + * @param trackType The track type of the initialized decoder. Either {@link C#TRACK_TYPE_AUDIO} + * or {@link C#TRACK_TYPE_VIDEO}. + * @param decoderName The decoder that was created. + * @param initializationDurationMs Time taken to initialize the decoder, in milliseconds. + */ + void onDecoderInitialized( + EventTime eventTime, int trackType, String decoderName, long initializationDurationMs); + + /** + * Called when an audio or video decoder input format changed. + * + * @param eventTime The event time. + * @param trackType The track type of the decoder whose format changed. Either {@link + * C#TRACK_TYPE_AUDIO} or {@link C#TRACK_TYPE_VIDEO}. + * @param format The new input format for the decoder. + */ + void onDecoderInputFormatChanged(EventTime eventTime, int trackType, Format format); + + /** + * Called when an audio or video decoder has been disabled. + * + * @param eventTime The event time. + * @param trackType The track type of the disabled decoder. Either {@link C#TRACK_TYPE_AUDIO} or + * {@link C#TRACK_TYPE_VIDEO}. + * @param decoderCounters The accumulated event counters associated with this decoder. + */ + void onDecoderDisabled(EventTime eventTime, int trackType, DecoderCounters decoderCounters); + + /** + * Called when the audio session id is set. + * + * @param eventTime The event time. + * @param audioSessionId The audio session id. + */ + void onAudioSessionId(EventTime eventTime, int audioSessionId); + + /** + * Called when an audio underrun occurred. + * + * @param eventTime The event time. + * @param bufferSize The size of the {@link AudioSink}'s buffer, in bytes. + * @param bufferSizeMs The size of the {@link AudioSink}'s buffer, in milliseconds, if it is + * configured for PCM output. {@link C#TIME_UNSET} if it is configured for passthrough output, + * as the buffered media can have a variable bitrate so the duration may be unknown. + * @param elapsedSinceLastFeedMs The time since the {@link AudioSink} was last fed data. + */ + void onAudioUnderrun( + EventTime eventTime, int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs); + + /** + * Called after video frames have been dropped. + * + * @param eventTime The event time. + * @param droppedFrames The number of dropped frames since the last call to this method. + * @param elapsedMs The duration in milliseconds over which the frames were dropped. This duration + * is timed from when the renderer was started or from when dropped frames were last reported + * (whichever was more recent), and not from when the first of the reported drops occurred. + */ + void onDroppedVideoFrames(EventTime eventTime, int droppedFrames, long elapsedMs); + + /** + * Called before a frame is rendered for the first time since setting the surface, and each time + * there's a change in the size or pixel aspect ratio of the video being rendered. + * + * @param eventTime The event time. + * @param width The width of the video. + * @param height The height of the video. + * @param unappliedRotationDegrees For videos that require a rotation, this is the clockwise + * rotation in degrees that the application should apply for the video for it to be rendered + * in the correct orientation. This value will always be zero on API levels 21 and above, + * since the renderer will apply all necessary rotations internally. + * @param pixelWidthHeightRatio The width to height ratio of each pixel. + */ + void onVideoSizeChanged( + EventTime eventTime, + int width, + int height, + int unappliedRotationDegrees, + float pixelWidthHeightRatio); + + /** + * Called when a frame is rendered for the first time since setting the surface, and when a frame + * is rendered for the first time since the renderer was reset. + * + * @param eventTime The event time. + * @param surface The {@link Surface} to which a first frame has been rendered, or {@code null} if + * the renderer renders to something that isn't a {@link Surface}. + */ + void onRenderedFirstFrame(EventTime eventTime, Surface surface); + + /** + * Called each time drm keys are loaded. + * + * @param eventTime The event time. + */ + void onDrmKeysLoaded(EventTime eventTime); + + /** + * Called when a drm error occurs. These errors are just for informational purposes and the player + * may recover. + * + * @param eventTime The event time. + * @param error The error. + */ + void onDrmSessionManagerError(EventTime eventTime, Exception error); + + /** + * Called each time offline drm keys are restored. + * + * @param eventTime The event time. + */ + void onDrmKeysRestored(EventTime eventTime); + + /** + * Called each time offline drm keys are removed. + * + * @param eventTime The event time. + */ + void onDrmKeysRemoved(EventTime eventTime); +} 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 new file mode 100644 index 0000000000..4a49de56b0 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/DefaultAnalyticsListener.java @@ -0,0 +1,166 @@ +/* + * 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.analytics; + +import android.net.NetworkInfo; +import android.view.Surface; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.PlaybackParameters; +import com.google.android.exoplayer2.decoder.DecoderCounters; +import com.google.android.exoplayer2.metadata.Metadata; +import com.google.android.exoplayer2.source.MediaSourceEventListener.LoadEventInfo; +import com.google.android.exoplayer2.source.MediaSourceEventListener.MediaLoadData; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.trackselection.TrackSelectionArray; +import java.io.IOException; + +/** + * {@link AnalyticsListener} allowing selective overrides. All methods are implemented as no-ops. + */ +public abstract class DefaultAnalyticsListener implements AnalyticsListener { + + @Override + public void onPlayerStateChanged(EventTime eventTime, boolean playWhenReady, int playbackState) {} + + @Override + public void onTimelineChanged(EventTime eventTime, int reason) {} + + @Override + public void onPositionDiscontinuity(EventTime eventTime, int reason) {} + + @Override + public void onSeekStarted(EventTime eventTime) {} + + @Override + public void onSeekProcessed(EventTime eventTime) {} + + @Override + public void onPlaybackParametersChanged( + EventTime eventTime, PlaybackParameters playbackParameters) {} + + @Override + public void onRepeatModeChanged(EventTime eventTime, int repeatMode) {} + + @Override + public void onShuffleModeChanged(EventTime eventTime, boolean shuffleModeEnabled) {} + + @Override + public void onLoadingChanged(EventTime eventTime, boolean isLoading) {} + + @Override + public void onPlayerError(EventTime eventTime, ExoPlaybackException error) {} + + @Override + public void onTracksChanged( + EventTime eventTime, TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {} + + @Override + public void onLoadStarted( + EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) {} + + @Override + public void onLoadCompleted( + EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) {} + + @Override + public void onLoadCanceled( + EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) {} + + @Override + public void onLoadError( + EventTime eventTime, + LoadEventInfo loadEventInfo, + MediaLoadData mediaLoadData, + IOException error, + boolean wasCanceled) {} + + @Override + public void onDownstreamFormatChanged(EventTime eventTime, MediaLoadData mediaLoadData) {} + + @Override + public void onUpstreamDiscarded(EventTime eventTime, MediaLoadData mediaLoadData) {} + + @Override + public void onMediaPeriodCreated(EventTime eventTime) {} + + @Override + public void onMediaPeriodReleased(EventTime eventTime) {} + + @Override + public void onReadingStarted(EventTime eventTime) {} + + @Override + public void onBandwidthEstimate( + EventTime eventTime, int totalLoadTimeMs, long totalBytesLoaded, long bitrateEstimate) {} + + @Override + public void onViewportSizeChange(EventTime eventTime, int width, int height) {} + + @Override + public void onNetworkTypeChanged(EventTime eventTime, NetworkInfo networkInfo) {} + + @Override + public void onMetadata(EventTime eventTime, Metadata metadata) {} + + @Override + public void onDecoderEnabled( + EventTime eventTime, int trackType, DecoderCounters decoderCounters) {} + + @Override + public void onDecoderInitialized( + EventTime eventTime, int trackType, String decoderName, long initializationDurationMs) {} + + @Override + public void onDecoderInputFormatChanged(EventTime eventTime, int trackType, Format format) {} + + @Override + public void onDecoderDisabled( + EventTime eventTime, int trackType, DecoderCounters decoderCounters) {} + + @Override + public void onAudioSessionId(EventTime eventTime, int audioSessionId) {} + + @Override + public void onAudioUnderrun( + EventTime eventTime, int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {} + + @Override + public void onDroppedVideoFrames(EventTime eventTime, int droppedFrames, long elapsedMs) {} + + @Override + public void onVideoSizeChanged( + EventTime eventTime, + int width, + int height, + int unappliedRotationDegrees, + float pixelWidthHeightRatio) {} + + @Override + public void onRenderedFirstFrame(EventTime eventTime, Surface surface) {} + + @Override + public void onDrmKeysLoaded(EventTime eventTime) {} + + @Override + public void onDrmSessionManagerError(EventTime eventTime, Exception error) {} + + @Override + public void onDrmKeysRestored(EventTime eventTime) {} + + @Override + public void onDrmKeysRemoved(EventTime eventTime) {} +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java index f45a6a11c6..c61b8ff24c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java @@ -15,41 +15,35 @@ */ package com.google.android.exoplayer2.audio; -import static com.google.android.exoplayer2.audio.Ac3Util.Ac3SyncFrameInfo.STREAM_TYPE_TYPE0; -import static com.google.android.exoplayer2.audio.Ac3Util.Ac3SyncFrameInfo.STREAM_TYPE_TYPE1; -import static com.google.android.exoplayer2.audio.Ac3Util.Ac3SyncFrameInfo.STREAM_TYPE_UNDEFINED; - +import android.support.annotation.IntDef; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.audio.Ac3Util.SyncFrameInfo.StreamType; import com.google.android.exoplayer2.drm.DrmInitData; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableBitArray; import com.google.android.exoplayer2.util.ParsableByteArray; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.nio.ByteBuffer; /** Utility methods for parsing Dolby TrueHD and (E-)AC3 syncframes. */ public final class Ac3Util { - /** - * Holds sample format information as presented by a syncframe header. - */ - public static final class Ac3SyncFrameInfo { + /** Holds sample format information as presented by a syncframe header. */ + public static final class SyncFrameInfo { - /** - * Undefined AC3 stream type. - */ + /** AC3 stream types. See also ETSI TS 102 366 E.1.3.1.1. */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({STREAM_TYPE_UNDEFINED, STREAM_TYPE_TYPE0, STREAM_TYPE_TYPE1, STREAM_TYPE_TYPE2}) + public @interface StreamType {} + /** Undefined AC3 stream type. */ public static final int STREAM_TYPE_UNDEFINED = -1; - /** - * Type 0 AC3 stream type. See ETSI TS 102 366 E.1.3.1.1. - */ + /** Type 0 AC3 stream type. */ public static final int STREAM_TYPE_TYPE0 = 0; - /** - * Type 1 AC3 stream type. See ETSI TS 102 366 E.1.3.1.1. - */ + /** Type 1 AC3 stream type. */ public static final int STREAM_TYPE_TYPE1 = 1; - /** - * Type 2 AC3 stream type. See ETSI TS 102 366 E.1.3.1.1. - */ + /** Type 2 AC3 stream type. */ public static final int STREAM_TYPE_TYPE2 = 2; /** @@ -58,10 +52,10 @@ public final class Ac3Util { */ public final String mimeType; /** - * The type of the stream if {@link #mimeType} is {@link MimeTypes#AUDIO_E_AC3}, or - * {@link #STREAM_TYPE_UNDEFINED} otherwise. + * The type of the stream if {@link #mimeType} is {@link MimeTypes#AUDIO_E_AC3}, or {@link + * #STREAM_TYPE_UNDEFINED} otherwise. */ - public final int streamType; + public final @StreamType int streamType; /** * The audio sampling rate in Hz. */ @@ -79,8 +73,13 @@ public final class Ac3Util { */ public final int sampleCount; - private Ac3SyncFrameInfo(String mimeType, int streamType, int channelCount, int sampleRate, - int frameSize, int sampleCount) { + private SyncFrameInfo( + String mimeType, + @StreamType int streamType, + int channelCount, + int sampleRate, + int frameSize, + int sampleCount) { this.mimeType = mimeType; this.streamType = streamType; this.channelCount = channelCount; @@ -96,11 +95,11 @@ public final class Ac3Util { * of samples extracted from the container corresponding to one syncframe must be an integer * multiple of this value. */ - public static final int TRUEHD_RECHUNK_SAMPLE_COUNT = 8; + public static final int TRUEHD_RECHUNK_SAMPLE_COUNT = 16; /** * The number of bytes that must be parsed from a TrueHD syncframe to calculate the sample count. */ - public static final int TRUEHD_SYNCFRAME_PREFIX_LENGTH = 12; + public static final int TRUEHD_SYNCFRAME_PREFIX_LENGTH = 10; /** * The number of new samples per (E-)AC-3 audio block. @@ -212,13 +211,13 @@ public final class Ac3Util { * @param data The data to parse, positioned at the start of the syncframe. * @return The (E-)AC-3 format data parsed from the header. */ - public static Ac3SyncFrameInfo parseAc3SyncframeInfo(ParsableBitArray data) { + public static SyncFrameInfo parseAc3SyncframeInfo(ParsableBitArray data) { int initialPosition = data.getPosition(); data.skipBits(40); boolean isEac3 = data.readBits(5) == 16; data.setPosition(initialPosition); String mimeType; - int streamType = STREAM_TYPE_UNDEFINED; + @StreamType int streamType = SyncFrameInfo.STREAM_TYPE_UNDEFINED; int sampleRate; int acmod; int frameSize; @@ -228,7 +227,20 @@ public final class Ac3Util { if (isEac3) { // Syntax from ETSI TS 102 366 V1.2.1 subsections E.1.2.1 and E.1.2.2. data.skipBits(16); // syncword - streamType = data.readBits(2); + switch (data.readBits(2)) { // strmtyp + case 0: + streamType = SyncFrameInfo.STREAM_TYPE_TYPE0; + break; + case 1: + streamType = SyncFrameInfo.STREAM_TYPE_TYPE1; + break; + case 2: + streamType = SyncFrameInfo.STREAM_TYPE_TYPE2; + break; + default: + streamType = SyncFrameInfo.STREAM_TYPE_UNDEFINED; + break; + } data.skipBits(3); // substreamid frameSize = (data.readBits(11) + 1) * 2; int fscod = data.readBits(2); @@ -257,7 +269,7 @@ public final class Ac3Util { data.skipBits(8); // compr2 } } - if (streamType == STREAM_TYPE_TYPE1 && data.readBit()) { // chanmape + if (streamType == SyncFrameInfo.STREAM_TYPE_TYPE1 && data.readBit()) { // chanmape data.skipBits(16); // chanmap } if (data.readBit()) { // mixmdate @@ -273,7 +285,7 @@ public final class Ac3Util { if (lfeon && data.readBit()) { // lfemixlevcode data.skipBits(5); // lfemixlevcod } - if (streamType == STREAM_TYPE_TYPE0) { + if (streamType == SyncFrameInfo.STREAM_TYPE_TYPE0) { if (data.readBit()) { // pgmscle data.skipBits(6); //pgmscl } @@ -375,10 +387,11 @@ public final class Ac3Util { data.skipBit(); // sourcefscod } } - if (streamType == 0 && numblkscod != 3) { + if (streamType == SyncFrameInfo.STREAM_TYPE_TYPE0 && numblkscod != 3) { data.skipBit(); // convsync } - if (streamType == 2 && (numblkscod == 3 || data.readBit())) { // blkid + if (streamType == SyncFrameInfo.STREAM_TYPE_TYPE2 + && (numblkscod == 3 || data.readBit())) { // blkid data.skipBits(6); // frmsizecod } mimeType = MimeTypes.AUDIO_E_AC3; @@ -410,8 +423,8 @@ public final class Ac3Util { lfeon = data.readBit(); channelCount = CHANNEL_COUNT_BY_ACMOD[acmod] + (lfeon ? 1 : 0); } - return new Ac3SyncFrameInfo(mimeType, streamType, channelCount, sampleRate, frameSize, - sampleCount); + return new SyncFrameInfo( + mimeType, streamType, channelCount, sampleRate, frameSize, sampleCount); } /** @@ -450,6 +463,26 @@ public final class Ac3Util { : BLOCKS_PER_SYNCFRAME_BY_NUMBLKSCOD[(buffer.get(buffer.position() + 4) & 0x30) >> 4]); } + /** + * Returns the offset relative to the buffer's position of the start of a TrueHD syncframe, or + * {@link C#INDEX_UNSET} if no syncframe was found. The buffer's position is not modified. + * + * @param buffer The {@link ByteBuffer} within which to find a syncframe. + * @return The offset relative to the buffer's position of the start of a TrueHD syncframe, or + * {@link C#INDEX_UNSET} if no syncframe was found. + */ + public static int findTrueHdSyncframeOffset(ByteBuffer buffer) { + int startIndex = buffer.position(); + int endIndex = buffer.limit() - TRUEHD_SYNCFRAME_PREFIX_LENGTH; + for (int i = startIndex; i <= endIndex; i++) { + // The syncword ends 0xBA for TrueHD or 0xBB for MLP. + if ((buffer.getInt(i + 4) & 0xFEFFFFFF) == 0xBA6F72F8) { + return i - startIndex; + } + } + return C.INDEX_UNSET; + } + /** * Returns the number of audio samples represented by the given TrueHD syncframe, or 0 if the * buffer is not the start of a syncframe. @@ -461,30 +494,29 @@ public final class Ac3Util { */ public static int parseTrueHdSyncframeAudioSampleCount(byte[] syncframe) { // TODO: Link to specification if available. + // The syncword ends 0xBA for TrueHD or 0xBB for MLP. if (syncframe[4] != (byte) 0xF8 || syncframe[5] != (byte) 0x72 || syncframe[6] != (byte) 0x6F - || syncframe[7] != (byte) 0xBA) { + || (syncframe[7] & 0xFE) != 0xBA) { return 0; } - return 40 << (syncframe[8] & 7); + boolean isMlp = (syncframe[7] & 0xFF) == 0xBB; + return 40 << ((syncframe[isMlp ? 9 : 8] >> 4) & 0x07); } /** - * Reads the number of audio samples represented by the given TrueHD syncframe, or 0 if the buffer - * is not the start of a syncframe. The buffer's position is not modified. + * Reads the number of audio samples represented by a TrueHD syncframe. The buffer's position is + * not modified. * - * @param buffer The {@link ByteBuffer} from which to read the syncframe. Must have at least - * {@link #TRUEHD_SYNCFRAME_PREFIX_LENGTH} bytes remaining. - * @return The number of audio samples represented by the syncframe, or 0 if the buffer is not the - * start of a syncframe. + * @param buffer The {@link ByteBuffer} from which to read the syncframe. + * @param offset The offset of the start of the syncframe relative to the buffer's position. + * @return The number of audio samples represented by the syncframe. */ - public static int parseTrueHdSyncframeAudioSampleCount(ByteBuffer buffer) { + public static int parseTrueHdSyncframeAudioSampleCount(ByteBuffer buffer, int offset) { // TODO: Link to specification if available. - if (buffer.getInt(buffer.position() + 4) != 0xBA6F72F8) { - return 0; - } - return 40 << (buffer.get(buffer.position() + 8) & 0x07); + boolean isMlp = (buffer.get(buffer.position() + offset + 7) & 0xFF) == 0xBB; + return 40 << ((buffer.get(buffer.position() + offset + (isMlp ? 9 : 8)) >> 4) & 0x07); } private static int getAc3SyncframeSize(int fscod, int frmsizecod) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioProcessor.java index 8a3d624222..f82be31f72 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioProcessor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioProcessor.java @@ -22,19 +22,20 @@ import java.nio.ByteOrder; /** * Interface for audio processors, which take audio data as input and transform it, potentially * modifying its channel count, encoding and/or sample rate. - *

    - * Call {@link #configure(int, int, int)} to configure the processor to receive input audio, then - * call {@link #isActive()} to determine whether the processor is active. - * {@link #queueInput(ByteBuffer)}, {@link #queueEndOfStream()}, {@link #getOutput()}, - * {@link #isEnded()}, {@link #getOutputChannelCount()}, {@link #getOutputEncoding()} and - * {@link #getOutputSampleRateHz()} may only be called if the processor is active. Call - * {@link #reset()} to reset the processor to its unconfigured state. + * + *

    Call {@link #configure(int, int, int)} to configure the processor to receive input audio, then + * call {@link #isActive()} to determine whether the processor is active. {@link + * #queueInput(ByteBuffer)}, {@link #queueEndOfStream()}, {@link #getOutput()}, {@link #isEnded()}, + * {@link #getOutputChannelCount()}, {@link #getOutputEncoding()} and {@link + * #getOutputSampleRateHz()} may only be called if the processor is active. Call {@link #reset()} to + * reset the processor to its unconfigured state and release any resources. + * + *

    In addition to being able to modify the format of audio, implementations may allow parameters + * to be set that affect the output audio and whether the processor is active/inactive. */ public interface AudioProcessor { - /** - * Exception thrown when a processor can't be configured for a given input audio format. - */ + /** Exception thrown when a processor can't be configured for a given input audio format. */ final class UnhandledFormatException extends Exception { public UnhandledFormatException(int sampleRateHz, int channelCount, @C.Encoding int encoding) { @@ -44,33 +45,25 @@ public interface AudioProcessor { } - /** - * An empty, direct {@link ByteBuffer}. - */ + /** An empty, direct {@link ByteBuffer}. */ ByteBuffer EMPTY_BUFFER = ByteBuffer.allocateDirect(0).order(ByteOrder.nativeOrder()); /** - * Configures the processor to process input audio with the specified format. After calling this - * method, {@link #isActive()} returns whether the processor needs to handle buffers; if not, the - * processor will not accept any buffers until it is reconfigured. Returns {@code true} if the - * processor must be flushed, or if the value returned by {@link #isActive()} has changed as a - * result of the call. If it's active, {@link #getOutputSampleRateHz()}, - * {@link #getOutputChannelCount()} and {@link #getOutputEncoding()} return the processor's output - * format. + * Configures the processor to process input audio with the specified format and returns whether + * to {@link #flush()} it. After calling this method, if the processor is active, {@link + * #getOutputSampleRateHz()}, {@link #getOutputChannelCount()} and {@link #getOutputEncoding()} + * return its output format. * * @param sampleRateHz The sample rate of input audio in Hz. * @param channelCount The number of interleaved channels in input audio. * @param encoding The encoding of input audio. - * @return {@code true} if the processor must be flushed or the value returned by - * {@link #isActive()} has changed as a result of the call. + * @return Whether to {@link #flush()} the processor. * @throws UnhandledFormatException Thrown if the specified format can't be handled as input. */ boolean configure(int sampleRateHz, int channelCount, @C.Encoding int encoding) throws UnhandledFormatException; - /** - * Returns whether the processor is configured and active. - */ + /** Returns whether the processor is configured and will process input buffers. */ boolean isActive(); /** @@ -130,14 +123,9 @@ public interface AudioProcessor { */ boolean isEnded(); - /** - * Clears any state in preparation for receiving a new stream of input buffers. - */ + /** Clears any state in preparation for receiving a new stream of input buffers. */ void flush(); - /** - * Resets the processor to its unconfigured state. - */ + /** Resets the processor to its unconfigured state. */ void reset(); - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java index 6bb5bf7d8e..07584d575e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java @@ -192,17 +192,23 @@ public interface AudioSink { * @param outputChannels A mapping from input to output channels that is applied to this sink's * input as a preprocessing step, if handling PCM input. Specify {@code null} to leave the * input unchanged. Otherwise, the element at index {@code i} specifies index of the input - * channel to map to output channel {@code i} when preprocessing input buffers. After the - * map is applied the audio data will have {@code outputChannels.length} channels. - * @param trimStartSamples The number of audio samples to trim from the start of data written to - * the sink after this call. - * @param trimEndSamples The number of audio samples to trim from data written to the sink + * channel to map to output channel {@code i} when preprocessing input buffers. After the map + * is applied the audio data will have {@code outputChannels.length} channels. + * @param trimStartFrames The number of audio frames to trim from the start of data written to the + * sink after this call. + * @param trimEndFrames The number of audio frames to trim from data written to the sink * immediately preceding the next call to {@link #reset()} or this method. * @throws ConfigurationException If an error occurs configuring the sink. */ - void configure(@C.Encoding int inputEncoding, int inputChannelCount, int inputSampleRate, - int specifiedBufferSize, @Nullable int[] outputChannels, int trimStartSamples, - int trimEndSamples) throws ConfigurationException; + void configure( + @C.Encoding int inputEncoding, + int inputChannelCount, + int inputSampleRate, + int specifiedBufferSize, + @Nullable int[] outputChannels, + int trimStartFrames, + int trimEndFrames) + throws ConfigurationException; /** * Starts or resumes consuming audio if initialized. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTimestampPoller.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTimestampPoller.java new file mode 100644 index 0000000000..47120e7375 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTimestampPoller.java @@ -0,0 +1,307 @@ +/* + * 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.audio; + +import android.annotation.TargetApi; +import android.media.AudioTimestamp; +import android.media.AudioTrack; +import android.support.annotation.IntDef; +import android.support.annotation.Nullable; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.util.Util; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Polls the {@link AudioTrack} timestamp, if the platform supports it, taking care of polling at + * the appropriate rate to detect when the timestamp starts to advance. + * + *

    When the audio track isn't paused, call {@link #maybePollTimestamp(long)} regularly to check + * for timestamp updates. If it returns {@code true}, call {@link #getTimestampPositionFrames()} and + * {@link #getTimestampSystemTimeUs()} to access the updated timestamp, then call {@link + * #acceptTimestamp()} or {@link #rejectTimestamp()} to accept or reject it. + * + *

    If {@link #hasTimestamp()} returns {@code true}, call {@link #getTimestampSystemTimeUs()} to + * get the system time at which the latest timestamp was sampled and {@link + * #getTimestampPositionFrames()} to get its position in frames. If {@link #isTimestampAdvancing()} + * returns {@code true}, the caller should assume that the timestamp has been increasing in real + * time since it was sampled. Otherwise, it may be stationary. + * + *

    Call {@link #reset()} when pausing or resuming the track. + */ +/* package */ final class AudioTimestampPoller { + + /** Timestamp polling states. */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + STATE_INITIALIZING, + STATE_TIMESTAMP, + STATE_TIMESTAMP_ADVANCING, + STATE_NO_TIMESTAMP, + STATE_ERROR + }) + private @interface State {} + /** State when first initializing. */ + private static final int STATE_INITIALIZING = 0; + /** State when we have a timestamp and we don't know if it's advancing. */ + private static final int STATE_TIMESTAMP = 1; + /** State when we have a timestamp and we know it is advancing. */ + private static final int STATE_TIMESTAMP_ADVANCING = 2; + /** State when the no timestamp is available. */ + private static final int STATE_NO_TIMESTAMP = 3; + /** State when the last timestamp was rejected as invalid. */ + private static final int STATE_ERROR = 4; + + /** The polling interval for {@link #STATE_INITIALIZING} and {@link #STATE_TIMESTAMP}. */ + private static final int FAST_POLL_INTERVAL_US = 5_000; + /** + * The polling interval for {@link #STATE_TIMESTAMP_ADVANCING} and {@link #STATE_NO_TIMESTAMP}. + */ + private static final int SLOW_POLL_INTERVAL_US = 10_000_000; + /** The polling interval for {@link #STATE_ERROR}. */ + private static final int ERROR_POLL_INTERVAL_US = 500_000; + + /** + * The minimum duration to remain in {@link #STATE_INITIALIZING} if no timestamps are being + * returned before transitioning to {@link #STATE_NO_TIMESTAMP}. + */ + private static final int INITIALIZING_DURATION_US = 500_000; + + private final @Nullable AudioTimestampV19 audioTimestamp; + + private @State int state; + private long initializeSystemTimeUs; + private long sampleIntervalUs; + private long lastTimestampSampleTimeUs; + private long initialTimestampPositionFrames; + + /** + * Creates a new audio timestamp poller. + * + * @param audioTrack The audio track that will provide timestamps, if the platform supports it. + */ + public AudioTimestampPoller(AudioTrack audioTrack) { + if (Util.SDK_INT >= 19) { + audioTimestamp = new AudioTimestampV19(audioTrack); + reset(); + } else { + audioTimestamp = null; + updateState(STATE_NO_TIMESTAMP); + } + } + + /** + * Polls the timestamp if required and returns whether it was updated. If {@code true}, the latest + * timestamp is available via {@link #getTimestampSystemTimeUs()} and {@link + * #getTimestampPositionFrames()}, and the caller should call {@link #acceptTimestamp()} if the + * timestamp was valid, or {@link #rejectTimestamp()} otherwise. The values returned by {@link + * #hasTimestamp()} and {@link #isTimestampAdvancing()} may be updated. + * + * @param systemTimeUs The current system time, in microseconds. + * @return Whether the timestamp was updated. + */ + public boolean maybePollTimestamp(long systemTimeUs) { + if (audioTimestamp == null || (systemTimeUs - lastTimestampSampleTimeUs) < sampleIntervalUs) { + return false; + } + lastTimestampSampleTimeUs = systemTimeUs; + boolean updatedTimestamp = audioTimestamp.maybeUpdateTimestamp(); + switch (state) { + case STATE_INITIALIZING: + if (updatedTimestamp) { + if (audioTimestamp.getTimestampSystemTimeUs() >= initializeSystemTimeUs) { + // We have an initial timestamp, but don't know if it's advancing yet. + initialTimestampPositionFrames = audioTimestamp.getTimestampPositionFrames(); + updateState(STATE_TIMESTAMP); + } else { + // Drop the timestamp, as it was sampled before the last reset. + updatedTimestamp = false; + } + } else if (systemTimeUs - initializeSystemTimeUs > INITIALIZING_DURATION_US) { + // We haven't received a timestamp for a while, so they probably aren't available for the + // current audio route. Poll infrequently in case the route changes later. + // TODO: Ideally we should listen for audio route changes in order to detect when a + // timestamp becomes available again. + updateState(STATE_NO_TIMESTAMP); + } + break; + case STATE_TIMESTAMP: + if (updatedTimestamp) { + long timestampPositionFrames = audioTimestamp.getTimestampPositionFrames(); + if (timestampPositionFrames > initialTimestampPositionFrames) { + updateState(STATE_TIMESTAMP_ADVANCING); + } + } else { + reset(); + } + break; + case STATE_TIMESTAMP_ADVANCING: + if (!updatedTimestamp) { + // The audio route may have changed, so reset polling. + reset(); + } + break; + case STATE_NO_TIMESTAMP: + if (updatedTimestamp) { + // The audio route may have changed, so reset polling. + reset(); + } + break; + case STATE_ERROR: + // Do nothing. If the caller accepts any new timestamp we'll reset polling. + break; + default: + throw new IllegalStateException(); + } + return updatedTimestamp; + } + + /** + * Rejects the timestamp last polled in {@link #maybePollTimestamp(long)}. The instance will enter + * the error state and poll timestamps infrequently until the next call to {@link + * #acceptTimestamp()}. + */ + public void rejectTimestamp() { + updateState(STATE_ERROR); + } + + /** + * Accepts the timestamp last polled in {@link #maybePollTimestamp(long)}. If the instance is in + * the error state, it will begin to poll timestamps frequently again. + */ + public void acceptTimestamp() { + if (state == STATE_ERROR) { + reset(); + } + } + + /** + * Returns whether this instance has a timestamp that can be used to calculate the audio track + * position. If {@code true}, call {@link #getTimestampSystemTimeUs()} and {@link + * #getTimestampSystemTimeUs()} to access the timestamp. + */ + public boolean hasTimestamp() { + return state == STATE_TIMESTAMP || state == STATE_TIMESTAMP_ADVANCING; + } + + /** + * Returns whether the timestamp appears to be advancing. If {@code true}, call {@link + * #getTimestampSystemTimeUs()} and {@link #getTimestampSystemTimeUs()} to access the timestamp. A + * current position for the track can be extrapolated based on elapsed real time since the system + * time at which the timestamp was sampled. + */ + public boolean isTimestampAdvancing() { + return state == STATE_TIMESTAMP_ADVANCING; + } + + /** Resets polling. Should be called whenever the audio track is paused or resumed. */ + public void reset() { + if (audioTimestamp != null) { + updateState(STATE_INITIALIZING); + } + } + + /** + * If {@link #maybePollTimestamp(long)} or {@link #hasTimestamp()} returned {@code true}, returns + * the system time at which the latest timestamp was sampled, in microseconds. + */ + public long getTimestampSystemTimeUs() { + return audioTimestamp != null ? audioTimestamp.getTimestampSystemTimeUs() : C.TIME_UNSET; + } + + /** + * If {@link #maybePollTimestamp(long)} or {@link #hasTimestamp()} returned {@code true}, returns + * the latest timestamp's position in frames. + */ + public long getTimestampPositionFrames() { + return audioTimestamp != null ? audioTimestamp.getTimestampPositionFrames() : C.POSITION_UNSET; + } + + private void updateState(@State int state) { + this.state = state; + switch (state) { + case STATE_INITIALIZING: + // Force polling a timestamp immediately, and poll quickly. + lastTimestampSampleTimeUs = 0; + initialTimestampPositionFrames = C.POSITION_UNSET; + initializeSystemTimeUs = System.nanoTime() / 1000; + sampleIntervalUs = FAST_POLL_INTERVAL_US; + break; + case STATE_TIMESTAMP: + sampleIntervalUs = FAST_POLL_INTERVAL_US; + break; + case STATE_TIMESTAMP_ADVANCING: + case STATE_NO_TIMESTAMP: + sampleIntervalUs = SLOW_POLL_INTERVAL_US; + break; + case STATE_ERROR: + sampleIntervalUs = ERROR_POLL_INTERVAL_US; + break; + default: + throw new IllegalStateException(); + } + } + + @TargetApi(19) + private static final class AudioTimestampV19 { + + private final AudioTrack audioTrack; + private final AudioTimestamp audioTimestamp; + + private long rawTimestampFramePositionWrapCount; + private long lastTimestampRawPositionFrames; + private long lastTimestampPositionFrames; + + /** + * Creates a new {@link AudioTimestamp} wrapper. + * + * @param audioTrack The audio track that will provide timestamps. + */ + public AudioTimestampV19(AudioTrack audioTrack) { + this.audioTrack = audioTrack; + audioTimestamp = new AudioTimestamp(); + } + + /** + * Attempts to update the audio track timestamp. Returns {@code true} if the timestamp was + * updated, in which case the updated timestamp system time and position can be accessed with + * {@link #getTimestampSystemTimeUs()} and {@link #getTimestampPositionFrames()}. Returns {@code + * false} if no timestamp is available, in which case those methods should not be called. + */ + public boolean maybeUpdateTimestamp() { + boolean updated = audioTrack.getTimestamp(audioTimestamp); + if (updated) { + long rawPositionFrames = audioTimestamp.framePosition; + if (lastTimestampRawPositionFrames > rawPositionFrames) { + // The value must have wrapped around. + rawTimestampFramePositionWrapCount++; + } + lastTimestampRawPositionFrames = rawPositionFrames; + lastTimestampPositionFrames = + rawPositionFrames + (rawTimestampFramePositionWrapCount << 32); + } + return updated; + } + + public long getTimestampSystemTimeUs() { + return audioTimestamp.nanoTime / 1000; + } + + public long getTimestampPositionFrames() { + return lastTimestampPositionFrames; + } + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrackPositionTracker.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrackPositionTracker.java new file mode 100644 index 0000000000..4714db8902 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrackPositionTracker.java @@ -0,0 +1,535 @@ +/* + * 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.audio; + +import android.media.AudioTimestamp; +import android.media.AudioTrack; +import android.os.SystemClock; +import android.support.annotation.IntDef; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.reflect.Method; + +/** + * Wraps an {@link AudioTrack}, exposing a position based on {@link + * AudioTrack#getPlaybackHeadPosition()} and {@link AudioTrack#getTimestamp(AudioTimestamp)}. + * + *

    Call {@link #setAudioTrack(AudioTrack, int, int, int)} to set the audio track to wrap. Call + * {@link #mayHandleBuffer(long)} if there is input data to write to the track. If it returns false, + * the audio track position is stabilizing and no data may be written. Call {@link #start()} + * immediately before calling {@link AudioTrack#play()}. Call {@link #pause()} when pausing the + * track. Call {@link #handleEndOfStream(long)} when no more data will be written to the track. When + * the audio track will no longer be used, call {@link #reset()}. + */ +/* package */ final class AudioTrackPositionTracker { + + /** Listener for position tracker events. */ + public interface Listener { + + /** + * Called when the frame position is too far from the expected frame position. + * + * @param audioTimestampPositionFrames The frame position of the last known audio track + * timestamp. + * @param audioTimestampSystemTimeUs The system time associated with the last known audio track + * timestamp, in microseconds. + * @param systemTimeUs The current time. + * @param playbackPositionUs The current playback head position in microseconds. + */ + void onPositionFramesMismatch( + long audioTimestampPositionFrames, + long audioTimestampSystemTimeUs, + long systemTimeUs, + long playbackPositionUs); + + /** + * Called when the system time associated with the last known audio track timestamp is + * unexpectedly far from the current time. + * + * @param audioTimestampPositionFrames The frame position of the last known audio track + * timestamp. + * @param audioTimestampSystemTimeUs The system time associated with the last known audio track + * timestamp, in microseconds. + * @param systemTimeUs The current time. + * @param playbackPositionUs The current playback head position in microseconds. + */ + void onSystemTimeUsMismatch( + long audioTimestampPositionFrames, + long audioTimestampSystemTimeUs, + long systemTimeUs, + long playbackPositionUs); + + /** + * Called when the audio track has provided an invalid latency. + * + * @param latencyUs The reported latency in microseconds. + */ + void onInvalidLatency(long latencyUs); + + /** + * Called when the audio track runs out of data to play. + * + * @param bufferSize The size of the sink's buffer, in bytes. + * @param bufferSizeMs The size of the sink's buffer, in milliseconds, if it is configured for + * PCM output. {@link C#TIME_UNSET} if it is configured for encoded audio output, as the + * buffered media can have a variable bitrate so the duration may be unknown. + */ + void onUnderrun(int bufferSize, long bufferSizeMs); + } + + /** {@link AudioTrack} playback states. */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({PLAYSTATE_STOPPED, PLAYSTATE_PAUSED, PLAYSTATE_PLAYING}) + private @interface PlayState {} + /** @see AudioTrack#PLAYSTATE_STOPPED */ + private static final int PLAYSTATE_STOPPED = AudioTrack.PLAYSTATE_STOPPED; + /** @see AudioTrack#PLAYSTATE_PAUSED */ + private static final int PLAYSTATE_PAUSED = AudioTrack.PLAYSTATE_PAUSED; + /** @see AudioTrack#PLAYSTATE_PLAYING */ + private static final int PLAYSTATE_PLAYING = AudioTrack.PLAYSTATE_PLAYING; + + /** + * AudioTrack timestamps are deemed spurious if they are offset from the system clock by more than + * this amount. + * + *

    This is a fail safe that should not be required on correctly functioning devices. + */ + private static final long MAX_AUDIO_TIMESTAMP_OFFSET_US = 5 * C.MICROS_PER_SECOND; + + /** + * AudioTrack latencies are deemed impossibly large if they are greater than this amount. + * + *

    This is a fail safe that should not be required on correctly functioning devices. + */ + private static final long MAX_LATENCY_US = 5 * C.MICROS_PER_SECOND; + + private static final long FORCE_RESET_WORKAROUND_TIMEOUT_MS = 200; + + private static final int MAX_PLAYHEAD_OFFSET_COUNT = 10; + private static final int MIN_PLAYHEAD_OFFSET_SAMPLE_INTERVAL_US = 30000; + private static final int MIN_LATENCY_SAMPLE_INTERVAL_US = 500000; + + private final Listener listener; + private final long[] playheadOffsets; + + private AudioTrack audioTrack; + private int outputPcmFrameSize; + private int bufferSize; + private AudioTimestampPoller audioTimestampPoller; + private int outputSampleRate; + private boolean needsPassthroughWorkarounds; + private long bufferSizeUs; + + private long smoothedPlayheadOffsetUs; + private long lastPlayheadSampleTimeUs; + + private Method getLatencyMethod; + private long latencyUs; + private boolean hasData; + + private boolean isOutputPcm; + private long lastLatencySampleTimeUs; + private long lastRawPlaybackHeadPosition; + private long rawPlaybackHeadWrapCount; + private long passthroughWorkaroundPauseOffset; + private int nextPlayheadOffsetIndex; + private int playheadOffsetCount; + private long stopTimestampUs; + private long forceResetWorkaroundTimeMs; + private long stopPlaybackHeadPosition; + private long endPlaybackHeadPosition; + + /** + * Creates a new audio track position tracker. + * + * @param listener A listener for position tracking events. + */ + public AudioTrackPositionTracker(Listener listener) { + this.listener = Assertions.checkNotNull(listener); + if (Util.SDK_INT >= 18) { + try { + getLatencyMethod = AudioTrack.class.getMethod("getLatency", (Class[]) null); + } catch (NoSuchMethodException e) { + // There's no guarantee this method exists. Do nothing. + } + } + playheadOffsets = new long[MAX_PLAYHEAD_OFFSET_COUNT]; + } + + /** + * Sets the {@link AudioTrack} to wrap. Subsequent method calls on this instance relate to this + * track's position, until the next call to {@link #reset()}. + * + * @param audioTrack The audio track to wrap. + * @param outputEncoding The encoding of the audio track. + * @param outputPcmFrameSize For PCM output encodings, the frame size. The value is ignored + * otherwise. + * @param bufferSize The audio track buffer size in bytes. + */ + public void setAudioTrack( + AudioTrack audioTrack, + @C.Encoding int outputEncoding, + int outputPcmFrameSize, + int bufferSize) { + this.audioTrack = audioTrack; + this.outputPcmFrameSize = outputPcmFrameSize; + this.bufferSize = bufferSize; + audioTimestampPoller = new AudioTimestampPoller(audioTrack); + outputSampleRate = audioTrack.getSampleRate(); + needsPassthroughWorkarounds = needsPassthroughWorkarounds(outputEncoding); + isOutputPcm = Util.isEncodingPcm(outputEncoding); + bufferSizeUs = isOutputPcm ? framesToDurationUs(bufferSize / outputPcmFrameSize) : C.TIME_UNSET; + lastRawPlaybackHeadPosition = 0; + rawPlaybackHeadWrapCount = 0; + passthroughWorkaroundPauseOffset = 0; + hasData = false; + stopTimestampUs = C.TIME_UNSET; + forceResetWorkaroundTimeMs = C.TIME_UNSET; + latencyUs = 0; + } + + public long getCurrentPositionUs(boolean sourceEnded) { + if (audioTrack.getPlayState() == PLAYSTATE_PLAYING) { + maybeSampleSyncParams(); + } + + // If the device supports it, use the playback timestamp from AudioTrack.getTimestamp. + // Otherwise, derive a smoothed position by sampling the track's frame position. + long systemTimeUs = System.nanoTime() / 1000; + if (audioTimestampPoller.hasTimestamp()) { + // Calculate the speed-adjusted position using the timestamp (which may be in the future). + long timestampPositionFrames = audioTimestampPoller.getTimestampPositionFrames(); + long timestampPositionUs = framesToDurationUs(timestampPositionFrames); + if (!audioTimestampPoller.isTimestampAdvancing()) { + return timestampPositionUs; + } + long elapsedSinceTimestampUs = systemTimeUs - audioTimestampPoller.getTimestampSystemTimeUs(); + return timestampPositionUs + elapsedSinceTimestampUs; + } else { + long positionUs; + if (playheadOffsetCount == 0) { + // The AudioTrack has started, but we don't have any samples to compute a smoothed position. + positionUs = getPlaybackHeadPositionUs(); + } else { + // getPlaybackHeadPositionUs() only has a granularity of ~20 ms, so we base the position off + // the system clock (and a smoothed offset between it and the playhead position) so as to + // prevent jitter in the reported positions. + positionUs = systemTimeUs + smoothedPlayheadOffsetUs; + } + if (!sourceEnded) { + positionUs -= latencyUs; + } + return positionUs; + } + } + + /** Starts position tracking. Must be called immediately before {@link AudioTrack#play()}. */ + public void start() { + audioTimestampPoller.reset(); + } + + /** Returns whether the audio track is in the playing state. */ + public boolean isPlaying() { + return audioTrack.getPlayState() == PLAYSTATE_PLAYING; + } + + /** + * Checks the state of the audio track and returns whether the caller can write data to the track. + * Notifies {@link Listener#onUnderrun(int, long)} if the track has underrun. + * + * @param writtenFrames The number of frames that have been written. + * @return Whether the caller can write data to the track. + */ + public boolean mayHandleBuffer(long writtenFrames) { + @PlayState int playState = audioTrack.getPlayState(); + if (needsPassthroughWorkarounds) { + // An AC-3 audio track continues to play data written while it is paused. Stop writing so its + // buffer empties. See [Internal: b/18899620]. + if (playState == PLAYSTATE_PAUSED) { + // We force an underrun to pause the track, so don't notify the listener in this case. + hasData = false; + return false; + } + + // A new AC-3 audio track's playback position continues to increase from the old track's + // position for a short time after is has been released. Avoid writing data until the playback + // head position actually returns to zero. + if (playState == PLAYSTATE_STOPPED && getPlaybackHeadPosition() == 0) { + return false; + } + } + + boolean hadData = hasData; + hasData = hasPendingData(writtenFrames); + if (hadData && !hasData && playState != PLAYSTATE_STOPPED && listener != null) { + listener.onUnderrun(bufferSize, C.usToMs(bufferSizeUs)); + } + + return true; + } + + /** + * Returns an estimate of the number of additional bytes that can be written to the audio track's + * buffer without running out of space. + * + *

    May only be called if the output encoding is one of the PCM encodings. + * + * @param writtenBytes The number of bytes written to the audio track so far. + * @return An estimate of the number of bytes that can be written. + */ + public int getAvailableBufferSize(long writtenBytes) { + int bytesPending = (int) (writtenBytes - (getPlaybackHeadPosition() * outputPcmFrameSize)); + return bufferSize - bytesPending; + } + + /** Returns whether the track is in an invalid state and must be recreated. */ + public boolean isStalled(long writtenFrames) { + return forceResetWorkaroundTimeMs != C.TIME_UNSET + && writtenFrames > 0 + && SystemClock.elapsedRealtime() - forceResetWorkaroundTimeMs + >= FORCE_RESET_WORKAROUND_TIMEOUT_MS; + } + + /** + * Records the writing position at which the stream ended, so that the reported position can + * continue to increment while remaining data is played out. + * + * @param writtenFrames The number of frames that have been written. + */ + public void handleEndOfStream(long writtenFrames) { + stopPlaybackHeadPosition = getPlaybackHeadPosition(); + stopTimestampUs = SystemClock.elapsedRealtime() * 1000; + endPlaybackHeadPosition = writtenFrames; + } + + /** + * Returns whether the audio track has any pending data to play out at its current position. + * + * @param writtenFrames The number of frames written to the audio track. + * @return Whether the audio track has any pending data to play out. + */ + public boolean hasPendingData(long writtenFrames) { + return writtenFrames > getPlaybackHeadPosition() + || forceHasPendingData(); + } + + /** + * Pauses the audio track position tracker, returning whether the audio track needs to be paused + * to cause playback to pause. If {@code false} is returned the audio track will pause without + * further interaction, as the end of stream has been handled. + */ + public boolean pause() { + resetSyncParams(); + if (stopTimestampUs == C.TIME_UNSET) { + // The audio track is going to be paused, so reset the timestamp poller to ensure it doesn't + // supply an advancing position. + audioTimestampPoller.reset(); + return true; + } + // We've handled the end of the stream already, so there's no need to pause the track. + return false; + } + + /** + * Resets the position tracker. Should be called when the audio track previous passed to {@link + * #setAudioTrack(AudioTrack, int, int, int)} is no longer in use. + */ + public void reset() { + resetSyncParams(); + audioTrack = null; + audioTimestampPoller = null; + } + + private void maybeSampleSyncParams() { + long playbackPositionUs = getPlaybackHeadPositionUs(); + if (playbackPositionUs == 0) { + // The AudioTrack hasn't output anything yet. + return; + } + long systemTimeUs = System.nanoTime() / 1000; + if (systemTimeUs - lastPlayheadSampleTimeUs >= MIN_PLAYHEAD_OFFSET_SAMPLE_INTERVAL_US) { + // Take a new sample and update the smoothed offset between the system clock and the playhead. + playheadOffsets[nextPlayheadOffsetIndex] = playbackPositionUs - systemTimeUs; + nextPlayheadOffsetIndex = (nextPlayheadOffsetIndex + 1) % MAX_PLAYHEAD_OFFSET_COUNT; + if (playheadOffsetCount < MAX_PLAYHEAD_OFFSET_COUNT) { + playheadOffsetCount++; + } + lastPlayheadSampleTimeUs = systemTimeUs; + smoothedPlayheadOffsetUs = 0; + for (int i = 0; i < playheadOffsetCount; i++) { + smoothedPlayheadOffsetUs += playheadOffsets[i] / playheadOffsetCount; + } + } + + if (needsPassthroughWorkarounds) { + // Don't sample the timestamp and latency if this is an AC-3 passthrough AudioTrack on + // platform API versions 21/22, as incorrect values are returned. See [Internal: b/21145353]. + return; + } + + maybePollAndCheckTimestamp(systemTimeUs, playbackPositionUs); + maybeUpdateLatency(systemTimeUs); + } + + private void maybePollAndCheckTimestamp(long systemTimeUs, long playbackPositionUs) { + if (!audioTimestampPoller.maybePollTimestamp(systemTimeUs)) { + return; + } + + // Perform sanity checks on the timestamp and accept/reject it. + long audioTimestampSystemTimeUs = audioTimestampPoller.getTimestampSystemTimeUs(); + long audioTimestampPositionFrames = audioTimestampPoller.getTimestampPositionFrames(); + if (Math.abs(audioTimestampSystemTimeUs - systemTimeUs) > MAX_AUDIO_TIMESTAMP_OFFSET_US) { + listener.onSystemTimeUsMismatch( + audioTimestampPositionFrames, + audioTimestampSystemTimeUs, + systemTimeUs, + playbackPositionUs); + audioTimestampPoller.rejectTimestamp(); + } else if (Math.abs(framesToDurationUs(audioTimestampPositionFrames) - playbackPositionUs) + > MAX_AUDIO_TIMESTAMP_OFFSET_US) { + listener.onPositionFramesMismatch( + audioTimestampPositionFrames, + audioTimestampSystemTimeUs, + systemTimeUs, + playbackPositionUs); + audioTimestampPoller.rejectTimestamp(); + } else { + audioTimestampPoller.acceptTimestamp(); + } + } + + private void maybeUpdateLatency(long systemTimeUs) { + if (isOutputPcm + && getLatencyMethod != null + && systemTimeUs - lastLatencySampleTimeUs >= MIN_LATENCY_SAMPLE_INTERVAL_US) { + try { + // Compute the audio track latency, excluding the latency due to the buffer (leaving + // latency due to the mixer and audio hardware driver). + latencyUs = + (Integer) getLatencyMethod.invoke(audioTrack, (Object[]) null) * 1000L - bufferSizeUs; + // Sanity check that the latency is non-negative. + latencyUs = Math.max(latencyUs, 0); + // Sanity check that the latency isn't too large. + if (latencyUs > MAX_LATENCY_US) { + listener.onInvalidLatency(latencyUs); + latencyUs = 0; + } + } catch (Exception e) { + // The method existed, but doesn't work. Don't try again. + getLatencyMethod = null; + } + lastLatencySampleTimeUs = systemTimeUs; + } + } + + private long framesToDurationUs(long frameCount) { + return (frameCount * C.MICROS_PER_SECOND) / outputSampleRate; + } + + private void resetSyncParams() { + smoothedPlayheadOffsetUs = 0; + playheadOffsetCount = 0; + nextPlayheadOffsetIndex = 0; + lastPlayheadSampleTimeUs = 0; + } + + /** + * If passthrough workarounds are enabled, pausing is implemented by forcing the AudioTrack to + * underrun. In this case, still behave as if we have pending data, otherwise writing won't + * resume. + */ + private boolean forceHasPendingData() { + return needsPassthroughWorkarounds + && audioTrack.getPlayState() == AudioTrack.PLAYSTATE_PAUSED + && getPlaybackHeadPosition() == 0; + } + + /** + * Returns whether to work around problems with passthrough audio tracks. See [Internal: + * b/18899620, b/19187573, b/21145353]. + */ + private static boolean needsPassthroughWorkarounds(@C.Encoding int outputEncoding) { + return Util.SDK_INT < 23 + && (outputEncoding == C.ENCODING_AC3 || outputEncoding == C.ENCODING_E_AC3); + } + + private long getPlaybackHeadPositionUs() { + return framesToDurationUs(getPlaybackHeadPosition()); + } + + /** + * {@link AudioTrack#getPlaybackHeadPosition()} returns a value intended to be interpreted as an + * unsigned 32 bit integer, which also wraps around periodically. This method returns the playback + * head position as a long that will only wrap around if the value exceeds {@link Long#MAX_VALUE} + * (which in practice will never happen). + * + * @return The playback head position, in frames. + */ + private long getPlaybackHeadPosition() { + if (stopTimestampUs != C.TIME_UNSET) { + // Simulate the playback head position up to the total number of frames submitted. + long elapsedTimeSinceStopUs = (SystemClock.elapsedRealtime() * 1000) - stopTimestampUs; + long framesSinceStop = (elapsedTimeSinceStopUs * outputSampleRate) / C.MICROS_PER_SECOND; + return Math.min(endPlaybackHeadPosition, stopPlaybackHeadPosition + framesSinceStop); + } + + int state = audioTrack.getPlayState(); + if (state == PLAYSTATE_STOPPED) { + // The audio track hasn't been started. + return 0; + } + + long rawPlaybackHeadPosition = 0xFFFFFFFFL & audioTrack.getPlaybackHeadPosition(); + if (needsPassthroughWorkarounds) { + // Work around an issue with passthrough/direct AudioTracks on platform API versions 21/22 + // where the playback head position jumps back to zero on paused passthrough/direct audio + // tracks. See [Internal: b/19187573]. + if (state == PLAYSTATE_PAUSED && rawPlaybackHeadPosition == 0) { + passthroughWorkaroundPauseOffset = lastRawPlaybackHeadPosition; + } + rawPlaybackHeadPosition += passthroughWorkaroundPauseOffset; + } + + if (Util.SDK_INT <= 28) { + if (rawPlaybackHeadPosition == 0 + && lastRawPlaybackHeadPosition > 0 + && state == PLAYSTATE_PLAYING) { + // If connecting a Bluetooth audio device fails, the AudioTrack may be left in a state + // where its Java API is in the playing state, but the native track is stopped. When this + // happens the playback head position gets stuck at zero. In this case, return the old + // playback head position and force the track to be reset after + // {@link #FORCE_RESET_WORKAROUND_TIMEOUT_MS} has elapsed. + if (forceResetWorkaroundTimeMs == C.TIME_UNSET) { + forceResetWorkaroundTimeMs = SystemClock.elapsedRealtime(); + } + return lastRawPlaybackHeadPosition; + } else { + forceResetWorkaroundTimeMs = C.TIME_UNSET; + } + } + + if (lastRawPlaybackHeadPosition > rawPlaybackHeadPosition) { + // The value must have wrapped around. + rawPlaybackHeadWrapCount++; + } + lastRawPlaybackHeadPosition = rawPlaybackHeadPosition; + return rawPlaybackHeadPosition + (rawPlaybackHeadWrapCount << 32); + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java index 50b484b938..e53eb08c83 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java @@ -15,9 +15,11 @@ */ package com.google.android.exoplayer2.audio; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C.Encoding; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.util.Assertions; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.Arrays; @@ -30,17 +32,15 @@ import java.util.Arrays; private int channelCount; private int sampleRateHz; - private int[] pendingOutputChannels; + private @Nullable int[] pendingOutputChannels; private boolean active; - private int[] outputChannels; + private @Nullable int[] outputChannels; private ByteBuffer buffer; private ByteBuffer outputBuffer; private boolean inputEnded; - /** - * Creates a new processor that applies a channel mapping. - */ + /** Creates a new processor that applies a channel mapping. */ public ChannelMappingAudioProcessor() { buffer = EMPTY_BUFFER; outputBuffer = EMPTY_BUFFER; @@ -52,9 +52,11 @@ import java.util.Arrays; * Resets the channel mapping. After calling this method, call {@link #configure(int, int, int)} * to start using the new channel map. * + * @param outputChannels The mapping from input to output channel indices, or {@code null} to + * leave the input unchanged. * @see AudioSink#configure(int, int, int, int, int[], int, int) */ - public void setChannelMap(int[] outputChannels) { + public void setChannelMap(@Nullable int[] outputChannels) { pendingOutputChannels = outputChannels; } @@ -110,6 +112,7 @@ import java.util.Arrays; @Override public void queueInput(ByteBuffer inputBuffer) { + Assertions.checkState(outputChannels != null); int position = inputBuffer.position(); int limit = inputBuffer.limit(); int frameCount = (limit - position) / (2 * channelCount); @@ -161,6 +164,7 @@ import java.util.Arrays; channelCount = Format.NO_VALUE; sampleRateHz = Format.NO_VALUE; outputChannels = null; + pendingOutputChannels = null; active = false; } 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 6d12dc66e8..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 @@ -19,7 +19,6 @@ import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.media.AudioFormat; import android.media.AudioManager; -import android.media.AudioTimestamp; import android.media.AudioTrack; import android.os.ConditionVariable; import android.os.SystemClock; @@ -32,11 +31,12 @@ import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; -import java.lang.reflect.Method; 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 @@ -50,18 +50,108 @@ import java.util.ArrayList; public final class DefaultAudioSink implements AudioSink { /** - * Thrown when {@link AudioTrack#getTimestamp} returns a spurious timestamp, if - * {@link #failOnSpuriousAudioTimestamp} is set. + * Thrown when the audio track has provided a spurious timestamp, if {@link + * #failOnSpuriousAudioTimestamp} is set. */ public static final class InvalidAudioTrackTimestampException extends RuntimeException { - /** @param message The detail message for this exception. */ - public InvalidAudioTrackTimestampException(String message) { + /** + * Creates a new invalid timestamp exception with the specified message. + * + * @param message The detail message for this exception. + */ + private InvalidAudioTrackTimestampException(String message) { super(message); } } + /** + * 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. */ @@ -80,18 +170,6 @@ public final class DefaultAudioSink implements AudioSink { */ private static final int BUFFER_MULTIPLICATION_FACTOR = 4; - /** - * @see AudioTrack#PLAYSTATE_STOPPED - */ - private static final int PLAYSTATE_STOPPED = AudioTrack.PLAYSTATE_STOPPED; - /** - * @see AudioTrack#PLAYSTATE_PAUSED - */ - private static final int PLAYSTATE_PAUSED = AudioTrack.PLAYSTATE_PAUSED; - /** - * @see AudioTrack#PLAYSTATE_PLAYING - */ - private static final int PLAYSTATE_PLAYING = AudioTrack.PLAYSTATE_PLAYING; /** * @see AudioTrack#ERROR_BAD_VALUE */ @@ -116,21 +194,6 @@ public final class DefaultAudioSink implements AudioSink { private static final String TAG = "AudioTrack"; - /** - * AudioTrack timestamps are deemed spurious if they are offset from the system clock by more - * than this amount. - *

    - * This is a fail safe that should not be required on correctly functioning devices. - */ - private static final long MAX_AUDIO_TIMESTAMP_OFFSET_US = 5 * C.MICROS_PER_SECOND; - - /** - * AudioTrack latencies are deemed impossibly large if they are greater than this amount. - *

    - * This is a fail safe that should not be required on correctly functioning devices. - */ - private static final long MAX_LATENCY_US = 5 * C.MICROS_PER_SECOND; - /** * Represents states of the {@link #startMediaTimeUs} value. */ @@ -141,10 +204,6 @@ public final class DefaultAudioSink implements AudioSink { private static final int START_IN_SYNC = 1; private static final int START_NEED_SYNC = 2; - private static final int MAX_PLAYHEAD_OFFSET_COUNT = 10; - private static final int MIN_PLAYHEAD_OFFSET_SAMPLE_INTERVAL_US = 30000; - private static final int MIN_TIMESTAMP_SAMPLE_INTERVAL_US = 500000; - /** * Whether to enable a workaround for an issue where an audio effect does not keep its session * active across releasing/initializing a new audio track, on platform builds where @@ -164,51 +223,40 @@ 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 SonicAudioProcessor sonicAudioProcessor; private final AudioProcessor[] toIntPcmAvailableAudioProcessors; private final AudioProcessor[] toFloatPcmAvailableAudioProcessors; private final ConditionVariable releasingConditionVariable; - private final long[] playheadOffsets; - private final AudioTrackUtil audioTrackUtil; + private final AudioTrackPositionTracker audioTrackPositionTracker; private final ArrayDeque playbackParametersCheckpoints; @Nullable private Listener listener; - /** - * Used to keep the audio session active on pre-V21 builds (see {@link #initialize()}). - */ - private AudioTrack keepSessionIdAudioTrack; + /** Used to keep the audio session active on pre-V21 builds (see {@link #initialize()}). */ + @Nullable private AudioTrack keepSessionIdAudioTrack; + private AudioTrack audioTrack; private boolean isInputPcm; private boolean shouldConvertHighResIntPcmToFloat; private int inputSampleRate; - private int sampleRate; - private int channelConfig; + private int outputSampleRate; + private int outputChannelConfig; private @C.Encoding int outputEncoding; private AudioAttributes audioAttributes; private boolean processingEnabled; private boolean canApplyPlaybackParameters; private int bufferSize; - private long bufferSizeUs; - private PlaybackParameters drainingPlaybackParameters; + @Nullable private PlaybackParameters afterDrainPlaybackParameters; private PlaybackParameters playbackParameters; private long playbackParametersOffsetUs; private long playbackParametersPositionUs; - private ByteBuffer avSyncHeader; + @Nullable private ByteBuffer avSyncHeader; private int bytesUntilNextAvSync; - private int nextPlayheadOffsetIndex; - private int playheadOffsetCount; - private long smoothedPlayheadOffsetUs; - private long lastPlayheadSampleTimeUs; - private boolean audioTimestampSet; - private long lastTimestampSampleTimeUs; - - private Method getLatencyMethod; private int pcmFrameSize; private long submittedPcmBytes; private long submittedEncodedFrames; @@ -218,14 +266,12 @@ public final class DefaultAudioSink implements AudioSink { private int framesPerEncodedSample; private @StartMediaTimeState int startMediaTimeState; private long startMediaTimeUs; - private long resumeSystemTimeUs; - private long latencyUs; private float volume; - private AudioProcessor[] audioProcessors; + private AudioProcessor[] activeAudioProcessors; private ByteBuffer[] outputBuffers; - private ByteBuffer inputBuffer; - private ByteBuffer outputBuffer; + @Nullable private ByteBuffer inputBuffer; + @Nullable private ByteBuffer outputBuffer; private byte[] preV21OutputBuffer; private int preV21OutputBufferOffset; private int drainingAudioProcessorIndex; @@ -234,21 +280,24 @@ public final class DefaultAudioSink implements AudioSink { private boolean playing; private int audioSessionId; private boolean tunneling; - private boolean hasData; 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 @@ -262,45 +311,59 @@ 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); - if (Util.SDK_INT >= 18) { - try { - getLatencyMethod = - AudioTrack.class.getMethod("getLatency", (Class[]) null); - } catch (NoSuchMethodException e) { - // There's no guarantee this method exists. Do nothing. - } - } - if (Util.SDK_INT >= 19) { - audioTrackUtil = new AudioTrackUtilV19(); - } else { - audioTrackUtil = new AudioTrackUtil(); - } + audioTrackPositionTracker = new AudioTrackPositionTracker(new PositionTrackerListener()); channelMappingAudioProcessor = new ChannelMappingAudioProcessor(); trimmingAudioProcessor = new TrimmingAudioProcessor(); - sonicAudioProcessor = new SonicAudioProcessor(); - toIntPcmAvailableAudioProcessors = new AudioProcessor[4 + audioProcessors.length]; - toIntPcmAvailableAudioProcessors[0] = new ResamplingAudioProcessor(); - toIntPcmAvailableAudioProcessors[1] = channelMappingAudioProcessor; - toIntPcmAvailableAudioProcessors[2] = trimmingAudioProcessor; - System.arraycopy( - audioProcessors, 0, toIntPcmAvailableAudioProcessors, 3, audioProcessors.length); - toIntPcmAvailableAudioProcessors[3 + 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()}; - playheadOffsets = new long[MAX_PLAYHEAD_OFFSET_COUNT]; volume = 1.0f; startMediaTimeState = START_NOT_SET; audioAttributes = AudioAttributes.DEFAULT; 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<>(); } + // AudioSink implementation. + @Override public void setListener(Listener listener) { this.listener = listener; @@ -308,7 +371,7 @@ public final class DefaultAudioSink implements AudioSink { @Override public boolean isEncodingSupported(@C.Encoding int encoding) { - if (isEncodingPcm(encoding)) { + if (Util.isEncodingPcm(encoding)) { // AudioTrack supports 16-bit integer PCM output in all platform API versions, and float // output from platform API version 21 only. Other integer PCM encodings are resampled by this // sink to 16-bit PCM. @@ -320,52 +383,29 @@ public final class DefaultAudioSink implements AudioSink { @Override public long getCurrentPositionUs(boolean sourceEnded) { - if (!hasCurrentPositionUs()) { + if (!isInitialized() || startMediaTimeState == START_NOT_SET) { return CURRENT_POSITION_NOT_SET; } - - if (audioTrack.getPlayState() == PLAYSTATE_PLAYING) { - maybeSampleSyncParams(); - } - - // If the device supports it, use the playback timestamp from AudioTrack.getTimestamp. - // Otherwise, derive a smoothed position by sampling the track's frame position. - long systemClockUs = System.nanoTime() / 1000; - long positionUs; - if (audioTimestampSet) { - // Calculate the speed-adjusted position using the timestamp (which may be in the future). - long elapsedSinceTimestampUs = systemClockUs - (audioTrackUtil.getTimestampNanoTime() / 1000); - long elapsedSinceTimestampFrames = durationUsToFrames(elapsedSinceTimestampUs); - long elapsedFrames = audioTrackUtil.getTimestampFramePosition() + elapsedSinceTimestampFrames; - positionUs = framesToDurationUs(elapsedFrames); - } else { - if (playheadOffsetCount == 0) { - // The AudioTrack has started, but we don't have any samples to compute a smoothed position. - positionUs = audioTrackUtil.getPositionUs(); - } else { - // getPlayheadPositionUs() only has a granularity of ~20 ms, so we base the position off the - // system clock (and a smoothed offset between it and the playhead position) so as to - // prevent jitter in the reported positions. - positionUs = systemClockUs + smoothedPlayheadOffsetUs; - } - if (!sourceEnded) { - positionUs -= latencyUs; - } - } - + long positionUs = audioTrackPositionTracker.getCurrentPositionUs(sourceEnded); positionUs = Math.min(positionUs, framesToDurationUs(getWrittenFrames())); - return startMediaTimeUs + applySpeedup(positionUs); + return startMediaTimeUs + applySkipping(applySpeedup(positionUs)); } @Override - public void configure(@C.Encoding int inputEncoding, int inputChannelCount, int inputSampleRate, - int specifiedBufferSize, @Nullable int[] outputChannels, int trimStartSamples, - int trimEndSamples) throws ConfigurationException { + public void configure( + @C.Encoding int inputEncoding, + int inputChannelCount, + int inputSampleRate, + int specifiedBufferSize, + @Nullable int[] outputChannels, + int trimStartFrames, + int trimEndFrames) + throws ConfigurationException { boolean flush = false; this.inputSampleRate = inputSampleRate; int channelCount = inputChannelCount; int sampleRate = inputSampleRate; - isInputPcm = isEncodingPcm(inputEncoding); + isInputPcm = Util.isEncodingPcm(inputEncoding); shouldConvertHighResIntPcmToFloat = enableConvertHighResIntPcmToFloat && isEncodingSupported(C.ENCODING_PCM_32BIT) @@ -377,7 +417,7 @@ public final class DefaultAudioSink implements AudioSink { boolean processingEnabled = isInputPcm && inputEncoding != C.ENCODING_PCM_FLOAT; canApplyPlaybackParameters = processingEnabled && !shouldConvertHighResIntPcmToFloat; if (processingEnabled) { - trimmingAudioProcessor.setTrimSampleCount(trimStartSamples, trimEndSamples); + trimmingAudioProcessor.setTrimFrameCount(trimStartFrames, trimEndFrames); channelMappingAudioProcessor.setChannelMap(outputChannels); for (AudioProcessor audioProcessor : getAvailableAudioProcessors()) { try { @@ -444,8 +484,11 @@ public final class DefaultAudioSink implements AudioSink { channelConfig = AudioFormat.CHANNEL_OUT_STEREO; } - if (!flush && isInitialized() && outputEncoding == encoding && this.sampleRate == sampleRate - && this.channelConfig == channelConfig) { + if (!flush + && isInitialized() + && outputEncoding == encoding + && outputSampleRate == sampleRate + && outputChannelConfig == channelConfig) { // We already have an audio track with the correct sample rate, channel config and encoding. return; } @@ -453,12 +496,11 @@ public final class DefaultAudioSink implements AudioSink { reset(); this.processingEnabled = processingEnabled; - this.sampleRate = sampleRate; - this.channelConfig = channelConfig; + outputSampleRate = sampleRate; + outputChannelConfig = channelConfig; outputEncoding = encoding; - if (isInputPcm) { - outputPcmFrameSize = Util.getPcmFrameSize(outputEncoding, channelCount); - } + outputPcmFrameSize = + isInputPcm ? Util.getPcmFrameSize(outputEncoding, channelCount) : C.LENGTH_UNSET; if (specifiedBufferSize != 0) { bufferSize = specifiedBufferSize; } else if (isInputPcm) { @@ -483,11 +525,9 @@ public final class DefaultAudioSink implements AudioSink { bufferSize = (int) (PASSTHROUGH_BUFFER_DURATION_US * 192 * 6 * 1024 / C.MICROS_PER_SECOND); } } - bufferSizeUs = - isInputPcm ? framesToDurationUs(bufferSize / outputPcmFrameSize) : C.TIME_UNSET; } - private void resetAudioProcessors() { + private void setupAudioProcessors() { ArrayList newAudioProcessors = new ArrayList<>(); for (AudioProcessor audioProcessor : getAvailableAudioProcessors()) { if (audioProcessor.isActive()) { @@ -497,10 +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]; - for (int i = 0; i < count; i++) { - AudioProcessor audioProcessor = audioProcessors[i]; + flushAudioProcessors(); + } + + private void flushAudioProcessors() { + for (int i = 0; i < activeAudioProcessors.length; i++) { + AudioProcessor audioProcessor = activeAudioProcessors[i]; audioProcessor.flush(); outputBuffers[i] = audioProcessor.getOutput(); } @@ -515,13 +559,6 @@ public final class DefaultAudioSink implements AudioSink { releasingConditionVariable.block(); audioTrack = initializeAudioTrack(); - - // The old playback parameters may no longer be applicable so try to reset them now. - setPlaybackParameters(playbackParameters); - - // Flush and reset active audio processors. - resetAudioProcessors(); - int audioSessionId = audioTrack.getAudioSessionId(); if (enablePreV21AudioSessionWorkaround) { if (Util.SDK_INT < 21) { @@ -543,16 +580,22 @@ public final class DefaultAudioSink implements AudioSink { } } - audioTrackUtil.reconfigure(audioTrack, needsPassthroughWorkarounds()); + playbackParameters = + canApplyPlaybackParameters + ? audioProcessorChain.applyPlaybackParameters(playbackParameters) + : PlaybackParameters.DEFAULT; + setupAudioProcessors(); + + audioTrackPositionTracker.setAudioTrack( + audioTrack, outputEncoding, outputPcmFrameSize, bufferSize); setVolumeInternal(); - hasData = false; } @Override public void play() { playing = true; if (isInitialized()) { - resumeSystemTimeUs = System.nanoTime() / 1000; + audioTrackPositionTracker.start(); audioTrack.play(); } } @@ -577,29 +620,8 @@ public final class DefaultAudioSink implements AudioSink { } } - if (needsPassthroughWorkarounds()) { - // An AC-3 audio track continues to play data written while it is paused. Stop writing so its - // buffer empties. See [Internal: b/18899620]. - if (audioTrack.getPlayState() == PLAYSTATE_PAUSED) { - // We force an underrun to pause the track, so don't notify the listener in this case. - hasData = false; - return false; - } - - // A new AC-3 audio track's playback position continues to increase from the old track's - // position for a short time after is has been released. Avoid writing data until the playback - // head position actually returns to zero. - if (audioTrack.getPlayState() == PLAYSTATE_STOPPED - && audioTrackUtil.getPlaybackHeadPosition() != 0) { - return false; - } - } - - boolean hadData = hasData; - hasData = hasPendingData(); - if (hadData && !hasData && audioTrack.getPlayState() != PLAYSTATE_STOPPED && listener != null) { - long elapsedSinceLastFeedMs = SystemClock.elapsedRealtime() - lastFeedElapsedRealtimeMs; - listener.onUnderrun(bufferSize, C.usToMs(bufferSizeUs), elapsedSinceLastFeedMs); + if (!audioTrackPositionTracker.mayHandleBuffer(getWrittenFrames())) { + return false; } if (inputBuffer == null) { @@ -621,19 +643,22 @@ public final class DefaultAudioSink implements AudioSink { } } - if (drainingPlaybackParameters != null) { + if (afterDrainPlaybackParameters != null) { if (!drainAudioProcessorsToEndOfStream()) { // Don't process any more input until draining completes. return false; } + PlaybackParameters newPlaybackParameters = afterDrainPlaybackParameters; + afterDrainPlaybackParameters = null; + newPlaybackParameters = audioProcessorChain.applyPlaybackParameters(newPlaybackParameters); // Store the position and corresponding media time from which the parameters will apply. - playbackParametersCheckpoints.add(new PlaybackParametersCheckpoint( - drainingPlaybackParameters, Math.max(0, presentationTimeUs), - framesToDurationUs(getWrittenFrames()))); - drainingPlaybackParameters = null; - // The audio processors have drained, so flush them. This will cause any active speed - // adjustment audio processor to start producing audio with the new parameters. - resetAudioProcessors(); + playbackParametersCheckpoints.add( + new PlaybackParametersCheckpoint( + newPlaybackParameters, + Math.max(0, presentationTimeUs), + framesToDurationUs(getWrittenFrames()))); + // Update the set of active audio processors to take into account the new parameters. + setupAudioProcessors(); } if (startMediaTimeState == START_NOT_SET) { @@ -680,7 +705,7 @@ public final class DefaultAudioSink implements AudioSink { return true; } - if (audioTrackUtil.needsReset(getWrittenFrames())) { + if (audioTrackPositionTracker.isStalled(getWrittenFrames())) { Log.w(TAG, "Resetting stalled audio track"); reset(); return true; @@ -690,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] @@ -698,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; @@ -743,9 +768,7 @@ public final class DefaultAudioSink implements AudioSink { int bytesWritten = 0; if (Util.SDK_INT < 21) { // isInputPcm == true // Work out how many bytes we can write without the risk of blocking. - int bytesPending = - (int) (writtenPcmBytes - (audioTrackUtil.getPlaybackHeadPosition() * outputPcmFrameSize)); - int bytesToWrite = bufferSize - bytesPending; + int bytesToWrite = audioTrackPositionTracker.getAvailableBufferSize(writtenPcmBytes); if (bytesToWrite > 0) { bytesToWrite = Math.min(bytesRemaining, bytesToWrite); bytesWritten = audioTrack.write(preV21OutputBuffer, preV21OutputBufferOffset, bytesToWrite); @@ -787,7 +810,8 @@ public final class DefaultAudioSink implements AudioSink { if (drainAudioProcessorsToEndOfStream()) { // The audio processors have drained, so drain the underlying audio track. - audioTrackUtil.handleEndOfStream(getWrittenFrames()); + audioTrackPositionTracker.handleEndOfStream(getWrittenFrames()); + audioTrack.stop(); bytesUntilNextAvSync = 0; handledEndOfStream = true; } @@ -796,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(); } @@ -830,9 +854,7 @@ public final class DefaultAudioSink implements AudioSink { @Override public boolean hasPendingData() { - return isInitialized() - && (getWrittenFrames() > audioTrackUtil.getPlaybackHeadPosition() - || overrideHasPendingData()); + return isInitialized() && audioTrackPositionTracker.hasPendingData(getWrittenFrames()); } @Override @@ -841,11 +863,9 @@ public final class DefaultAudioSink implements AudioSink { this.playbackParameters = PlaybackParameters.DEFAULT; return this.playbackParameters; } - playbackParameters = new PlaybackParameters( - sonicAudioProcessor.setSpeed(playbackParameters.speed), - sonicAudioProcessor.setPitch(playbackParameters.pitch)); PlaybackParameters lastSetPlaybackParameters = - drainingPlaybackParameters != null ? drainingPlaybackParameters + afterDrainPlaybackParameters != null + ? afterDrainPlaybackParameters : !playbackParametersCheckpoints.isEmpty() ? playbackParametersCheckpoints.getLast().playbackParameters : this.playbackParameters; @@ -853,9 +873,10 @@ public final class DefaultAudioSink implements AudioSink { if (isInitialized()) { // Drain the audio processors so we can determine the frame position at which the new // parameters apply. - drainingPlaybackParameters = playbackParameters; + afterDrainPlaybackParameters = playbackParameters; } else { - this.playbackParameters = playbackParameters; + // Update the playback parameters now. + this.playbackParameters = audioProcessorChain.applyPlaybackParameters(playbackParameters); } } return this.playbackParameters; @@ -928,9 +949,8 @@ public final class DefaultAudioSink implements AudioSink { @Override public void pause() { playing = false; - if (isInitialized()) { - resetSyncParams(); - audioTrackUtil.pause(); + if (isInitialized() && audioTrackPositionTracker.pause()) { + audioTrack.pause(); } } @@ -942,9 +962,9 @@ public final class DefaultAudioSink implements AudioSink { writtenPcmBytes = 0; writtenEncodedFrames = 0; framesPerEncodedSample = 0; - if (drainingPlaybackParameters != null) { - playbackParameters = drainingPlaybackParameters; - drainingPlaybackParameters = null; + if (afterDrainPlaybackParameters != null) { + playbackParameters = afterDrainPlaybackParameters; + afterDrainPlaybackParameters = null; } else if (!playbackParametersCheckpoints.isEmpty()) { playbackParameters = playbackParametersCheckpoints.getLast().playbackParameters; } @@ -953,26 +973,19 @@ public final class DefaultAudioSink implements AudioSink { playbackParametersPositionUs = 0; inputBuffer = null; outputBuffer = null; - for (int i = 0; i < audioProcessors.length; i++) { - AudioProcessor audioProcessor = audioProcessors[i]; - audioProcessor.flush(); - outputBuffers[i] = audioProcessor.getOutput(); - } + flushAudioProcessors(); handledEndOfStream = false; drainingAudioProcessorIndex = C.INDEX_UNSET; avSyncHeader = null; bytesUntilNextAvSync = 0; startMediaTimeState = START_NOT_SET; - latencyUs = 0; - resetSyncParams(); - int playState = audioTrack.getPlayState(); - if (playState == PLAYSTATE_PLAYING) { + if (audioTrackPositionTracker.isPlaying()) { audioTrack.pause(); } // AudioTrack.release can take some time, so we call it on a background thread. final AudioTrack toRelease = audioTrack; audioTrack = null; - audioTrackUtil.reconfigure(null, false); + audioTrackPositionTracker.reset(); releasingConditionVariable.close(); new Thread() { @Override @@ -1021,21 +1034,14 @@ public final class DefaultAudioSink implements AudioSink { }.start(); } - /** - * Returns whether {@link #getCurrentPositionUs} can return the current playback position. - */ - private boolean hasCurrentPositionUs() { - return isInitialized() && startMediaTimeState != START_NOT_SET; - } - - /** - * 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; @@ -1047,96 +1053,17 @@ 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( positionUs - playbackParametersPositionUs, playbackParameters.speed); } - /** - * Updates the audio track latency and playback position parameters. - */ - private void maybeSampleSyncParams() { - long playbackPositionUs = audioTrackUtil.getPositionUs(); - if (playbackPositionUs == 0) { - // The AudioTrack hasn't output anything yet. - return; - } - long systemClockUs = System.nanoTime() / 1000; - if (systemClockUs - lastPlayheadSampleTimeUs >= MIN_PLAYHEAD_OFFSET_SAMPLE_INTERVAL_US) { - // Take a new sample and update the smoothed offset between the system clock and the playhead. - playheadOffsets[nextPlayheadOffsetIndex] = playbackPositionUs - systemClockUs; - nextPlayheadOffsetIndex = (nextPlayheadOffsetIndex + 1) % MAX_PLAYHEAD_OFFSET_COUNT; - if (playheadOffsetCount < MAX_PLAYHEAD_OFFSET_COUNT) { - playheadOffsetCount++; - } - lastPlayheadSampleTimeUs = systemClockUs; - smoothedPlayheadOffsetUs = 0; - for (int i = 0; i < playheadOffsetCount; i++) { - smoothedPlayheadOffsetUs += playheadOffsets[i] / playheadOffsetCount; - } - } - - if (needsPassthroughWorkarounds()) { - // Don't sample the timestamp and latency if this is an AC-3 passthrough AudioTrack on - // platform API versions 21/22, as incorrect values are returned. See [Internal: b/21145353]. - return; - } - - if (systemClockUs - lastTimestampSampleTimeUs >= MIN_TIMESTAMP_SAMPLE_INTERVAL_US) { - audioTimestampSet = audioTrackUtil.updateTimestamp(); - if (audioTimestampSet) { - // Perform sanity checks on the timestamp. - long audioTimestampUs = audioTrackUtil.getTimestampNanoTime() / 1000; - long audioTimestampFramePosition = audioTrackUtil.getTimestampFramePosition(); - if (audioTimestampUs < resumeSystemTimeUs) { - // The timestamp corresponds to a time before the track was most recently resumed. - audioTimestampSet = false; - } else if (Math.abs(audioTimestampUs - systemClockUs) > MAX_AUDIO_TIMESTAMP_OFFSET_US) { - // The timestamp time base is probably wrong. - String message = "Spurious audio timestamp (system clock mismatch): " - + audioTimestampFramePosition + ", " + audioTimestampUs + ", " + systemClockUs + ", " - + playbackPositionUs + ", " + getSubmittedFrames() + ", " + getWrittenFrames(); - if (failOnSpuriousAudioTimestamp) { - throw new InvalidAudioTrackTimestampException(message); - } - Log.w(TAG, message); - audioTimestampSet = false; - } else if (Math.abs(framesToDurationUs(audioTimestampFramePosition) - playbackPositionUs) - > MAX_AUDIO_TIMESTAMP_OFFSET_US) { - // The timestamp frame position is probably wrong. - String message = "Spurious audio timestamp (frame position mismatch): " - + audioTimestampFramePosition + ", " + audioTimestampUs + ", " + systemClockUs + ", " - + playbackPositionUs + ", " + getSubmittedFrames() + ", " + getWrittenFrames(); - if (failOnSpuriousAudioTimestamp) { - throw new InvalidAudioTrackTimestampException(message); - } - Log.w(TAG, message); - audioTimestampSet = false; - } - } - if (getLatencyMethod != null && isInputPcm) { - try { - // Compute the audio track latency, excluding the latency due to the buffer (leaving - // latency due to the mixer and audio hardware driver). - latencyUs = (Integer) getLatencyMethod.invoke(audioTrack, (Object[]) null) * 1000L - - bufferSizeUs; - // Sanity check that the latency is non-negative. - latencyUs = Math.max(latencyUs, 0); - // Sanity check that the latency isn't too large. - if (latencyUs > MAX_LATENCY_US) { - Log.w(TAG, "Ignoring impossibly large audio latency: " + latencyUs); - latencyUs = 0; - } - } catch (Exception e) { - // The method existed, but doesn't work. Don't try again. - getLatencyMethod = null; - } - } - lastTimestampSampleTimeUs = systemClockUs; - } + private long applySkipping(long positionUs) { + return positionUs + framesToDurationUs(audioProcessorChain.getSkippedOutputFrameCount()); } private boolean isInitialized() { @@ -1148,11 +1075,11 @@ public final class DefaultAudioSink implements AudioSink { } private long framesToDurationUs(long frameCount) { - return (frameCount * C.MICROS_PER_SECOND) / sampleRate; + return (frameCount * C.MICROS_PER_SECOND) / outputSampleRate; } private long durationUsToFrames(long durationUs) { - return (durationUs * sampleRate) / C.MICROS_PER_SECOND; + return (durationUs * outputSampleRate) / C.MICROS_PER_SECOND; } private long getSubmittedFrames() { @@ -1163,36 +1090,6 @@ public final class DefaultAudioSink implements AudioSink { return isInputPcm ? (writtenPcmBytes / outputPcmFrameSize) : writtenEncodedFrames; } - private void resetSyncParams() { - smoothedPlayheadOffsetUs = 0; - playheadOffsetCount = 0; - nextPlayheadOffsetIndex = 0; - lastPlayheadSampleTimeUs = 0; - audioTimestampSet = false; - lastTimestampSampleTimeUs = 0; - } - - /** - * Returns whether to work around problems with passthrough audio tracks. - * See [Internal: b/18899620, b/19187573, b/21145353]. - */ - private boolean needsPassthroughWorkarounds() { - return Util.SDK_INT < 23 - && (outputEncoding == C.ENCODING_AC3 || outputEncoding == C.ENCODING_E_AC3); - } - - /** - * Returns whether the audio track should behave as though it has pending data. This is to work - * around an issue on platform API versions 21/22 where AC-3 audio tracks can't be paused, so we - * empty their buffers when paused. In this case, they should still behave as if they have - * pending data, otherwise writing will never resume. - */ - private boolean overrideHasPendingData() { - return needsPassthroughWorkarounds() - && audioTrack.getPlayState() == PLAYSTATE_PAUSED - && audioTrack.getPlaybackHeadPosition() == 0; - } - private AudioTrack initializeAudioTrack() throws InitializationException { AudioTrack audioTrack; if (Util.SDK_INT >= 21) { @@ -1200,12 +1097,25 @@ public final class DefaultAudioSink implements AudioSink { } else { int streamType = Util.getStreamTypeForAudioUsage(audioAttributes.usage); if (audioSessionId == C.AUDIO_SESSION_ID_UNSET) { - audioTrack = new AudioTrack(streamType, sampleRate, channelConfig, outputEncoding, - bufferSize, MODE_STREAM); + audioTrack = + new AudioTrack( + streamType, + outputSampleRate, + outputChannelConfig, + outputEncoding, + bufferSize, + MODE_STREAM); } else { // Re-attach to the same audio session. - audioTrack = new AudioTrack(streamType, sampleRate, channelConfig, outputEncoding, - bufferSize, MODE_STREAM, audioSessionId); + audioTrack = + new AudioTrack( + streamType, + outputSampleRate, + outputChannelConfig, + outputEncoding, + bufferSize, + MODE_STREAM, + audioSessionId); } } @@ -1217,7 +1127,7 @@ public final class DefaultAudioSink implements AudioSink { // The track has already failed to initialize, so it wouldn't be that surprising if release // were to fail too. Swallow the exception. } - throw new InitializationException(state, sampleRate, channelConfig, bufferSize); + throw new InitializationException(state, outputSampleRate, outputChannelConfig, bufferSize); } return audioTrack; } @@ -1234,11 +1144,12 @@ public final class DefaultAudioSink implements AudioSink { } else { attributes = audioAttributes.getAudioAttributesV21(); } - AudioFormat format = new AudioFormat.Builder() - .setChannelMask(channelConfig) - .setEncoding(outputEncoding) - .setSampleRate(sampleRate) - .build(); + AudioFormat format = + new AudioFormat.Builder() + .setChannelMask(outputChannelConfig) + .setEncoding(outputEncoding) + .setSampleRate(outputSampleRate) + .build(); int audioSessionId = this.audioSessionId != C.AUDIO_SESSION_ID_UNSET ? this.audioSessionId : AudioManager.AUDIO_SESSION_ID_GENERATE; return new AudioTrack(attributes, format, bufferSize, MODE_STREAM, audioSessionId); @@ -1259,12 +1170,6 @@ public final class DefaultAudioSink implements AudioSink { : toIntPcmAvailableAudioProcessors; } - private static boolean isEncodingPcm(@C.Encoding int encoding) { - return encoding == C.ENCODING_PCM_8BIT || encoding == C.ENCODING_PCM_16BIT - || encoding == C.ENCODING_PCM_24BIT || encoding == C.ENCODING_PCM_32BIT - || encoding == C.ENCODING_PCM_FLOAT; - } - private static int getFramesPerEncodedSample(@C.Encoding int encoding, ByteBuffer buffer) { if (encoding == C.ENCODING_DTS || encoding == C.ENCODING_DTS_HD) { return DtsUtil.parseDtsAudioSampleCount(buffer); @@ -1273,8 +1178,11 @@ public final class DefaultAudioSink implements AudioSink { } else if (encoding == C.ENCODING_E_AC3) { return Ac3Util.parseEAc3SyncframeAudioSampleCount(buffer); } else if (encoding == C.ENCODING_DOLBY_TRUEHD) { - return Ac3Util.parseTrueHdSyncframeAudioSampleCount(buffer) - * Ac3Util.TRUEHD_RECHUNK_SAMPLE_COUNT; + int syncframeOffset = Ac3Util.findTrueHdSyncframeOffset(buffer); + return syncframeOffset == C.INDEX_UNSET + ? 0 + : (Ac3Util.parseTrueHdSyncframeAudioSampleCount(buffer, syncframeOffset) + * Ac3Util.TRUEHD_RECHUNK_SAMPLE_COUNT); } else { throw new IllegalStateException("Unexpected audio encoding: " + encoding); } @@ -1334,237 +1242,6 @@ public final class DefaultAudioSink implements AudioSink { audioTrack.setStereoVolume(volume, volume); } - /** - * Wraps an {@link AudioTrack} to expose useful utility methods. - */ - private static class AudioTrackUtil { - - private static final long FORCE_RESET_WORKAROUND_TIMEOUT_MS = 200; - - protected AudioTrack audioTrack; - private boolean needsPassthroughWorkaround; - private int sampleRate; - private long lastRawPlaybackHeadPosition; - private long rawPlaybackHeadWrapCount; - private long passthroughWorkaroundPauseOffset; - - private long stopTimestampUs; - private long forceResetWorkaroundTimeMs; - private long stopPlaybackHeadPosition; - private long endPlaybackHeadPosition; - - /** - * Reconfigures the audio track utility helper to use the specified {@code audioTrack}. - * - * @param audioTrack The audio track to wrap. - * @param needsPassthroughWorkaround Whether to workaround issues with pausing AC-3 passthrough - * audio tracks on platform API version 21/22. - */ - public void reconfigure(AudioTrack audioTrack, boolean needsPassthroughWorkaround) { - this.audioTrack = audioTrack; - this.needsPassthroughWorkaround = needsPassthroughWorkaround; - stopTimestampUs = C.TIME_UNSET; - forceResetWorkaroundTimeMs = C.TIME_UNSET; - lastRawPlaybackHeadPosition = 0; - rawPlaybackHeadWrapCount = 0; - passthroughWorkaroundPauseOffset = 0; - if (audioTrack != null) { - sampleRate = audioTrack.getSampleRate(); - } - } - - /** - * Stops the audio track in a way that ensures media written to it is played out in full, and - * that {@link #getPlaybackHeadPosition()} and {@link #getPositionUs()} continue to increment as - * the remaining media is played out. - * - * @param writtenFrames The total number of frames that have been written. - */ - public void handleEndOfStream(long writtenFrames) { - stopPlaybackHeadPosition = getPlaybackHeadPosition(); - stopTimestampUs = SystemClock.elapsedRealtime() * 1000; - endPlaybackHeadPosition = writtenFrames; - audioTrack.stop(); - } - - /** - * Pauses the audio track unless the end of the stream has been handled, in which case calling - * this method does nothing. - */ - public void pause() { - if (stopTimestampUs != C.TIME_UNSET) { - // We don't want to knock the audio track back into the paused state. - return; - } - audioTrack.pause(); - } - - /** - * Returns whether the track is in an invalid state and must be reset. - * - * @see #getPlaybackHeadPosition() - */ - public boolean needsReset(long writtenFrames) { - return forceResetWorkaroundTimeMs != C.TIME_UNSET && writtenFrames > 0 - && SystemClock.elapsedRealtime() - forceResetWorkaroundTimeMs - >= FORCE_RESET_WORKAROUND_TIMEOUT_MS; - } - - /** - * {@link AudioTrack#getPlaybackHeadPosition()} returns a value intended to be interpreted as an - * unsigned 32 bit integer, which also wraps around periodically. This method returns the - * playback head position as a long that will only wrap around if the value exceeds - * {@link Long#MAX_VALUE} (which in practice will never happen). - * - * @return The playback head position, in frames. - */ - public long getPlaybackHeadPosition() { - if (stopTimestampUs != C.TIME_UNSET) { - // Simulate the playback head position up to the total number of frames submitted. - long elapsedTimeSinceStopUs = (SystemClock.elapsedRealtime() * 1000) - stopTimestampUs; - long framesSinceStop = (elapsedTimeSinceStopUs * sampleRate) / C.MICROS_PER_SECOND; - return Math.min(endPlaybackHeadPosition, stopPlaybackHeadPosition + framesSinceStop); - } - - int state = audioTrack.getPlayState(); - if (state == PLAYSTATE_STOPPED) { - // The audio track hasn't been started. - return 0; - } - - long rawPlaybackHeadPosition = 0xFFFFFFFFL & audioTrack.getPlaybackHeadPosition(); - if (needsPassthroughWorkaround) { - // Work around an issue with passthrough/direct AudioTracks on platform API versions 21/22 - // where the playback head position jumps back to zero on paused passthrough/direct audio - // tracks. See [Internal: b/19187573]. - if (state == PLAYSTATE_PAUSED && rawPlaybackHeadPosition == 0) { - passthroughWorkaroundPauseOffset = lastRawPlaybackHeadPosition; - } - rawPlaybackHeadPosition += passthroughWorkaroundPauseOffset; - } - - if (Util.SDK_INT <= 28) { - if (rawPlaybackHeadPosition == 0 && lastRawPlaybackHeadPosition > 0 - && state == PLAYSTATE_PLAYING) { - // If connecting a Bluetooth audio device fails, the AudioTrack may be left in a state - // where its Java API is in the playing state, but the native track is stopped. When this - // happens the playback head position gets stuck at zero. In this case, return the old - // playback head position and force the track to be reset after - // {@link #FORCE_RESET_WORKAROUND_TIMEOUT_MS} has elapsed. - if (forceResetWorkaroundTimeMs == C.TIME_UNSET) { - forceResetWorkaroundTimeMs = SystemClock.elapsedRealtime(); - } - return lastRawPlaybackHeadPosition; - } else { - forceResetWorkaroundTimeMs = C.TIME_UNSET; - } - } - - if (lastRawPlaybackHeadPosition > rawPlaybackHeadPosition) { - // The value must have wrapped around. - rawPlaybackHeadWrapCount++; - } - lastRawPlaybackHeadPosition = rawPlaybackHeadPosition; - return rawPlaybackHeadPosition + (rawPlaybackHeadWrapCount << 32); - } - - /** - * Returns the duration of played media since reconfiguration, in microseconds. - */ - public long getPositionUs() { - return (getPlaybackHeadPosition() * C.MICROS_PER_SECOND) / sampleRate; - } - - /** - * Updates the values returned by {@link #getTimestampNanoTime()} and - * {@link #getTimestampFramePosition()}. - * - * @return Whether the timestamp values were updated. - */ - public boolean updateTimestamp() { - return false; - } - - /** - * Returns the {@link android.media.AudioTimestamp#nanoTime} obtained during the most recent - * call to {@link #updateTimestamp()} that returned true. - * - * @return The nanoTime obtained during the most recent call to {@link #updateTimestamp()} that - * returned true. - * @throws UnsupportedOperationException If the implementation does not support audio timestamp - * queries. {@link #updateTimestamp()} will always return false in this case. - */ - public long getTimestampNanoTime() { - // Should never be called if updateTimestamp() returned false. - throw new UnsupportedOperationException(); - } - - /** - * Returns the {@link android.media.AudioTimestamp#framePosition} obtained during the most - * recent call to {@link #updateTimestamp()} that returned true. The value is adjusted so that - * wrap around only occurs if the value exceeds {@link Long#MAX_VALUE} (which in practice will - * never happen). - * - * @return The framePosition obtained during the most recent call to {@link #updateTimestamp()} - * that returned true. - * @throws UnsupportedOperationException If the implementation does not support audio timestamp - * queries. {@link #updateTimestamp()} will always return false in this case. - */ - public long getTimestampFramePosition() { - // Should never be called if updateTimestamp() returned false. - throw new UnsupportedOperationException(); - } - - } - - @TargetApi(19) - private static class AudioTrackUtilV19 extends AudioTrackUtil { - - private final AudioTimestamp audioTimestamp; - - private long rawTimestampFramePositionWrapCount; - private long lastRawTimestampFramePosition; - private long lastTimestampFramePosition; - - public AudioTrackUtilV19() { - audioTimestamp = new AudioTimestamp(); - } - - @Override - public void reconfigure(AudioTrack audioTrack, boolean needsPassthroughWorkaround) { - super.reconfigure(audioTrack, needsPassthroughWorkaround); - rawTimestampFramePositionWrapCount = 0; - lastRawTimestampFramePosition = 0; - lastTimestampFramePosition = 0; - } - - @Override - public boolean updateTimestamp() { - boolean updated = audioTrack.getTimestamp(audioTimestamp); - if (updated) { - long rawFramePosition = audioTimestamp.framePosition; - if (lastRawTimestampFramePosition > rawFramePosition) { - // The value must have wrapped around. - rawTimestampFramePositionWrapCount++; - } - lastRawTimestampFramePosition = rawFramePosition; - lastTimestampFramePosition = rawFramePosition + (rawTimestampFramePositionWrapCount << 32); - } - return updated; - } - - @Override - public long getTimestampNanoTime() { - return audioTimestamp.nanoTime; - } - - @Override - public long getTimestampFramePosition() { - return lastTimestampFramePosition; - } - - } - /** * Stores playback parameters with the position and media time at which they apply. */ @@ -1583,4 +1260,69 @@ public final class DefaultAudioSink implements AudioSink { } + private final class PositionTrackerListener implements AudioTrackPositionTracker.Listener { + + @Override + public void onPositionFramesMismatch( + long audioTimestampPositionFrames, + long audioTimestampSystemTimeUs, + long systemTimeUs, + long playbackPositionUs) { + String message = + "Spurious audio timestamp (frame position mismatch): " + + audioTimestampPositionFrames + + ", " + + audioTimestampSystemTimeUs + + ", " + + systemTimeUs + + ", " + + playbackPositionUs + + ", " + + getSubmittedFrames() + + ", " + + getWrittenFrames(); + if (failOnSpuriousAudioTimestamp) { + throw new InvalidAudioTrackTimestampException(message); + } + Log.w(TAG, message); + } + + @Override + public void onSystemTimeUsMismatch( + long audioTimestampPositionFrames, + long audioTimestampSystemTimeUs, + long systemTimeUs, + long playbackPositionUs) { + String message = + "Spurious audio timestamp (system clock mismatch): " + + audioTimestampPositionFrames + + ", " + + audioTimestampSystemTimeUs + + ", " + + systemTimeUs + + ", " + + playbackPositionUs + + ", " + + getSubmittedFrames() + + ", " + + getWrittenFrames(); + if (failOnSpuriousAudioTimestamp) { + throw new InvalidAudioTrackTimestampException(message); + } + Log.w(TAG, message); + } + + @Override + public void onInvalidLatency(long latencyUs) { + Log.w(TAG, "Ignoring impossibly large audio latency: " + latencyUs); + } + + @Override + public void onUnderrun(int bufferSize, long bufferSizeMs) { + if (listener != null) { + long elapsedSinceLastFeedMs = SystemClock.elapsedRealtime() - lastFeedElapsedRealtimeMs; + listener.onUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs); + } + } + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/FloatResamplingAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/FloatResamplingAudioProcessor.java index 215b04821b..e3c91cd344 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/FloatResamplingAudioProcessor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/FloatResamplingAudioProcessor.java @@ -17,7 +17,6 @@ package com.google.android.exoplayer2.audio; 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.Util; import java.nio.ByteBuffer; import java.nio.ByteOrder; @@ -86,8 +85,6 @@ import java.nio.ByteOrder; @Override public void queueInput(ByteBuffer inputBuffer) { - Assertions.checkState(isActive()); - boolean isInput32Bit = sourceEncoding == C.ENCODING_PCM_32BIT; int position = inputBuffer.position(); int limit = inputBuffer.limit(); @@ -150,10 +147,10 @@ import java.nio.ByteOrder; @Override public void reset() { flush(); - buffer = EMPTY_BUFFER; sampleRateHz = Format.NO_VALUE; channelCount = Format.NO_VALUE; sourceEncoding = C.ENCODING_INVALID; + buffer = EMPTY_BUFFER; } /** 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 33a67554a5..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 @@ -15,7 +15,10 @@ */ 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; @@ -37,6 +40,7 @@ import com.google.android.exoplayer2.mediacodec.MediaCodecInfo; import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer; import com.google.android.exoplayer2.mediacodec.MediaCodecSelector; import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException; +import com.google.android.exoplayer2.mediacodec.MediaFormatUtil; import com.google.android.exoplayer2.util.MediaClock; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; @@ -59,9 +63,11 @@ 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; + private int codecMaxInputSize; private boolean passthroughEnabled; private boolean codecNeedsDiscardChannelsWorkaround; private android.media.MediaFormat passthroughMediaFormat; @@ -75,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. @@ -91,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. @@ -121,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. @@ -146,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. @@ -169,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()); } @@ -230,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); } @@ -254,20 +309,33 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media @Override protected void configureCodec(MediaCodecInfo codecInfo, MediaCodec codec, Format format, MediaCrypto crypto) { + codecMaxInputSize = getCodecMaxInputSize(codecInfo, format, getStreamFormats()); codecNeedsDiscardChannelsWorkaround = codecNeedsDiscardChannelsWorkaround(codecInfo.name); - MediaFormat mediaFormat = getMediaFormatForPlayback(format); + 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, 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 public MediaClock getMediaClock() { return this; @@ -288,8 +356,8 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media pcmEncoding = MimeTypes.AUDIO_RAW.equals(newFormat.sampleMimeType) ? newFormat.pcmEncoding : C.ENCODING_PCM_16BIT; channelCount = newFormat.channelCount; - encoderDelay = newFormat.encoderDelay != Format.NO_VALUE ? newFormat.encoderDelay : 0; - encoderPadding = newFormat.encoderPadding != Format.NO_VALUE ? newFormat.encoderPadding : 0; + encoderDelay = newFormat.encoderDelay; + encoderPadding = newFormat.encoderPadding; } @Override @@ -380,8 +448,8 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media @Override protected void onStopped() { - audioSink.pause(); updateCurrentPosition(); + audioSink.pause(); super.onStopped(); } @@ -494,6 +562,87 @@ 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( + 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 format.maxInputSize; + } + + /** + * Returns the framework {@link MediaFormat} that can be used to configure a {@link MediaCodec} + * 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, String codecMimeType, int codecMaxInputSize) { + MediaFormat mediaFormat = new MediaFormat(); + // Set format parameters that should always be set. + 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); + // Set codec max values. + MediaFormatUtil.maybeSetInteger(mediaFormat, MediaFormat.KEY_MAX_INPUT_SIZE, codecMaxInputSize); + // Set codec configuration values. + if (Util.SDK_INT >= 23) { + mediaFormat.setInteger(MediaFormat.KEY_PRIORITY, 0 /* realtime priority */); + } + return mediaFormat; + } + private void updateCurrentPosition() { long newCurrentPositionUs = audioSink.getCurrentPositionUs(isEnded()); if (newCurrentPositionUs != AudioSink.CURRENT_POSITION_NOT_SET) { @@ -505,6 +654,25 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media } } + /** + * Returns whether a codec with suitable maximum input size will support adaptation between two + * {@link Format}s. + * + * @param first The first format. + * @param second The second format. + * @return Whether the codec will support adaptation between the two {@link Format}s. + */ + private static boolean areAdaptationCompatible(Format first, Format second) { + return first.sampleMimeType.equals(second.sampleMimeType) + && first.channelCount == second.channelCount + && first.sampleRate == second.sampleRate + && first.encoderDelay == 0 + && first.encoderPadding == 0 + && second.encoderDelay == 0 + && second.encoderPadding == 0 + && first.initializationDataEquals(second); + } + /** * Returns whether the decoder is known to output six audio channels when provided with input with * fewer than six channels. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java index 01123f3c59..eac0bffd65 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java @@ -28,15 +28,12 @@ import java.nio.ByteOrder; private int sampleRateHz; private int channelCount; - @C.PcmEncoding - private int encoding; + private @C.PcmEncoding int encoding; private ByteBuffer buffer; private ByteBuffer outputBuffer; private boolean inputEnded; - /** - * Creates a new audio processor that converts audio data to {@link C#ENCODING_PCM_16BIT}. - */ + /** Creates a new audio processor that converts audio data to {@link C#ENCODING_PCM_16BIT}. */ public ResamplingAudioProcessor() { sampleRateHz = Format.NO_VALUE; channelCount = Format.NO_VALUE; @@ -59,9 +56,6 @@ import java.nio.ByteOrder; this.sampleRateHz = sampleRateHz; this.channelCount = channelCount; this.encoding = encoding; - if (encoding == C.ENCODING_PCM_16BIT) { - buffer = EMPTY_BUFFER; - } return true; } @@ -139,6 +133,7 @@ import java.nio.ByteOrder; } break; case C.ENCODING_PCM_16BIT: + case C.ENCODING_PCM_FLOAT: case C.ENCODING_INVALID: case Format.NO_VALUE: default: @@ -177,10 +172,10 @@ import java.nio.ByteOrder; @Override public void reset() { flush(); - buffer = EMPTY_BUFFER; sampleRateHz = Format.NO_VALUE; channelCount = Format.NO_VALUE; encoding = C.ENCODING_INVALID; + buffer = EMPTY_BUFFER; } } 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 new file mode 100644 index 0000000000..a289ced128 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/SilenceSkippingAudioProcessor.java @@ -0,0 +1,412 @@ +/* + * 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.audio; + +import android.support.annotation.IntDef; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/** + * An {@link AudioProcessor} that skips silence in the input stream. Input and output are 16-bit + * PCM. + */ +public final class SilenceSkippingAudioProcessor implements AudioProcessor { + + /** + * The minimum duration of audio that must be below {@link #SILENCE_THRESHOLD_LEVEL} to classify + * that part of audio as silent, in microseconds. + */ + private static final long MINIMUM_SILENCE_DURATION_US = 100_000; + /** + * The duration of silence by which to extend non-silent sections, in microseconds. The value must + * not exceed {@link #MINIMUM_SILENCE_DURATION_US}. + */ + private static final long PADDING_SILENCE_US = 10_000; + /** + * The absolute level below which an individual PCM sample is classified as silent. Note: the + * specified value will be rounded so that the threshold check only depends on the more + * significant byte, for efficiency. + */ + private static final short SILENCE_THRESHOLD_LEVEL = 1024; + + /** + * Threshold for classifying an individual PCM sample as silent based on its more significant + * byte. This is {@link #SILENCE_THRESHOLD_LEVEL} divided by 256 with rounding. + */ + private static final byte SILENCE_THRESHOLD_LEVEL_MSB = (SILENCE_THRESHOLD_LEVEL + 128) >> 8; + + /** Trimming states. */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + STATE_NOISY, + STATE_MAYBE_SILENT, + STATE_SILENT, + }) + private @interface State {} + /** State when the input is not silent. */ + private static final int STATE_NOISY = 0; + /** State when the input may be silent but we haven't read enough yet to know. */ + private static final int STATE_MAYBE_SILENT = 1; + /** State when the input is silent. */ + private static final int STATE_SILENT = 2; + + private int channelCount; + private int sampleRateHz; + private int bytesPerFrame; + + private boolean enabled; + + private ByteBuffer buffer; + private ByteBuffer outputBuffer; + private boolean inputEnded; + + /** + * Buffers audio data that may be classified as silence while in {@link #STATE_MAYBE_SILENT}. If + * the input becomes noisy before the buffer has filled, it will be output. Otherwise, the buffer + * contents will be dropped and the state will transition to {@link #STATE_SILENT}. + */ + private byte[] maybeSilenceBuffer; + + /** + * Stores the latest part of the input while silent. It will be output as padding if the next + * input is noisy. + */ + private byte[] paddingBuffer; + + private @State int state; + private int maybeSilenceBufferSize; + private int paddingSize; + private boolean hasOutputNoise; + private long skippedFrames; + + /** Creates a new silence trimming audio processor. */ + public SilenceSkippingAudioProcessor() { + buffer = EMPTY_BUFFER; + outputBuffer = EMPTY_BUFFER; + channelCount = Format.NO_VALUE; + sampleRateHz = Format.NO_VALUE; + maybeSilenceBuffer = new byte[0]; + paddingBuffer = new byte[0]; + } + + /** + * Sets whether to skip silence in the input. Calling this method will discard any data buffered + * within the processor, and may update the value returned by {@link #isActive()}. + * + * @param enabled Whether to skip silence in the input. + */ + public void setEnabled(boolean enabled) { + this.enabled = enabled; + flush(); + } + + /** + * Returns the total number of frames of input audio that were skipped due to being classified as + * silence since the last call to {@link #flush()}. + */ + public long getSkippedFrames() { + return skippedFrames; + } + + // AudioProcessor implementation. + + @Override + public boolean configure(int sampleRateHz, int channelCount, int encoding) + throws UnhandledFormatException { + if (encoding != C.ENCODING_PCM_16BIT) { + throw new UnhandledFormatException(sampleRateHz, channelCount, encoding); + } + if (this.sampleRateHz == sampleRateHz && this.channelCount == channelCount) { + return false; + } + this.sampleRateHz = sampleRateHz; + this.channelCount = channelCount; + bytesPerFrame = channelCount * 2; + return true; + } + + @Override + public boolean isActive() { + return sampleRateHz != Format.NO_VALUE && enabled; + } + + @Override + public int getOutputChannelCount() { + return channelCount; + } + + @Override + public @C.Encoding int getOutputEncoding() { + return C.ENCODING_PCM_16BIT; + } + + @Override + public int getOutputSampleRateHz() { + return sampleRateHz; + } + + @Override + public void queueInput(ByteBuffer inputBuffer) { + while (inputBuffer.hasRemaining() && !outputBuffer.hasRemaining()) { + switch (state) { + case STATE_NOISY: + processNoisy(inputBuffer); + break; + case STATE_MAYBE_SILENT: + processMaybeSilence(inputBuffer); + break; + case STATE_SILENT: + processSilence(inputBuffer); + break; + default: + throw new IllegalStateException(); + } + } + } + + @Override + public void queueEndOfStream() { + inputEnded = true; + if (maybeSilenceBufferSize > 0) { + // We haven't received enough silence to transition to the silent state, so output the buffer. + output(maybeSilenceBuffer, maybeSilenceBufferSize); + } + if (!hasOutputNoise) { + skippedFrames += paddingSize / bytesPerFrame; + } + } + + @Override + public ByteBuffer getOutput() { + ByteBuffer outputBuffer = this.outputBuffer; + this.outputBuffer = EMPTY_BUFFER; + return outputBuffer; + } + + @SuppressWarnings("ReferenceEquality") + @Override + public boolean isEnded() { + return inputEnded && outputBuffer == EMPTY_BUFFER; + } + + @Override + public void flush() { + if (isActive()) { + int maybeSilenceBufferSize = durationUsToFrames(MINIMUM_SILENCE_DURATION_US) * bytesPerFrame; + if (maybeSilenceBuffer.length != maybeSilenceBufferSize) { + maybeSilenceBuffer = new byte[maybeSilenceBufferSize]; + } + paddingSize = durationUsToFrames(PADDING_SILENCE_US) * bytesPerFrame; + if (paddingBuffer.length != paddingSize) { + paddingBuffer = new byte[paddingSize]; + } + } + state = STATE_NOISY; + outputBuffer = EMPTY_BUFFER; + inputEnded = false; + skippedFrames = 0; + maybeSilenceBufferSize = 0; + hasOutputNoise = false; + } + + @Override + public void reset() { + enabled = false; + flush(); + buffer = EMPTY_BUFFER; + channelCount = Format.NO_VALUE; + sampleRateHz = Format.NO_VALUE; + paddingSize = 0; + maybeSilenceBuffer = new byte[0]; + paddingBuffer = new byte[0]; + } + + // Internal methods. + + /** + * Incrementally processes new input from {@code inputBuffer} while in {@link #STATE_NOISY}, + * updating the state if needed. + */ + private void processNoisy(ByteBuffer inputBuffer) { + int limit = inputBuffer.limit(); + + // Check if there's any noise within the maybe silence buffer duration. + inputBuffer.limit(Math.min(limit, inputBuffer.position() + maybeSilenceBuffer.length)); + int noiseLimit = findNoiseLimit(inputBuffer); + if (noiseLimit == inputBuffer.position()) { + // The buffer contains the start of possible silence. + state = STATE_MAYBE_SILENT; + } else { + inputBuffer.limit(noiseLimit); + output(inputBuffer); + } + + // Restore the limit. + inputBuffer.limit(limit); + } + + /** + * Incrementally processes new input from {@code inputBuffer} while in {@link + * #STATE_MAYBE_SILENT}, updating the state if needed. + */ + private void processMaybeSilence(ByteBuffer inputBuffer) { + int limit = inputBuffer.limit(); + int noisePosition = findNoisePosition(inputBuffer); + int maybeSilenceInputSize = noisePosition - inputBuffer.position(); + int maybeSilenceBufferRemaining = maybeSilenceBuffer.length - maybeSilenceBufferSize; + if (noisePosition < limit && maybeSilenceInputSize < maybeSilenceBufferRemaining) { + // The maybe silence buffer isn't full, so output it and switch back to the noisy state. + output(maybeSilenceBuffer, maybeSilenceBufferSize); + maybeSilenceBufferSize = 0; + state = STATE_NOISY; + } else { + // Fill as much of the maybe silence buffer as possible. + int bytesToWrite = Math.min(maybeSilenceInputSize, maybeSilenceBufferRemaining); + inputBuffer.limit(inputBuffer.position() + bytesToWrite); + inputBuffer.get(maybeSilenceBuffer, maybeSilenceBufferSize, bytesToWrite); + maybeSilenceBufferSize += bytesToWrite; + if (maybeSilenceBufferSize == maybeSilenceBuffer.length) { + // We've reached a period of silence, so skip it, taking in to account padding for both + // the noisy to silent transition and any future silent to noisy transition. + if (hasOutputNoise) { + output(maybeSilenceBuffer, paddingSize); + skippedFrames += (maybeSilenceBufferSize - paddingSize * 2) / bytesPerFrame; + } else { + skippedFrames += (maybeSilenceBufferSize - paddingSize) / bytesPerFrame; + } + updatePaddingBuffer(inputBuffer, maybeSilenceBuffer, maybeSilenceBufferSize); + maybeSilenceBufferSize = 0; + state = STATE_SILENT; + } + + // Restore the limit. + inputBuffer.limit(limit); + } + } + + /** + * Incrementally processes new input from {@code inputBuffer} while in {@link #STATE_SILENT}, + * updating the state if needed. + */ + private void processSilence(ByteBuffer inputBuffer) { + int limit = inputBuffer.limit(); + int noisyPosition = findNoisePosition(inputBuffer); + inputBuffer.limit(noisyPosition); + skippedFrames += inputBuffer.remaining() / bytesPerFrame; + updatePaddingBuffer(inputBuffer, paddingBuffer, paddingSize); + if (noisyPosition < limit) { + // Output the padding, which may include previous input as well as new input, then transition + // back to the noisy state. + output(paddingBuffer, paddingSize); + state = STATE_NOISY; + + // Restore the limit. + inputBuffer.limit(limit); + } + } + + /** + * Copies {@code length} elements from {@code data} to populate a new output buffer from the + * processor. + */ + private void output(byte[] data, int length) { + prepareForOutput(length); + buffer.put(data, 0, length); + buffer.flip(); + outputBuffer = buffer; + } + + /** + * Copies remaining bytes from {@code data} to populate a new output buffer from the processor. + */ + private void output(ByteBuffer data) { + prepareForOutput(data.remaining()); + buffer.put(data); + buffer.flip(); + outputBuffer = buffer; + } + + /** Prepares to output {@code size} bytes in {@code buffer}. */ + private void prepareForOutput(int size) { + if (buffer.capacity() < size) { + buffer = ByteBuffer.allocateDirect(size).order(ByteOrder.nativeOrder()); + } else { + buffer.clear(); + } + if (size > 0) { + hasOutputNoise = true; + } + } + + /** + * Fills {@link #paddingBuffer} using data from {@code input}, plus any additional buffered data + * at the end of {@code buffer} (up to its {@code size}) required to fill it, advancing the input + * position. + */ + private void updatePaddingBuffer(ByteBuffer input, byte[] buffer, int size) { + int fromInputSize = Math.min(input.remaining(), paddingSize); + int fromBufferSize = paddingSize - fromInputSize; + System.arraycopy( + /* src= */ buffer, + /* srcPos= */ size - fromBufferSize, + /* dest= */ paddingBuffer, + /* destPos= */ 0, + /* length= */ fromBufferSize); + input.position(input.limit() - fromInputSize); + input.get(paddingBuffer, fromBufferSize, fromInputSize); + } + + /** + * Returns the number of input frames corresponding to {@code durationUs} microseconds of audio. + */ + private int durationUsToFrames(long durationUs) { + return (int) ((durationUs * sampleRateHz) / C.MICROS_PER_SECOND); + } + + /** + * Returns the earliest byte position in [position, limit) of {@code buffer} that contains a frame + * classified as a noisy frame, or the limit of the buffer if no such frame exists. + */ + private int findNoisePosition(ByteBuffer buffer) { + // The input is in ByteOrder.nativeOrder(), which is little endian on Android. + for (int i = buffer.position() + 1; i < buffer.limit(); i += 2) { + if (Math.abs(buffer.get(i)) > SILENCE_THRESHOLD_LEVEL_MSB) { + // Round to the start of the frame. + return bytesPerFrame * (i / bytesPerFrame); + } + } + return buffer.limit(); + } + + /** + * Returns the earliest byte position in [position, limit) of {@code buffer} such that all frames + * from the byte position to the limit are classified as silent. + */ + private int findNoiseLimit(ByteBuffer buffer) { + // The input is in ByteOrder.nativeOrder(), which is little endian on Android. + for (int i = buffer.limit() - 1; i >= buffer.position(); i -= 2) { + if (Math.abs(buffer.get(i)) > SILENCE_THRESHOLD_LEVEL_MSB) { + // Return the start of the next frame. + return bytesPerFrame * (i / bytesPerFrame) + bytesPerFrame; + } + } + return buffer.position(); + } +} 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 83c33ee6d7..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); } /** @@ -522,8 +533,8 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements @Override protected void onStopped() { - audioSink.pause(); updateCurrentPosition(); + audioSink.pause(); } @Override @@ -651,8 +662,8 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements audioTrackNeedsConfigure = true; } - encoderDelay = newFormat.encoderDelay == Format.NO_VALUE ? 0 : newFormat.encoderDelay; - encoderPadding = newFormat.encoderPadding == Format.NO_VALUE ? 0 : newFormat.encoderPadding; + encoderDelay = newFormat.encoderDelay; + encoderPadding = newFormat.encoderPadding; eventDispatcher.inputFormatChanged(newFormat); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/Sonic.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/Sonic.java index daab04e4ab..0bf6baa4d0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/Sonic.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/Sonic.java @@ -32,27 +32,24 @@ import java.util.Arrays; private static final int AMDF_FREQUENCY = 4000; private final int inputSampleRateHz; - private final int numChannels; + private final int channelCount; private final float speed; private final float pitch; private final float rate; private final int minPeriod; private final int maxPeriod; - private final int maxRequired; + private final int maxRequiredFrameCount; private final short[] downSampleBuffer; - private int inputBufferSize; private short[] inputBuffer; - private int outputBufferSize; + private int inputFrameCount; private short[] outputBuffer; - private int pitchBufferSize; + private int outputFrameCount; private short[] pitchBuffer; + private int pitchFrameCount; private int oldRatePosition; private int newRatePosition; - private int numInputSamples; - private int numOutputSamples; - private int numPitchSamples; - private int remainingInputToCopy; + private int remainingInputToCopyFrameCount; private int prevPeriod; private int prevMinDiff; private int minDiff; @@ -62,31 +59,25 @@ import java.util.Arrays; * Creates a new Sonic audio stream processor. * * @param inputSampleRateHz The sample rate of input audio, in hertz. - * @param numChannels The number of channels in the input audio. + * @param channelCount The number of channels in the input audio. * @param speed The speedup factor for output audio. * @param pitch The pitch factor for output audio. * @param outputSampleRateHz The sample rate for output audio, in hertz. */ - public Sonic(int inputSampleRateHz, int numChannels, float speed, float pitch, - int outputSampleRateHz) { + public Sonic( + int inputSampleRateHz, int channelCount, float speed, float pitch, int outputSampleRateHz) { this.inputSampleRateHz = inputSampleRateHz; - this.numChannels = numChannels; - minPeriod = inputSampleRateHz / MAXIMUM_PITCH; - maxPeriod = inputSampleRateHz / MINIMUM_PITCH; - maxRequired = 2 * maxPeriod; - downSampleBuffer = new short[maxRequired]; - inputBufferSize = maxRequired; - inputBuffer = new short[maxRequired * numChannels]; - outputBufferSize = maxRequired; - outputBuffer = new short[maxRequired * numChannels]; - pitchBufferSize = maxRequired; - pitchBuffer = new short[maxRequired * numChannels]; - oldRatePosition = 0; - newRatePosition = 0; - prevPeriod = 0; + this.channelCount = channelCount; this.speed = speed; this.pitch = pitch; - this.rate = (float) inputSampleRateHz / outputSampleRateHz; + rate = (float) inputSampleRateHz / outputSampleRateHz; + minPeriod = inputSampleRateHz / MAXIMUM_PITCH; + maxPeriod = inputSampleRateHz / MINIMUM_PITCH; + maxRequiredFrameCount = 2 * maxPeriod; + downSampleBuffer = new short[maxRequiredFrameCount]; + inputBuffer = new short[maxRequiredFrameCount * channelCount]; + outputBuffer = new short[maxRequiredFrameCount * channelCount]; + pitchBuffer = new short[maxRequiredFrameCount * channelCount]; } /** @@ -96,11 +87,11 @@ import java.util.Arrays; * @param buffer A {@link ShortBuffer} containing input data between its position and limit. */ public void queueInput(ShortBuffer buffer) { - int samplesToWrite = buffer.remaining() / numChannels; - int bytesToWrite = samplesToWrite * numChannels * 2; - enlargeInputBufferIfNeeded(samplesToWrite); - buffer.get(inputBuffer, numInputSamples * numChannels, bytesToWrite / 2); - numInputSamples += samplesToWrite; + int framesToWrite = buffer.remaining() / channelCount; + int bytesToWrite = framesToWrite * channelCount * 2; + inputBuffer = ensureSpaceForAdditionalFrames(inputBuffer, inputFrameCount, framesToWrite); + buffer.get(inputBuffer, inputFrameCount * channelCount, bytesToWrite / 2); + inputFrameCount += framesToWrite; processStreamInput(); } @@ -111,11 +102,15 @@ import java.util.Arrays; * @param buffer A {@link ShortBuffer} into which output will be written. */ public void getOutput(ShortBuffer buffer) { - int samplesToRead = Math.min(buffer.remaining() / numChannels, numOutputSamples); - buffer.put(outputBuffer, 0, samplesToRead * numChannels); - numOutputSamples -= samplesToRead; - System.arraycopy(outputBuffer, samplesToRead * numChannels, outputBuffer, 0, - numOutputSamples * numChannels); + int framesToRead = Math.min(buffer.remaining() / channelCount, outputFrameCount); + buffer.put(outputBuffer, 0, framesToRead * channelCount); + outputFrameCount -= framesToRead; + System.arraycopy( + outputBuffer, + framesToRead * channelCount, + outputBuffer, + 0, + outputFrameCount * channelCount); } /** @@ -123,80 +118,105 @@ import java.util.Arrays; * added to the output, but flushing in the middle of words could introduce distortion. */ public void queueEndOfStream() { - int remainingSamples = numInputSamples; + int remainingFrameCount = inputFrameCount; float s = speed / pitch; float r = rate * pitch; - int expectedOutputSamples = - numOutputSamples + (int) ((remainingSamples / s + numPitchSamples) / r + 0.5f); + int expectedOutputFrames = + outputFrameCount + (int) ((remainingFrameCount / s + pitchFrameCount) / r + 0.5f); // Add enough silence to flush both input and pitch buffers. - enlargeInputBufferIfNeeded(remainingSamples + 2 * maxRequired); - for (int xSample = 0; xSample < 2 * maxRequired * numChannels; xSample++) { - inputBuffer[remainingSamples * numChannels + xSample] = 0; + inputBuffer = + ensureSpaceForAdditionalFrames( + inputBuffer, inputFrameCount, remainingFrameCount + 2 * maxRequiredFrameCount); + for (int xSample = 0; xSample < 2 * maxRequiredFrameCount * channelCount; xSample++) { + inputBuffer[remainingFrameCount * channelCount + xSample] = 0; } - numInputSamples += 2 * maxRequired; + inputFrameCount += 2 * maxRequiredFrameCount; processStreamInput(); - // Throw away any extra samples we generated due to the silence we added. - if (numOutputSamples > expectedOutputSamples) { - numOutputSamples = expectedOutputSamples; + // Throw away any extra frames we generated due to the silence we added. + if (outputFrameCount > expectedOutputFrames) { + outputFrameCount = expectedOutputFrames; } // Empty input and pitch buffers. - numInputSamples = 0; - remainingInputToCopy = 0; - numPitchSamples = 0; + inputFrameCount = 0; + remainingInputToCopyFrameCount = 0; + pitchFrameCount = 0; } - /** - * Returns the number of output samples that can be read with {@link #getOutput(ShortBuffer)}. - */ - public int getSamplesAvailable() { - return numOutputSamples; + /** Clears state in preparation for receiving a new stream of input buffers. */ + public void flush() { + inputFrameCount = 0; + outputFrameCount = 0; + pitchFrameCount = 0; + oldRatePosition = 0; + newRatePosition = 0; + remainingInputToCopyFrameCount = 0; + prevPeriod = 0; + prevMinDiff = 0; + minDiff = 0; + maxDiff = 0; + } + + /** Returns the number of output frames that can be read with {@link #getOutput(ShortBuffer)}. */ + public int getFramesAvailable() { + return outputFrameCount; } // Internal methods. - private void enlargeOutputBufferIfNeeded(int numSamples) { - if (numOutputSamples + numSamples > outputBufferSize) { - outputBufferSize += (outputBufferSize / 2) + numSamples; - outputBuffer = Arrays.copyOf(outputBuffer, outputBufferSize * numChannels); + /** + * Returns {@code buffer} or a copy of it, such that there is enough space in the returned buffer + * to store {@code newFrameCount} additional frames. + * + * @param buffer The buffer. + * @param frameCount The number of frames already in the buffer. + * @param additionalFrameCount The number of additional frames that need to be stored in the + * buffer. + * @return A buffer with enough space for the additional frames. + */ + private short[] ensureSpaceForAdditionalFrames( + short[] buffer, int frameCount, int additionalFrameCount) { + int currentCapacityFrames = buffer.length / channelCount; + if (frameCount + additionalFrameCount <= currentCapacityFrames) { + return buffer; + } else { + int newCapacityFrames = 3 * currentCapacityFrames / 2 + additionalFrameCount; + return Arrays.copyOf(buffer, newCapacityFrames * channelCount); } } - private void enlargeInputBufferIfNeeded(int numSamples) { - if (numInputSamples + numSamples > inputBufferSize) { - inputBufferSize += (inputBufferSize / 2) + numSamples; - inputBuffer = Arrays.copyOf(inputBuffer, inputBufferSize * numChannels); - } + private void removeProcessedInputFrames(int positionFrames) { + int remainingFrames = inputFrameCount - positionFrames; + System.arraycopy( + inputBuffer, positionFrames * channelCount, inputBuffer, 0, remainingFrames * channelCount); + inputFrameCount = remainingFrames; } - private void removeProcessedInputSamples(int position) { - int remainingSamples = numInputSamples - position; - System.arraycopy(inputBuffer, position * numChannels, inputBuffer, 0, - remainingSamples * numChannels); - numInputSamples = remainingSamples; + private void copyToOutput(short[] samples, int positionFrames, int frameCount) { + outputBuffer = ensureSpaceForAdditionalFrames(outputBuffer, outputFrameCount, frameCount); + System.arraycopy( + samples, + positionFrames * channelCount, + outputBuffer, + outputFrameCount * channelCount, + frameCount * channelCount); + outputFrameCount += frameCount; } - private void copyToOutput(short[] samples, int position, int numSamples) { - enlargeOutputBufferIfNeeded(numSamples); - System.arraycopy(samples, position * numChannels, outputBuffer, numOutputSamples * numChannels, - numSamples * numChannels); - numOutputSamples += numSamples; - } - - private int copyInputToOutput(int position) { - int numSamples = Math.min(maxRequired, remainingInputToCopy); - copyToOutput(inputBuffer, position, numSamples); - remainingInputToCopy -= numSamples; - return numSamples; + private int copyInputToOutput(int positionFrames) { + int frameCount = Math.min(maxRequiredFrameCount, remainingInputToCopyFrameCount); + copyToOutput(inputBuffer, positionFrames, frameCount); + remainingInputToCopyFrameCount -= frameCount; + return frameCount; } private void downSampleInput(short[] samples, int position, int skip) { // If skip is greater than one, average skip samples together and write them to the down-sample - // buffer. If numChannels is greater than one, mix the channels together as we down sample. - int numSamples = maxRequired / skip; - int samplesPerValue = numChannels * skip; - position *= numChannels; - for (int i = 0; i < numSamples; i++) { + // buffer. If channelCount is greater than one, mix the channels together as we down sample. + int frameCount = maxRequiredFrameCount / skip; + int samplesPerValue = channelCount * skip; + position *= channelCount; + for (int i = 0; i < frameCount; i++) { int value = 0; for (int j = 0; j < samplesPerValue; j++) { value += samples[position + i * samplesPerValue + j]; @@ -213,7 +233,7 @@ import java.util.Arrays; int worstPeriod = 255; int minDiff = 1; int maxDiff = 0; - position *= numChannels; + position *= channelCount; for (int period = minPeriod; period <= maxPeriod; period++) { int diff = 0; for (int i = 0; i < period; i++) { @@ -242,28 +262,22 @@ import java.util.Arrays; * Returns whether the previous pitch period estimate is a better approximation, which can occur * at the abrupt end of voiced words. */ - private boolean previousPeriodBetter(int minDiff, int maxDiff, boolean preferNewPeriod) { + private boolean previousPeriodBetter(int minDiff, int maxDiff) { if (minDiff == 0 || prevPeriod == 0) { return false; } - if (preferNewPeriod) { - if (maxDiff > minDiff * 3) { - // Got a reasonable match this period - return false; - } - if (minDiff * 2 <= prevMinDiff * 3) { - // Mismatch is not that much greater this period - return false; - } - } else { - if (minDiff <= prevMinDiff) { - return false; - } + if (maxDiff > minDiff * 3) { + // Got a reasonable match this period. + return false; + } + if (minDiff * 2 <= prevMinDiff * 3) { + // Mismatch is not that much greater this period. + return false; } return true; } - private int findPitchPeriod(short[] samples, int position, boolean preferNewPeriod) { + private int findPitchPeriod(short[] samples, int position) { // Find the pitch period. This is a critical step, and we may have to try multiple ways to get a // good answer. This version uses AMDF. To improve speed, we down sample by an integer factor // get in the 11 kHz range, and then do it again with a narrower frequency range without down @@ -271,7 +285,7 @@ import java.util.Arrays; int period; int retPeriod; int skip = inputSampleRateHz > AMDF_FREQUENCY ? inputSampleRateHz / AMDF_FREQUENCY : 1; - if (numChannels == 1 && skip == 1) { + if (channelCount == 1 && skip == 1) { period = findPitchPeriodInRange(samples, position, minPeriod, maxPeriod); } else { downSampleInput(samples, position, skip); @@ -286,7 +300,7 @@ import java.util.Arrays; if (maxP > maxPeriod) { maxP = maxPeriod; } - if (numChannels == 1) { + if (channelCount == 1) { period = findPitchPeriodInRange(samples, position, minP, maxP); } else { downSampleInput(samples, position, 1); @@ -294,7 +308,7 @@ import java.util.Arrays; } } } - if (previousPeriodBetter(minDiff, maxDiff, preferNewPeriod)) { + if (previousPeriodBetter(minDiff, maxDiff)) { retPeriod = prevPeriod; } else { retPeriod = period; @@ -304,30 +318,35 @@ import java.util.Arrays; return retPeriod; } - private void moveNewSamplesToPitchBuffer(int originalNumOutputSamples) { - int numSamples = numOutputSamples - originalNumOutputSamples; - if (numPitchSamples + numSamples > pitchBufferSize) { - pitchBufferSize += (pitchBufferSize / 2) + numSamples; - pitchBuffer = Arrays.copyOf(pitchBuffer, pitchBufferSize * numChannels); - } - System.arraycopy(outputBuffer, originalNumOutputSamples * numChannels, pitchBuffer, - numPitchSamples * numChannels, numSamples * numChannels); - numOutputSamples = originalNumOutputSamples; - numPitchSamples += numSamples; + private void moveNewSamplesToPitchBuffer(int originalOutputFrameCount) { + int frameCount = outputFrameCount - originalOutputFrameCount; + pitchBuffer = ensureSpaceForAdditionalFrames(pitchBuffer, pitchFrameCount, frameCount); + System.arraycopy( + outputBuffer, + originalOutputFrameCount * channelCount, + pitchBuffer, + pitchFrameCount * channelCount, + frameCount * channelCount); + outputFrameCount = originalOutputFrameCount; + pitchFrameCount += frameCount; } - private void removePitchSamples(int numSamples) { - if (numSamples == 0) { + private void removePitchFrames(int frameCount) { + if (frameCount == 0) { return; } - System.arraycopy(pitchBuffer, numSamples * numChannels, pitchBuffer, 0, - (numPitchSamples - numSamples) * numChannels); - numPitchSamples -= numSamples; + System.arraycopy( + pitchBuffer, + frameCount * channelCount, + pitchBuffer, + 0, + (pitchFrameCount - frameCount) * channelCount); + pitchFrameCount -= frameCount; } private short interpolate(short[] in, int inPos, int oldSampleRate, int newSampleRate) { short left = in[inPos]; - short right = in[inPos + numChannels]; + short right = in[inPos + channelCount]; int position = newRatePosition * oldSampleRate; int leftPosition = oldRatePosition * newSampleRate; int rightPosition = (oldRatePosition + 1) * newSampleRate; @@ -336,8 +355,8 @@ import java.util.Arrays; return (short) ((ratio * left + (width - ratio) * right) / width); } - private void adjustRate(float rate, int originalNumOutputSamples) { - if (numOutputSamples == originalNumOutputSamples) { + private void adjustRate(float rate, int originalOutputFrameCount) { + if (outputFrameCount == originalOutputFrameCount) { return; } int newSampleRate = (int) (inputSampleRateHz / rate); @@ -347,17 +366,19 @@ import java.util.Arrays; newSampleRate /= 2; oldSampleRate /= 2; } - moveNewSamplesToPitchBuffer(originalNumOutputSamples); + moveNewSamplesToPitchBuffer(originalOutputFrameCount); // Leave at least one pitch sample in the buffer. - for (int position = 0; position < numPitchSamples - 1; position++) { + for (int position = 0; position < pitchFrameCount - 1; position++) { while ((oldRatePosition + 1) * newSampleRate > newRatePosition * oldSampleRate) { - enlargeOutputBufferIfNeeded(1); - for (int i = 0; i < numChannels; i++) { - outputBuffer[numOutputSamples * numChannels + i] = - interpolate(pitchBuffer, position * numChannels + i, oldSampleRate, newSampleRate); + outputBuffer = + ensureSpaceForAdditionalFrames( + outputBuffer, outputFrameCount, /* additionalFrameCount= */ 1); + for (int i = 0; i < channelCount; i++) { + outputBuffer[outputFrameCount * channelCount + i] = + interpolate(pitchBuffer, position * channelCount + i, oldSampleRate, newSampleRate); } newRatePosition++; - numOutputSamples++; + outputFrameCount++; } oldRatePosition++; if (oldRatePosition == oldSampleRate) { @@ -366,91 +387,117 @@ import java.util.Arrays; newRatePosition = 0; } } - removePitchSamples(numPitchSamples - 1); + removePitchFrames(pitchFrameCount - 1); } private int skipPitchPeriod(short[] samples, int position, float speed, int period) { // Skip over a pitch period, and copy period/speed samples to the output. - int newSamples; + int newFrameCount; if (speed >= 2.0f) { - newSamples = (int) (period / (speed - 1.0f)); + newFrameCount = (int) (period / (speed - 1.0f)); } else { - newSamples = period; - remainingInputToCopy = (int) (period * (2.0f - speed) / (speed - 1.0f)); + newFrameCount = period; + remainingInputToCopyFrameCount = (int) (period * (2.0f - speed) / (speed - 1.0f)); } - enlargeOutputBufferIfNeeded(newSamples); - overlapAdd(newSamples, numChannels, outputBuffer, numOutputSamples, samples, position, samples, + outputBuffer = ensureSpaceForAdditionalFrames(outputBuffer, outputFrameCount, newFrameCount); + overlapAdd( + newFrameCount, + channelCount, + outputBuffer, + outputFrameCount, + samples, + position, + samples, position + period); - numOutputSamples += newSamples; - return newSamples; + outputFrameCount += newFrameCount; + return newFrameCount; } private int insertPitchPeriod(short[] samples, int position, float speed, int period) { // Insert a pitch period, and determine how much input to copy directly. - int newSamples; + int newFrameCount; if (speed < 0.5f) { - newSamples = (int) (period * speed / (1.0f - speed)); + newFrameCount = (int) (period * speed / (1.0f - speed)); } else { - newSamples = period; - remainingInputToCopy = (int) (period * (2.0f * speed - 1.0f) / (1.0f - speed)); + newFrameCount = period; + remainingInputToCopyFrameCount = (int) (period * (2.0f * speed - 1.0f) / (1.0f - speed)); } - enlargeOutputBufferIfNeeded(period + newSamples); - System.arraycopy(samples, position * numChannels, outputBuffer, numOutputSamples * numChannels, - period * numChannels); - overlapAdd(newSamples, numChannels, outputBuffer, numOutputSamples + period, samples, - position + period, samples, position); - numOutputSamples += period + newSamples; - return newSamples; + outputBuffer = + ensureSpaceForAdditionalFrames(outputBuffer, outputFrameCount, period + newFrameCount); + System.arraycopy( + samples, + position * channelCount, + outputBuffer, + outputFrameCount * channelCount, + period * channelCount); + overlapAdd( + newFrameCount, + channelCount, + outputBuffer, + outputFrameCount + period, + samples, + position + period, + samples, + position); + outputFrameCount += period + newFrameCount; + return newFrameCount; } private void changeSpeed(float speed) { - if (numInputSamples < maxRequired) { + if (inputFrameCount < maxRequiredFrameCount) { return; } - int numSamples = numInputSamples; - int position = 0; + int frameCount = inputFrameCount; + int positionFrames = 0; do { - if (remainingInputToCopy > 0) { - position += copyInputToOutput(position); + if (remainingInputToCopyFrameCount > 0) { + positionFrames += copyInputToOutput(positionFrames); } else { - int period = findPitchPeriod(inputBuffer, position, true); + int period = findPitchPeriod(inputBuffer, positionFrames); if (speed > 1.0) { - position += period + skipPitchPeriod(inputBuffer, position, speed, period); + positionFrames += period + skipPitchPeriod(inputBuffer, positionFrames, speed, period); } else { - position += insertPitchPeriod(inputBuffer, position, speed, period); + positionFrames += insertPitchPeriod(inputBuffer, positionFrames, speed, period); } } - } while (position + maxRequired <= numSamples); - removeProcessedInputSamples(position); + } while (positionFrames + maxRequiredFrameCount <= frameCount); + removeProcessedInputFrames(positionFrames); } private void processStreamInput() { // Resample as many pitch periods as we have buffered on the input. - int originalNumOutputSamples = numOutputSamples; + int originalOutputFrameCount = outputFrameCount; float s = speed / pitch; float r = rate * pitch; if (s > 1.00001 || s < 0.99999) { changeSpeed(s); } else { - copyToOutput(inputBuffer, 0, numInputSamples); - numInputSamples = 0; + copyToOutput(inputBuffer, 0, inputFrameCount); + inputFrameCount = 0; } if (r != 1.0f) { - adjustRate(r, originalNumOutputSamples); + adjustRate(r, originalOutputFrameCount); } } - private static void overlapAdd(int numSamples, int numChannels, short[] out, int outPos, - short[] rampDown, int rampDownPos, short[] rampUp, int rampUpPos) { - for (int i = 0; i < numChannels; i++) { - int o = outPos * numChannels + i; - int u = rampUpPos * numChannels + i; - int d = rampDownPos * numChannels + i; - for (int t = 0; t < numSamples; t++) { - out[o] = (short) ((rampDown[d] * (numSamples - t) + rampUp[u] * t) / numSamples); - o += numChannels; - d += numChannels; - u += numChannels; + private static void overlapAdd( + int frameCount, + int channelCount, + short[] out, + int outPosition, + short[] rampDown, + int rampDownPosition, + short[] rampUp, + int rampUpPosition) { + for (int i = 0; i < channelCount; i++) { + int o = outPosition * channelCount + i; + int u = rampUpPosition * channelCount + i; + int d = rampDownPosition * channelCount + i; + for (int t = 0; t < frameCount; t++) { + out[o] = (short) ((rampDown[d] * (frameCount - t) + rampUp[u] * t) / frameCount); + o += channelCount; + d += channelCount; + u += channelCount; } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/SonicAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/SonicAudioProcessor.java index 370ddb2809..2ca2d47828 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/SonicAudioProcessor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/SonicAudioProcessor.java @@ -15,9 +15,11 @@ */ package com.google.android.exoplayer2.audio; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C.Encoding; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; import java.nio.ByteBuffer; import java.nio.ByteOrder; @@ -60,15 +62,14 @@ public final class SonicAudioProcessor implements AudioProcessor { */ private static final int MIN_BYTES_FOR_SPEEDUP_CALCULATION = 1024; - private int pendingOutputSampleRateHz; private int channelCount; private int sampleRateHz; - - private Sonic sonic; private float speed; private float pitch; private int outputSampleRateHz; + private int pendingOutputSampleRateHz; + private @Nullable Sonic sonic; private ByteBuffer buffer; private ShortBuffer shortBuffer; private ByteBuffer outputBuffer; @@ -92,24 +93,36 @@ public final class SonicAudioProcessor implements AudioProcessor { } /** - * Sets the playback speed. The new speed will take effect after a call to {@link #flush()}. + * Sets the playback speed. Calling this method will discard any data buffered within the + * processor, and may update the value returned by {@link #isActive()}. * * @param speed The requested new playback speed. * @return The actual new playback speed. */ public float setSpeed(float speed) { - this.speed = Util.constrainValue(speed, MINIMUM_SPEED, MAXIMUM_SPEED); - return this.speed; + speed = Util.constrainValue(speed, MINIMUM_SPEED, MAXIMUM_SPEED); + if (this.speed != speed) { + this.speed = speed; + sonic = null; + } + flush(); + return speed; } /** - * Sets the playback pitch. The new pitch will take effect after a call to {@link #flush()}. + * Sets the playback pitch. Calling this method will discard any data buffered within the + * processor, and may update the value returned by {@link #isActive()}. * * @param pitch The requested new pitch. * @return The actual new pitch. */ public float setPitch(float pitch) { - this.pitch = Util.constrainValue(pitch, MINIMUM_PITCH, MAXIMUM_PITCH); + pitch = Util.constrainValue(pitch, MINIMUM_PITCH, MAXIMUM_PITCH); + if (this.pitch != pitch) { + this.pitch = pitch; + sonic = null; + } + flush(); return pitch; } @@ -159,13 +172,16 @@ public final class SonicAudioProcessor implements AudioProcessor { this.sampleRateHz = sampleRateHz; this.channelCount = channelCount; this.outputSampleRateHz = outputSampleRateHz; + sonic = null; return true; } @Override public boolean isActive() { - return Math.abs(speed - 1f) >= CLOSE_THRESHOLD || Math.abs(pitch - 1f) >= CLOSE_THRESHOLD - || outputSampleRateHz != sampleRateHz; + return sampleRateHz != Format.NO_VALUE + && (Math.abs(speed - 1f) >= CLOSE_THRESHOLD + || Math.abs(pitch - 1f) >= CLOSE_THRESHOLD + || outputSampleRateHz != sampleRateHz); } @Override @@ -185,6 +201,7 @@ public final class SonicAudioProcessor implements AudioProcessor { @Override public void queueInput(ByteBuffer inputBuffer) { + Assertions.checkState(sonic != null); if (inputBuffer.hasRemaining()) { ShortBuffer shortBuffer = inputBuffer.asShortBuffer(); int inputSize = inputBuffer.remaining(); @@ -192,7 +209,7 @@ public final class SonicAudioProcessor implements AudioProcessor { sonic.queueInput(shortBuffer); inputBuffer.position(inputBuffer.position() + inputSize); } - int outputSize = sonic.getSamplesAvailable() * channelCount * 2; + int outputSize = sonic.getFramesAvailable() * channelCount * 2; if (outputSize > 0) { if (buffer.capacity() < outputSize) { buffer = ByteBuffer.allocateDirect(outputSize).order(ByteOrder.nativeOrder()); @@ -210,6 +227,7 @@ public final class SonicAudioProcessor implements AudioProcessor { @Override public void queueEndOfStream() { + Assertions.checkState(sonic != null); sonic.queueEndOfStream(); inputEnded = true; } @@ -223,12 +241,18 @@ public final class SonicAudioProcessor implements AudioProcessor { @Override public boolean isEnded() { - return inputEnded && (sonic == null || sonic.getSamplesAvailable() == 0); + return inputEnded && (sonic == null || sonic.getFramesAvailable() == 0); } @Override public void flush() { - sonic = new Sonic(sampleRateHz, channelCount, speed, pitch, outputSampleRateHz); + if (isActive()) { + if (sonic == null) { + sonic = new Sonic(sampleRateHz, channelCount, speed, pitch, outputSampleRateHz); + } else { + sonic.flush(); + } + } outputBuffer = EMPTY_BUFFER; inputBytes = 0; outputBytes = 0; @@ -237,17 +261,19 @@ public final class SonicAudioProcessor implements AudioProcessor { @Override public void reset() { - sonic = null; - buffer = EMPTY_BUFFER; - shortBuffer = buffer.asShortBuffer(); - outputBuffer = EMPTY_BUFFER; + speed = 1f; + pitch = 1f; channelCount = Format.NO_VALUE; sampleRateHz = Format.NO_VALUE; outputSampleRateHz = Format.NO_VALUE; + buffer = EMPTY_BUFFER; + shortBuffer = buffer.asShortBuffer(); + outputBuffer = EMPTY_BUFFER; + pendingOutputSampleRateHz = SAMPLE_RATE_NO_CHANGE; + sonic = null; inputBytes = 0; outputBytes = 0; inputEnded = false; - pendingOutputSampleRateHz = SAMPLE_RATE_NO_CHANGE; } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/TrimmingAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/TrimmingAudioProcessor.java index 9ff1c158dd..ccaa9c3fed 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/TrimmingAudioProcessor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/TrimmingAudioProcessor.java @@ -22,14 +22,12 @@ import com.google.android.exoplayer2.util.Util; import java.nio.ByteBuffer; import java.nio.ByteOrder; -/** - * Audio processor for trimming samples from the start/end of data. - */ +/** Audio processor for trimming samples from the start/end of data. */ /* package */ final class TrimmingAudioProcessor implements AudioProcessor { private boolean isActive; - private int trimStartSamples; - private int trimEndSamples; + private int trimStartFrames; + private int trimEndFrames; private int channelCount; private int sampleRateHz; @@ -40,27 +38,27 @@ import java.nio.ByteOrder; private int endBufferSize; private boolean inputEnded; - /** - * Creates a new audio processor for trimming samples from the start/end of data. - */ + /** Creates a new audio processor for trimming samples from the start/end of data. */ public TrimmingAudioProcessor() { buffer = EMPTY_BUFFER; outputBuffer = EMPTY_BUFFER; channelCount = Format.NO_VALUE; + sampleRateHz = Format.NO_VALUE; + endBuffer = new byte[0]; } /** - * Sets the number of audio samples to trim from the start and end of audio passed to this + * Sets the number of audio frames to trim from the start and end of audio passed to this * processor. After calling this method, call {@link #configure(int, int, int)} to apply the new - * trimming sample counts. + * trimming frame counts. * - * @param trimStartSamples The number of audio samples to trim from the start of audio. - * @param trimEndSamples The number of audio samples to trim from the end of audio. + * @param trimStartFrames The number of audio frames to trim from the start of audio. + * @param trimEndFrames The number of audio frames to trim from the end of audio. * @see AudioSink#configure(int, int, int, int, int[], int, int) */ - public void setTrimSampleCount(int trimStartSamples, int trimEndSamples) { - this.trimStartSamples = trimStartSamples; - this.trimEndSamples = trimEndSamples; + public void setTrimFrameCount(int trimStartFrames, int trimEndFrames) { + this.trimStartFrames = trimStartFrames; + this.trimEndFrames = trimEndFrames; } @Override @@ -71,11 +69,11 @@ import java.nio.ByteOrder; } this.channelCount = channelCount; this.sampleRateHz = sampleRateHz; - endBuffer = new byte[trimEndSamples * channelCount * 2]; + endBuffer = new byte[trimEndFrames * channelCount * 2]; endBufferSize = 0; - pendingTrimStartBytes = trimStartSamples * channelCount * 2; + pendingTrimStartBytes = trimStartFrames * channelCount * 2; boolean wasActive = isActive; - isActive = trimStartSamples != 0 || trimEndSamples != 0; + isActive = trimStartFrames != 0 || trimEndFrames != 0; return wasActive != isActive; } @@ -182,7 +180,7 @@ import java.nio.ByteOrder; buffer = EMPTY_BUFFER; channelCount = Format.NO_VALUE; sampleRateHz = Format.NO_VALUE; - endBuffer = null; + endBuffer = new byte[0]; } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/ClearKeyUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/ClearKeyUtil.java index ee337dcc51..87dbc7a65c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/ClearKeyUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/ClearKeyUtil.java @@ -17,8 +17,6 @@ package com.google.android.exoplayer2.drm; import android.util.Log; import com.google.android.exoplayer2.util.Util; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -29,7 +27,6 @@ import org.json.JSONObject; /* package */ final class ClearKeyUtil { private static final String TAG = "ClearKeyUtil"; - private static final Pattern REQUEST_KIDS_PATTERN = Pattern.compile("\"kids\":\\[\"(.*?)\"]"); private ClearKeyUtil() {} @@ -43,21 +40,12 @@ import org.json.JSONObject; if (Util.SDK_INT >= 27) { return request; } - // Prior to O-MR1 the ClearKey CDM encoded the values in the "kids" array using Base64 rather - // than Base64Url. See [Internal: b/64388098]. Any "/" characters that ended up in the request - // as a result were not escaped as "\/". We know the exact request format from the platform's - // InitDataParser.cpp, so we can use a regexp rather than parsing the JSON. + // Prior to O-MR1 the ClearKey CDM encoded the values in the "kids" array using Base64 encoding + // rather than Base64Url encoding. See [Internal: b/64388098]. We know the exact request format + // from the platform's InitDataParser.cpp. Since there aren't any "+" or "/" symbols elsewhere + // in the request, it's safe to fix the encoding by replacement through the whole request. String requestString = Util.fromUtf8Bytes(request); - Matcher requestKidsMatcher = REQUEST_KIDS_PATTERN.matcher(requestString); - if (!requestKidsMatcher.find()) { - Log.e(TAG, "Failed to adjust request data: " + requestString); - return request; - } - int kidsStartIndex = requestKidsMatcher.start(1); - int kidsEndIndex = requestKidsMatcher.end(1); - StringBuilder adjustedRequestBuilder = new StringBuilder(requestString); - base64ToBase64Url(adjustedRequestBuilder, kidsStartIndex, kidsEndIndex); - return Util.getUtf8Bytes(adjustedRequestBuilder.toString()); + return Util.getUtf8Bytes(base64ToBase64Url(requestString)); } /** @@ -71,39 +59,39 @@ import org.json.JSONObject; return response; } // Prior to O-MR1 the ClearKey CDM expected Base64 encoding rather than Base64Url encoding for - // the "k" and "kid" strings. See [Internal: b/64388098]. + // the "k" and "kid" strings. See [Internal: b/64388098]. We know that the ClearKey CDM only + // looks at the k, kid and kty parameters in each key, so can ignore the rest of the response. try { JSONObject responseJson = new JSONObject(Util.fromUtf8Bytes(response)); + StringBuilder adjustedResponseBuilder = new StringBuilder("{\"keys\":["); JSONArray keysArray = responseJson.getJSONArray("keys"); for (int i = 0; i < keysArray.length(); i++) { + if (i != 0) { + adjustedResponseBuilder.append(","); + } JSONObject key = keysArray.getJSONObject(i); - key.put("k", base64UrlToBase64(key.getString("k"))); - key.put("kid", base64UrlToBase64(key.getString("kid"))); + adjustedResponseBuilder.append("{\"k\":\""); + adjustedResponseBuilder.append(base64UrlToBase64(key.getString("k"))); + adjustedResponseBuilder.append("\",\"kid\":\""); + adjustedResponseBuilder.append(base64UrlToBase64(key.getString("kid"))); + adjustedResponseBuilder.append("\",\"kty\":\""); + adjustedResponseBuilder.append(key.getString("kty")); + adjustedResponseBuilder.append("\"}"); } - return Util.getUtf8Bytes(responseJson.toString()); + adjustedResponseBuilder.append("]}"); + return Util.getUtf8Bytes(adjustedResponseBuilder.toString()); } catch (JSONException e) { Log.e(TAG, "Failed to adjust response data: " + Util.fromUtf8Bytes(response), e); return response; } } - private static void base64ToBase64Url(StringBuilder base64, int startIndex, int endIndex) { - for (int i = startIndex; i < endIndex; i++) { - switch (base64.charAt(i)) { - case '+': - base64.setCharAt(i, '-'); - break; - case '/': - base64.setCharAt(i, '_'); - break; - default: - break; - } - } + private static String base64ToBase64Url(String base64) { + return base64.replace('+', '-').replace('/', '_'); } - private static String base64UrlToBase64(String base64) { - return base64.replace('-', '+').replace('_', '/'); + private static String base64UrlToBase64(String base64Url) { + return base64Url.replace('-', '+').replace('_', '/'); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java index 25fdaba5b8..c57b023139 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java @@ -25,6 +25,7 @@ import android.os.Message; import android.util.Log; import android.util.Pair; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.drm.DefaultDrmSessionEventListener.EventDispatcher; import com.google.android.exoplayer2.drm.ExoMediaDrm.DefaultKeyRequest; import com.google.android.exoplayer2.drm.ExoMediaDrm.KeyRequest; import com.google.android.exoplayer2.drm.ExoMediaDrm.ProvisionRequest; @@ -80,8 +81,7 @@ import java.util.UUID; private final String mimeType; private final @DefaultDrmSessionManager.Mode int mode; private final HashMap optionalKeyRequestParameters; - private final Handler eventHandler; - private final DefaultDrmSessionManager.EventListener eventListener; + private final EventDispatcher eventDispatcher; private final int initialDrmRequestRetryCount; /* package */ final MediaDrmCallback callback; @@ -109,17 +109,22 @@ import java.util.UUID; * @param optionalKeyRequestParameters The optional key request parameters. * @param callback The media DRM callback. * @param playbackLooper The playback looper. - * @param eventHandler The handler to post listener events. - * @param eventListener The DRM session manager event listener. + * @param eventDispatcher The dispatcher for DRM session manager events. * @param initialDrmRequestRetryCount The number of times to retry for initial provisioning and * key request before reporting error. */ - public DefaultDrmSession(UUID uuid, ExoMediaDrm mediaDrm, - ProvisioningManager provisioningManager, byte[] initData, String mimeType, - @DefaultDrmSessionManager.Mode int mode, byte[] offlineLicenseKeySetId, - HashMap optionalKeyRequestParameters, MediaDrmCallback callback, - Looper playbackLooper, Handler eventHandler, - DefaultDrmSessionManager.EventListener eventListener, + public DefaultDrmSession( + UUID uuid, + ExoMediaDrm mediaDrm, + ProvisioningManager provisioningManager, + byte[] initData, + String mimeType, + @DefaultDrmSessionManager.Mode int mode, + byte[] offlineLicenseKeySetId, + HashMap optionalKeyRequestParameters, + MediaDrmCallback callback, + Looper playbackLooper, + EventDispatcher eventDispatcher, int initialDrmRequestRetryCount) { this.uuid = uuid; this.provisioningManager = provisioningManager; @@ -129,9 +134,7 @@ import java.util.UUID; this.optionalKeyRequestParameters = optionalKeyRequestParameters; this.callback = callback; this.initialDrmRequestRetryCount = initialDrmRequestRetryCount; - - this.eventHandler = eventHandler; - this.eventListener = eventListener; + this.eventDispatcher = eventDispatcher; state = STATE_OPENING; postResponseHandler = new PostResponseHandler(playbackLooper); @@ -306,14 +309,7 @@ import java.util.UUID; onError(new KeysExpiredException()); } else { state = STATE_OPENED_WITH_KEYS; - if (eventHandler != null && eventListener != null) { - eventHandler.post(new Runnable() { - @Override - public void run() { - eventListener.onDrmKeysRestored(); - } - }); - } + eventDispatcher.drmKeysRestored(); } } break; @@ -391,14 +387,7 @@ import java.util.UUID; } if (mode == DefaultDrmSessionManager.MODE_RELEASE) { mediaDrm.provideKeyResponse(offlineLicenseKeySetId, responseData); - if (eventHandler != null && eventListener != null) { - eventHandler.post(new Runnable() { - @Override - public void run() { - eventListener.onDrmKeysRemoved(); - } - }); - } + eventDispatcher.drmKeysRemoved(); } else { byte[] keySetId = mediaDrm.provideKeyResponse(sessionId, responseData); if ((mode == DefaultDrmSessionManager.MODE_DOWNLOAD @@ -407,14 +396,7 @@ import java.util.UUID; offlineLicenseKeySetId = keySetId; } state = STATE_OPENED_WITH_KEYS; - if (eventHandler != null && eventListener != null) { - eventHandler.post(new Runnable() { - @Override - public void run() { - eventListener.onDrmKeysLoaded(); - } - }); - } + eventDispatcher.drmKeysLoaded(); } } catch (Exception e) { onKeysError(e); @@ -438,14 +420,7 @@ import java.util.UUID; private void onError(final Exception e) { lastException = new DrmSessionException(e); - if (eventHandler != null && eventListener != null) { - eventHandler.post(new Runnable() { - @Override - public void run() { - eventListener.onDrmSessionManagerError(e); - } - }); - } + eventDispatcher.drmSessionManagerError(e); if (state != STATE_OPENED_WITH_KEYS) { state = STATE_ERROR; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionEventListener.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionEventListener.java new file mode 100644 index 0000000000..7cdee7c537 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionEventListener.java @@ -0,0 +1,141 @@ +/* + * 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.drm; + +import android.os.Handler; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.util.Assertions; +import java.util.concurrent.CopyOnWriteArrayList; + +/** Listener of {@link DefaultDrmSessionManager} events. */ +public interface DefaultDrmSessionEventListener { + + /** Called each time keys are loaded. */ + void onDrmKeysLoaded(); + + /** + * Called when a drm error occurs. + * + *

    This method being called does not indicate that playback has failed, or that it will fail. + * The player may be able to recover from the error and continue. Hence applications should + * not implement this method to display a user visible error or initiate an application + * level retry ({@link Player.EventListener#onPlayerError} is the appropriate place to implement + * such behavior). This method is called to provide the application with an opportunity to log the + * error if it wishes to do so. + * + * @param error The corresponding exception. + */ + void onDrmSessionManagerError(Exception error); + + /** Called each time offline keys are restored. */ + void onDrmKeysRestored(); + + /** Called each time offline keys are removed. */ + void onDrmKeysRemoved(); + + /** Dispatches drm events to all registered listeners. */ + final class EventDispatcher { + + private final CopyOnWriteArrayList listeners; + + /** Creates event dispatcher. */ + public EventDispatcher() { + listeners = new CopyOnWriteArrayList<>(); + } + + /** Adds listener to event dispatcher. */ + public void addListener(Handler handler, DefaultDrmSessionEventListener eventListener) { + Assertions.checkArgument(handler != null && eventListener != null); + listeners.add(new HandlerAndListener(handler, eventListener)); + } + + /** Removes listener from event dispatcher. */ + public void removeListener(DefaultDrmSessionEventListener eventListener) { + for (HandlerAndListener handlerAndListener : listeners) { + if (handlerAndListener.listener == eventListener) { + listeners.remove(handlerAndListener); + } + } + } + + /** Dispatches {@link DefaultDrmSessionEventListener#onDrmKeysLoaded()}. */ + public void drmKeysLoaded() { + for (HandlerAndListener handlerAndListener : listeners) { + final DefaultDrmSessionEventListener listener = handlerAndListener.listener; + handlerAndListener.handler.post( + new Runnable() { + @Override + public void run() { + listener.onDrmKeysLoaded(); + } + }); + } + } + + /** Dispatches {@link DefaultDrmSessionEventListener#onDrmSessionManagerError(Exception)}. */ + public void drmSessionManagerError(final Exception e) { + for (HandlerAndListener handlerAndListener : listeners) { + final DefaultDrmSessionEventListener listener = handlerAndListener.listener; + handlerAndListener.handler.post( + new Runnable() { + @Override + public void run() { + listener.onDrmSessionManagerError(e); + } + }); + } + } + + /** Dispatches {@link DefaultDrmSessionEventListener#onDrmKeysRestored()}. */ + public void drmKeysRestored() { + for (HandlerAndListener handlerAndListener : listeners) { + final DefaultDrmSessionEventListener listener = handlerAndListener.listener; + handlerAndListener.handler.post( + new Runnable() { + @Override + public void run() { + listener.onDrmKeysRestored(); + } + }); + } + } + + /** Dispatches {@link DefaultDrmSessionEventListener#onDrmKeysRemoved()}. */ + public void drmKeysRemoved() { + for (HandlerAndListener handlerAndListener : listeners) { + final DefaultDrmSessionEventListener listener = handlerAndListener.listener; + handlerAndListener.handler.post( + new Runnable() { + @Override + public void run() { + listener.onDrmKeysRemoved(); + } + }); + } + } + + private static final class HandlerAndListener { + + public final Handler handler; + public final DefaultDrmSessionEventListener listener; + + public HandlerAndListener(Handler handler, DefaultDrmSessionEventListener eventListener) { + this.handler = handler; + this.listener = eventListener; + } + } + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java index ca0302cdca..66c9e5cde7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java @@ -25,8 +25,8 @@ import android.support.annotation.NonNull; import android.text.TextUtils; import android.util.Log; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.drm.DefaultDrmSession.ProvisioningManager; +import com.google.android.exoplayer2.drm.DefaultDrmSessionEventListener.EventDispatcher; import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; import com.google.android.exoplayer2.drm.DrmSession.DrmSessionException; import com.google.android.exoplayer2.drm.ExoMediaDrm.OnEventListener; @@ -48,41 +48,9 @@ import java.util.UUID; public class DefaultDrmSessionManager implements DrmSessionManager, ProvisioningManager { - /** - * Listener of {@link DefaultDrmSessionManager} events. - */ - public interface EventListener { - - /** - * Called each time keys are loaded. - */ - void onDrmKeysLoaded(); - - /** - * Called when a drm error occurs. - *

    - * This method being called does not indicate that playback has failed, or that it will fail. - * The player may be able to recover from the error and continue. Hence applications should - * not implement this method to display a user visible error or initiate an application - * level retry ({@link Player.EventListener#onPlayerError} is the appropriate place to implement - * such behavior). This method is called to provide the application with an opportunity to log - * the error if it wishes to do so. - * - * @param e The corresponding exception. - */ - void onDrmSessionManagerError(Exception e); - - /** - * Called each time offline keys are restored. - */ - void onDrmKeysRestored(); - - /** - * Called each time offline keys are removed. - */ - void onDrmKeysRemoved(); - - } + /** @deprecated Use {@link DefaultDrmSessionEventListener}. */ + @Deprecated + public interface EventListener extends DefaultDrmSessionEventListener {} /** * Signals that the {@link DrmInitData} passed to {@link #acquireSession} does not contain does @@ -127,8 +95,7 @@ public class DefaultDrmSessionManager implements DrmSe private final ExoMediaDrm mediaDrm; private final MediaDrmCallback callback; private final HashMap optionalKeyRequestParameters; - private final Handler eventHandler; - private final EventListener eventListener; + private final EventDispatcher eventDispatcher; private final boolean multiSession; private final int initialDrmRequestRetryCount; @@ -141,40 +108,70 @@ public class DefaultDrmSessionManager implements DrmSe /* package */ volatile MediaDrmHandler mediaDrmHandler; + /** + * @deprecated Use {@link #newWidevineInstance(MediaDrmCallback, HashMap)} and {@link + * #addListener(Handler, DefaultDrmSessionEventListener)}. + */ + @Deprecated + public static DefaultDrmSessionManager newWidevineInstance( + MediaDrmCallback callback, + HashMap optionalKeyRequestParameters, + Handler eventHandler, + DefaultDrmSessionEventListener eventListener) + throws UnsupportedDrmException { + DefaultDrmSessionManager drmSessionManager = + newWidevineInstance(callback, optionalKeyRequestParameters); + if (eventHandler != null && eventListener != null) { + drmSessionManager.addListener(eventHandler, eventListener); + } + return drmSessionManager; + } + /** * Instantiates a new instance using the Widevine scheme. * * @param callback Performs key and provisioning requests. * @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument * to {@link ExoMediaDrm#getKeyRequest(byte[], byte[], String, int, HashMap)}. May be null. - * @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. * @throws UnsupportedDrmException If the specified DRM scheme is not supported. */ public static DefaultDrmSessionManager newWidevineInstance( - MediaDrmCallback callback, HashMap optionalKeyRequestParameters, - Handler eventHandler, EventListener eventListener) throws UnsupportedDrmException { - return newFrameworkInstance(C.WIDEVINE_UUID, callback, optionalKeyRequestParameters, - eventHandler, eventListener); + MediaDrmCallback callback, HashMap optionalKeyRequestParameters) + throws UnsupportedDrmException { + return newFrameworkInstance(C.WIDEVINE_UUID, callback, optionalKeyRequestParameters); + } + + /** + * @deprecated Use {@link #newPlayReadyInstance(MediaDrmCallback, String)} and {@link + * #addListener(Handler, DefaultDrmSessionEventListener)}. + */ + @Deprecated + public static DefaultDrmSessionManager newPlayReadyInstance( + MediaDrmCallback callback, + String customData, + Handler eventHandler, + DefaultDrmSessionEventListener eventListener) + throws UnsupportedDrmException { + DefaultDrmSessionManager drmSessionManager = + newPlayReadyInstance(callback, customData); + if (eventHandler != null && eventListener != null) { + drmSessionManager.addListener(eventHandler, eventListener); + } + return drmSessionManager; } /** * Instantiates a new instance using the PlayReady scheme. - *

    - * Note that PlayReady is unsupported by most Android devices, with the exception of Android TV + * + *

    Note that PlayReady is unsupported by most Android devices, with the exception of Android TV * devices, which do provide support. * * @param callback Performs key and provisioning requests. * @param customData Optional custom data to include in requests generated by the instance. - * @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. * @throws UnsupportedDrmException If the specified DRM scheme is not supported. */ public static DefaultDrmSessionManager newPlayReadyInstance( - MediaDrmCallback callback, String customData, Handler eventHandler, - EventListener eventListener) throws UnsupportedDrmException { + MediaDrmCallback callback, String customData) throws UnsupportedDrmException { HashMap optionalKeyRequestParameters; if (!TextUtils.isEmpty(customData)) { optionalKeyRequestParameters = new HashMap<>(); @@ -182,8 +179,27 @@ public class DefaultDrmSessionManager implements DrmSe } else { optionalKeyRequestParameters = null; } - return newFrameworkInstance(C.PLAYREADY_UUID, callback, optionalKeyRequestParameters, - eventHandler, eventListener); + return newFrameworkInstance(C.PLAYREADY_UUID, callback, optionalKeyRequestParameters); + } + + /** + * @deprecated Use {@link #newFrameworkInstance(UUID, MediaDrmCallback, HashMap)} and {@link + * #addListener(Handler, DefaultDrmSessionEventListener)}. + */ + @Deprecated + public static DefaultDrmSessionManager newFrameworkInstance( + UUID uuid, + MediaDrmCallback callback, + HashMap optionalKeyRequestParameters, + Handler eventHandler, + DefaultDrmSessionEventListener eventListener) + throws UnsupportedDrmException { + DefaultDrmSessionManager drmSessionManager = + newFrameworkInstance(uuid, callback, optionalKeyRequestParameters); + if (eventHandler != null && eventListener != null) { + drmSessionManager.addListener(eventHandler, eventListener); + } + return drmSessionManager; } /** @@ -193,34 +209,76 @@ public class DefaultDrmSessionManager implements DrmSe * @param callback Performs key and provisioning requests. * @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument * to {@link ExoMediaDrm#getKeyRequest(byte[], byte[], String, int, HashMap)}. May be null. - * @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. * @throws UnsupportedDrmException If the specified DRM scheme is not supported. */ public static DefaultDrmSessionManager newFrameworkInstance( - UUID uuid, MediaDrmCallback callback, HashMap optionalKeyRequestParameters, - Handler eventHandler, EventListener eventListener) throws UnsupportedDrmException { - return new DefaultDrmSessionManager<>(uuid, FrameworkMediaDrm.newInstance(uuid), callback, - optionalKeyRequestParameters, eventHandler, eventListener, false, + UUID uuid, MediaDrmCallback callback, HashMap optionalKeyRequestParameters) + throws UnsupportedDrmException { + return new DefaultDrmSessionManager<>( + uuid, + FrameworkMediaDrm.newInstance(uuid), + callback, + optionalKeyRequestParameters, + /* multiSession= */ false, INITIAL_DRM_REQUEST_RETRY_COUNT); } /** - * @param uuid The UUID of the drm scheme. - * @param mediaDrm An underlying {@link ExoMediaDrm} for use by the manager. - * @param callback Performs key and provisioning requests. - * @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument - * to {@link ExoMediaDrm#getKeyRequest(byte[], byte[], String, int, HashMap)}. May be null. - * @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. + * @deprecated Use {@link #DefaultDrmSessionManager(UUID, ExoMediaDrm, MediaDrmCallback, HashMap)} + * and {@link #addListener(Handler, DefaultDrmSessionEventListener)}. */ - public DefaultDrmSessionManager(UUID uuid, ExoMediaDrm mediaDrm, MediaDrmCallback callback, - HashMap optionalKeyRequestParameters, Handler eventHandler, - EventListener eventListener) { - this(uuid, mediaDrm, callback, optionalKeyRequestParameters, eventHandler, eventListener, - false, INITIAL_DRM_REQUEST_RETRY_COUNT); + @Deprecated + public DefaultDrmSessionManager( + UUID uuid, + ExoMediaDrm mediaDrm, + MediaDrmCallback callback, + HashMap optionalKeyRequestParameters, + Handler eventHandler, + DefaultDrmSessionEventListener eventListener) { + this(uuid, mediaDrm, callback, optionalKeyRequestParameters); + if (eventHandler != null && eventListener != null) { + addListener(eventHandler, eventListener); + } + } + + /** + * @param uuid The UUID of the drm scheme. + * @param mediaDrm An underlying {@link ExoMediaDrm} for use by the manager. + * @param callback Performs key and provisioning requests. + * @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument + * to {@link ExoMediaDrm#getKeyRequest(byte[], byte[], String, int, HashMap)}. May be null. + */ + public DefaultDrmSessionManager( + UUID uuid, + ExoMediaDrm mediaDrm, + MediaDrmCallback callback, + HashMap optionalKeyRequestParameters) { + this( + uuid, + mediaDrm, + callback, + optionalKeyRequestParameters, + /* multiSession= */ false, + INITIAL_DRM_REQUEST_RETRY_COUNT); + } + + /** + * @deprecated Use {@link #DefaultDrmSessionManager(UUID, ExoMediaDrm, MediaDrmCallback, HashMap, + * boolean)} and {@link #addListener(Handler, DefaultDrmSessionEventListener)}. + */ + @Deprecated + public DefaultDrmSessionManager( + UUID uuid, + ExoMediaDrm mediaDrm, + MediaDrmCallback callback, + HashMap optionalKeyRequestParameters, + Handler eventHandler, + DefaultDrmSessionEventListener eventListener, + boolean multiSession) { + this(uuid, mediaDrm, callback, optionalKeyRequestParameters, multiSession); + if (eventHandler != null && eventListener != null) { + addListener(eventHandler, eventListener); + } } /** @@ -229,17 +287,48 @@ public class DefaultDrmSessionManager implements DrmSe * @param callback Performs key and provisioning requests. * @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument * to {@link ExoMediaDrm#getKeyRequest(byte[], byte[], String, int, HashMap)}. May be null. - * @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. * @param multiSession A boolean that specify whether multiple key session support is enabled. * Default is false. */ - public DefaultDrmSessionManager(UUID uuid, ExoMediaDrm mediaDrm, MediaDrmCallback callback, - HashMap optionalKeyRequestParameters, Handler eventHandler, - EventListener eventListener, boolean multiSession) { - this(uuid, mediaDrm, callback, optionalKeyRequestParameters, eventHandler, eventListener, - multiSession, INITIAL_DRM_REQUEST_RETRY_COUNT); + public DefaultDrmSessionManager( + UUID uuid, + ExoMediaDrm mediaDrm, + MediaDrmCallback callback, + HashMap optionalKeyRequestParameters, + boolean multiSession) { + this( + uuid, + mediaDrm, + callback, + optionalKeyRequestParameters, + multiSession, + INITIAL_DRM_REQUEST_RETRY_COUNT); + } + + /** + * @deprecated Use {@link #DefaultDrmSessionManager(UUID, ExoMediaDrm, MediaDrmCallback, HashMap, + * boolean, int)} and {@link #addListener(Handler, DefaultDrmSessionEventListener)}. + */ + @Deprecated + public DefaultDrmSessionManager( + UUID uuid, + ExoMediaDrm mediaDrm, + MediaDrmCallback callback, + HashMap optionalKeyRequestParameters, + Handler eventHandler, + DefaultDrmSessionEventListener eventListener, + boolean multiSession, + int initialDrmRequestRetryCount) { + this( + uuid, + mediaDrm, + callback, + optionalKeyRequestParameters, + multiSession, + initialDrmRequestRetryCount); + if (eventHandler != null && eventListener != null) { + addListener(eventHandler, eventListener); + } } /** @@ -248,17 +337,18 @@ public class DefaultDrmSessionManager implements DrmSe * @param callback Performs key and provisioning requests. * @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument * to {@link ExoMediaDrm#getKeyRequest(byte[], byte[], String, int, HashMap)}. May be null. - * @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. * @param multiSession A boolean that specify whether multiple key session support is enabled. * Default is false. * @param initialDrmRequestRetryCount The number of times to retry for initial provisioning and * key request before reporting error. */ - public DefaultDrmSessionManager(UUID uuid, ExoMediaDrm mediaDrm, MediaDrmCallback callback, - HashMap optionalKeyRequestParameters, Handler eventHandler, - EventListener eventListener, boolean multiSession, int initialDrmRequestRetryCount) { + public DefaultDrmSessionManager( + UUID uuid, + ExoMediaDrm mediaDrm, + MediaDrmCallback callback, + HashMap optionalKeyRequestParameters, + boolean multiSession, + int initialDrmRequestRetryCount) { Assertions.checkNotNull(uuid); Assertions.checkNotNull(mediaDrm); Assertions.checkArgument(!C.COMMON_PSSH_UUID.equals(uuid), "Use C.CLEARKEY_UUID instead"); @@ -266,8 +356,7 @@ public class DefaultDrmSessionManager implements DrmSe this.mediaDrm = mediaDrm; this.callback = callback; this.optionalKeyRequestParameters = optionalKeyRequestParameters; - this.eventHandler = eventHandler; - this.eventListener = eventListener; + this.eventDispatcher = new EventDispatcher(); this.multiSession = multiSession; this.initialDrmRequestRetryCount = initialDrmRequestRetryCount; mode = MODE_PLAYBACK; @@ -279,6 +368,25 @@ public class DefaultDrmSessionManager implements DrmSe mediaDrm.setOnEventListener(new MediaDrmEventListener()); } + /** + * Adds a {@link DefaultDrmSessionEventListener} to listen to drm session events. + * + * @param handler A handler to use when delivering events to {@code eventListener}. + * @param eventListener A listener of events. + */ + public final void addListener(Handler handler, DefaultDrmSessionEventListener eventListener) { + eventDispatcher.addListener(handler, eventListener); + } + + /** + * Removes a {@link DefaultDrmSessionEventListener} from the list of drm session event listeners. + * + * @param eventListener The listener to remove. + */ + public final void removeListener(DefaultDrmSessionEventListener eventListener) { + eventDispatcher.removeListener(eventListener); + } + /** * Provides access to {@link ExoMediaDrm#getPropertyString(String)}. *

    @@ -383,8 +491,9 @@ public class DefaultDrmSessionManager implements DrmSe return true; } else if (C.CENC_TYPE_cbc1.equals(schemeType) || C.CENC_TYPE_cbcs.equals(schemeType) || C.CENC_TYPE_cens.equals(schemeType)) { - // AES-CBC and pattern encryption are supported on API 24 onwards. - return Util.SDK_INT >= 24; + // API support for AES-CBC and pattern encryption was added in API 24. However, the + // implementation was not stable until API 25. + return Util.SDK_INT >= 25; } // Unknown schemes, assume one of them is supported. return true; @@ -406,15 +515,7 @@ public class DefaultDrmSessionManager implements DrmSe SchemeData data = getSchemeData(drmInitData, uuid, false); if (data == null) { final MissingSchemeDataException error = new MissingSchemeDataException(uuid); - if (eventHandler != null && eventListener != null) { - eventHandler.post( - new Runnable() { - @Override - public void run() { - eventListener.onDrmSessionManagerError(error); - } - }); - } + eventDispatcher.drmSessionManagerError(error); return new ErrorStateDrmSession<>(new DrmSessionException(error)); } initData = getSchemeInitData(data, uuid); @@ -437,9 +538,20 @@ public class DefaultDrmSessionManager implements DrmSe if (session == null) { // Create a new session. - session = new DefaultDrmSession<>(uuid, mediaDrm, this, initData, mimeType, mode, - offlineLicenseKeySetId, optionalKeyRequestParameters, callback, playbackLooper, - eventHandler, eventListener, initialDrmRequestRetryCount); + session = + new DefaultDrmSession<>( + uuid, + mediaDrm, + this, + initData, + mimeType, + mode, + offlineLicenseKeySetId, + optionalKeyRequestParameters, + callback, + playbackLooper, + eventDispatcher, + initialDrmRequestRetryCount); sessions.add(session); } session.acquire(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java index 0c7cb0ef01..4a59667dc8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java @@ -39,18 +39,13 @@ public final class DrmInitData implements Comparator, Parcelable { *

    The result is generated as follows. * *

      - *
    1. - * Include all {@link SchemeData}s from {@code manifestData} where {@link - * SchemeData#hasData()} is true. - *
    2. - *
    3. - * Include all {@link SchemeData}s in {@code mediaData} where {@link SchemeData#hasData()} is - * true and for which we did not include an entry from the manifest targeting the same UUID. - *
    4. - *
    5. - * If available, the scheme type from the manifest is used. If not, the scheme type from the - * media is used. - *
    6. + *
    7. Include all {@link SchemeData}s from {@code manifestData} where {@link + * SchemeData#hasData()} is true. + *
    8. Include all {@link SchemeData}s in {@code mediaData} where {@link SchemeData#hasData()} + * is true and for which we did not include an entry from the manifest targeting the same + * UUID. + *
    9. If available, the scheme type from the manifest is used. If not, the scheme type from the + * media is used. *
    * * @param manifestData DRM session acquisition data obtained from the manifest. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/ErrorStateDrmSession.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/ErrorStateDrmSession.java index 576f0a08a9..d30e670c3f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/ErrorStateDrmSession.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/ErrorStateDrmSession.java @@ -18,10 +18,8 @@ package com.google.android.exoplayer2.drm; import com.google.android.exoplayer2.util.Assertions; import java.util.Map; -/** - * A {@link DrmSession} that's in a terminal error state. - */ -/* package */ final class ErrorStateDrmSession implements DrmSession { +/** A {@link DrmSession} that's in a terminal error state. */ +public final class ErrorStateDrmSession implements DrmSession { private final DrmSessionException error; 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/drm/HttpMediaDrmCallback.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java index dfbf3dee07..4a93ac8333 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java @@ -24,10 +24,12 @@ import com.google.android.exoplayer2.drm.ExoMediaDrm.ProvisionRequest; import com.google.android.exoplayer2.upstream.DataSourceInputStream; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.HttpDataSource; +import com.google.android.exoplayer2.upstream.HttpDataSource.InvalidResponseCodeException; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.UUID; @@ -37,6 +39,8 @@ import java.util.UUID; @TargetApi(18) public final class HttpMediaDrmCallback implements MediaDrmCallback { + private static final int MAX_MANUAL_REDIRECTS = 5; + private final HttpDataSource.Factory dataSourceFactory; private final String defaultLicenseUrl; private final boolean forceDefaultLicenseUrl; @@ -138,14 +142,46 @@ public final class HttpMediaDrmCallback implements MediaDrmCallback { dataSource.setRequestProperty(requestProperty.getKey(), requestProperty.getValue()); } } - DataSpec dataSpec = new DataSpec(Uri.parse(url), data, 0, 0, C.LENGTH_UNSET, null, - DataSpec.FLAG_ALLOW_GZIP); - DataSourceInputStream inputStream = new DataSourceInputStream(dataSource, dataSpec); - try { - return Util.toByteArray(inputStream); - } finally { - Util.closeQuietly(inputStream); + + int manualRedirectCount = 0; + while (true) { + DataSpec dataSpec = + new DataSpec( + Uri.parse(url), + data, + /* absoluteStreamPosition= */ 0, + /* position= */ 0, + /* length= */ C.LENGTH_UNSET, + /* key= */ null, + DataSpec.FLAG_ALLOW_GZIP); + DataSourceInputStream inputStream = new DataSourceInputStream(dataSource, dataSpec); + try { + return Util.toByteArray(inputStream); + } catch (InvalidResponseCodeException e) { + // For POST requests, the underlying network stack will not normally follow 307 or 308 + // redirects automatically. Do so manually here. + boolean manuallyRedirect = + (e.responseCode == 307 || e.responseCode == 308) + && manualRedirectCount++ < MAX_MANUAL_REDIRECTS; + url = manuallyRedirect ? getRedirectUrl(e) : null; + if (url == null) { + throw e; + } + } finally { + Util.closeQuietly(inputStream); + } } } + private static String getRedirectUrl(InvalidResponseCodeException exception) { + Map> headerFields = exception.headerFields; + if (headerFields != null) { + List locationHeaders = headerFields.get("Location"); + if (locationHeaders != null && !locationHeaders.isEmpty()) { + return locationHeaders.get(0); + } + } + return null; + } + } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java index 481bea66c3..9298c16cb0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java @@ -21,7 +21,6 @@ import android.os.Handler; import android.os.HandlerThread; import android.util.Pair; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.drm.DefaultDrmSessionManager.EventListener; import com.google.android.exoplayer2.drm.DefaultDrmSessionManager.Mode; import com.google.android.exoplayer2.drm.DrmSession.DrmSessionException; import com.google.android.exoplayer2.upstream.HttpDataSource; @@ -90,10 +89,12 @@ public final class OfflineLicenseHelper { * @throws UnsupportedDrmException If the Widevine DRM scheme is unsupported or cannot be * instantiated. * @see DefaultDrmSessionManager#DefaultDrmSessionManager(java.util.UUID, ExoMediaDrm, - * MediaDrmCallback, HashMap, Handler, EventListener) + * MediaDrmCallback, HashMap, Handler, DefaultDrmSessionEventListener) */ public static OfflineLicenseHelper newWidevineInstance( - String defaultLicenseUrl, boolean forceDefaultLicenseUrl, Factory httpDataSourceFactory, + String defaultLicenseUrl, + boolean forceDefaultLicenseUrl, + Factory httpDataSourceFactory, HashMap optionalKeyRequestParameters) throws UnsupportedDrmException { return new OfflineLicenseHelper<>(C.WIDEVINE_UUID, @@ -111,36 +112,41 @@ public final class OfflineLicenseHelper { * @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument * to {@link MediaDrm#getKeyRequest(byte[], byte[], String, int, HashMap)}. May be null. * @see DefaultDrmSessionManager#DefaultDrmSessionManager(java.util.UUID, ExoMediaDrm, - * MediaDrmCallback, HashMap, Handler, EventListener) + * MediaDrmCallback, HashMap, Handler, DefaultDrmSessionEventListener) */ - public OfflineLicenseHelper(UUID uuid, ExoMediaDrm mediaDrm, MediaDrmCallback callback, + public OfflineLicenseHelper( + UUID uuid, + ExoMediaDrm mediaDrm, + MediaDrmCallback callback, HashMap optionalKeyRequestParameters) { handlerThread = new HandlerThread("OfflineLicenseHelper"); handlerThread.start(); conditionVariable = new ConditionVariable(); - EventListener eventListener = new EventListener() { - @Override - public void onDrmKeysLoaded() { - conditionVariable.open(); - } + DefaultDrmSessionEventListener eventListener = + new DefaultDrmSessionEventListener() { + @Override + public void onDrmKeysLoaded() { + conditionVariable.open(); + } - @Override - public void onDrmSessionManagerError(Exception e) { - conditionVariable.open(); - } + @Override + public void onDrmSessionManagerError(Exception e) { + conditionVariable.open(); + } - @Override - public void onDrmKeysRestored() { - conditionVariable.open(); - } + @Override + public void onDrmKeysRestored() { + conditionVariable.open(); + } - @Override - public void onDrmKeysRemoved() { - conditionVariable.open(); - } - }; - drmSessionManager = new DefaultDrmSessionManager<>(uuid, mediaDrm, callback, - optionalKeyRequestParameters, new Handler(handlerThread.getLooper()), eventListener); + @Override + public void onDrmKeysRemoved() { + conditionVariable.open(); + } + }; + drmSessionManager = + new DefaultDrmSessionManager<>(uuid, mediaDrm, callback, optionalKeyRequestParameters); + drmSessionManager.addListener(new Handler(handlerThread.getLooper()), eventListener); } /** 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/Id3Peeker.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/Id3Peeker.java new file mode 100644 index 0000000000..8dbcfafaf2 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/Id3Peeker.java @@ -0,0 +1,87 @@ +/* + * 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; + +import android.support.annotation.Nullable; +import com.google.android.exoplayer2.metadata.Metadata; +import com.google.android.exoplayer2.metadata.id3.Id3Decoder; +import com.google.android.exoplayer2.util.ParsableByteArray; +import java.io.EOFException; +import java.io.IOException; + +/** + * Peeks data from the beginning of an {@link ExtractorInput} to determine if there is any ID3 tag. + */ +public final class Id3Peeker { + + private final ParsableByteArray scratch; + + public Id3Peeker() { + scratch = new ParsableByteArray(Id3Decoder.ID3_HEADER_LENGTH); + } + + /** + * Peeks ID3 data from the input and parses the first ID3 tag. + * + * @param input The {@link ExtractorInput} from which data should be peeked. + * @param id3FramePredicate Determines which ID3 frames are decoded. May be null to decode all + * frames. + * @return The first ID3 tag decoded into a {@link Metadata} object. May be null if ID3 tag is not + * present in the input. + * @throws IOException If an error occurred peeking from the input. + * @throws InterruptedException If the thread was interrupted. + */ + @Nullable + public Metadata peekId3Data( + ExtractorInput input, @Nullable Id3Decoder.FramePredicate id3FramePredicate) + throws IOException, InterruptedException { + int peekedId3Bytes = 0; + Metadata metadata = null; + while (true) { + try { + input.peekFully(scratch.data, 0, Id3Decoder.ID3_HEADER_LENGTH); + } catch (EOFException e) { + // If input has less than ID3_HEADER_LENGTH, ignore the rest. + break; + } + scratch.setPosition(0); + if (scratch.readUnsignedInt24() != Id3Decoder.ID3_TAG) { + // Not an ID3 tag. + break; + } + scratch.skipBytes(3); // Skip major version, minor version and flags. + int framesLength = scratch.readSynchSafeInt(); + int tagLength = Id3Decoder.ID3_HEADER_LENGTH + framesLength; + + if (metadata == null) { + byte[] id3Data = new byte[tagLength]; + System.arraycopy(scratch.data, 0, id3Data, 0, Id3Decoder.ID3_HEADER_LENGTH); + input.peekFully(id3Data, Id3Decoder.ID3_HEADER_LENGTH, framesLength); + + metadata = new Id3Decoder(id3FramePredicate).decode(id3Data, tagLength); + } else { + input.advancePeekPosition(framesLength); + } + + peekedId3Bytes += tagLength; + } + + input.resetPeekPosition(); + input.advancePeekPosition(peekedId3Bytes); + return metadata; + } +} 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/mkv/DefaultEbmlReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReader.java index 2c6130677f..21cb3775e5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReader.java @@ -15,12 +15,15 @@ */ package com.google.android.exoplayer2.extractor.mkv; +import android.support.annotation.IntDef; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.util.Assertions; import java.io.EOFException; import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.Stack; /** @@ -28,6 +31,10 @@ import java.util.Stack; */ /* package */ final class DefaultEbmlReader implements EbmlReader { + @Retention(RetentionPolicy.SOURCE) + @IntDef({ELEMENT_STATE_READ_ID, ELEMENT_STATE_READ_CONTENT_SIZE, ELEMENT_STATE_READ_CONTENT}) + private @interface ElementState {} + private static final int ELEMENT_STATE_READ_ID = 0; private static final int ELEMENT_STATE_READ_CONTENT_SIZE = 1; private static final int ELEMENT_STATE_READ_CONTENT = 2; @@ -44,7 +51,7 @@ import java.util.Stack; private final VarintReader varintReader = new VarintReader(); private EbmlReaderOutput output; - private int elementState; + private @ElementState int elementState; private int elementId; private long elementContentSize; @@ -88,23 +95,23 @@ import java.util.Stack; elementState = ELEMENT_STATE_READ_CONTENT; } - int type = output.getElementType(elementId); + @EbmlReaderOutput.ElementType int type = output.getElementType(elementId); switch (type) { - case TYPE_MASTER: + case EbmlReaderOutput.TYPE_MASTER: long elementContentPosition = input.getPosition(); long elementEndPosition = elementContentPosition + elementContentSize; masterElementsStack.add(new MasterElement(elementId, elementEndPosition)); output.startMasterElement(elementId, elementContentPosition, elementContentSize); elementState = ELEMENT_STATE_READ_ID; return true; - case TYPE_UNSIGNED_INT: + case EbmlReaderOutput.TYPE_UNSIGNED_INT: if (elementContentSize > MAX_INTEGER_ELEMENT_SIZE_BYTES) { throw new ParserException("Invalid integer size: " + elementContentSize); } output.integerElement(elementId, readInteger(input, (int) elementContentSize)); elementState = ELEMENT_STATE_READ_ID; return true; - case TYPE_FLOAT: + case EbmlReaderOutput.TYPE_FLOAT: if (elementContentSize != VALID_FLOAT32_ELEMENT_SIZE_BYTES && elementContentSize != VALID_FLOAT64_ELEMENT_SIZE_BYTES) { throw new ParserException("Invalid float size: " + elementContentSize); @@ -112,18 +119,18 @@ import java.util.Stack; output.floatElement(elementId, readFloat(input, (int) elementContentSize)); elementState = ELEMENT_STATE_READ_ID; return true; - case TYPE_STRING: + case EbmlReaderOutput.TYPE_STRING: if (elementContentSize > Integer.MAX_VALUE) { throw new ParserException("String element size: " + elementContentSize); } output.stringElement(elementId, readString(input, (int) elementContentSize)); elementState = ELEMENT_STATE_READ_ID; return true; - case TYPE_BINARY: + case EbmlReaderOutput.TYPE_BINARY: output.binaryElement(elementId, (int) elementContentSize, input); elementState = ELEMENT_STATE_READ_ID; return true; - case TYPE_UNKNOWN: + case EbmlReaderOutput.TYPE_UNKNOWN: input.skipFully((int) elementContentSize); elementState = ELEMENT_STATE_READ_ID; break; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/EbmlReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/EbmlReader.java index dc059d2cc8..9987b3c8e6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/EbmlReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/EbmlReader.java @@ -28,31 +28,6 @@ import java.io.IOException; */ /* package */ interface EbmlReader { - /** - * Type for unknown elements. - */ - int TYPE_UNKNOWN = 0; - /** - * Type for elements that contain child elements. - */ - int TYPE_MASTER = 1; - /** - * Type for integer value elements of up to 8 bytes. - */ - int TYPE_UNSIGNED_INT = 2; - /** - * Type for string elements. - */ - int TYPE_STRING = 3; - /** - * Type for binary elements. - */ - int TYPE_BINARY = 4; - /** - * Type for IEEE floating point value elements of either 4 or 8 bytes. - */ - int TYPE_FLOAT = 5; - /** * Initializes the extractor with an {@link EbmlReaderOutput}. * diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/EbmlReaderOutput.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/EbmlReaderOutput.java index 6c97e802b9..b1cd508c8e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/EbmlReaderOutput.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/EbmlReaderOutput.java @@ -15,24 +15,46 @@ */ package com.google.android.exoplayer2.extractor.mkv; +import android.support.annotation.IntDef; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.extractor.ExtractorInput; import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; /** * Defines EBML element IDs/types and reacts to events. */ /* package */ interface EbmlReaderOutput { + /** EBML element types. */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({TYPE_UNKNOWN, TYPE_MASTER, TYPE_UNSIGNED_INT, TYPE_STRING, TYPE_BINARY, TYPE_FLOAT}) + @interface ElementType {} + /** Type for unknown elements. */ + int TYPE_UNKNOWN = 0; + /** Type for elements that contain child elements. */ + int TYPE_MASTER = 1; + /** Type for integer value elements of up to 8 bytes. */ + int TYPE_UNSIGNED_INT = 2; + /** Type for string elements. */ + int TYPE_STRING = 3; + /** Type for binary elements. */ + int TYPE_BINARY = 4; + /** Type for IEEE floating point value elements of either 4 or 8 bytes. */ + int TYPE_FLOAT = 5; + /** * Maps an element ID to a corresponding type. - *

    - * If {@link EbmlReader#TYPE_UNKNOWN} is returned then the element is skipped. Note that all - * children of a skipped element are also skipped. + * + *

    If {@link #TYPE_UNKNOWN} is returned then the element is skipped. Note that all children of + * a skipped element are also skipped. * * @param id The element ID to map. - * @return One of the {@code TYPE_} constants defined in {@link EbmlReader}. + * @return One of {@link #TYPE_UNKNOWN}, {@link #TYPE_MASTER}, {@link #TYPE_UNSIGNED_INT}, {@link + * #TYPE_STRING}, {@link #TYPE_BINARY} and {@link #TYPE_FLOAT}. */ + @ElementType int getElementType(int id); /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java index 57128f45f0..1049554f7a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.extractor.mkv; import android.support.annotation.IntDef; import android.support.annotation.Nullable; import android.util.Log; +import android.util.Pair; import android.util.SparseArray; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; @@ -219,6 +220,7 @@ public final class MatroskaExtractor implements Extractor { private static final int LACING_EBML = 3; private static final int FOURCC_COMPRESSION_VC1 = 0x31435657; + private static final int FOURCC_COMPRESSION_DIVX = 0x58564944; /** * A template for the prefix that must be added to each subrip sample. The 12 byte end timecode @@ -446,100 +448,6 @@ public final class MatroskaExtractor implements Extractor { return Extractor.RESULT_CONTINUE; } - /* package */ int getElementType(int id) { - switch (id) { - case ID_EBML: - case ID_SEGMENT: - case ID_SEEK_HEAD: - case ID_SEEK: - case ID_INFO: - case ID_CLUSTER: - case ID_TRACKS: - case ID_TRACK_ENTRY: - case ID_AUDIO: - case ID_VIDEO: - case ID_CONTENT_ENCODINGS: - case ID_CONTENT_ENCODING: - case ID_CONTENT_COMPRESSION: - case ID_CONTENT_ENCRYPTION: - case ID_CONTENT_ENCRYPTION_AES_SETTINGS: - case ID_CUES: - case ID_CUE_POINT: - case ID_CUE_TRACK_POSITIONS: - case ID_BLOCK_GROUP: - case ID_PROJECTION: - case ID_COLOUR: - case ID_MASTERING_METADATA: - return EbmlReader.TYPE_MASTER; - case ID_EBML_READ_VERSION: - case ID_DOC_TYPE_READ_VERSION: - case ID_SEEK_POSITION: - case ID_TIMECODE_SCALE: - case ID_TIME_CODE: - case ID_BLOCK_DURATION: - case ID_PIXEL_WIDTH: - case ID_PIXEL_HEIGHT: - case ID_DISPLAY_WIDTH: - case ID_DISPLAY_HEIGHT: - case ID_DISPLAY_UNIT: - case ID_TRACK_NUMBER: - case ID_TRACK_TYPE: - case ID_FLAG_DEFAULT: - case ID_FLAG_FORCED: - case ID_DEFAULT_DURATION: - case ID_CODEC_DELAY: - case ID_SEEK_PRE_ROLL: - case ID_CHANNELS: - case ID_AUDIO_BIT_DEPTH: - case ID_CONTENT_ENCODING_ORDER: - case ID_CONTENT_ENCODING_SCOPE: - case ID_CONTENT_COMPRESSION_ALGORITHM: - case ID_CONTENT_ENCRYPTION_ALGORITHM: - case ID_CONTENT_ENCRYPTION_AES_SETTINGS_CIPHER_MODE: - case ID_CUE_TIME: - case ID_CUE_CLUSTER_POSITION: - case ID_REFERENCE_BLOCK: - case ID_STEREO_MODE: - case ID_COLOUR_RANGE: - case ID_COLOUR_TRANSFER: - case ID_COLOUR_PRIMARIES: - case ID_MAX_CLL: - case ID_MAX_FALL: - return EbmlReader.TYPE_UNSIGNED_INT; - case ID_DOC_TYPE: - case ID_CODEC_ID: - case ID_LANGUAGE: - return EbmlReader.TYPE_STRING; - case ID_SEEK_ID: - case ID_CONTENT_COMPRESSION_SETTINGS: - case ID_CONTENT_ENCRYPTION_KEY_ID: - case ID_SIMPLE_BLOCK: - case ID_BLOCK: - case ID_CODEC_PRIVATE: - case ID_PROJECTION_PRIVATE: - return EbmlReader.TYPE_BINARY; - case ID_DURATION: - case ID_SAMPLING_FREQUENCY: - case ID_PRIMARY_R_CHROMATICITY_X: - case ID_PRIMARY_R_CHROMATICITY_Y: - case ID_PRIMARY_G_CHROMATICITY_X: - case ID_PRIMARY_G_CHROMATICITY_Y: - case ID_PRIMARY_B_CHROMATICITY_X: - case ID_PRIMARY_B_CHROMATICITY_Y: - case ID_WHITE_POINT_CHROMATICITY_X: - case ID_WHITE_POINT_CHROMATICITY_Y: - case ID_LUMNINANCE_MAX: - case ID_LUMNINANCE_MIN: - return EbmlReader.TYPE_FLOAT; - default: - return EbmlReader.TYPE_UNKNOWN; - } - } - - /* package */ boolean isLevel1Element(int id) { - return id == ID_SEGMENT_INFO || id == ID_CLUSTER || id == ID_CUES || id == ID_TRACKS; - } - /* package */ void startMasterElement(int id, long contentPosition, long contentSize) throws ParserException { switch (id) { @@ -1499,12 +1407,98 @@ public final class MatroskaExtractor implements Extractor { @Override public int getElementType(int id) { - return MatroskaExtractor.this.getElementType(id); + switch (id) { + case ID_EBML: + case ID_SEGMENT: + case ID_SEEK_HEAD: + case ID_SEEK: + case ID_INFO: + case ID_CLUSTER: + case ID_TRACKS: + case ID_TRACK_ENTRY: + case ID_AUDIO: + case ID_VIDEO: + case ID_CONTENT_ENCODINGS: + case ID_CONTENT_ENCODING: + case ID_CONTENT_COMPRESSION: + case ID_CONTENT_ENCRYPTION: + case ID_CONTENT_ENCRYPTION_AES_SETTINGS: + case ID_CUES: + case ID_CUE_POINT: + case ID_CUE_TRACK_POSITIONS: + case ID_BLOCK_GROUP: + case ID_PROJECTION: + case ID_COLOUR: + case ID_MASTERING_METADATA: + return TYPE_MASTER; + case ID_EBML_READ_VERSION: + case ID_DOC_TYPE_READ_VERSION: + case ID_SEEK_POSITION: + case ID_TIMECODE_SCALE: + case ID_TIME_CODE: + case ID_BLOCK_DURATION: + case ID_PIXEL_WIDTH: + case ID_PIXEL_HEIGHT: + case ID_DISPLAY_WIDTH: + case ID_DISPLAY_HEIGHT: + case ID_DISPLAY_UNIT: + case ID_TRACK_NUMBER: + case ID_TRACK_TYPE: + case ID_FLAG_DEFAULT: + case ID_FLAG_FORCED: + case ID_DEFAULT_DURATION: + case ID_CODEC_DELAY: + case ID_SEEK_PRE_ROLL: + case ID_CHANNELS: + case ID_AUDIO_BIT_DEPTH: + case ID_CONTENT_ENCODING_ORDER: + case ID_CONTENT_ENCODING_SCOPE: + case ID_CONTENT_COMPRESSION_ALGORITHM: + case ID_CONTENT_ENCRYPTION_ALGORITHM: + case ID_CONTENT_ENCRYPTION_AES_SETTINGS_CIPHER_MODE: + case ID_CUE_TIME: + case ID_CUE_CLUSTER_POSITION: + case ID_REFERENCE_BLOCK: + case ID_STEREO_MODE: + case ID_COLOUR_RANGE: + case ID_COLOUR_TRANSFER: + case ID_COLOUR_PRIMARIES: + case ID_MAX_CLL: + case ID_MAX_FALL: + return TYPE_UNSIGNED_INT; + case ID_DOC_TYPE: + case ID_CODEC_ID: + case ID_LANGUAGE: + return TYPE_STRING; + case ID_SEEK_ID: + case ID_CONTENT_COMPRESSION_SETTINGS: + case ID_CONTENT_ENCRYPTION_KEY_ID: + case ID_SIMPLE_BLOCK: + case ID_BLOCK: + case ID_CODEC_PRIVATE: + case ID_PROJECTION_PRIVATE: + return TYPE_BINARY; + case ID_DURATION: + case ID_SAMPLING_FREQUENCY: + case ID_PRIMARY_R_CHROMATICITY_X: + case ID_PRIMARY_R_CHROMATICITY_Y: + case ID_PRIMARY_G_CHROMATICITY_X: + case ID_PRIMARY_G_CHROMATICITY_Y: + case ID_PRIMARY_B_CHROMATICITY_X: + case ID_PRIMARY_B_CHROMATICITY_Y: + case ID_WHITE_POINT_CHROMATICITY_X: + case ID_WHITE_POINT_CHROMATICITY_Y: + case ID_LUMNINANCE_MAX: + case ID_LUMNINANCE_MIN: + return TYPE_FLOAT; + default: + return TYPE_UNKNOWN; + } } @Override public boolean isLevel1Element(int id) { - return MatroskaExtractor.this.isLevel1Element(id); + return id == ID_SEGMENT_INFO || id == ID_CLUSTER || id == ID_CUES || id == ID_TRACKS; } @Override @@ -1711,13 +1705,9 @@ public final class MatroskaExtractor implements Extractor { nalUnitLengthFieldLength = hevcConfig.nalUnitLengthFieldLength; break; case CODEC_ID_FOURCC: - initializationData = parseFourCcVc1Private(new ParsableByteArray(codecPrivate)); - if (initializationData != null) { - mimeType = MimeTypes.VIDEO_VC1; - } else { - Log.w(TAG, "Unsupported FourCC. Setting mimeType to " + MimeTypes.VIDEO_UNKNOWN); - mimeType = MimeTypes.VIDEO_UNKNOWN; - } + Pair> pair = parseFourCcPrivate(new ParsableByteArray(codecPrivate)); + mimeType = pair.first; + initializationData = pair.second; break; case CODEC_ID_THEORA: // TODO: This can be set to the real mimeType if/when we work out what initializationData @@ -1931,39 +1921,44 @@ public final class MatroskaExtractor implements Extractor { /** * Builds initialization data for a {@link Format} from FourCC codec private data. - *

    - * VC1 is the only supported compression type. * - * @return The initialization data for the {@link Format}, or null if the compression type is - * not VC1. + *

    VC1 and H263 are the only supported compression types. + * + * @return The codec mime type and initialization data. If the compression type is not supported + * then the mime type is set to {@link MimeTypes#VIDEO_UNKNOWN} and the initialization data + * is {@code null}. * @throws ParserException If the initialization data could not be built. */ - private static List parseFourCcVc1Private(ParsableByteArray buffer) + private static Pair> parseFourCcPrivate(ParsableByteArray buffer) throws ParserException { try { buffer.skipBytes(16); // size(4), width(4), height(4), planes(2), bitcount(2). long compression = buffer.readLittleEndianUnsignedInt(); - if (compression != FOURCC_COMPRESSION_VC1) { - return null; - } - - // Search for the initialization data from the end of the BITMAPINFOHEADER. The last 20 - // bytes of which are: sizeImage(4), xPel/m (4), yPel/m (4), clrUsed(4), clrImportant(4). - int startOffset = buffer.getPosition() + 20; - byte[] bufferData = buffer.data; - for (int offset = startOffset; offset < bufferData.length - 4; offset++) { - if (bufferData[offset] == 0x00 && bufferData[offset + 1] == 0x00 - && bufferData[offset + 2] == 0x01 && bufferData[offset + 3] == 0x0F) { - // We've found the initialization data. - byte[] initializationData = Arrays.copyOfRange(bufferData, offset, bufferData.length); - return Collections.singletonList(initializationData); + if (compression == FOURCC_COMPRESSION_DIVX) { + return new Pair<>(MimeTypes.VIDEO_H263, null); + } else if (compression == FOURCC_COMPRESSION_VC1) { + // Search for the initialization data from the end of the BITMAPINFOHEADER. The last 20 + // bytes of which are: sizeImage(4), xPel/m (4), yPel/m (4), clrUsed(4), clrImportant(4). + int startOffset = buffer.getPosition() + 20; + byte[] bufferData = buffer.data; + for (int offset = startOffset; offset < bufferData.length - 4; offset++) { + if (bufferData[offset] == 0x00 + && bufferData[offset + 1] == 0x00 + && bufferData[offset + 2] == 0x01 + && bufferData[offset + 3] == 0x0F) { + // We've found the initialization data. + byte[] initializationData = Arrays.copyOfRange(bufferData, offset, bufferData.length); + return new Pair<>(MimeTypes.VIDEO_VC1, Collections.singletonList(initializationData)); + } } + throw new ParserException("Failed to find FourCC VC1 initialization data"); } - - throw new ParserException("Failed to find FourCC VC1 initialization data"); } catch (ArrayIndexOutOfBoundsException e) { - throw new ParserException("Error parsing FourCC VC1 codec private"); + throw new ParserException("Error parsing FourCC private data"); } + + Log.w(TAG, "Unknown FourCC. Setting mimeType to " + MimeTypes.VIDEO_UNKNOWN); + return new Pair<>(MimeTypes.VIDEO_UNKNOWN, null); } /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java index 5c56dc460a..bd786191a0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java @@ -24,6 +24,7 @@ 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.GaplessInfoHolder; +import com.google.android.exoplayer2.extractor.Id3Peeker; import com.google.android.exoplayer2.extractor.MpegAudioHeader; import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.SeekMap; @@ -99,6 +100,7 @@ public final class Mp3Extractor implements Extractor { private final ParsableByteArray scratch; private final MpegAudioHeader synchronizedHeader; private final GaplessInfoHolder gaplessInfoHolder; + private final Id3Peeker id3Peeker; // Extractor outputs. private ExtractorOutput extractorOutput; @@ -135,6 +137,7 @@ public final class Mp3Extractor implements Extractor { synchronizedHeader = new MpegAudioHeader(); gaplessInfoHolder = new GaplessInfoHolder(); basisTimeUs = C.TIME_UNSET; + id3Peeker = new Id3Peeker(); } // Extractor implementation. @@ -181,11 +184,23 @@ public final class Mp3Extractor implements Extractor { seeker = getConstantBitrateSeeker(input); } extractorOutput.seekMap(seeker); - trackOutput.format(Format.createAudioSampleFormat(null, synchronizedHeader.mimeType, null, - Format.NO_VALUE, MpegAudioHeader.MAX_FRAME_SIZE_BYTES, synchronizedHeader.channels, - synchronizedHeader.sampleRate, Format.NO_VALUE, gaplessInfoHolder.encoderDelay, - gaplessInfoHolder.encoderPadding, null, null, 0, null, - (flags & FLAG_DISABLE_ID3_METADATA) != 0 ? null : metadata)); + trackOutput.format( + Format.createAudioSampleFormat( + /* id= */ null, + synchronizedHeader.mimeType, + /* codecs= */ null, + /* bitrate= */ Format.NO_VALUE, + MpegAudioHeader.MAX_FRAME_SIZE_BYTES, + synchronizedHeader.channels, + synchronizedHeader.sampleRate, + /* pcmEncoding= */ Format.NO_VALUE, + gaplessInfoHolder.encoderDelay, + gaplessInfoHolder.encoderPadding, + /* initializationData= */ null, + /* drmInitData= */ null, + /* selectionFlags= */ 0, + /* language= */ null, + (flags & FLAG_DISABLE_ID3_METADATA) != 0 ? null : metadata)); } return readSample(input); } @@ -242,7 +257,15 @@ public final class Mp3Extractor implements Extractor { int searchLimitBytes = sniffing ? MAX_SNIFF_BYTES : MAX_SYNC_BYTES; input.resetPeekPosition(); if (input.getPosition() == 0) { - peekId3Data(input); + // We need to parse enough ID3 metadata to retrieve any gapless playback information even + // if ID3 metadata parsing is disabled. + boolean onlyDecodeGaplessInfoFrames = (flags & FLAG_DISABLE_ID3_METADATA) != 0; + Id3Decoder.FramePredicate id3FramePredicate = + onlyDecodeGaplessInfoFrames ? GaplessInfoHolder.GAPLESS_INFO_ID3_FRAME_PREDICATE : null; + metadata = id3Peeker.peekId3Data(input, id3FramePredicate); + if (metadata != null) { + gaplessInfoHolder.setFromMetadata(metadata); + } peekedId3Bytes = (int) input.getPeekPosition(); if (!sniffing) { input.skipFully(peekedId3Bytes); @@ -296,49 +319,6 @@ public final class Mp3Extractor implements Extractor { return true; } - /** - * Peeks ID3 data from the input, including gapless playback information. - * - * @param input The {@link ExtractorInput} from which data should be peeked. - * @throws IOException If an error occurred peeking from the input. - * @throws InterruptedException If the thread was interrupted. - */ - private void peekId3Data(ExtractorInput input) throws IOException, InterruptedException { - int peekedId3Bytes = 0; - while (true) { - input.peekFully(scratch.data, 0, Id3Decoder.ID3_HEADER_LENGTH); - scratch.setPosition(0); - if (scratch.readUnsignedInt24() != Id3Decoder.ID3_TAG) { - // Not an ID3 tag. - break; - } - scratch.skipBytes(3); // Skip major version, minor version and flags. - int framesLength = scratch.readSynchSafeInt(); - int tagLength = Id3Decoder.ID3_HEADER_LENGTH + framesLength; - - if (metadata == null) { - byte[] id3Data = new byte[tagLength]; - System.arraycopy(scratch.data, 0, id3Data, 0, Id3Decoder.ID3_HEADER_LENGTH); - input.peekFully(id3Data, Id3Decoder.ID3_HEADER_LENGTH, framesLength); - // We need to parse enough ID3 metadata to retrieve any gapless playback information even - // if ID3 metadata parsing is disabled. - Id3Decoder.FramePredicate id3FramePredicate = (flags & FLAG_DISABLE_ID3_METADATA) != 0 - ? GaplessInfoHolder.GAPLESS_INFO_ID3_FRAME_PREDICATE : null; - metadata = new Id3Decoder(id3FramePredicate).decode(id3Data, tagLength); - if (metadata != null) { - gaplessInfoHolder.setFromMetadata(metadata); - } - } else { - input.advancePeekPosition(framesLength); - } - - peekedId3Bytes += tagLength; - } - - input.resetPeekPosition(); - input.advancePeekPosition(peekedId3Bytes); - } - /** * Consumes the next frame from the {@code input} if it contains VBRI or Xing seeking metadata, * returning a {@link Seeker} if the metadata was present and valid, or {@code null} otherwise. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java index 30358ff7c7..a6e2524f0b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java @@ -53,6 +53,12 @@ import java.util.List; private static final int TYPE_clcp = Util.getIntegerCodeForString("clcp"); private static final int TYPE_meta = Util.getIntegerCodeForString("meta"); + /** + * The threshold number of samples to trim from the start/end of an audio track when applying an + * edit below which gapless info can be used (rather than removing samples from the sample table). + */ + private static final int MAX_GAPLESS_TRIM_SIZE_SAMPLES = 3; + /** * Parses a trak atom (defined in 14496-12). * @@ -311,22 +317,18 @@ import java.util.List; // See the BMFF spec (ISO 14496-12) subsection 8.6.6. Edit lists that require prerolling from a // sync sample after reordering are not supported. Partial audio sample truncation is only - // supported in edit lists with one edit that removes less than one sample from the start/end of - // the track, for gapless audio playback. This implementation handles simple discarding/delaying - // of samples. The extractor may place further restrictions on what edited streams are playable. + // supported in edit lists with one edit that removes less than MAX_GAPLESS_TRIM_SIZE_SAMPLES + // samples from the start/end of the track. This implementation handles simple + // discarding/delaying of samples. The extractor may place further restrictions on what edited + // streams are playable. - if (track.editListDurations.length == 1 && track.type == C.TRACK_TYPE_AUDIO + if (track.editListDurations.length == 1 + && track.type == C.TRACK_TYPE_AUDIO && timestamps.length >= 2) { - // Handle the edit by setting gapless playback metadata, if possible. This implementation - // assumes that only one "roll" sample is needed, which is the case for AAC, so the start/end - // points of the edit must lie within the first/last samples respectively. long editStartTime = track.editListMediaTimes[0]; long editEndTime = editStartTime + Util.scaleLargeTimestamp(track.editListDurations[0], track.timescale, track.movieTimescale); - if (timestamps[0] <= editStartTime - && editStartTime < timestamps[1] - && timestamps[timestamps.length - 1] < editEndTime - && editEndTime <= duration) { + if (canApplyEditWithGaplessInfo(timestamps, duration, editStartTime, editEndTime)) { long paddingTimeUnits = duration - editEndTime; long encoderDelay = Util.scaleLargeTimestamp(editStartTime - timestamps[0], track.format.sampleRate, track.timescale); @@ -1180,6 +1182,19 @@ import java.util.List; return size; } + /** Returns whether it's possible to apply the specified edit using gapless playback info. */ + private static boolean canApplyEditWithGaplessInfo( + long[] timestamps, long duration, long editStartTime, long editEndTime) { + int lastIndex = timestamps.length - 1; + int latestDelayIndex = Util.constrainValue(MAX_GAPLESS_TRIM_SIZE_SAMPLES, 0, lastIndex); + int earliestPaddingIndex = + Util.constrainValue(timestamps.length - MAX_GAPLESS_TRIM_SIZE_SAMPLES, 0, lastIndex); + return timestamps[0] <= editStartTime + && editStartTime < timestamps[latestDelayIndex] + && timestamps[earliestPaddingIndex] < editEndTime + && editEndTime <= duration; + } + private AtomParsers() { // Prevent instantiation. } 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 7e40f6d2ee..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; @@ -134,11 +134,9 @@ public final class FragmentedMp4Extractor implements Extractor { private final ParsableByteArray nalStartCode; private final ParsableByteArray nalPrefix; private final ParsableByteArray nalBuffer; - private final ParsableByteArray encryptionSignalByte; - private final ParsableByteArray defaultInitializationVector; // Adjusts sample timestamps. - private final TimestampAdjuster timestampAdjuster; + private final @Nullable TimestampAdjuster timestampAdjuster; // Parser state. private final ParsableByteArray atomHeader; @@ -154,6 +152,7 @@ public final class FragmentedMp4Extractor implements Extractor { private ParsableByteArray atomData; private long endOfMdatPosition; private int pendingMetadataSampleBytes; + private long pendingSeekTimeUs; private long durationUs; private long segmentIndexEarliestPresentationTimeUs; @@ -186,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()); } @@ -207,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); } @@ -223,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 @@ -233,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; @@ -246,13 +256,12 @@ public final class FragmentedMp4Extractor implements Extractor { nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE); nalPrefix = new ParsableByteArray(5); nalBuffer = new ParsableByteArray(); - encryptionSignalByte = new ParsableByteArray(1); - defaultInitializationVector = new ParsableByteArray(); extendedTypeScratch = new byte[16]; containerAtoms = new Stack<>(); pendingMetadataSampleInfos = new ArrayDeque<>(); trackBundles = new SparseArray<>(); durationUs = C.TIME_UNSET; + pendingSeekTimeUs = C.TIME_UNSET; segmentIndexEarliestPresentationTimeUs = C.TIME_UNSET; enterReadingAtomHeaderState(); } @@ -282,6 +291,7 @@ public final class FragmentedMp4Extractor implements Extractor { } pendingMetadataSampleInfos.clear(); pendingMetadataSampleBytes = 0; + pendingSeekTimeUs = timeUs; containerAtoms.clear(); enterReadingAtomHeaderState(); } @@ -516,6 +526,14 @@ public final class FragmentedMp4Extractor implements Extractor { trackBundles.valueAt(i).updateDrmInitData(drmInitData); } } + // If we have a pending seek, advance tracks to their preceding sync frames. + if (pendingSeekTimeUs != C.TIME_UNSET) { + int trackCount = trackBundles.size(); + for (int i = 0; i < trackCount; i++) { + trackBundles.valueAt(i).seek(pendingSeekTimeUs); + } + pendingSeekTimeUs = C.TIME_UNSET; + } } private void maybeInitExtraTracks() { @@ -1097,16 +1115,18 @@ public final class FragmentedMp4Extractor implements Extractor { } /** - * Attempts to extract the next sample in the current mdat atom. - *

    - * If there are no more samples in the current mdat atom then the parser state is transitioned + * Attempts to read the next sample in the current mdat atom. The read sample may be output or + * skipped. + * + *

    If there are no more samples in the current mdat atom then the parser state is transitioned * to {@link #STATE_READING_ATOM_HEADER} and {@code false} is returned. - *

    - * It is possible for a sample to be extracted in part in the case that an exception is thrown. In - * this case the method can be called again to extract the remainder of the sample. + * + *

    It is possible for a sample to be partially read in the case that an exception is thrown. In + * this case the method can be called again to read the remainder of the sample. * * @param input The {@link ExtractorInput} from which to read data. - * @return Whether a sample was extracted. + * @return Whether a sample was read. The read sample may have been output or skipped. False + * indicates that there are no samples left to read in the current mdat. * @throws IOException If an error occurs reading from the input. * @throws InterruptedException If the thread is interrupted. */ @@ -1138,18 +1158,26 @@ public final class FragmentedMp4Extractor implements Extractor { input.skipFully(bytesToSkip); this.currentTrackBundle = currentTrackBundle; } + sampleSize = currentTrackBundle.fragment .sampleSizeTable[currentTrackBundle.currentSampleIndex]; - if (currentTrackBundle.fragment.definesEncryptionData) { - sampleBytesWritten = appendSampleEncryptionData(currentTrackBundle); - sampleSize += sampleBytesWritten; - } else { - sampleBytesWritten = 0; + + if (currentTrackBundle.currentSampleIndex < currentTrackBundle.firstSampleToOutputIndex) { + input.skipFully(sampleSize); + currentTrackBundle.skipSampleEncryptionData(); + if (!currentTrackBundle.next()) { + currentTrackBundle = null; + } + parserState = STATE_READING_SAMPLE_START; + return true; } + if (currentTrackBundle.track.sampleTransformation == Track.TRANSFORMATION_CEA608_CDAT) { sampleSize -= Atom.HEADER_SIZE; input.skipFully(Atom.HEADER_SIZE); } + sampleBytesWritten = currentTrackBundle.outputSampleEncryptionData(); + sampleSize += sampleBytesWritten; parserState = STATE_READING_SAMPLE_CONTINUE; sampleCurrentNalBytesRemaining = 0; } @@ -1237,13 +1265,7 @@ public final class FragmentedMp4Extractor implements Extractor { // After we have the sampleTimeUs, we can commit all the pending metadata samples outputPendingMetadataSamples(sampleTimeUs); - - currentTrackBundle.currentSampleIndex++; - currentTrackBundle.currentSampleInTrackRun++; - if (currentTrackBundle.currentSampleInTrackRun - == fragment.trunLength[currentTrackBundle.currentTrackRunIndex]) { - currentTrackBundle.currentTrackRunIndex++; - currentTrackBundle.currentSampleInTrackRun = 0; + if (!currentTrackBundle.next()) { currentTrackBundle = null; } parserState = STATE_READING_SAMPLE_START; @@ -1286,57 +1308,6 @@ public final class FragmentedMp4Extractor implements Extractor { return nextTrackBundle; } - /** - * Appends the corresponding encryption data to the {@link TrackOutput} contained in the given - * {@link TrackBundle}. - * - * @param trackBundle The {@link TrackBundle} that contains the {@link Track} for which the - * Sample encryption data must be output. - * @return The number of written bytes. - */ - private int appendSampleEncryptionData(TrackBundle trackBundle) { - TrackFragment trackFragment = trackBundle.fragment; - int sampleDescriptionIndex = trackFragment.header.sampleDescriptionIndex; - TrackEncryptionBox encryptionBox = trackFragment.trackEncryptionBox != null - ? trackFragment.trackEncryptionBox - : trackBundle.track.getSampleDescriptionEncryptionBox(sampleDescriptionIndex); - - ParsableByteArray initializationVectorData; - int vectorSize; - if (encryptionBox.initializationVectorSize != 0) { - initializationVectorData = trackFragment.sampleEncryptionData; - vectorSize = encryptionBox.initializationVectorSize; - } else { - // The default initialization vector should be used. - byte[] initVectorData = encryptionBox.defaultInitializationVector; - defaultInitializationVector.reset(initVectorData, initVectorData.length); - initializationVectorData = defaultInitializationVector; - vectorSize = initVectorData.length; - } - - boolean subsampleEncryption = trackFragment - .sampleHasSubsampleEncryptionTable[trackBundle.currentSampleIndex]; - - // Write the signal byte, containing the vector size and the subsample encryption flag. - encryptionSignalByte.data[0] = (byte) (vectorSize | (subsampleEncryption ? 0x80 : 0)); - encryptionSignalByte.setPosition(0); - TrackOutput output = trackBundle.output; - output.sampleData(encryptionSignalByte, 1); - // Write the vector. - output.sampleData(initializationVectorData, vectorSize); - // If we don't have subsample encryption data, we're done. - if (!subsampleEncryption) { - return 1 + vectorSize; - } - // Write the subsample encryption data. - ParsableByteArray subsampleEncryptionData = trackFragment.sampleEncryptionData; - int subsampleCount = subsampleEncryptionData.readUnsignedShort(); - subsampleEncryptionData.skipBytes(-2); - int subsampleDataLength = 2 + 6 * subsampleCount; - output.sampleData(subsampleEncryptionData, subsampleDataLength); - return 1 + vectorSize + subsampleDataLength; - } - /** Returns DrmInitData from leaf atoms. */ private static DrmInitData getDrmInitDataFromAtoms(List leafChildren) { ArrayList schemeDatas = null; @@ -1397,18 +1368,24 @@ public final class FragmentedMp4Extractor implements Extractor { */ private static final class TrackBundle { - public final TrackFragment fragment; public final TrackOutput output; + public final TrackFragment fragment; public Track track; public DefaultSampleValues defaultSampleValues; public int currentSampleIndex; public int currentSampleInTrackRun; public int currentTrackRunIndex; + public int firstSampleToOutputIndex; + + private final ParsableByteArray encryptionSignalByte; + private final ParsableByteArray defaultInitializationVector; public TrackBundle(TrackOutput output) { - fragment = new TrackFragment(); this.output = output; + fragment = new TrackFragment(); + encryptionSignalByte = new ParsableByteArray(1); + defaultInitializationVector = new ParsableByteArray(); } public void init(Track track, DefaultSampleValues defaultSampleValues) { @@ -1418,13 +1395,6 @@ public final class FragmentedMp4Extractor implements Extractor { reset(); } - public void reset() { - fragment.reset(); - currentSampleIndex = 0; - currentTrackRunIndex = 0; - currentSampleInTrackRun = 0; - } - public void updateDrmInitData(DrmInitData drmInitData) { TrackEncryptionBox encryptionBox = track.getSampleDescriptionEncryptionBox(fragment.header.sampleDescriptionIndex); @@ -1432,6 +1402,120 @@ public final class FragmentedMp4Extractor implements Extractor { output.format(track.format.copyWithDrmInitData(drmInitData.copyWithSchemeType(schemeType))); } + /** Resets the current fragment and sample indices. */ + public void reset() { + fragment.reset(); + currentSampleIndex = 0; + currentTrackRunIndex = 0; + currentSampleInTrackRun = 0; + firstSampleToOutputIndex = 0; + } + + /** + * Advances {@link #firstSampleToOutputIndex} to point to the sync sample before the specified + * seek time in the current fragment. + * + * @param timeUs The seek time, in microseconds. + */ + public void seek(long timeUs) { + long timeMs = C.usToMs(timeUs); + int searchIndex = currentSampleIndex; + while (searchIndex < fragment.sampleCount + && fragment.getSamplePresentationTime(searchIndex) < timeMs) { + if (fragment.sampleIsSyncFrameTable[searchIndex]) { + firstSampleToOutputIndex = searchIndex; + } + searchIndex++; + } + } + + /** + * Advances the indices in the bundle to point to the next sample in the current fragment. If + * the current sample is the last one in the current fragment, then the advanced state will be + * {@code currentSampleIndex == fragment.sampleCount}, {@code currentTrackRunIndex == + * fragment.trunCount} and {@code #currentSampleInTrackRun == 0}. + * + * @return Whether the next sample is in the same track run as the previous one. + */ + public boolean next() { + currentSampleIndex++; + currentSampleInTrackRun++; + if (currentSampleInTrackRun == fragment.trunLength[currentTrackRunIndex]) { + currentTrackRunIndex++; + currentSampleInTrackRun = 0; + return false; + } + return true; + } + + /** + * Outputs the encryption data for the current sample. + * + * @return The number of written bytes. + */ + public int outputSampleEncryptionData() { + if (!fragment.definesEncryptionData) { + return 0; + } + + TrackEncryptionBox encryptionBox = getEncryptionBox(); + ParsableByteArray initializationVectorData; + int vectorSize; + if (encryptionBox.initializationVectorSize != 0) { + initializationVectorData = fragment.sampleEncryptionData; + vectorSize = encryptionBox.initializationVectorSize; + } else { + // The default initialization vector should be used. + byte[] initVectorData = encryptionBox.defaultInitializationVector; + defaultInitializationVector.reset(initVectorData, initVectorData.length); + initializationVectorData = defaultInitializationVector; + vectorSize = initVectorData.length; + } + + boolean subsampleEncryption = fragment.sampleHasSubsampleEncryptionTable[currentSampleIndex]; + + // Write the signal byte, containing the vector size and the subsample encryption flag. + encryptionSignalByte.data[0] = (byte) (vectorSize | (subsampleEncryption ? 0x80 : 0)); + encryptionSignalByte.setPosition(0); + output.sampleData(encryptionSignalByte, 1); + // Write the vector. + output.sampleData(initializationVectorData, vectorSize); + // If we don't have subsample encryption data, we're done. + if (!subsampleEncryption) { + return 1 + vectorSize; + } + // Write the subsample encryption data. + ParsableByteArray subsampleEncryptionData = fragment.sampleEncryptionData; + int subsampleCount = subsampleEncryptionData.readUnsignedShort(); + subsampleEncryptionData.skipBytes(-2); + int subsampleDataLength = 2 + 6 * subsampleCount; + output.sampleData(subsampleEncryptionData, subsampleDataLength); + return 1 + vectorSize + subsampleDataLength; + } + + /** Skips the encryption data for the current sample. */ + private void skipSampleEncryptionData() { + if (!fragment.definesEncryptionData) { + return; + } + + ParsableByteArray sampleEncryptionData = fragment.sampleEncryptionData; + TrackEncryptionBox encryptionBox = getEncryptionBox(); + if (encryptionBox.initializationVectorSize != 0) { + sampleEncryptionData.skipBytes(encryptionBox.initializationVectorSize); + } + if (fragment.sampleHasSubsampleEncryptionTable[currentSampleIndex]) { + sampleEncryptionData.skipBytes(6 * sampleEncryptionData.readUnsignedShort()); + } + } + + private TrackEncryptionBox getEncryptionBox() { + int sampleDescriptionIndex = fragment.header.sampleDescriptionIndex; + return fragment.trackEncryptionBox != null + ? fragment.trackEncryptionBox + : track.getSampleDescriptionEncryptionBox(sampleDescriptionIndex); + } + } } 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/extractor/ts/Ac3Reader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java index 8383bfb8d2..4141f83370 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java @@ -19,6 +19,7 @@ import android.support.annotation.IntDef; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.audio.Ac3Util; +import com.google.android.exoplayer2.audio.Ac3Util.SyncFrameInfo; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; @@ -187,7 +188,7 @@ public final class Ac3Reader implements ElementaryStreamReader { @SuppressWarnings("ReferenceEquality") private void parseHeader() { headerScratchBits.setPosition(0); - Ac3Util.Ac3SyncFrameInfo frameInfo = Ac3Util.parseAc3SyncframeInfo(headerScratchBits); + SyncFrameInfo frameInfo = Ac3Util.parseAc3SyncframeInfo(headerScratchBits); if (format == null || frameInfo.channelCount != format.channelCount || frameInfo.sampleRate != format.sampleRate || frameInfo.mimeType != format.sampleMimeType) { 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 2e5b04f4a9..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 @@ -22,6 +22,7 @@ import android.media.MediaCodecInfo.AudioCapabilities; import android.media.MediaCodecInfo.CodecCapabilities; import android.media.MediaCodecInfo.CodecProfileLevel; import android.media.MediaCodecInfo.VideoCapabilities; +import android.support.annotation.Nullable; import android.util.Log; import android.util.Pair; import com.google.android.exoplayer2.Format; @@ -37,6 +38,12 @@ public final class MediaCodecInfo { public static final String TAG = "MediaCodecInfo"; + /** + * The value returned by {@link #getMaxSupportedInstances()} if the upper bound on the maximum + * number of supported instances is unknown. + */ + public static final int MAX_SUPPORTED_INSTANCES_UNKNOWN = -1; + /** * The name of the decoder. *

    @@ -45,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. * @@ -69,8 +85,8 @@ public final class MediaCodecInfo { */ public final boolean secure; - private final String mimeType; - private final CodecCapabilities capabilities; + /** Whether this instance describes a passthrough codec. */ + public final boolean passthrough; /** * Creates an instance representing an audio passthrough decoder. @@ -79,7 +95,13 @@ public final class MediaCodecInfo { * @return The created instance. */ public static MediaCodecInfo newPassthroughInstance(String name) { - return new MediaCodecInfo(name, null, null, false, false); + return new MediaCodecInfo( + name, + /* mimeType= */ null, + /* capabilities= */ null, + /* passthrough= */ true, + /* forceDisableAdaptive= */ false, + /* forceSecure= */ false); } /** @@ -92,7 +114,13 @@ public final class MediaCodecInfo { */ public static MediaCodecInfo newInstance(String name, String mimeType, CodecCapabilities capabilities) { - return new MediaCodecInfo(name, mimeType, capabilities, false, false); + return new MediaCodecInfo( + name, + mimeType, + capabilities, + /* passthrough= */ false, + /* forceDisableAdaptive= */ false, + /* forceSecure= */ false); } /** @@ -105,16 +133,27 @@ public final class MediaCodecInfo { * @param forceSecure Whether {@link #secure} should be forced to {@code true}. * @return The created instance. */ - public static MediaCodecInfo newInstance(String name, String mimeType, - CodecCapabilities capabilities, boolean forceDisableAdaptive, boolean forceSecure) { - return new MediaCodecInfo(name, mimeType, capabilities, forceDisableAdaptive, forceSecure); + public static MediaCodecInfo newInstance( + String name, + String mimeType, + CodecCapabilities capabilities, + boolean forceDisableAdaptive, + boolean forceSecure) { + return new MediaCodecInfo( + name, mimeType, capabilities, /* passthrough= */ false, forceDisableAdaptive, forceSecure); } - private MediaCodecInfo(String name, String mimeType, CodecCapabilities capabilities, - boolean forceDisableAdaptive, boolean forceSecure) { + private MediaCodecInfo( + String name, + @Nullable String mimeType, + @Nullable CodecCapabilities capabilities, + boolean passthrough, + boolean forceDisableAdaptive, + boolean forceSecure) { this.name = Assertions.checkNotNull(name); 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)); @@ -130,6 +169,19 @@ public final class MediaCodecInfo { : capabilities.profileLevels; } + /** + * Returns an upper bound on the maximum number of supported instances, or {@link + * #MAX_SUPPORTED_INSTANCES_UNKNOWN} if unknown. Applications should not expect to operate more + * instances than the returned maximum. + * + * @see CodecCapabilities#getMaxSupportedInstances() + */ + public int getMaxSupportedInstances() { + return (Util.SDK_INT < 23 || capabilities == null) + ? MAX_SUPPORTED_INSTANCES_UNKNOWN + : getMaxSupportedInstancesV23(capabilities); + } + /** * Whether the decoder supports the given {@code codec}. If there is insufficient information to * decide, returns true. @@ -362,4 +414,8 @@ public final class MediaCodecInfo { : capabilities.areSizeAndRateSupported(width, height, frameRate); } + @TargetApi(23) + private static int getMaxSupportedInstancesV23(CodecCapabilities capabilities) { + return capabilities.getMaxSupportedInstances(); + } } 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 2e8fc602a2..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,6 +129,27 @@ public abstract class MediaCodecRenderer extends BaseRenderer { */ private static final long MAX_CODEC_HOTSWAP_TIME_MS = 1000; + /** + * The possible return values for {@link #canKeepCodec(MediaCodec, MediaCodecInfo, Format, + * Format)}. + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + KEEP_CODEC_RESULT_NO, + KEEP_CODEC_RESULT_YES_WITHOUT_RECONFIGURATION, + KEEP_CODEC_RESULT_YES_WITH_RECONFIGURATION + }) + protected @interface KeepCodecResult {} + /** The codec cannot be kept. */ + protected static final int KEEP_CODEC_RESULT_NO = 0; + /** The codec can be kept. No reconfiguration is required. */ + protected static final int KEEP_CODEC_RESULT_YES_WITHOUT_RECONFIGURATION = 1; + /** + * The codec can be kept, but must be reconfigured by prefixing the next input buffer with the new + * format's configuration data. + */ + protected static final int KEEP_CODEC_RESULT_YES_WITH_RECONFIGURATION = 3; + @Retention(RetentionPolicy.SOURCE) @IntDef({RECONFIGURATION_STATE_NONE, RECONFIGURATION_STATE_WRITE_PENDING, RECONFIGURATION_STATE_QUEUE_PENDING}) @@ -432,21 +453,6 @@ public abstract class MediaCodecRenderer extends BaseRenderer { return codecInfo; } - /** - * Returns the framework {@link MediaFormat} that can be used to configure a {@link MediaCodec} - * for decoding the given {@link Format} for playback. - * - * @param format The format of the media. - * @return The framework media format. - */ - protected final MediaFormat getMediaFormatForPlayback(Format format) { - MediaFormat mediaFormat = format.getFrameworkMediaFormatV16(); - if (Util.SDK_INT >= 23) { - configureMediaFormatForPlaybackV23(mediaFormat); - } - return mediaFormat; - } - @Override protected void onEnabled(boolean joining) throws ExoPlaybackException { decoderCounters = new DecoderCounters(); @@ -863,8 +869,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer { Format oldFormat = format; format = newFormat; - boolean drmInitDataChanged = !Util.areEqual(format.drmInitData, oldFormat == null ? null - : oldFormat.drmInitData); + boolean drmInitDataChanged = + !Util.areEqual(format.drmInitData, oldFormat == null ? null : oldFormat.drmInitData); if (drmInitDataChanged) { if (format.drmInitData != null) { if (drmSessionManager == null) { @@ -880,15 +886,31 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } } - if (pendingDrmSession == drmSession && codec != null - && canReconfigureCodec(codec, codecInfo.adaptive, oldFormat, format)) { - codecReconfigured = true; - codecReconfigurationState = RECONFIGURATION_STATE_WRITE_PENDING; - codecNeedsAdaptationWorkaroundBuffer = - codecAdaptationWorkaroundMode == ADAPTATION_WORKAROUND_MODE_ALWAYS - || (codecAdaptationWorkaroundMode == ADAPTATION_WORKAROUND_MODE_SAME_RESOLUTION - && format.width == oldFormat.width && format.height == oldFormat.height); - } else { + boolean keepingCodec = false; + if (pendingDrmSession == drmSession && codec != null) { + switch (canKeepCodec(codec, codecInfo, oldFormat, format)) { + case KEEP_CODEC_RESULT_NO: + // Do nothing. + break; + case KEEP_CODEC_RESULT_YES_WITHOUT_RECONFIGURATION: + keepingCodec = true; + break; + case KEEP_CODEC_RESULT_YES_WITH_RECONFIGURATION: + keepingCodec = true; + codecReconfigured = true; + codecReconfigurationState = RECONFIGURATION_STATE_WRITE_PENDING; + codecNeedsAdaptationWorkaroundBuffer = + codecAdaptationWorkaroundMode == ADAPTATION_WORKAROUND_MODE_ALWAYS + || (codecAdaptationWorkaroundMode == ADAPTATION_WORKAROUND_MODE_SAME_RESOLUTION + && format.width == oldFormat.width + && format.height == oldFormat.height); + break; + default: + throw new IllegalStateException(); // Never happens. + } + } + + if (!keepingCodec) { if (codecReceivedBuffers) { // Signal end of stream and wait for any final output buffers before re-initialization. codecReinitializationState = REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM; @@ -937,23 +959,20 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } /** - * Determines whether the existing {@link MediaCodec} should be reconfigured for a new format by - * sending codec specific initialization data at the start of the next input buffer. If true is - * returned then the {@link MediaCodec} instance will be reconfigured in this way. If false is - * returned then the instance will be released, and a new instance will be created for the new - * format. - *

    - * The default implementation returns false. + * Determines whether the existing {@link MediaCodec} can be kept for a new format, and if it can + * whether it requires reconfiguration. + * + *

    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 existing instance can be reconfigured. + * @return Whether the instance can be kept, and if it can whether it requires reconfiguration. */ - protected boolean canReconfigureCodec(MediaCodec codec, boolean codecIsAdaptive, Format oldFormat, - Format newFormat) { - return false; + protected @KeepCodecResult int canKeepCodec( + MediaCodec codec, MediaCodecInfo codecInfo, Format oldFormat, Format newFormat) { + return KEEP_CODEC_RESULT_NO; } @Override @@ -1011,7 +1030,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer { shouldSkipAdaptationWorkaroundOutputBuffer = false; codec.releaseOutputBuffer(outputIndex, false); return true; - } else if ((outputBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) { + } else if (outputBufferInfo.size == 0 + && (outputBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) { // The dequeued buffer indicates the end of the stream. Process it immediately. processEndOfStream(); return false; @@ -1078,8 +1098,12 @@ public abstract class MediaCodecRenderer extends BaseRenderer { if (processedOutputBuffer) { onProcessedOutputBuffer(outputBufferInfo.presentationTimeUs); + boolean isEndOfStream = (outputBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0; resetOutputBuffer(); - return true; + if (!isEndOfStream) { + return true; + } + processEndOfStream(); } return false; @@ -1185,11 +1209,6 @@ public abstract class MediaCodecRenderer extends BaseRenderer { return false; } - @TargetApi(23) - private static void configureMediaFormatForPlaybackV23(MediaFormat mediaFormat) { - mediaFormat.setInteger(MediaFormat.KEY_PRIORITY, 0 /* realtime priority */); - } - /** * Returns whether the decoder is known to fail when flushed. *

    diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java index b80780884c..49f7361bc5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java @@ -354,15 +354,15 @@ public final class MediaCodecUtil { // Work around https://github.com/google/ExoPlayer/issues/3249. if (Util.SDK_INT < 24 && ("OMX.SEC.aac.dec".equals(name) || "OMX.Exynos.AAC.Decoder".equals(name)) - && Util.MANUFACTURER.equals("samsung") + && "samsung".equals(Util.MANUFACTURER) && (Util.DEVICE.startsWith("zeroflte") // Galaxy S6 || Util.DEVICE.startsWith("zerolte") // Galaxy S6 Edge || Util.DEVICE.startsWith("zenlte") // Galaxy S6 Edge+ - || Util.DEVICE.equals("SC-05G") // Galaxy S6 - || Util.DEVICE.equals("marinelteatt") // Galaxy S6 Active - || Util.DEVICE.equals("404SC") // Galaxy S6 Edge - || Util.DEVICE.equals("SC-04G") - || Util.DEVICE.equals("SCV31"))) { + || "SC-05G".equals(Util.DEVICE) // Galaxy S6 + || "marinelteatt".equals(Util.DEVICE) // Galaxy S6 Active + || "404SC".equals(Util.DEVICE) // Galaxy S6 Edge + || "SC-04G".equals(Util.DEVICE) + || "SCV31".equals(Util.DEVICE))) { return false; } @@ -421,7 +421,7 @@ public final class MediaCodecUtil { */ private static boolean codecNeedsDisableAdaptationWorkaround(String name) { return Util.SDK_INT <= 22 - && (Util.MODEL.equals("ODROID-XU3") || Util.MODEL.equals("Nexus 10")) + && ("ODROID-XU3".equals(Util.MODEL) || "Nexus 10".equals(Util.MODEL)) && ("OMX.Exynos.AVC.Decoder".equals(name) || "OMX.Exynos.AVC.Decoder.secure".equals(name)); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaFormatUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaFormatUtil.java new file mode 100644 index 0000000000..3cfefc0736 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaFormatUtil.java @@ -0,0 +1,111 @@ +/* + * 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.mediacodec; + +import android.annotation.TargetApi; +import android.media.MediaFormat; +import android.support.annotation.Nullable; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.video.ColorInfo; +import java.nio.ByteBuffer; +import java.util.List; + +/** Helper class for configuring {@link MediaFormat} instances. */ +@TargetApi(16) +public final class MediaFormatUtil { + + private MediaFormatUtil() {} + + /** + * Sets a {@link MediaFormat} {@link String} value. + * + * @param format The {@link MediaFormat} being configured. + * @param key The key to set. + * @param value The value to set. + */ + public static void setString(MediaFormat format, String key, String value) { + format.setString(key, value); + } + + /** + * Sets a {@link MediaFormat}'s codec specific data buffers. + * + * @param format The {@link MediaFormat} being configured. + * @param csdBuffers The csd buffers to set. + */ + public static void setCsdBuffers(MediaFormat format, List csdBuffers) { + for (int i = 0; i < csdBuffers.size(); i++) { + format.setByteBuffer("csd-" + i, ByteBuffer.wrap(csdBuffers.get(i))); + } + } + + /** + * Sets a {@link MediaFormat} integer value. Does nothing if {@code value} is {@link + * Format#NO_VALUE}. + * + * @param format The {@link MediaFormat} being configured. + * @param key The key to set. + * @param value The value to set. + */ + public static void maybeSetInteger(MediaFormat format, String key, int value) { + if (value != Format.NO_VALUE) { + format.setInteger(key, value); + } + } + + /** + * Sets a {@link MediaFormat} float value. Does nothing if {@code value} is {@link + * Format#NO_VALUE}. + * + * @param format The {@link MediaFormat} being configured. + * @param key The key to set. + * @param value The value to set. + */ + public static void maybeSetFloat(MediaFormat format, String key, float value) { + if (value != Format.NO_VALUE) { + format.setFloat(key, value); + } + } + + /** + * Sets a {@link MediaFormat} {@link ByteBuffer} value. Does nothing if {@code value} is null. + * + * @param format The {@link MediaFormat} being configured. + * @param key The key to set. + * @param value The {@link byte[]} that will be wrapped to obtain the value. + */ + public static void maybeSetByteBuffer(MediaFormat format, String key, @Nullable byte[] value) { + if (value != null) { + format.setByteBuffer(key, ByteBuffer.wrap(value)); + } + } + + /** + * Sets a {@link MediaFormat}'s color information. Does nothing if {@code colorInfo} is null. + * + * @param format The {@link MediaFormat} being configured. + * @param colorInfo The color info to set. + */ + @SuppressWarnings("InlinedApi") + public static void maybeSetColorInfo(MediaFormat format, @Nullable ColorInfo colorInfo) { + if (colorInfo != null) { + maybeSetInteger(format, MediaFormat.KEY_COLOR_TRANSFER, colorInfo.colorTransfer); + maybeSetInteger(format, MediaFormat.KEY_COLOR_STANDARD, colorInfo.colorSpace); + maybeSetInteger(format, MediaFormat.KEY_COLOR_RANGE, colorInfo.colorRange); + maybeSetByteBuffer(format, MediaFormat.KEY_HDR_STATIC_INFO, colorInfo.hdrStaticInfo); + } + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/Metadata.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/Metadata.java index 40c05a5602..a8c9d0b5a8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/Metadata.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/Metadata.java @@ -92,6 +92,8 @@ public final class Metadata implements Parcelable { return Arrays.hashCode(entries); } + // Parcelable implementation. + @Override public int describeContents() { return 0; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessage.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessage.java index 57e7f0bfd6..0612c18e18 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessage.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessage.java @@ -117,6 +117,11 @@ public final class EventMessage implements Metadata.Entry { && Util.areEqual(value, other.value) && Arrays.equals(messageData, other.messageData); } + @Override + public String toString() { + return "EMSG: scheme=" + schemeIdUri + ", id=" + id + ", value=" + value; + } + // Parcelable implementation. @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ApicFrame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ApicFrame.java index c64be24a31..eafb0286ce 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ApicFrame.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ApicFrame.java @@ -72,6 +72,13 @@ public final class ApicFrame extends Id3Frame { return result; } + @Override + public String toString() { + return id + ": mimeType=" + mimeType + ", description=" + description; + } + + // Parcelable implementation. + @Override public void writeToParcel(Parcel dest, int flags) { dest.writeString(mimeType); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/CommentFrame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/CommentFrame.java index b7cc937ac4..b43a46349c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/CommentFrame.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/CommentFrame.java @@ -66,6 +66,13 @@ public final class CommentFrame extends Id3Frame { return result; } + @Override + public String toString() { + return id + ": language=" + language + ", description=" + description; + } + + // Parcelable implementation. + @Override public void writeToParcel(Parcel dest, int flags) { dest.writeString(id); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/GeobFrame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/GeobFrame.java index 79e145fc7c..0ed429055b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/GeobFrame.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/GeobFrame.java @@ -71,6 +71,19 @@ public final class GeobFrame extends Id3Frame { return result; } + @Override + public String toString() { + return id + + ": mimeType=" + + mimeType + + ", filename=" + + filename + + ", description=" + + description; + } + + // Parcelable implementation. + @Override public void writeToParcel(Parcel dest, int flags) { dest.writeString(mimeType); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java index 7646af718d..ad24bac6c4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java @@ -53,6 +53,16 @@ public final class Id3Decoder implements MetadataDecoder { } + /** A predicate that indicates no frames should be decoded. */ + public static final FramePredicate NO_FRAMES_PREDICATE = + new FramePredicate() { + + @Override + public boolean evaluate(int majorVersion, int id0, int id1, int id2, int id3) { + return false; + } + }; + private static final String TAG = "Id3Decoder"; /** @@ -518,7 +528,7 @@ public final class Id3Decoder implements MetadataDecoder { if (majorVersion == 2) { mimeTypeEndIndex = 2; mimeType = "image/" + Util.toLowerInvariant(new String(data, 0, 3, "ISO-8859-1")); - if (mimeType.equals("image/jpg")) { + if ("image/jpg".equals(mimeType)) { mimeType = "image/jpeg"; } } else { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Frame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Frame.java index 9948f730eb..433c52bdcc 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Frame.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Frame.java @@ -32,6 +32,11 @@ public abstract class Id3Frame implements Metadata.Entry { this.id = Assertions.checkNotNull(id); } + @Override + public String toString() { + return id; + } + @Override public int describeContents() { return 0; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/PrivFrame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/PrivFrame.java index fe55f5ddc0..db6db2ea4f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/PrivFrame.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/PrivFrame.java @@ -62,6 +62,12 @@ public final class PrivFrame extends Id3Frame { return result; } + @Override + public String toString() { + return id + ": owner=" + owner; + } + // Parcelable implementation. + @Override public void writeToParcel(Parcel dest, int flags) { dest.writeString(owner); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java index 6221062e33..3374db5d8d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java @@ -61,6 +61,13 @@ public final class TextInformationFrame extends Id3Frame { return result; } + @Override + public String toString() { + return id + ": value=" + value; + } + + // Parcelable implementation. + @Override public void writeToParcel(Parcel dest, int flags) { dest.writeString(id); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/UrlLinkFrame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/UrlLinkFrame.java index 2148b921e1..775ab5dd3e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/UrlLinkFrame.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/UrlLinkFrame.java @@ -61,6 +61,13 @@ public final class UrlLinkFrame extends Id3Frame { return result; } + @Override + public String toString() { + return id + ": url=" + url; + } + + // Parcelable implementation. + @Override public void writeToParcel(Parcel dest, int flags) { dest.writeString(id); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceCommand.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceCommand.java index 8dfa3b8942..b0c3e34cde 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceCommand.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceCommand.java @@ -22,6 +22,13 @@ import com.google.android.exoplayer2.metadata.Metadata; */ public abstract class SpliceCommand implements Metadata.Entry { + @Override + public String toString() { + return "SCTE-35 splice command: type=" + getClass().getSimpleName(); + } + + // Parcelable implementation. + @Override public int describeContents() { return 0; 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 new file mode 100644 index 0000000000..e37e09a090 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/ActionFile.java @@ -0,0 +1,98 @@ +/* + * 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.offline; + +import com.google.android.exoplayer2.offline.DownloadAction.Deserializer; +import com.google.android.exoplayer2.util.AtomicFile; +import com.google.android.exoplayer2.util.Util; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; + +/** + * Stores and loads {@link DownloadAction}s to/from a file. + */ +public final class ActionFile { + + /* package */ static final int VERSION = 0; + + private final AtomicFile atomicFile; + private final File actionFile; + + /** + * @param actionFile File to be used to store and load {@link DownloadAction}s. + */ + public ActionFile(File actionFile) { + this.actionFile = actionFile; + atomicFile = new AtomicFile(actionFile); + } + + /** + * Loads {@link DownloadAction}s from file. + * + * @param deserializers {@link Deserializer}s to deserialize DownloadActions. + * @return Loaded DownloadActions. If the action file doesn't exists returns an empty array. + * @throws IOException If there is an error during loading. + */ + public DownloadAction[] load(Deserializer... deserializers) throws IOException { + if (!actionFile.exists()) { + return new DownloadAction[0]; + } + InputStream inputStream = null; + try { + inputStream = atomicFile.openRead(); + DataInputStream dataInputStream = new DataInputStream(inputStream); + int version = dataInputStream.readInt(); + 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); + } + return actions; + } finally { + Util.closeQuietly(inputStream); + } + } + + /** + * Stores {@link DownloadAction}s to file. + * + * @param downloadActions DownloadActions to store to file. + * @throws IOException If there is an error during storing. + */ + public void store(DownloadAction... downloadActions) throws IOException { + DataOutputStream output = null; + try { + output = new DataOutputStream(atomicFile.startWrite()); + output.writeInt(VERSION); + output.writeInt(downloadActions.length); + for (DownloadAction action : downloadActions) { + DownloadAction.serializeToStream(action, output); + } + atomicFile.endWrite(output); + // Avoid calling close twice. + output = null; + } finally { + Util.closeQuietly(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 new file mode 100644 index 0000000000..cf061f3745 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadAction.java @@ -0,0 +1,163 @@ +/* + * 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.offline; + +import android.net.Uri; +import android.support.annotation.Nullable; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +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 { + + /** Used to deserialize {@link DownloadAction}s. */ + public abstract static class Deserializer { + + public final String type; + public final int version; + + public Deserializer(String type, int version) { + this.type = type; + this.version = version; + } + + /** + * Deserializes an action from the {@code input}. + * + * @param version The version of the serialized action. + * @param input The stream from which to read the action. + * @see DownloadAction#writeToStream(DataOutputStream) + */ + public abstract DownloadAction readFromStream(int version, DataInputStream input) + throws IOException; + } + + /** + * 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 {@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 { + // 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.type) && deserializer.version >= version) { + return deserializer.readFromStream(version, dataInputStream); + } + } + throw new DownloadException("No deserializer found for:" + type + ", " + version); + } + + /** Serializes {@code action} type and data into the {@code output}. */ + public static void serializeToStream(DownloadAction action, OutputStream output) + throws IOException { + // Don't close the stream as it closes the underlying stream too. + DataOutputStream dataOutputStream = new DataOutputStream(output); + dataOutputStream.writeUTF(action.type); + dataOutputStream.writeInt(action.version); + action.writeToStream(dataOutputStream); + dataOutputStream.flush(); + } + + /** 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 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. */ + public final byte[] toByteArray() { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + try { + serializeToStream(this, output); + } catch (IOException e) { + // ByteArrayOutputStream shouldn't throw IOException. + throw new IllegalStateException(); + } + return output.toByteArray(); + } + + /** Returns whether this is an action for the same media as the {@code other}. */ + public boolean isSameMedia(DownloadAction other) { + return uri.equals(other.uri); + } + + /** Serializes itself into the {@code output}. */ + protected abstract void writeToStream(DataOutputStream output) throws IOException; + + /** Creates a {@link Downloader} with the given parameters. */ + protected abstract Downloader createDownloader( + DownloaderConstructorHelper downloaderConstructorHelper); + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) { + return false; + } + DownloadAction that = (DownloadAction) o; + 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 = 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 new file mode 100644 index 0000000000..8be822b6ca --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java @@ -0,0 +1,844 @@ +/* + * 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.offline; + +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; +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. + * + *

    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 {@link DownloadManager} events. */ + public interface Listener { + /** + * Called when all actions have been restored. + * + * @param downloadManager The reporting instance. + */ + 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. + * + * @param downloadManager The reporting instance. + */ + void onIdle(DownloadManager downloadManager); + } + + /** The default maximum number of simultaneous download tasks. */ + public static final int DEFAULT_MAX_SIMULTANEOUS_DOWNLOADS = 1; + /** 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"; + private static final boolean DEBUG = false; + + private final DownloaderConstructorHelper downloaderConstructorHelper; + private final int maxActiveDownloadTasks; + private final int minRetryCount; + private final ActionFile actionFile; + private final DownloadAction.Deserializer[] deserializers; + 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 int nextTaskId; + private boolean initialized; + private boolean released; + private boolean downloadsStopped; + + /** + * Creates a {@link DownloadManager}. + * + * @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( + Cache cache, + DataSource.Factory upstreamDataSourceFactory, + File actionSaveFile, + Deserializer... deserializers) { + this( + new DownloaderConstructorHelper(cache, upstreamDataSourceFactory), + actionSaveFile, + deserializers); + } + + /** + * Constructs a {@link DownloadManager}. + * + * @param constructorHelper A {@link DownloaderConstructorHelper} to create {@link Downloader}s + * for downloading data. + * @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, + File actionFile, + Deserializer... deserializers) { + Assertions.checkArgument(deserializers.length > 0, "At least one Deserializer is required."); + + this.downloaderConstructorHelper = constructorHelper; + this.maxActiveDownloadTasks = maxSimultaneousDownloads; + this.minRetryCount = minRetryCount; + this.actionFile = new ActionFile(actionFile); + this.deserializers = deserializers; + this.downloadsStopped = true; + + tasks = new ArrayList<>(); + activeDownloadTasks = new ArrayList<>(); + + Looper looper = Looper.myLooper(); + if (looper == null) { + looper = Looper.getMainLooper(); + } + handler = new Handler(looper); + + fileIOThread = new HandlerThread("DownloadManager file i/o"); + fileIOThread.start(); + fileIOHandler = new Handler(fileIOThread.getLooper()); + + listeners = new CopyOnWriteArraySet<>(); + + loadActions(); + logd("Created"); + } + + /** + * 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(); + } + final ConditionVariable fileIOFinishedCondition = new ConditionVariable(); + fileIOHandler.post(new Runnable() { + @Override + public void run() { + fileIOFinishedCondition.open(); + } + }); + fileIOFinishedCondition.block(); + fileIOThread.quit(); + logd("Released"); + } + + private Task addTaskForAction(DownloadAction action) { + Task task = new Task(nextTaskId++, this, action, minRetryCount); + tasks.add(task); + logd("Task is added", task); + return task; + } + + /** + * Iterates through the task queue and starts any task if all of the following are true: + * + *

      + *
    • It hasn't started yet. + *
    • There are no preceding conflicting tasks. + *
    • If it's a download task then there are no preceding download tasks on hold and the + * maximum number of active downloads hasn't been reached. + *
    + * + * If the task is a remove action then preceding conflicting tasks are canceled. + */ + private void maybeStartTasks() { + if (!initialized || released) { + return; + } + + boolean skipDownloadActions = downloadsStopped + || activeDownloadTasks.size() == maxActiveDownloadTasks; + for (int i = 0; i < tasks.size(); i++) { + Task task = tasks.get(i); + if (!task.canStart()) { + continue; + } + + DownloadAction action = task.action; + boolean isRemoveAction = action.isRemoveAction; + if (!isRemoveAction && skipDownloadActions) { + continue; + } + + boolean canStartTask = true; + for (int j = 0; j < i; j++) { + Task otherTask = tasks.get(j); + if (otherTask.action.isSameMedia(action)) { + if (isRemoveAction) { + canStartTask = false; + logd(task + " clashes with " + otherTask); + otherTask.cancel(); + // Continue loop to cancel any other preceding clashing tasks. + } else if (otherTask.action.isRemoveAction) { + canStartTask = false; + skipDownloadActions = true; + break; + } + } + } + + if (canStartTask) { + task.start(); + if (!isRemoveAction) { + activeDownloadTasks.add(task); + skipDownloadActions = activeDownloadTasks.size() == maxActiveDownloadTasks; + } + } + } + } + + private void maybeNotifyListenersIdle() { + if (!isIdle()) { + return; + } + logd("Notify idle state"); + for (Listener listener : listeners) { + listener.onIdle(this); + } + } + + private void onTaskStateChange(Task task) { + if (released) { + return; + } + logd("Task state is changed", task); + boolean stopped = !task.isActive(); + if (stopped) { + activeDownloadTasks.remove(task); + } + notifyListenersTaskStateChange(task); + if (task.isFinished()) { + tasks.remove(task); + saveActions(); + } + if (stopped) { + maybeStartTasks(); + maybeNotifyListenersIdle(); + } + } + + private void notifyListenersTaskStateChange(Task task) { + TaskState taskState = task.getDownloadState(); + for (Listener listener : listeners) { + listener.onTaskStateChanged(this, taskState); + } + } + + private void loadActions() { + fileIOHandler.post( + new Runnable() { + @Override + public void run() { + DownloadAction[] loadedActions; + try { + loadedActions = actionFile.load(DownloadManager.this.deserializers); + logd("Action file is loaded."); + } catch (Throwable e) { + Log.e(TAG, "Action file loading failed.", e); + loadedActions = new DownloadAction[0]; + } + final DownloadAction[] actions = loadedActions; + handler.post( + new Runnable() { + @Override + public void run() { + 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); + } + } + } + }); + } + }); + } + + private void saveActions() { + if (released) { + return; + } + final DownloadAction[] actions = new DownloadAction[tasks.size()]; + for (int i = 0; i < tasks.size(); i++) { + actions[i] = tasks.get(i).action; + } + fileIOHandler.post(new Runnable() { + @Override + public void run() { + try { + actionFile.store(actions); + logd("Actions persisted."); + } catch (IOException e) { + Log.e(TAG, "Persisting actions failed.", e); + } + } + }); + } + + private static void logd(String message) { + if (DEBUG) { + Log.d(TAG, message); + } + } + + private static void logd(String message, Task task) { + logd(message + ": " + task); + } + + /** Represents state of a task. */ + public static final class TaskState { + + /** + * Task states. + * + *

    Transition diagram: + * + *

    +     *                    -> canceled
    +     * queued <-> started -> completed
    +     *                    -> failed
    +     * 
    + */ + @Retention(RetentionPolicy.SOURCE) + @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_COMPLETED = 2; + /** The task was canceled. */ + public static final int STATE_CANCELED = 3; + /** The task failed. */ + public static final int STATE_FAILED = 4; + + /** Returns the state string for the given state value. */ + public static String getStateString(@State int state) { + switch (state) { + case STATE_QUEUED: + return "QUEUED"; + case STATE_STARTED: + return "STARTED"; + case STATE_COMPLETED: + return "COMPLETED"; + case STATE_CANCELED: + return "CANCELED"; + case STATE_FAILED: + return "FAILED"; + default: + throw new IllegalStateException(); + } + } + + /** The unique task id. */ + public final int taskId; + /** The action being executed. */ + public final DownloadAction action; + /** The state of the task. */ + public final @State int state; + + /** + * 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 total number of downloaded bytes. */ + public final long downloadedBytes; + + /** If {@link #state} is {@link #STATE_FAILED} then this is the cause, otherwise null. */ + public final Throwable error; + + private TaskState( + int taskId, + DownloadAction action, + @State int state, + float downloadPercentage, + long downloadedBytes, + Throwable error) { + this.taskId = taskId; + this.action = action; + this.state = state; + this.downloadPercentage = downloadPercentage; + this.downloadedBytes = downloadedBytes; + this.error = error; + } + + } + + private static final class Task implements Runnable { + + /** + * Task states. + * + *

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

    +     *             +------+-------+---------+-----------+-----------+--------+--------+------+
    +     *             |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_COMPLETED, + STATE_CANCELED, + STATE_FAILED, + STATE_QUEUED_CANCELING, + STATE_STARTED_CANCELING, + STATE_STARTED_STOPPING + }) + public @interface InternalState {} + /** The task is about to be canceled. */ + public static final int STATE_QUEUED_CANCELING = 5; + /** The task is about to be canceled. */ + public static final int STATE_STARTED_CANCELING = 6; + /** The task is about to be stopped. */ + public static final int STATE_STARTED_STOPPING = 7; + + private final int id; + private final DownloadManager downloadManager; + private final DownloadAction action; + private final int minRetryCount; + private volatile @InternalState int currentState; + private volatile Downloader downloader; + private Thread thread; + private Throwable error; + + private Task( + int id, DownloadManager downloadManager, DownloadAction action, int minRetryCount) { + this.id = id; + this.downloadManager = downloadManager; + this.action = action; + this.currentState = STATE_QUEUED; + this.minRetryCount = minRetryCount; + } + + public TaskState getDownloadState() { + int externalState = getExternalState(); + return new TaskState( + id, action, externalState, getDownloadPercentage(), getDownloadedBytes(), error); + } + + /** Returns whether the task is finished. */ + public boolean isFinished() { + return currentState == STATE_FAILED + || currentState == STATE_COMPLETED + || currentState == STATE_CANCELED; + } + + /** Returns whether the task is started. */ + public boolean isActive() { + return currentState == STATE_QUEUED_CANCELING + || currentState == STATE_STARTED + || currentState == STATE_STARTED_STOPPING + || currentState == STATE_STARTED_CANCELING; + } + + /** + * Returns the estimated download percentage, or {@link C#PERCENTAGE_UNSET} if no estimate is + * available. + */ + public float getDownloadPercentage() { + return downloader != null ? downloader.getDownloadPercentage() : C.PERCENTAGE_UNSET; + } + + /** Returns the total number of downloaded bytes. */ + public long getDownloadedBytes() { + return downloader != null ? downloader.getDownloadedBytes() : 0; + } + + @Override + public String toString() { + if (!DEBUG) { + return super.toString(); + } + return action.type + + ' ' + + (action.isRemoveAction ? "remove" : "download") + + ' ' + + getStateString(); + } + + private String getStateString() { + switch (currentState) { + case STATE_QUEUED_CANCELING: + case STATE_STARTED_CANCELING: + return "CANCELING"; + case STATE_STARTED_STOPPING: + return "STOPPING"; + default: + return TaskState.getStateString(currentState); + } + } + + private int getExternalState() { + switch (currentState) { + case STATE_QUEUED_CANCELING: + return STATE_QUEUED; + case STATE_STARTED_CANCELING: + case STATE_STARTED_STOPPING: + return STATE_STARTED; + default: + return currentState; + } + } + + private void start() { + if (changeStateAndNotify(STATE_QUEUED, STATE_STARTED)) { + thread = new Thread(this); + thread.start(); + } + } + + private boolean canStart() { + return currentState == STATE_QUEUED; + } + + private void cancel() { + if (changeStateAndNotify(STATE_QUEUED, STATE_QUEUED_CANCELING)) { + downloadManager.handler.post( + new Runnable() { + @Override + public void run() { + changeStateAndNotify(STATE_QUEUED_CANCELING, STATE_CANCELED); + } + }); + } else if (changeStateAndNotify(STATE_STARTED, STATE_STARTED_CANCELING)) { + cancelDownload(); + } + } + + private void stop() { + if (changeStateAndNotify(STATE_STARTED, STATE_STARTED_STOPPING)) { + logd("Stopping", this); + thread.interrupt(); + } + } + + private boolean changeStateAndNotify(@InternalState int oldState, @InternalState int newState) { + return changeStateAndNotify(oldState, newState, null); + } + + private boolean changeStateAndNotify( + @InternalState int oldState, @InternalState int newState, Throwable error) { + if (currentState != oldState) { + return false; + } + currentState = newState; + this.error = error; + boolean isInternalState = currentState != getExternalState(); + if (!isInternalState) { + downloadManager.onTaskStateChange(this); + } + return true; + } + + private void cancelDownload() { + if (downloader != null) { + downloader.cancel(); + } + thread.interrupt(); + } + + // Methods running on download thread. + + @Override + public void run() { + logd("Task is started", this); + Throwable error = null; + try { + downloader = action.createDownloader(downloadManager.downloaderConstructorHelper); + if (action.isRemoveAction) { + downloader.remove(); + } else { + int errorCount = 0; + long errorPosition = C.LENGTH_UNSET; + while (!Thread.interrupted()) { + try { + downloader.download(); + break; + } catch (IOException e) { + long downloadedBytes = downloader.getDownloadedBytes(); + if (downloadedBytes != errorPosition) { + logd("Reset error count. downloadedBytes = " + downloadedBytes, this); + errorPosition = downloadedBytes; + errorCount = 0; + } + if (currentState != STATE_STARTED || ++errorCount > minRetryCount) { + throw e; + } + logd("Download error. Retry " + errorCount, this); + Thread.sleep(getRetryDelayMillis(errorCount)); + } + } + } + } catch (Throwable e){ + error = e; + } + final Throwable finalError = error; + downloadManager.handler.post( + new Runnable() { + @Override + public void run() { + if (changeStateAndNotify( + STATE_STARTED, + finalError != null ? STATE_FAILED : STATE_COMPLETED, + finalError) + || changeStateAndNotify(STATE_STARTED_CANCELING, STATE_CANCELED) + || changeStateAndNotify(STATE_STARTED_STOPPING, STATE_QUEUED)) { + return; + } + throw new IllegalStateException(); + } + }); + } + + private int getRetryDelayMillis(int errorCount) { + return Math.min((errorCount - 1) * 1000, 5000); + } + } + +} 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 new file mode 100644 index 0000000000..908aae481a --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java @@ -0,0 +1,457 @@ +/* + * 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.offline; + +import android.app.Notification; +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.Nullable; +import android.support.annotation.StringRes; +import android.util.Log; +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} for downloading media. */ +public abstract class DownloadService extends Service { + + /** Starts a download service without adding a new {@link DownloadAction}. */ + public static final String ACTION_INIT = + "com.google.android.exoplayer.downloadService.action.INIT"; + + /** 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"; + + /** 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"; + + /** Starts download tasks. */ + private static final String ACTION_START_DOWNLOADS = + "com.google.android.exoplayer.downloadService.action.START_DOWNLOADS"; + + /** Stops download tasks. */ + private static final String ACTION_STOP_DOWNLOADS = + "com.google.android.exoplayer.downloadService.action.STOP_DOWNLOADS"; + + /** 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 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 ForegroundNotificationUpdater foregroundNotificationUpdater; + private final @Nullable String channelId; + private final @StringRes int channelName; + + private DownloadManager downloadManager; + private DownloadManagerListener downloadManagerListener; + private int lastStartId; + private boolean startedInForeground; + + /** + * Creates a DownloadService with {@link #DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL}. + * + * @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 buildAddActionIntent( + Context context, + Class clazz, + DownloadAction downloadAction, + boolean foreground) { + return new Intent(context, clazz) + .setAction(ACTION_ADD) + .putExtra(KEY_DOWNLOAD_ACTION, downloadAction.toByteArray()) + .putExtra(KEY_FOREGROUND, foreground); + } + + /** + * Starts the service, adding an action to be executed. + * + * @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 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(); + downloadManagerListener = new DownloadManagerListener(); + downloadManager.addListener(downloadManagerListener); + + 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(); + } + + @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"); + foregroundNotificationUpdater.stopPeriodicUpdates(); + downloadManager.removeListener(downloadManagerListener); + if (downloadManager.getTaskCount() == 0) { + synchronized (requirementsHelpers) { + RequirementsHelper requirementsHelper = requirementsHelpers.remove(getClass()); + if (requirementsHelper != null) { + requirementsHelper.stop(); + } + } + } + } + + @Nullable + @Override + public IBinder onBind(Intent intent) { + return null; + } + + /** + * Returns a {@link DownloadManager} to be used to downloaded content. Called only once in the + * 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} 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. By default the only requirement is that the + * device has network connectivity. + */ + protected Requirements getRequirements() { + return new Requirements(Requirements.NETWORK_TYPE_ANY, false, false); + } + + /** + * 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. + } + + 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); + } + + private void logd(String message) { + if (DEBUG) { + Log.d(TAG, message); + } + } + + private final class DownloadManagerListener implements DownloadManager.Listener { + @Override + public void onInitialized(DownloadManager downloadManager) { + // Do nothing. + } + + @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 periodicUpdatesStarted; + private boolean notificationDisplayed; + + public ForegroundNotificationUpdater(int notificationId, long updateInterval) { + this.notificationId = notificationId; + this.updateInterval = updateInterval; + this.handler = new Handler(Looper.getMainLooper()); + } + + 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() { + update(); + } + } + + 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() { + requirementsWatcher.start(); + } + + public void stop() { + requirementsWatcher.stop(); + if (scheduler != null) { + scheduler.cancel(); + } + } + + @Override + public void requirementsMet(RequirementsWatcher requirementsWatcher) { + startServiceWithAction(DownloadService.ACTION_START_DOWNLOADS); + if (scheduler != null) { + scheduler.cancel(); + } + } + + @Override + public void requirementsNotMet(RequirementsWatcher requirementsWatcher) { + startServiceWithAction(DownloadService.ACTION_STOP_DOWNLOADS); + if (scheduler != null) { + 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).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 new file mode 100644 index 0000000000..02ef7a7aa7 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloadAction.java @@ -0,0 +1,108 @@ +/* + * 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.offline; + +import android.net.Uri; +import android.support.annotation.Nullable; +import com.google.android.exoplayer2.upstream.cache.CacheUtil; +import com.google.android.exoplayer2.util.Util; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; + +/** An action to download or remove downloaded progressive streams. */ +public final class ProgressiveDownloadAction extends DownloadAction { + + private static final String TYPE = "progressive"; + private static final int VERSION = 0; + + 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); + } + }; + + 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. + */ + public ProgressiveDownloadAction( + Uri uri, boolean isRemoveAction, @Nullable byte[] data, @Nullable String customCacheKey) { + super(TYPE, VERSION, uri, isRemoveAction, data); + this.customCacheKey = customCacheKey; + } + + @Override + protected ProgressiveDownloader createDownloader(DownloaderConstructorHelper constructorHelper) { + return new ProgressiveDownloader(uri, customCacheKey, constructorHelper); + } + + @Override + protected void writeToStream(DataOutputStream output) throws IOException { + 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); + } + } + + @Override + public boolean isSameMedia(DownloadAction other) { + return ((other instanceof ProgressiveDownloadAction) + && getCacheKey().equals(((ProgressiveDownloadAction) other).getCacheKey())); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!super.equals(o)) { + return false; + } + ProgressiveDownloadAction that = (ProgressiveDownloadAction) o; + return Util.areEqual(customCacheKey, that.customCacheKey); + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + (customCacheKey != null ? customCacheKey.hashCode() : 0); + return result; + } + + private String getCacheKey() { + 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 new file mode 100644 index 0000000000..f6a32a1253 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloadAction.java @@ -0,0 +1,133 @@ +/* + * 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.offline; + +import android.net.Uri; +import android.support.annotation.Nullable; +import com.google.android.exoplayer2.util.Assertions; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +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 { + + /** + * Base class for {@link SegmentDownloadAction} {@link Deserializer}s. + * + * @param The type of the representation key object. + */ + protected abstract static class SegmentDownloadActionDeserializer extends Deserializer { + + public SegmentDownloadActionDeserializer(String type, int version) { + super(type, version); + } + + @Override + 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(); + List keys = new ArrayList<>(); + for (int i = 0; i < keyCount; i++) { + keys.add(readKey(input)); + } + return createDownloadAction(uri, isRemoveAction, data, keys); + } + + /** Deserializes a key from the {@code input}. */ + protected abstract K readKey(DataInputStream input) throws IOException; + + /** Returns a {@link DownloadAction}. */ + protected abstract DownloadAction createDownloadAction( + Uri manifestUri, boolean isRemoveAction, byte[] data, List keys); + } + + public final List keys; + + /** + * @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( + 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 void writeToStream(DataOutputStream output) throws IOException { + 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 equals(Object o) { + if (this == o) { + return true; + } + if (!super.equals(o)) { + return false; + } + SegmentDownloadAction that = (SegmentDownloadAction) o; + return keys.equals(that.keys); + } + + @Override + public int hashCode() { + int result = super.hashCode(); + 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 6abb950254..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 are null or empty, all representations are downloaded. - */ - public final void selectRepresentations(K[] keys) { - this.keys = (keys != null && 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 new file mode 100644 index 0000000000..b3737eb8bc --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/PlatformScheduler.java @@ -0,0 +1,170 @@ +/* + * 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.scheduler; + +import android.annotation.TargetApi; +import android.app.job.JobInfo; +import android.app.job.JobParameters; +import android.app.job.JobScheduler; +import android.app.job.JobService; +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} that uses {@link JobScheduler}. To use this scheduler, you must add {@link + * PlatformSchedulerService} to your manifest: + * + *

    {@literal
    + * 
    + *
    + * 
    + * }
    + */ +@TargetApi(21) +public final class PlatformScheduler implements Scheduler { + + private static final String TAG = "PlatformScheduler"; + 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 ComponentName jobServiceComponentName; + private final JobScheduler jobScheduler; + + /** + * @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. + */ + @RequiresPermission(android.Manifest.permission.RECEIVE_BOOT_COMPLETED) + public PlatformScheduler(Context context, int jobId) { + this.jobId = jobId; + jobServiceComponentName = new ComponentName(context, PlatformSchedulerService.class); + jobScheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE); + } + + @Override + public boolean schedule(Requirements requirements, String servicePackage, String serviceAction) { + JobInfo jobInfo = + buildJobInfo(jobId, jobServiceComponentName, requirements, serviceAction, servicePackage); + int result = jobScheduler.schedule(jobInfo); + logd("Scheduling job: " + jobId + " result: " + result); + return result == JobScheduler.RESULT_SUCCESS; + } + + @Override + public boolean cancel() { + logd("Canceling job: " + jobId); + jobScheduler.cancel(jobId); + return true; + } + + // @RequiresPermission constructor annotation should ensure the permission is present. + @SuppressWarnings("MissingPermission") + private static JobInfo buildJobInfo( + int jobId, + ComponentName jobServiceComponentName, + Requirements requirements, + String serviceAction, + String servicePackage) { + JobInfo.Builder builder = new JobInfo.Builder(jobId, jobServiceComponentName); + + int networkType; + switch (requirements.getRequiredNetworkType()) { + case Requirements.NETWORK_TYPE_NONE: + networkType = JobInfo.NETWORK_TYPE_NONE; + break; + case Requirements.NETWORK_TYPE_ANY: + networkType = JobInfo.NETWORK_TYPE_ANY; + break; + case Requirements.NETWORK_TYPE_UNMETERED: + networkType = JobInfo.NETWORK_TYPE_UNMETERED; + break; + case Requirements.NETWORK_TYPE_NOT_ROAMING: + if (Util.SDK_INT >= 24) { + networkType = JobInfo.NETWORK_TYPE_NOT_ROAMING; + } else { + throw new UnsupportedOperationException(); + } + break; + case Requirements.NETWORK_TYPE_METERED: + if (Util.SDK_INT >= 26) { + networkType = JobInfo.NETWORK_TYPE_METERED; + } else { + throw new UnsupportedOperationException(); + } + break; + default: + throw new UnsupportedOperationException(); + } + + builder.setRequiredNetworkType(networkType); + builder.setRequiresDeviceIdle(requirements.isIdleRequired()); + builder.setRequiresCharging(requirements.isChargingRequired()); + builder.setPersisted(true); + + PersistableBundle extras = new PersistableBundle(); + extras.putString(KEY_SERVICE_ACTION, serviceAction); + extras.putString(KEY_SERVICE_PACKAGE, servicePackage); + extras.putInt(KEY_REQUIREMENTS, requirements.getRequirementsData()); + builder.setExtras(extras); + + return builder.build(); + } + + private static void logd(String message) { + if (DEBUG) { + Log.d(TAG, message); + } + } + + /** 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 started"); + PersistableBundle extras = params.getExtras(); + Requirements requirements = new Requirements(extras.getInt(KEY_REQUIREMENTS)); + if (requirements.checkRequirements(this)) { + 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); + Util.startForegroundService(this, intent); + } else { + logd("Requirements are not met"); + jobFinished(params, /* needsReschedule */ true); + } + return false; + } + + @Override + public boolean onStopJob(JobParameters params) { + 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 new file mode 100644 index 0000000000..30b07da3eb --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Requirements.java @@ -0,0 +1,237 @@ +/* + * 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.scheduler; + +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.ConnectivityManager; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.net.NetworkInfo; +import android.os.BatteryManager; +import android.os.PowerManager; +import android.support.annotation.IntDef; +import android.util.Log; +import com.google.android.exoplayer2.util.Util; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Defines a set of device state requirements. + */ +public final class Requirements { + + /** Network types. */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + NETWORK_TYPE_NONE, + NETWORK_TYPE_ANY, + NETWORK_TYPE_UNMETERED, + NETWORK_TYPE_NOT_ROAMING, + NETWORK_TYPE_METERED, + }) + public @interface NetworkType {} + /** This job doesn't require network connectivity. */ + public static final int NETWORK_TYPE_NONE = 0; + /** This job requires network connectivity. */ + public static final int NETWORK_TYPE_ANY = 1; + /** This job requires network connectivity that is unmetered. */ + public static final int NETWORK_TYPE_UNMETERED = 2; + /** This job requires network connectivity that is not roaming. */ + public static final int NETWORK_TYPE_NOT_ROAMING = 3; + /** This job requires metered connectivity such as most cellular data networks. */ + public static final int NETWORK_TYPE_METERED = 4; + /** This job requires the device to be idle. */ + private static final int DEVICE_IDLE = 8; + /** This job requires the device to be charging. */ + private static final int DEVICE_CHARGING = 16; + + private static final int NETWORK_TYPE_MASK = 7; + + private static final String TAG = "Requirements"; + + private static final String[] NETWORK_TYPE_STRINGS; + + static { + if (Scheduler.DEBUG) { + NETWORK_TYPE_STRINGS = + new String[] { + "NETWORK_TYPE_NONE", + "NETWORK_TYPE_ANY", + "NETWORK_TYPE_UNMETERED", + "NETWORK_TYPE_NOT_ROAMING", + "NETWORK_TYPE_METERED" + }; + } else { + NETWORK_TYPE_STRINGS = null; + } + } + + private final int requirements; + + /** + * @param networkType Required network type. + * @param charging Whether the device should be charging. + * @param idle Whether the device should be idle. + */ + public Requirements(@NetworkType int networkType, boolean charging, boolean idle) { + this(networkType | (charging ? DEVICE_CHARGING : 0) | (idle ? DEVICE_IDLE : 0)); + } + + /** @param requirementsData The value returned by {@link #getRequirementsData()}. */ + public Requirements(int requirementsData) { + this.requirements = requirementsData; + } + + /** Returns required network type. */ + public int getRequiredNetworkType() { + return requirements & NETWORK_TYPE_MASK; + } + + /** Returns whether the device should be charging. */ + public boolean isChargingRequired() { + return (requirements & DEVICE_CHARGING) != 0; + } + + /** Returns whether the device should be idle. */ + public boolean isIdleRequired() { + return (requirements & DEVICE_IDLE) != 0; + } + + /** + * Returns whether the requirements are met. + * + * @param context Any context. + */ + public boolean checkRequirements(Context context) { + return checkNetworkRequirements(context) + && checkChargingRequirement(context) + && checkIdleRequirement(context); + } + + /** Returns the encoded requirements data which can be used with {@link #Requirements(int)}. */ + public int getRequirementsData() { + return requirements; + } + + private boolean checkNetworkRequirements(Context context) { + int networkRequirement = getRequiredNetworkType(); + if (networkRequirement == NETWORK_TYPE_NONE) { + return true; + } + ConnectivityManager connectivityManager = + (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo(); + if (networkInfo == null || !networkInfo.isConnected()) { + logd("No network info or no connection."); + return false; + } + if (!checkInternetConnectivity(connectivityManager)) { + return false; + } + if (networkRequirement == NETWORK_TYPE_ANY) { + return true; + } + if (networkRequirement == NETWORK_TYPE_NOT_ROAMING) { + boolean roaming = networkInfo.isRoaming(); + logd("Roaming: " + roaming); + return !roaming; + } + boolean activeNetworkMetered = isActiveNetworkMetered(connectivityManager, networkInfo); + logd("Metered network: " + activeNetworkMetered); + if (networkRequirement == NETWORK_TYPE_UNMETERED) { + return !activeNetworkMetered; + } + if (networkRequirement == NETWORK_TYPE_METERED) { + return activeNetworkMetered; + } + throw new IllegalStateException(); + } + + private boolean checkChargingRequirement(Context context) { + if (!isChargingRequired()) { + return true; + } + Intent batteryStatus = + context.registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); + if (batteryStatus == null) { + return false; + } + int status = batteryStatus.getIntExtra(BatteryManager.EXTRA_STATUS, -1); + return status == BatteryManager.BATTERY_STATUS_CHARGING + || status == BatteryManager.BATTERY_STATUS_FULL; + } + + private boolean checkIdleRequirement(Context context) { + if (!isIdleRequired()) { + return true; + } + PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + return Util.SDK_INT >= 23 + ? !powerManager.isDeviceIdleMode() + : Util.SDK_INT >= 20 ? !powerManager.isInteractive() : !powerManager.isScreenOn(); + } + + private static boolean checkInternetConnectivity(ConnectivityManager connectivityManager) { + if (Util.SDK_INT < 23) { + // TODO Check internet connectivity using http://clients3.google.com/generate_204 on API + // levels prior to 23. + return true; + } + Network activeNetwork = connectivityManager.getActiveNetwork(); + if (activeNetwork == null) { + logd("No active network."); + return false; + } + NetworkCapabilities networkCapabilities = + connectivityManager.getNetworkCapabilities(activeNetwork); + boolean validated = + networkCapabilities == null + || !networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED); + logd("Network capability validated: " + validated); + return !validated; + } + + private static boolean isActiveNetworkMetered( + ConnectivityManager connectivityManager, NetworkInfo networkInfo) { + if (Util.SDK_INT >= 16) { + return connectivityManager.isActiveNetworkMetered(); + } + int type = networkInfo.getType(); + return type != ConnectivityManager.TYPE_WIFI + && type != ConnectivityManager.TYPE_BLUETOOTH + && type != ConnectivityManager.TYPE_ETHERNET; + } + + private static void logd(String message) { + if (Scheduler.DEBUG) { + Log.d(TAG, message); + } + } + + @Override + public String toString() { + if (!Scheduler.DEBUG) { + return super.toString(); + } + return "requirements{" + + NETWORK_TYPE_STRINGS[getRequiredNetworkType()] + + (isChargingRequired() ? ",charging" : "") + + (isIdleRequired() ? ",idle" : "") + + '}'; + } +} 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 new file mode 100644 index 0000000000..46aa55f094 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/RequirementsWatcher.java @@ -0,0 +1,211 @@ +/* + * 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.scheduler; + +import android.annotation.TargetApi; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.ConnectivityManager; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.net.NetworkRequest; +import android.os.Handler; +import android.os.Looper; +import android.os.PowerManager; +import android.support.annotation.RequiresApi; +import android.util.Log; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; + +/** + * Watches whether the {@link Requirements} are met and notifies the {@link Listener} on changes. + */ +public final class RequirementsWatcher { + + /** + * Notified when RequirementsWatcher instance first created and on changes whether the {@link + * Requirements} are met. + */ + public interface Listener { + + /** + * Called when the requirements are met. + * + * @param requirementsWatcher Calling instance. + */ + void requirementsMet(RequirementsWatcher requirementsWatcher); + + /** + * Called when the requirements are not met. + * + * @param requirementsWatcher Calling instance. + */ + void requirementsNotMet(RequirementsWatcher requirementsWatcher); + } + + private static final String TAG = "RequirementsWatcher"; + + private final Context context; + private final Listener listener; + private final Requirements requirements; + private DeviceStatusChangeReceiver receiver; + + private boolean requirementsWereMet; + private CapabilityValidatedCallback networkCallback; + + /** + * @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.getApplicationContext(); + logd(this + " created"); + } + + /** + * Starts watching for changes. Must be called from a thread that has an associated {@link + * Looper}. Listener methods are called on the caller thread. + */ + public void start() { + Assertions.checkNotNull(Looper.myLooper()); + + checkRequirements(true); + + IntentFilter filter = new IntentFilter(); + if (requirements.getRequiredNetworkType() != Requirements.NETWORK_TYPE_NONE) { + if (Util.SDK_INT >= 23) { + registerNetworkCallbackV23(); + } else { + filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION); + } + } + if (requirements.isChargingRequired()) { + filter.addAction(Intent.ACTION_POWER_CONNECTED); + filter.addAction(Intent.ACTION_POWER_DISCONNECTED); + } + if (requirements.isIdleRequired()) { + if (Util.SDK_INT >= 23) { + filter.addAction(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED); + } else { + filter.addAction(Intent.ACTION_SCREEN_ON); + filter.addAction(Intent.ACTION_SCREEN_OFF); + } + } + receiver = new DeviceStatusChangeReceiver(); + context.registerReceiver(receiver, filter, null, new Handler()); + logd(this + " started"); + } + + /** Stops watching for changes. */ + public void stop() { + context.unregisterReceiver(receiver); + receiver = null; + if (networkCallback != null) { + unregisterNetworkCallback(); + } + logd(this + " stopped"); + } + + /** Returns watched {@link Requirements}. */ + public Requirements getRequirements() { + return requirements; + } + + @Override + public String toString() { + if (!Scheduler.DEBUG) { + return super.toString(); + } + return "RequirementsWatcher{" + requirements + '}'; + } + + @TargetApi(23) + private void registerNetworkCallbackV23() { + ConnectivityManager connectivityManager = + (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkRequest request = + new NetworkRequest.Builder() + .addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) + .build(); + networkCallback = new CapabilityValidatedCallback(); + connectivityManager.registerNetworkCallback(request, networkCallback); + } + + private void unregisterNetworkCallback() { + if (Util.SDK_INT >= 21) { + ConnectivityManager connectivityManager = + (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + connectivityManager.unregisterNetworkCallback(networkCallback); + networkCallback = null; + } + } + + private void checkRequirements(boolean force) { + boolean requirementsAreMet = requirements.checkRequirements(context); + if (!force) { + if (requirementsAreMet == requirementsWereMet) { + logd("requirementsAreMet is still " + requirementsAreMet); + return; + } + } + requirementsWereMet = requirementsAreMet; + if (requirementsAreMet) { + logd("start job"); + listener.requirementsMet(this); + } else { + logd("stop job"); + listener.requirementsNotMet(this); + } + } + + private static void logd(String message) { + if (Scheduler.DEBUG) { + Log.d(TAG, message); + } + } + + private class DeviceStatusChangeReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + if (!isInitialStickyBroadcast()) { + logd(RequirementsWatcher.this + " received " + intent.getAction()); + checkRequirements(false); + } + } + } + + @RequiresApi(api = 21) + private final class CapabilityValidatedCallback extends ConnectivityManager.NetworkCallback { + @Override + public void onAvailable(Network network) { + super.onAvailable(network); + logd(RequirementsWatcher.this + " NetworkCallback.onAvailable"); + checkRequirements(false); + } + + @Override + public void onLost(Network network) { + super.onLost(network); + logd(RequirementsWatcher.this + " NetworkCallback.onLost"); + checkRequirements(false); + } + } +} 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 new file mode 100644 index 0000000000..1b225d9a4d --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Scheduler.java @@ -0,0 +1,50 @@ +/* + * 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.scheduler; + +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; + + /** + * Schedules a service to be started in the foreground when some {@link Requirements} are met. + * Anything that was previously scheduled will be canceled. + * + *

    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(Requirements requirements, String servicePackage, String serviceAction); + + /** + * Cancels anything that was previously scheduled, or else does nothing. + * + * @return Whether cancellation was successful. + */ + boolean cancel(); +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/AbstractConcatenatedTimeline.java b/library/core/src/main/java/com/google/android/exoplayer2/source/AbstractConcatenatedTimeline.java index 696a6f6fad..8663b4c05c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/AbstractConcatenatedTimeline.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/AbstractConcatenatedTimeline.java @@ -155,13 +155,14 @@ import com.google.android.exoplayer2.Timeline; } @Override - public final Window getWindow(int windowIndex, Window window, boolean setIds, - long defaultPositionProjectionUs) { + public final Window getWindow( + int windowIndex, Window window, boolean setTag, long defaultPositionProjectionUs) { int childIndex = getChildIndexByWindowIndex(windowIndex); int firstWindowIndexInChild = getFirstWindowIndexByChildIndex(childIndex); int firstPeriodIndexInChild = getFirstPeriodIndexByChildIndex(childIndex); - getTimelineByChildIndex(childIndex).getWindow(windowIndex - firstWindowIndexInChild, window, - setIds, defaultPositionProjectionUs); + getTimelineByChildIndex(childIndex) + .getWindow( + windowIndex - firstWindowIndexInChild, window, setTag, defaultPositionProjectionUs); window.firstPeriodIndex += firstPeriodIndexInChild; window.lastPeriodIndex += firstPeriodIndexInChild; return window; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/BaseMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/BaseMediaSource.java new file mode 100644 index 0000000000..32526361f5 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/BaseMediaSource.java @@ -0,0 +1,153 @@ +/* + * 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; + +import android.os.Handler; +import android.support.annotation.Nullable; +import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.util.Assertions; +import java.util.ArrayList; + +/** + * Base {@link MediaSource} implementation to handle parallel reuse and to keep a list of {@link + * MediaSourceEventListener}s. + * + *

    Whenever an implementing subclass needs to provide a new timeline and/or manifest, it must + * call {@link #refreshSourceInfo(Timeline, Object)} to notify all listeners. + */ +public abstract class BaseMediaSource implements MediaSource { + + private final ArrayList sourceInfoListeners; + private final MediaSourceEventListener.EventDispatcher eventDispatcher; + + private ExoPlayer player; + private Timeline timeline; + private Object manifest; + + public BaseMediaSource() { + sourceInfoListeners = new ArrayList<>(/* initialCapacity= */ 1); + eventDispatcher = new MediaSourceEventListener.EventDispatcher(); + } + + /** + * Starts source preparation. This method is called at most once until the next call to {@link + * #releaseSourceInternal()}. + * + * @param player The player for which this source is being prepared. + * @param isTopLevelSource Whether this source has been passed directly to {@link + * ExoPlayer#prepare(MediaSource)} or {@link ExoPlayer#prepare(MediaSource, boolean, + * boolean)}. + */ + protected abstract void prepareSourceInternal(ExoPlayer player, boolean isTopLevelSource); + + /** + * Releases the source. This method is called exactly once after each call to {@link + * #prepareSourceInternal(ExoPlayer, boolean)}. + */ + protected abstract void releaseSourceInternal(); + + /** + * Updates timeline and manifest and notifies all listeners of the update. + * + * @param timeline The new {@link Timeline}. + * @param manifest The new manifest. May be null. + */ + protected final void refreshSourceInfo(Timeline timeline, @Nullable Object manifest) { + this.timeline = timeline; + this.manifest = manifest; + for (SourceInfoRefreshListener listener : sourceInfoListeners) { + listener.onSourceInfoRefreshed(/* source= */ this, timeline, manifest); + } + } + + /** + * Returns a {@link MediaSourceEventListener.EventDispatcher} which dispatches all events to the + * registered listeners with the specified media period id. + * + * @param mediaPeriodId The {@link MediaPeriodId} to be reported with the events. May be null, if + * the events do not belong to a specific media period. + * @return An event dispatcher with pre-configured media period id. + */ + protected final MediaSourceEventListener.EventDispatcher createEventDispatcher( + @Nullable MediaPeriodId mediaPeriodId) { + return eventDispatcher.withParameters( + /* windowIndex= */ 0, mediaPeriodId, /* mediaTimeOffsetMs= */ 0); + } + + /** + * Returns a {@link MediaSourceEventListener.EventDispatcher} which dispatches all events to the + * registered listeners with the specified media period id and time offset. + * + * @param mediaPeriodId The {@link MediaPeriodId} to be reported with the events. + * @param mediaTimeOffsetMs The offset to be added to all media times, in milliseconds. + * @return An event dispatcher with pre-configured media period id and time offset. + */ + protected final MediaSourceEventListener.EventDispatcher createEventDispatcher( + MediaPeriodId mediaPeriodId, long mediaTimeOffsetMs) { + Assertions.checkArgument(mediaPeriodId != null); + return eventDispatcher.withParameters(/* windowIndex= */ 0, mediaPeriodId, mediaTimeOffsetMs); + } + + /** + * Returns a {@link MediaSourceEventListener.EventDispatcher} which dispatches all events to the + * registered listeners with the specified window index, media period id and time offset. + * + * @param windowIndex The timeline window index to be reported with the events. + * @param mediaPeriodId The {@link MediaPeriodId} to be reported with the events. May be null, if + * the events do not belong to a specific media period. + * @param mediaTimeOffsetMs The offset to be added to all media times, in milliseconds. + * @return An event dispatcher with pre-configured media period id and time offset. + */ + protected final MediaSourceEventListener.EventDispatcher createEventDispatcher( + int windowIndex, @Nullable MediaPeriodId mediaPeriodId, long mediaTimeOffsetMs) { + return eventDispatcher.withParameters(windowIndex, mediaPeriodId, mediaTimeOffsetMs); + } + + @Override + public final void addEventListener(Handler handler, MediaSourceEventListener eventListener) { + eventDispatcher.addEventListener(handler, eventListener); + } + + @Override + public final void removeEventListener(MediaSourceEventListener eventListener) { + eventDispatcher.removeEventListener(eventListener); + } + + @Override + public final void prepareSource( + ExoPlayer player, boolean isTopLevelSource, SourceInfoRefreshListener listener) { + Assertions.checkArgument(this.player == null || this.player == player); + sourceInfoListeners.add(listener); + if (this.player == null) { + this.player = player; + prepareSourceInternal(player, isTopLevelSource); + } else if (timeline != null) { + listener.onSourceInfoRefreshed(/* source= */ this, timeline, manifest); + } + } + + @Override + public final void releaseSource(SourceInfoRefreshListener listener) { + sourceInfoListeners.remove(listener); + if (sourceInfoListeners.isEmpty()) { + player = null; + timeline = null; + manifest = null; + releaseSourceInternal(); + } + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java index f14c0faad4..c078053110 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java @@ -23,6 +23,7 @@ import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; +import com.google.android.exoplayer2.util.Util; import java.io.IOException; /** @@ -43,35 +44,36 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb /* package */ long endUs; /** - * Creates a new clipping media period that provides a clipped view of the specified - * {@link MediaPeriod}'s sample streams. - *

    - * The clipping start/end positions must be specified by calling {@link #setClipping(long, long)} - * on the playback thread before preparation completes. - *

    - * If the start point is guaranteed to be a key frame, pass {@code false} to {@code + * Creates a new clipping media period that provides a clipped view of the specified {@link + * MediaPeriod}'s sample streams. + * + *

    If the start point is guaranteed to be a key frame, pass {@code false} to {@code * enableInitialPositionDiscontinuity} to suppress an initial discontinuity when the period is * first read from. * * @param mediaPeriod The media period to clip. * @param enableInitialDiscontinuity Whether the initial discontinuity should be enabled. + * @param startUs The clipping start time, in microseconds. + * @param endUs The clipping end time, in microseconds, or {@link C#TIME_END_OF_SOURCE} to + * indicate the end of the period. */ - public ClippingMediaPeriod(MediaPeriod mediaPeriod, boolean enableInitialDiscontinuity) { + public ClippingMediaPeriod( + MediaPeriod mediaPeriod, boolean enableInitialDiscontinuity, long startUs, long endUs) { this.mediaPeriod = mediaPeriod; sampleStreams = new ClippingSampleStream[0]; - pendingInitialDiscontinuityPositionUs = enableInitialDiscontinuity ? 0 : C.TIME_UNSET; - startUs = C.TIME_UNSET; - endUs = C.TIME_UNSET; + pendingInitialDiscontinuityPositionUs = enableInitialDiscontinuity ? startUs : C.TIME_UNSET; + this.startUs = startUs; + this.endUs = endUs; } /** - * Sets the clipping start/end times for this period, in microseconds. + * Updates the clipping start/end times for this period, in microseconds. * * @param startUs The clipping start time, in microseconds. * @param endUs The clipping end time, in microseconds, or {@link C#TIME_END_OF_SOURCE} to * indicate the end of the period. */ - public void setClipping(long startUs, long endUs) { + public void updateClipping(long startUs, long endUs) { this.startUs = startUs; this.endUs = endUs; } @@ -79,7 +81,7 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb @Override public void prepare(MediaPeriod.Callback callback, long positionUs) { this.callback = callback; - mediaPeriod.prepare(this, startUs + positionUs); + mediaPeriod.prepare(this, positionUs); } @Override @@ -101,13 +103,19 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb sampleStreams[i] = (ClippingSampleStream) streams[i]; childStreams[i] = sampleStreams[i] != null ? sampleStreams[i].childStream : null; } - long enablePositionUs = mediaPeriod.selectTracks(selections, mayRetainStreamFlags, - childStreams, streamResetFlags, positionUs + startUs) - startUs; - pendingInitialDiscontinuityPositionUs = isPendingInitialDiscontinuity() && positionUs == 0 - && shouldKeepInitialDiscontinuity(startUs, selections) ? enablePositionUs : C.TIME_UNSET; - Assertions.checkState(enablePositionUs == positionUs - || (enablePositionUs >= 0 - && (endUs == C.TIME_END_OF_SOURCE || startUs + enablePositionUs <= endUs))); + long enablePositionUs = + mediaPeriod.selectTracks( + selections, mayRetainStreamFlags, childStreams, streamResetFlags, positionUs); + pendingInitialDiscontinuityPositionUs = + isPendingInitialDiscontinuity() + && positionUs == startUs + && shouldKeepInitialDiscontinuity(startUs, selections) + ? enablePositionUs + : C.TIME_UNSET; + Assertions.checkState( + enablePositionUs == positionUs + || (enablePositionUs >= startUs + && (endUs == C.TIME_END_OF_SOURCE || enablePositionUs <= endUs))); for (int i = 0; i < streams.length; i++) { if (childStreams[i] == null) { sampleStreams[i] = null; @@ -121,12 +129,12 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb @Override public void discardBuffer(long positionUs, boolean toKeyframe) { - mediaPeriod.discardBuffer(positionUs + startUs, toKeyframe); + mediaPeriod.discardBuffer(positionUs, toKeyframe); } @Override public void reevaluateBuffer(long positionUs) { - mediaPeriod.reevaluateBuffer(positionUs + startUs); + mediaPeriod.reevaluateBuffer(positionUs); } @Override @@ -144,7 +152,7 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb } Assertions.checkState(discontinuityUs >= startUs); Assertions.checkState(endUs == C.TIME_END_OF_SOURCE || discontinuityUs <= endUs); - return discontinuityUs - startUs; + return discontinuityUs; } @Override @@ -154,7 +162,7 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb || (endUs != C.TIME_END_OF_SOURCE && bufferedPositionUs >= endUs)) { return C.TIME_END_OF_SOURCE; } - return Math.max(0, bufferedPositionUs - startUs); + return bufferedPositionUs; } @Override @@ -165,23 +173,21 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb sampleStream.clearSentEos(); } } - long offsetPositionUs = positionUs + startUs; - long seekUs = mediaPeriod.seekToUs(offsetPositionUs); + long seekUs = mediaPeriod.seekToUs(positionUs); Assertions.checkState( - seekUs == offsetPositionUs + seekUs == positionUs || (seekUs >= startUs && (endUs == C.TIME_END_OF_SOURCE || seekUs <= endUs))); - return seekUs - startUs; + return seekUs; } @Override public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) { if (positionUs == startUs) { // Never adjust seeks to the start of the clipped view. - return 0; + return startUs; } - long offsetPositionUs = positionUs + startUs; - SeekParameters clippedSeekParameters = clipSeekParameters(offsetPositionUs, seekParameters); - return mediaPeriod.getAdjustedSeekPositionUs(offsetPositionUs, clippedSeekParameters) - startUs; + SeekParameters clippedSeekParameters = clipSeekParameters(positionUs, seekParameters); + return mediaPeriod.getAdjustedSeekPositionUs(positionUs, clippedSeekParameters); } @Override @@ -191,19 +197,18 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb || (endUs != C.TIME_END_OF_SOURCE && nextLoadPositionUs >= endUs)) { return C.TIME_END_OF_SOURCE; } - return nextLoadPositionUs - startUs; + return nextLoadPositionUs; } @Override public boolean continueLoading(long positionUs) { - return mediaPeriod.continueLoading(positionUs + startUs); + return mediaPeriod.continueLoading(positionUs); } // MediaPeriod.Callback implementation. @Override public void onPrepared(MediaPeriod mediaPeriod) { - Assertions.checkState(startUs != C.TIME_UNSET && endUs != C.TIME_UNSET); callback.onPrepared(this); } @@ -216,17 +221,20 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb return pendingInitialDiscontinuityPositionUs != C.TIME_UNSET; } - private SeekParameters clipSeekParameters(long offsetPositionUs, SeekParameters seekParameters) { - long toleranceBeforeMs = Math.min(offsetPositionUs - startUs, seekParameters.toleranceBeforeUs); - long toleranceAfterMs = - endUs == C.TIME_END_OF_SOURCE - ? seekParameters.toleranceAfterUs - : Math.min(endUs - offsetPositionUs, seekParameters.toleranceAfterUs); - if (toleranceBeforeMs == seekParameters.toleranceBeforeUs - && toleranceAfterMs == seekParameters.toleranceAfterUs) { + private SeekParameters clipSeekParameters(long positionUs, SeekParameters seekParameters) { + long toleranceBeforeUs = + Util.constrainValue( + seekParameters.toleranceBeforeUs, /* min= */ 0, /* max= */ positionUs - startUs); + long toleranceAfterUs = + Util.constrainValue( + seekParameters.toleranceAfterUs, + /* min= */ 0, + /* max= */ endUs == C.TIME_END_OF_SOURCE ? Long.MAX_VALUE : endUs - positionUs); + if (toleranceBeforeUs == seekParameters.toleranceBeforeUs + && toleranceAfterUs == seekParameters.toleranceAfterUs) { return seekParameters; } else { - return new SeekParameters(toleranceBeforeMs, toleranceAfterMs); + return new SeekParameters(toleranceBeforeUs, toleranceAfterUs); } } @@ -293,7 +301,7 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb int result = childStream.readData(formatHolder, buffer, requireFormat); if (result == C.RESULT_FORMAT_READ) { Format format = formatHolder.format; - if (format.encoderDelay != Format.NO_VALUE || format.encoderPadding != Format.NO_VALUE) { + if (format.encoderDelay != 0 || format.encoderPadding != 0) { // Clear gapless playback metadata if the start/end points don't match the media. int encoderDelay = startUs != 0 ? 0 : format.encoderDelay; int encoderPadding = endUs != C.TIME_END_OF_SOURCE ? 0 : format.encoderPadding; @@ -310,9 +318,6 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb sentEos = true; return C.RESULT_BUFFER_READ; } - if (result == C.RESULT_BUFFER_READ && !buffer.isEndOfStream()) { - buffer.timeUs -= startUs; - } return result; } @@ -321,7 +326,7 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb if (isPendingInitialDiscontinuity()) { return C.RESULT_NOTHING_READ; } - return childStream.skipData(startUs + positionUs); + return childStream.skipData(positionUs); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java index 9ff704e75a..f633dd8f15 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java @@ -29,111 +29,190 @@ import java.util.ArrayList; /** * {@link MediaSource} that wraps a source and clips its timeline based on specified start/end - * positions. The wrapped source must consist of a single period that starts at the beginning of the - * corresponding window. + * positions. The wrapped source must consist of a single period. */ public final class ClippingMediaSource extends CompositeMediaSource { - /** - * Thrown when a {@link ClippingMediaSource} cannot clip its wrapped source. - */ + /** Thrown when a {@link ClippingMediaSource} cannot clip its wrapped source. */ public static final class IllegalClippingException extends IOException { - /** - * The reason the clipping failed. - */ + /** The reason clipping failed. */ @Retention(RetentionPolicy.SOURCE) - @IntDef({REASON_INVALID_PERIOD_COUNT, REASON_PERIOD_OFFSET_IN_WINDOW, - REASON_NOT_SEEKABLE_TO_START, REASON_START_EXCEEDS_END}) + @IntDef({REASON_INVALID_PERIOD_COUNT, REASON_NOT_SEEKABLE_TO_START, REASON_START_EXCEEDS_END}) public @interface Reason {} - /** - * The wrapped source doesn't consist of a single period. - */ + /** The wrapped source doesn't consist of a single period. */ public static final int REASON_INVALID_PERIOD_COUNT = 0; - /** - * The wrapped source period doesn't start at the beginning of the corresponding window. - */ - public static final int REASON_PERIOD_OFFSET_IN_WINDOW = 1; - /** - * The wrapped source is not seekable and a non-zero clipping start position was specified. - */ - public static final int REASON_NOT_SEEKABLE_TO_START = 2; - /** - * The wrapped source ends before the specified clipping start position. - */ - public static final int REASON_START_EXCEEDS_END = 3; + /** The wrapped source is not seekable and a non-zero clipping start position was specified. */ + public static final int REASON_NOT_SEEKABLE_TO_START = 1; + /** The wrapped source ends before the specified clipping start position. */ + public static final int REASON_START_EXCEEDS_END = 2; - /** - * The reason clipping failed. - */ - @Reason - public final int reason; + /** The reason clipping failed. */ + public final @Reason int reason; /** * @param reason The reason clipping failed. */ public IllegalClippingException(@Reason int reason) { + super("Illegal clipping: " + getReasonDescription(reason)); this.reason = reason; } + private static String getReasonDescription(@Reason int reason) { + switch (reason) { + case REASON_INVALID_PERIOD_COUNT: + return "invalid period count"; + case REASON_NOT_SEEKABLE_TO_START: + return "not seekable to start"; + case REASON_START_EXCEEDS_END: + return "start exceeds end"; + default: + return "unknown"; + } + } } private final MediaSource mediaSource; private final long startUs; private final long endUs; private final boolean enableInitialDiscontinuity; + private final boolean allowDynamicClippingUpdates; + private final boolean relativeToDefaultPosition; private final ArrayList mediaPeriods; + private final Timeline.Window window; - private MediaSource.Listener sourceListener; + private @Nullable Object manifest; + private ClippingTimeline clippingTimeline; private IllegalClippingException clippingError; + private long periodStartUs; + private long periodEndUs; /** - * Creates a new clipping source that wraps the specified source. + * Creates a new clipping source that wraps the specified source and provides samples between the + * specified start and end position. * * @param mediaSource The single-period source to wrap. - * @param startPositionUs The start position within {@code mediaSource}'s timeline at which to - * start providing samples, in microseconds. - * @param endPositionUs The end position within {@code mediaSource}'s timeline at which to stop + * @param startPositionUs The start position within {@code mediaSource}'s window at which to start + * providing samples, in microseconds. + * @param endPositionUs The end position within {@code mediaSource}'s window at which to stop * providing samples, in microseconds. Specify {@link C#TIME_END_OF_SOURCE} to provide samples * from the specified start point up to the end of the source. Specifying a position that * exceeds the {@code mediaSource}'s duration will also result in the end of the source not * being clipped. */ public ClippingMediaSource(MediaSource mediaSource, long startPositionUs, long endPositionUs) { - this(mediaSource, startPositionUs, endPositionUs, true); + this( + mediaSource, + startPositionUs, + endPositionUs, + /* enableInitialDiscontinuity= */ true, + /* allowDynamicClippingUpdates= */ false, + /* relativeToDefaultPosition= */ false); } /** - * Creates a new clipping source that wraps the specified source. - *

    - * If the start point is guaranteed to be a key frame, pass {@code false} to - * {@code enableInitialPositionDiscontinuity} to suppress an initial discontinuity when a period - * is first read from. + * Creates a new clipping source that wraps the specified source and provides samples between the + * specified start and end position. * * @param mediaSource The single-period source to wrap. - * @param startPositionUs The start position within {@code mediaSource}'s timeline at which to - * start providing samples, in microseconds. - * @param endPositionUs The end position within {@code mediaSource}'s timeline at which to stop + * @param startPositionUs The start position within {@code mediaSource}'s window at which to start + * providing samples, in microseconds. + * @param endPositionUs The end position within {@code mediaSource}'s window at which to stop * providing samples, in microseconds. Specify {@link C#TIME_END_OF_SOURCE} to provide samples * from the specified start point up to the end of the source. Specifying a position that * exceeds the {@code mediaSource}'s duration will also result in the end of the source not * being clipped. * @param enableInitialDiscontinuity Whether the initial discontinuity should be enabled. */ - public ClippingMediaSource(MediaSource mediaSource, long startPositionUs, long endPositionUs, + // TODO: remove this when the new API is public. + @Deprecated + public ClippingMediaSource( + MediaSource mediaSource, + long startPositionUs, + long endPositionUs, boolean enableInitialDiscontinuity) { + this( + mediaSource, + startPositionUs, + endPositionUs, + enableInitialDiscontinuity, + /* allowDynamicClippingUpdates= */ false, + /* relativeToDefaultPosition= */ false); + } + + /** + * Creates a new clipping source that wraps the specified source and provides samples from the + * default position for the specified duration. + * + * @param mediaSource The single-period source to wrap. + * @param durationUs The duration from the default position in the window in {@code mediaSource}'s + * timeline at which to stop providing samples. Specifying a duration that exceeds the {@code + * mediaSource}'s duration will result in the end of the source not being clipped. + */ + public ClippingMediaSource(MediaSource mediaSource, long durationUs) { + this( + mediaSource, + /* startPositionUs= */ 0, + /* endPositionUs= */ durationUs, + /* enableInitialDiscontinuity= */ true, + /* allowDynamicClippingUpdates= */ false, + /* relativeToDefaultPosition= */ true); + } + + /** + * Creates a new clipping source that wraps the specified source. + * + *

    If the start point is guaranteed to be a key frame, pass {@code false} to {@code + * enableInitialPositionDiscontinuity} to suppress an initial discontinuity when a period is first + * read from. + * + *

    For live streams, if the clipping positions should move with the live window, pass {@code + * true} to {@code allowDynamicClippingUpdates}. Otherwise, the live stream ends when the playback + * reaches {@code endPositionUs} in the last reported live window at the time a media period was + * created. + * + * @param mediaSource The single-period source to wrap. + * @param startPositionUs The start position at which to start providing samples, in microseconds. + * If {@code relativeToDefaultPosition} is {@code false}, this position is relative to the + * start of the window in {@code mediaSource}'s timeline. If {@code relativeToDefaultPosition} + * is {@code true}, this position is relative to the default position in the window in {@code + * mediaSource}'s timeline. + * @param endPositionUs The end position at which to stop providing samples, in microseconds. + * Specify {@link C#TIME_END_OF_SOURCE} to provide samples from the specified start point up + * to the end of the source. Specifying a position that exceeds the {@code mediaSource}'s + * duration will also result in the end of the source not being clipped. If {@code + * relativeToDefaultPosition} is {@code false}, the specified position is relative to the + * start of the window in {@code mediaSource}'s timeline. If {@code relativeToDefaultPosition} + * is {@code true}, this position is relative to the default position in the window in {@code + * mediaSource}'s timeline. + * @param enableInitialDiscontinuity Whether the initial discontinuity should be enabled. + * @param allowDynamicClippingUpdates Whether the clipping of active media periods moves with a + * live window. If {@code false}, playback ends when it reaches {@code endPositionUs} in the + * last reported live window at the time a media period was created. + * @param relativeToDefaultPosition Whether {@code startPositionUs} and {@code endPositionUs} are + * relative to the default position in the window in {@code mediaSource}'s timeline. + */ + public ClippingMediaSource( + MediaSource mediaSource, + long startPositionUs, + long endPositionUs, + boolean enableInitialDiscontinuity, + boolean allowDynamicClippingUpdates, + boolean relativeToDefaultPosition) { Assertions.checkArgument(startPositionUs >= 0); this.mediaSource = Assertions.checkNotNull(mediaSource); startUs = startPositionUs; endUs = endPositionUs; this.enableInitialDiscontinuity = enableInitialDiscontinuity; + this.allowDynamicClippingUpdates = allowDynamicClippingUpdates; + this.relativeToDefaultPosition = relativeToDefaultPosition; mediaPeriods = new ArrayList<>(); + window = new Timeline.Window(); } @Override - public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { - super.prepareSource(player, isTopLevelSource, listener); - sourceListener = listener; + public void prepareSourceInternal(ExoPlayer player, boolean isTopLevelSource) { + super.prepareSourceInternal(player, isTopLevelSource); prepareChildSource(/* id= */ null, mediaSource); } @@ -147,10 +226,13 @@ public final class ClippingMediaSource extends CompositeMediaSource { @Override public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { - ClippingMediaPeriod mediaPeriod = new ClippingMediaPeriod( - mediaSource.createPeriod(id, allocator), enableInitialDiscontinuity); + ClippingMediaPeriod mediaPeriod = + new ClippingMediaPeriod( + mediaSource.createPeriod(id, allocator), + enableInitialDiscontinuity, + periodStartUs, + periodEndUs); mediaPeriods.add(mediaPeriod); - mediaPeriod.setClipping(startUs, endUs); return mediaPeriod; } @@ -158,13 +240,16 @@ public final class ClippingMediaSource extends CompositeMediaSource { public void releasePeriod(MediaPeriod mediaPeriod) { Assertions.checkState(mediaPeriods.remove(mediaPeriod)); mediaSource.releasePeriod(((ClippingMediaPeriod) mediaPeriod).mediaPeriod); + if (mediaPeriods.isEmpty() && !allowDynamicClippingUpdates) { + refreshClippedTimeline(clippingTimeline.timeline); + } } @Override - public void releaseSource() { - super.releaseSource(); + public void releaseSourceInternal() { + super.releaseSourceInternal(); clippingError = null; - sourceListener = null; + clippingTimeline = null; } @Override @@ -173,18 +258,60 @@ public final class ClippingMediaSource extends CompositeMediaSource { if (clippingError != null) { return; } - ClippingTimeline clippingTimeline; + this.manifest = manifest; + refreshClippedTimeline(timeline); + } + + private void refreshClippedTimeline(Timeline timeline) { + long windowStartUs; + long windowEndUs; + timeline.getWindow(/* windowIndex= */ 0, window); + long windowPositionInPeriodUs = window.getPositionInFirstPeriodUs(); + if (clippingTimeline == null || mediaPeriods.isEmpty() || allowDynamicClippingUpdates) { + windowStartUs = startUs; + windowEndUs = endUs; + if (relativeToDefaultPosition) { + long windowDefaultPositionUs = window.getDefaultPositionUs(); + windowStartUs += windowDefaultPositionUs; + windowEndUs += windowDefaultPositionUs; + } + periodStartUs = windowPositionInPeriodUs + windowStartUs; + periodEndUs = + endUs == C.TIME_END_OF_SOURCE + ? C.TIME_END_OF_SOURCE + : windowPositionInPeriodUs + windowEndUs; + int count = mediaPeriods.size(); + for (int i = 0; i < count; i++) { + mediaPeriods.get(i).updateClipping(periodStartUs, periodEndUs); + } + } else { + // Keep window fixed at previous period position. + windowStartUs = periodStartUs - windowPositionInPeriodUs; + windowEndUs = + endUs == C.TIME_END_OF_SOURCE + ? C.TIME_END_OF_SOURCE + : periodEndUs - windowPositionInPeriodUs; + } try { - clippingTimeline = new ClippingTimeline(timeline, startUs, endUs); + clippingTimeline = new ClippingTimeline(timeline, windowStartUs, windowEndUs); } catch (IllegalClippingException e) { clippingError = e; return; } - sourceListener.onSourceInfoRefreshed(this, clippingTimeline, manifest); - int count = mediaPeriods.size(); - for (int i = 0; i < count; i++) { - mediaPeriods.get(i).setClipping(startUs, endUs); + refreshSourceInfo(clippingTimeline, manifest); + } + + @Override + protected long getMediaTimeForChildMediaTime(Void id, long mediaTimeMs) { + if (mediaTimeMs == C.TIME_UNSET) { + return C.TIME_UNSET; } + long startMs = C.usToMs(startUs); + long clippedTimeMs = Math.max(0, mediaTimeMs - startMs); + if (endUs != C.TIME_END_OF_SOURCE) { + clippedTimeMs = Math.min(C.usToMs(endUs) - startMs, clippedTimeMs); + } + return clippedTimeMs; } /** @@ -194,6 +321,8 @@ public final class ClippingMediaSource extends CompositeMediaSource { private final long startUs; private final long endUs; + private final long durationUs; + private final boolean isDynamic; /** * Creates a new clipping timeline that wraps the specified timeline. @@ -210,11 +339,9 @@ public final class ClippingMediaSource extends CompositeMediaSource { if (timeline.getPeriodCount() != 1) { throw new IllegalClippingException(IllegalClippingException.REASON_INVALID_PERIOD_COUNT); } - if (timeline.getPeriod(0, new Period()).getPositionInWindowUs() != 0) { - throw new IllegalClippingException(IllegalClippingException.REASON_PERIOD_OFFSET_IN_WINDOW); - } Window window = timeline.getWindow(0, new Window(), false); - long resolvedEndUs = endUs == C.TIME_END_OF_SOURCE ? window.durationUs : endUs; + startUs = Math.max(0, startUs); + long resolvedEndUs = endUs == C.TIME_END_OF_SOURCE ? window.durationUs : Math.max(0, endUs); if (window.durationUs != C.TIME_UNSET) { if (resolvedEndUs > window.durationUs) { resolvedEndUs = window.durationUs; @@ -228,13 +355,21 @@ public final class ClippingMediaSource extends CompositeMediaSource { } this.startUs = startUs; this.endUs = resolvedEndUs; + durationUs = resolvedEndUs == C.TIME_UNSET ? C.TIME_UNSET : (resolvedEndUs - startUs); + isDynamic = + window.isDynamic + && (resolvedEndUs == C.TIME_UNSET + || (window.durationUs != C.TIME_UNSET && resolvedEndUs == window.durationUs)); } @Override - public Window getWindow(int windowIndex, Window window, boolean setIds, - long defaultPositionProjectionUs) { - window = timeline.getWindow(0, window, setIds, defaultPositionProjectionUs); - window.durationUs = endUs != C.TIME_UNSET ? endUs - startUs : C.TIME_UNSET; + public Window getWindow( + int windowIndex, Window window, boolean setTag, long defaultPositionProjectionUs) { + timeline.getWindow( + /* windowIndex= */ 0, window, setTag, /* defaultPositionProjectionUs= */ 0); + window.positionInFirstPeriodUs += startUs; + window.durationUs = durationUs; + window.isDynamic = isDynamic; if (window.defaultPositionUs != C.TIME_UNSET) { window.defaultPositionUs = Math.max(window.defaultPositionUs, startUs); window.defaultPositionUs = endUs == C.TIME_UNSET ? window.defaultPositionUs @@ -253,11 +388,12 @@ public final class ClippingMediaSource extends CompositeMediaSource { @Override public Period getPeriod(int periodIndex, Period period, boolean setIds) { - period = timeline.getPeriod(0, period, setIds); - period.durationUs = endUs != C.TIME_UNSET ? endUs - startUs : C.TIME_UNSET; - return period; + timeline.getPeriod(/* periodIndex= */ 0, period, setIds); + long positionInClippedWindowUs = period.getPositionInWindowUs() - startUs; + long periodDurationUs = + durationUs == C.TIME_UNSET ? C.TIME_UNSET : durationUs - positionInClippedWindowUs; + return period.set( + period.id, period.uid, /* windowIndex= */ 0, periodDurationUs, positionInClippedWindowUs); } - } - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeMediaSource.java index 6472fe3c2f..f5c4b3a16d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeMediaSource.java @@ -15,11 +15,14 @@ */ package com.google.android.exoplayer2.source; +import android.os.Handler; import android.support.annotation.CallSuper; import android.support.annotation.Nullable; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.source.MediaSourceEventListener.MediaLoadData; import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.util.HashMap; @@ -28,10 +31,12 @@ import java.util.HashMap; * * @param The type of the id used to identify prepared child sources. */ -public abstract class CompositeMediaSource implements MediaSource { +public abstract class CompositeMediaSource extends BaseMediaSource { + + private final HashMap childSources; - private final HashMap childSources; private ExoPlayer player; + private Handler eventHandler; /** Create composite media source without child sources. */ protected CompositeMediaSource() { @@ -40,23 +45,25 @@ public abstract class CompositeMediaSource implements MediaSource { @Override @CallSuper - public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { + public void prepareSourceInternal(ExoPlayer player, boolean isTopLevelSource) { this.player = player; + eventHandler = new Handler(); } @Override @CallSuper public void maybeThrowSourceInfoRefreshError() throws IOException { - for (MediaSource childSource : childSources.values()) { - childSource.maybeThrowSourceInfoRefreshError(); + for (MediaSourceAndListener childSource : childSources.values()) { + childSource.mediaSource.maybeThrowSourceInfoRefreshError(); } } @Override @CallSuper - public void releaseSource() { - for (MediaSource childSource : childSources.values()) { - childSource.releaseSource(); + public void releaseSourceInternal() { + for (MediaSourceAndListener childSource : childSources.values()) { + childSource.mediaSource.releaseSource(childSource.listener); + childSource.mediaSource.removeEventListener(childSource.eventListener); } childSources.clear(); player = null; @@ -81,24 +88,25 @@ public abstract class CompositeMediaSource implements MediaSource { * this method. * *

    Any child sources that aren't explicitly released with {@link #releaseChildSource(Object)} - * will be released in {@link #releaseSource()}. + * will be released in {@link #releaseSourceInternal()}. * * @param id A unique id to identify the child source preparation. Null is allowed as an id. * @param mediaSource The child {@link MediaSource}. */ - protected void prepareChildSource(@Nullable final T id, final MediaSource mediaSource) { + protected final void prepareChildSource(@Nullable final T id, MediaSource mediaSource) { Assertions.checkArgument(!childSources.containsKey(id)); - childSources.put(id, mediaSource); - mediaSource.prepareSource( - player, - /* isTopLevelSource= */ false, - new Listener() { + SourceInfoRefreshListener sourceListener = + new SourceInfoRefreshListener() { @Override public void onSourceInfoRefreshed( MediaSource source, Timeline timeline, @Nullable Object manifest) { - onChildSourceInfoRefreshed(id, mediaSource, timeline, manifest); + onChildSourceInfoRefreshed(id, source, timeline, manifest); } - }); + }; + MediaSourceEventListener eventListener = new ForwardingEventListener(id); + childSources.put(id, new MediaSourceAndListener(mediaSource, sourceListener, eventListener)); + mediaSource.addEventListener(eventHandler, eventListener); + mediaSource.prepareSource(player, /* isTopLevelSource= */ false, sourceListener); } /** @@ -106,8 +114,196 @@ public abstract class CompositeMediaSource implements MediaSource { * * @param id The unique id used to prepare the child source. */ - protected void releaseChildSource(@Nullable T id) { - MediaSource removedChild = childSources.remove(id); - removedChild.releaseSource(); + protected final void releaseChildSource(@Nullable T id) { + MediaSourceAndListener removedChild = childSources.remove(id); + removedChild.mediaSource.releaseSource(removedChild.listener); + removedChild.mediaSource.removeEventListener(removedChild.eventListener); + } + + /** + * Returns the window index in the composite source corresponding to the specified window index in + * a child source. The default implementation does not change the window index. + * + * @param id The unique id used to prepare the child source. + * @param windowIndex A window index of the child source. + * @return The corresponding window index in the composite source. + */ + protected int getWindowIndexForChildWindowIndex(@Nullable T id, int windowIndex) { + return windowIndex; + } + + /** + * Returns the {@link MediaPeriodId} in the composite source corresponding to the specified {@link + * MediaPeriodId} in a child source. The default implementation does not change the media period + * id. + * + * @param id The unique id used to prepare the child source. + * @param mediaPeriodId A {@link MediaPeriodId} of the child source. + * @return The corresponding {@link MediaPeriodId} in the composite source. Null if no + * corresponding media period id can be determined. + */ + protected @Nullable MediaPeriodId getMediaPeriodIdForChildMediaPeriodId( + @Nullable T id, MediaPeriodId mediaPeriodId) { + return mediaPeriodId; + } + + /** + * Returns the media time in the composite source corresponding to the specified media time in a + * child source. The default implementation does not change the media time. + * + * @param id The unique id used to prepare the child source. + * @param mediaTimeMs A media time of the child source, in milliseconds. + * @return The corresponding media time in the composite source, in milliseconds. + */ + protected long getMediaTimeForChildMediaTime(@Nullable T id, long mediaTimeMs) { + return mediaTimeMs; + } + + private static final class MediaSourceAndListener { + + public final MediaSource mediaSource; + public final SourceInfoRefreshListener listener; + public final MediaSourceEventListener eventListener; + + public MediaSourceAndListener( + MediaSource mediaSource, + SourceInfoRefreshListener listener, + MediaSourceEventListener eventListener) { + this.mediaSource = mediaSource; + this.listener = listener; + this.eventListener = eventListener; + } + } + + private final class ForwardingEventListener implements MediaSourceEventListener { + + private final @Nullable T id; + private EventDispatcher eventDispatcher; + + public ForwardingEventListener(@Nullable T id) { + this.eventDispatcher = createEventDispatcher(/* mediaPeriodId= */ null); + this.id = id; + } + + @Override + public void onMediaPeriodCreated(int windowIndex, MediaPeriodId mediaPeriodId) { + if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { + eventDispatcher.mediaPeriodCreated(); + } + } + + @Override + public void onMediaPeriodReleased(int windowIndex, MediaPeriodId mediaPeriodId) { + if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { + eventDispatcher.mediaPeriodReleased(); + } + } + + @Override + public void onLoadStarted( + int windowIndex, + @Nullable MediaPeriodId mediaPeriodId, + LoadEventInfo loadEventData, + MediaLoadData mediaLoadData) { + if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { + eventDispatcher.loadStarted(loadEventData, maybeUpdateMediaLoadData(mediaLoadData)); + } + } + + @Override + public void onLoadCompleted( + int windowIndex, + @Nullable MediaPeriodId mediaPeriodId, + LoadEventInfo loadEventData, + MediaLoadData mediaLoadData) { + if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { + eventDispatcher.loadCompleted(loadEventData, maybeUpdateMediaLoadData(mediaLoadData)); + } + } + + @Override + public void onLoadCanceled( + int windowIndex, + @Nullable MediaPeriodId mediaPeriodId, + LoadEventInfo loadEventData, + MediaLoadData mediaLoadData) { + if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { + eventDispatcher.loadCanceled(loadEventData, maybeUpdateMediaLoadData(mediaLoadData)); + } + } + + @Override + public void onLoadError( + int windowIndex, + @Nullable MediaPeriodId mediaPeriodId, + LoadEventInfo loadEventData, + MediaLoadData mediaLoadData, + IOException error, + boolean wasCanceled) { + if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { + eventDispatcher.loadError( + loadEventData, maybeUpdateMediaLoadData(mediaLoadData), error, wasCanceled); + } + } + + @Override + public void onReadingStarted(int windowIndex, MediaPeriodId mediaPeriodId) { + if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { + eventDispatcher.readingStarted(); + } + } + + @Override + public void onUpstreamDiscarded( + int windowIndex, @Nullable MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData) { + if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { + eventDispatcher.upstreamDiscarded(maybeUpdateMediaLoadData(mediaLoadData)); + } + } + + @Override + public void onDownstreamFormatChanged( + int windowIndex, @Nullable MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData) { + if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { + eventDispatcher.downstreamFormatChanged(maybeUpdateMediaLoadData(mediaLoadData)); + } + } + + /** Updates the event dispatcher and returns whether the event should be dispatched. */ + private boolean maybeUpdateEventDispatcher( + int childWindowIndex, @Nullable MediaPeriodId childMediaPeriodId) { + MediaPeriodId mediaPeriodId = null; + if (childMediaPeriodId != null) { + mediaPeriodId = getMediaPeriodIdForChildMediaPeriodId(id, childMediaPeriodId); + if (mediaPeriodId == null) { + // Media period not found. Ignore event. + return false; + } + } + int windowIndex = getWindowIndexForChildWindowIndex(id, childWindowIndex); + if (eventDispatcher.windowIndex != windowIndex + || !Util.areEqual(eventDispatcher.mediaPeriodId, mediaPeriodId)) { + eventDispatcher = + createEventDispatcher(windowIndex, mediaPeriodId, /* mediaTimeOffsetMs= */ 0); + } + return true; + } + + private MediaLoadData maybeUpdateMediaLoadData(MediaLoadData mediaLoadData) { + long mediaStartTimeMs = getMediaTimeForChildMediaTime(id, mediaLoadData.mediaStartTimeMs); + long mediaEndTimeMs = getMediaTimeForChildMediaTime(id, mediaLoadData.mediaEndTimeMs); + if (mediaStartTimeMs == mediaLoadData.mediaStartTimeMs + && mediaEndTimeMs == mediaLoadData.mediaEndTimeMs) { + return mediaLoadData; + } + return new MediaLoadData( + mediaLoadData.dataType, + mediaLoadData.trackType, + mediaLoadData.trackFormat, + mediaLoadData.trackSelectionReason, + mediaLoadData.trackSelectionData, + mediaStartTimeMs, + mediaEndTimeMs); + } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java index c29367e109..3e39139918 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java @@ -15,202 +15,755 @@ */ package com.google.android.exoplayer2.source; +import android.os.Handler; +import android.os.Looper; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.util.SparseIntArray; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.PlayerMessage; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.source.ConcatenatingMediaSource.MediaSourceHolder; import com.google.android.exoplayer2.source.ShuffleOrder.DefaultShuffleOrder; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; -import java.util.HashMap; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; import java.util.IdentityHashMap; +import java.util.List; import java.util.Map; /** - * Concatenates multiple {@link MediaSource}s. It is valid for the same {@link MediaSource} instance - * to be present more than once in the concatenation. + * Concatenates multiple {@link MediaSource}s. The list of {@link MediaSource}s can be modified + * during playback. It is valid for the same {@link MediaSource} instance to be present more than + * once in the concatenation. Access to this class is thread-safe. */ -public final class ConcatenatingMediaSource extends CompositeMediaSource { +public class ConcatenatingMediaSource extends CompositeMediaSource + implements PlayerMessage.Target { - private final MediaSource[] mediaSources; - private final Timeline[] timelines; - private final Object[] manifests; - private final Map sourceIndexByMediaPeriod; + private static final int MSG_ADD = 0; + private static final int MSG_ADD_MULTIPLE = 1; + private static final int MSG_REMOVE = 2; + private static final int MSG_MOVE = 3; + private static final int MSG_CLEAR = 4; + private static final int MSG_NOTIFY_LISTENER = 5; + private static final int MSG_ON_COMPLETION = 6; + + // Accessed on the app thread. + private final List mediaSourcesPublic; + + // Accessed on the playback thread. + private final List mediaSourceHolders; + private final MediaSourceHolder query; + private final Map mediaSourceByMediaPeriod; + private final List pendingOnCompletionActions; private final boolean isAtomic; - private final ShuffleOrder shuffleOrder; + private final Timeline.Window window; - private Listener listener; - private ConcatenatedTimeline timeline; + private ExoPlayer player; + private boolean listenerNotificationScheduled; + private ShuffleOrder shuffleOrder; + private int windowCount; + private int periodCount; + + /** Creates a new concatenating media source. */ + public ConcatenatingMediaSource() { + this(/* isAtomic= */ false, new DefaultShuffleOrder(0)); + } + + /** + * Creates a new concatenating media source. + * + * @param isAtomic Whether the concatenating media source will be treated as atomic, i.e., treated + * as a single item for repeating and shuffling. + */ + public ConcatenatingMediaSource(boolean isAtomic) { + this(isAtomic, new DefaultShuffleOrder(0)); + } + + /** + * Creates a new concatenating media source with a custom shuffle order. + * + * @param isAtomic Whether the concatenating media source will be treated as atomic, i.e., treated + * as a single item for repeating and shuffling. + * @param shuffleOrder The {@link ShuffleOrder} to use when shuffling the child media sources. + */ + public ConcatenatingMediaSource(boolean isAtomic, ShuffleOrder shuffleOrder) { + this(isAtomic, shuffleOrder, new MediaSource[0]); + } /** * @param mediaSources The {@link MediaSource}s to concatenate. It is valid for the same * {@link MediaSource} instance to be present more than once in the array. */ public ConcatenatingMediaSource(MediaSource... mediaSources) { - this(false, mediaSources); + this(/* isAtomic= */ false, mediaSources); } /** - * @param isAtomic Whether the concatenated media source shall be treated as atomic, - * i.e., treated as a single item for repeating and shuffling. - * @param mediaSources The {@link MediaSource}s to concatenate. It is valid for the same - * {@link MediaSource} instance to be present more than once in the array. + * @param isAtomic Whether the concatenating media source will be treated as atomic, i.e., treated + * as a single item for repeating and shuffling. + * @param mediaSources The {@link MediaSource}s to concatenate. It is valid for the same {@link + * MediaSource} instance to be present more than once in the array. */ public ConcatenatingMediaSource(boolean isAtomic, MediaSource... mediaSources) { - this(isAtomic, new DefaultShuffleOrder(mediaSources.length), mediaSources); + this(isAtomic, new DefaultShuffleOrder(0), mediaSources); } /** - * @param isAtomic Whether the concatenated media source shall be treated as atomic, - * i.e., treated as a single item for repeating and shuffling. - * @param shuffleOrder The {@link ShuffleOrder} to use when shuffling the child media sources. The - * number of elements in the shuffle order must match the number of concatenated - * {@link MediaSource}s. - * @param mediaSources The {@link MediaSource}s to concatenate. It is valid for the same - * {@link MediaSource} instance to be present more than once in the array. + * @param isAtomic Whether the concatenating media source will be treated as atomic, i.e., treated + * as a single item for repeating and shuffling. + * @param shuffleOrder The {@link ShuffleOrder} to use when shuffling the child media sources. + * @param mediaSources The {@link MediaSource}s to concatenate. It is valid for the same {@link + * MediaSource} instance to be present more than once in the array. */ - public ConcatenatingMediaSource(boolean isAtomic, ShuffleOrder shuffleOrder, - MediaSource... mediaSources) { + public ConcatenatingMediaSource( + boolean isAtomic, ShuffleOrder shuffleOrder, MediaSource... mediaSources) { for (MediaSource mediaSource : mediaSources) { Assertions.checkNotNull(mediaSource); } - Assertions.checkArgument(shuffleOrder.getLength() == mediaSources.length); - this.mediaSources = mediaSources; + this.shuffleOrder = shuffleOrder.getLength() > 0 ? shuffleOrder.cloneAndClear() : shuffleOrder; + this.mediaSourceByMediaPeriod = new IdentityHashMap<>(); + this.mediaSourcesPublic = new ArrayList<>(); + this.mediaSourceHolders = new ArrayList<>(); + this.pendingOnCompletionActions = new ArrayList<>(); + this.query = new MediaSourceHolder(/* mediaSource= */ null); this.isAtomic = isAtomic; - this.shuffleOrder = shuffleOrder; - timelines = new Timeline[mediaSources.length]; - manifests = new Object[mediaSources.length]; - sourceIndexByMediaPeriod = new HashMap<>(); + window = new Timeline.Window(); + addMediaSources(Arrays.asList(mediaSources)); + } + + /** + * Appends a {@link MediaSource} to the playlist. + * + * @param mediaSource The {@link MediaSource} to be added to the list. + */ + public final synchronized void addMediaSource(MediaSource mediaSource) { + addMediaSource(mediaSourcesPublic.size(), mediaSource, null); + } + + /** + * Appends a {@link MediaSource} to the playlist and executes a custom action on completion. + * + * @param mediaSource The {@link MediaSource} to be added to the list. + * @param actionOnCompletion A {@link Runnable} which is executed immediately after the media + * source has been added to the playlist. + */ + public final synchronized void addMediaSource( + MediaSource mediaSource, @Nullable Runnable actionOnCompletion) { + addMediaSource(mediaSourcesPublic.size(), mediaSource, actionOnCompletion); + } + + /** + * Adds a {@link MediaSource} to the playlist. + * + * @param index The index at which the new {@link MediaSource} will be inserted. This index must + * be in the range of 0 <= index <= {@link #getSize()}. + * @param mediaSource The {@link MediaSource} to be added to the list. + */ + public final synchronized void addMediaSource(int index, MediaSource mediaSource) { + addMediaSource(index, mediaSource, null); + } + + /** + * Adds a {@link MediaSource} to the playlist and executes a custom action on completion. + * + * @param index The index at which the new {@link MediaSource} will be inserted. This index must + * be in the range of 0 <= index <= {@link #getSize()}. + * @param mediaSource The {@link MediaSource} to be added to the list. + * @param actionOnCompletion A {@link Runnable} which is executed immediately after the media + * source has been added to the playlist. + */ + public final synchronized void addMediaSource( + int index, MediaSource mediaSource, @Nullable Runnable actionOnCompletion) { + Assertions.checkNotNull(mediaSource); + MediaSourceHolder mediaSourceHolder = new MediaSourceHolder(mediaSource); + mediaSourcesPublic.add(index, mediaSourceHolder); + if (player != null) { + player + .createMessage(this) + .setType(MSG_ADD) + .setPayload(new MessageData<>(index, mediaSourceHolder, actionOnCompletion)) + .send(); + } else if (actionOnCompletion != null) { + actionOnCompletion.run(); + } + } + + /** + * Appends multiple {@link MediaSource}s to the playlist. + * + * @param mediaSources A collection of {@link MediaSource}s to be added to the list. The media + * sources are added in the order in which they appear in this collection. + */ + public final synchronized void addMediaSources(Collection mediaSources) { + addMediaSources(mediaSourcesPublic.size(), mediaSources, null); + } + + /** + * Appends multiple {@link MediaSource}s to the playlist and executes a custom action on + * completion. + * + * @param mediaSources A collection of {@link MediaSource}s to be added to the list. The media + * sources are added in the order in which they appear in this collection. + * @param actionOnCompletion A {@link Runnable} which is executed immediately after the media + * sources have been added to the playlist. + */ + public final synchronized void addMediaSources( + Collection mediaSources, @Nullable Runnable actionOnCompletion) { + addMediaSources(mediaSourcesPublic.size(), mediaSources, actionOnCompletion); + } + + /** + * Adds multiple {@link MediaSource}s to the playlist. + * + * @param index The index at which the new {@link MediaSource}s will be inserted. This index must + * be in the range of 0 <= index <= {@link #getSize()}. + * @param mediaSources A collection of {@link MediaSource}s to be added to the list. The media + * sources are added in the order in which they appear in this collection. + */ + public final synchronized void addMediaSources(int index, Collection mediaSources) { + addMediaSources(index, mediaSources, null); + } + + /** + * Adds multiple {@link MediaSource}s to the playlist and executes a custom action on completion. + * + * @param index The index at which the new {@link MediaSource}s will be inserted. This index must + * be in the range of 0 <= index <= {@link #getSize()}. + * @param mediaSources A collection of {@link MediaSource}s to be added to the list. The media + * sources are added in the order in which they appear in this collection. + * @param actionOnCompletion A {@link Runnable} which is executed immediately after the media + * sources have been added to the playlist. + */ + public final synchronized void addMediaSources( + int index, Collection mediaSources, @Nullable Runnable actionOnCompletion) { + for (MediaSource mediaSource : mediaSources) { + Assertions.checkNotNull(mediaSource); + } + List mediaSourceHolders = new ArrayList<>(mediaSources.size()); + for (MediaSource mediaSource : mediaSources) { + mediaSourceHolders.add(new MediaSourceHolder(mediaSource)); + } + mediaSourcesPublic.addAll(index, mediaSourceHolders); + if (player != null && !mediaSources.isEmpty()) { + player + .createMessage(this) + .setType(MSG_ADD_MULTIPLE) + .setPayload(new MessageData<>(index, mediaSourceHolders, actionOnCompletion)) + .send(); + } else if (actionOnCompletion != null) { + actionOnCompletion.run(); + } + } + + /** + * Removes a {@link MediaSource} from the playlist. + * + *

    Note: If you want to move the instance, it's preferable to use {@link #moveMediaSource(int, + * int)} instead. + * + * @param index The index at which the media source will be removed. This index must be in the + * range of 0 <= index < {@link #getSize()}. + */ + public final synchronized void removeMediaSource(int index) { + removeMediaSource(index, null); + } + + /** + * Removes a {@link MediaSource} from the playlist and executes a custom action on completion. + * + *

    Note: If you want to move the instance, it's preferable to use {@link #moveMediaSource(int, + * int)} instead. + * + * @param index The index at which the media source will be removed. This index must be in the + * range of 0 <= index < {@link #getSize()}. + * @param actionOnCompletion A {@link Runnable} which is executed immediately after the media + * source has been removed from the playlist. + */ + public final synchronized void removeMediaSource( + int index, @Nullable Runnable actionOnCompletion) { + mediaSourcesPublic.remove(index); + if (player != null) { + player + .createMessage(this) + .setType(MSG_REMOVE) + .setPayload(new MessageData<>(index, null, actionOnCompletion)) + .send(); + } else if (actionOnCompletion != null) { + actionOnCompletion.run(); + } + } + + /** + * Moves an existing {@link MediaSource} within the playlist. + * + * @param currentIndex The current index of the media source in the playlist. This index must be + * in the range of 0 <= index < {@link #getSize()}. + * @param newIndex The target index of the media source in the playlist. This index must be in the + * range of 0 <= index < {@link #getSize()}. + */ + public final synchronized void moveMediaSource(int currentIndex, int newIndex) { + moveMediaSource(currentIndex, newIndex, null); + } + + /** + * Moves an existing {@link MediaSource} within the playlist and executes a custom action on + * completion. + * + * @param currentIndex The current index of the media source in the playlist. This index must be + * in the range of 0 <= index < {@link #getSize()}. + * @param newIndex The target index of the media source in the playlist. This index must be in the + * range of 0 <= index < {@link #getSize()}. + * @param actionOnCompletion A {@link Runnable} which is executed immediately after the media + * source has been moved. + */ + public final synchronized void moveMediaSource( + int currentIndex, int newIndex, @Nullable Runnable actionOnCompletion) { + if (currentIndex == newIndex) { + return; + } + mediaSourcesPublic.add(newIndex, mediaSourcesPublic.remove(currentIndex)); + if (player != null) { + player + .createMessage(this) + .setType(MSG_MOVE) + .setPayload(new MessageData<>(currentIndex, newIndex, actionOnCompletion)) + .send(); + } else if (actionOnCompletion != null) { + actionOnCompletion.run(); + } + } + + /** Clears the playlist. */ + public final synchronized void clear() { + clear(/* actionOnCompletion= */ null); + } + + /** + * Clears the playlist and executes a custom action on completion. + * + * @param actionOnCompletion A {@link Runnable} which is executed immediately after the playlist + * has been cleared. + */ + public final synchronized void clear(@Nullable Runnable actionOnCompletion) { + mediaSourcesPublic.clear(); + if (player != null) { + player + .createMessage(this) + .setType(MSG_CLEAR) + .setPayload(actionOnCompletion != null ? new EventDispatcher(actionOnCompletion) : null) + .send(); + } else if (actionOnCompletion != null) { + actionOnCompletion.run(); + } + } + + /** Returns the number of media sources in the playlist. */ + public final synchronized int getSize() { + return mediaSourcesPublic.size(); + } + + /** + * Returns the {@link MediaSource} at a specified index. + * + * @param index An index in the range of 0 <= index <= {@link #getSize()}. + * @return The {@link MediaSource} at this index. + */ + public final synchronized MediaSource getMediaSource(int index) { + return mediaSourcesPublic.get(index).mediaSource; } @Override - public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { - super.prepareSource(player, isTopLevelSource, listener); - this.listener = listener; - boolean[] duplicateFlags = buildDuplicateFlags(mediaSources); - if (mediaSources.length == 0) { - listener.onSourceInfoRefreshed(this, Timeline.EMPTY, null); + public final synchronized void prepareSourceInternal(ExoPlayer player, boolean isTopLevelSource) { + super.prepareSourceInternal(player, isTopLevelSource); + this.player = player; + if (mediaSourcesPublic.isEmpty()) { + notifyListener(); } else { - for (int i = 0; i < mediaSources.length; i++) { - if (!duplicateFlags[i]) { - prepareChildSource(i, mediaSources[i]); - } - } + shuffleOrder = shuffleOrder.cloneAndInsert(0, mediaSourcesPublic.size()); + addMediaSourcesInternal(0, mediaSourcesPublic); + scheduleListenerNotification(/* actionOnCompletion= */ null); } } @Override - public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { - int sourceIndex = timeline.getChildIndexByPeriodIndex(id.periodIndex); - MediaPeriodId periodIdInSource = id.copyWithPeriodIndex( - id.periodIndex - timeline.getFirstPeriodIndexByChildIndex(sourceIndex)); - MediaPeriod mediaPeriod = mediaSources[sourceIndex].createPeriod(periodIdInSource, allocator); - sourceIndexByMediaPeriod.put(mediaPeriod, sourceIndex); + public final MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { + int mediaSourceHolderIndex = findMediaSourceHolderByPeriodIndex(id.periodIndex); + MediaSourceHolder holder = mediaSourceHolders.get(mediaSourceHolderIndex); + MediaPeriodId idInSource = + id.copyWithPeriodIndex(id.periodIndex - holder.firstPeriodIndexInChild); + DeferredMediaPeriod mediaPeriod = + new DeferredMediaPeriod(holder.mediaSource, idInSource, allocator); + mediaSourceByMediaPeriod.put(mediaPeriod, holder); + holder.activeMediaPeriods.add(mediaPeriod); + if (holder.isPrepared) { + mediaPeriod.createPeriod(); + } return mediaPeriod; } @Override - public void releasePeriod(MediaPeriod mediaPeriod) { - int sourceIndex = sourceIndexByMediaPeriod.get(mediaPeriod); - sourceIndexByMediaPeriod.remove(mediaPeriod); - mediaSources[sourceIndex].releasePeriod(mediaPeriod); + public final void releasePeriod(MediaPeriod mediaPeriod) { + MediaSourceHolder holder = mediaSourceByMediaPeriod.remove(mediaPeriod); + ((DeferredMediaPeriod) mediaPeriod).releasePeriod(); + holder.activeMediaPeriods.remove(mediaPeriod); + if (holder.activeMediaPeriods.isEmpty() && holder.isRemoved) { + releaseChildSource(holder); + } } @Override - public void releaseSource() { - super.releaseSource(); - listener = null; - timeline = null; + public final void releaseSourceInternal() { + super.releaseSourceInternal(); + mediaSourceHolders.clear(); + player = null; + shuffleOrder = shuffleOrder.cloneAndClear(); + windowCount = 0; + periodCount = 0; } @Override - protected void onChildSourceInfoRefreshed( - Integer sourceFirstIndex, + protected final void onChildSourceInfoRefreshed( + MediaSourceHolder mediaSourceHolder, MediaSource mediaSource, - Timeline sourceTimeline, - @Nullable Object sourceManifest) { - // Set the timeline and manifest. - timelines[sourceFirstIndex] = sourceTimeline; - manifests[sourceFirstIndex] = sourceManifest; - // Also set the timeline and manifest for any duplicate entries of the same source. - for (int i = sourceFirstIndex + 1; i < mediaSources.length; i++) { - if (mediaSources[i] == mediaSource) { - timelines[i] = sourceTimeline; - manifests[i] = sourceManifest; - } - } - for (Timeline timeline : timelines) { - if (timeline == null) { - // Don't invoke the listener until all sources have timelines. - return; - } - } - timeline = new ConcatenatedTimeline(timelines.clone(), isAtomic, shuffleOrder); - listener.onSourceInfoRefreshed(this, timeline, manifests.clone()); + Timeline timeline, + @Nullable Object manifest) { + updateMediaSourceInternal(mediaSourceHolder, timeline); } - private static boolean[] buildDuplicateFlags(MediaSource[] mediaSources) { - boolean[] duplicateFlags = new boolean[mediaSources.length]; - IdentityHashMap sources = new IdentityHashMap<>(mediaSources.length); - for (int i = 0; i < mediaSources.length; i++) { - MediaSource source = mediaSources[i]; - if (!sources.containsKey(source)) { - sources.put(source, null); - } else { - duplicateFlags[i] = true; + @Override + protected @Nullable MediaPeriodId getMediaPeriodIdForChildMediaPeriodId( + MediaSourceHolder mediaSourceHolder, MediaPeriodId mediaPeriodId) { + for (int i = 0; i < mediaSourceHolder.activeMediaPeriods.size(); i++) { + // Ensure the reported media period id has the same window sequence number as the one created + // by this media source. Otherwise it does not belong to this child source. + if (mediaSourceHolder.activeMediaPeriods.get(i).id.windowSequenceNumber + == mediaPeriodId.windowSequenceNumber) { + return mediaPeriodId.copyWithPeriodIndex( + mediaPeriodId.periodIndex + mediaSourceHolder.firstPeriodIndexInChild); } } - return duplicateFlags; + return null; } - /** - * A {@link Timeline} that is the concatenation of one or more {@link Timeline}s. - */ + @Override + protected int getWindowIndexForChildWindowIndex( + MediaSourceHolder mediaSourceHolder, int windowIndex) { + return windowIndex + mediaSourceHolder.firstWindowIndexInChild; + } + + @Override + @SuppressWarnings("unchecked") + public final void handleMessage(int messageType, Object message) throws ExoPlaybackException { + switch (messageType) { + case MSG_ADD: + MessageData addMessage = (MessageData) message; + shuffleOrder = shuffleOrder.cloneAndInsert(addMessage.index, 1); + addMediaSourceInternal(addMessage.index, addMessage.customData); + scheduleListenerNotification(addMessage.actionOnCompletion); + break; + case MSG_ADD_MULTIPLE: + MessageData> addMultipleMessage = + (MessageData>) message; + shuffleOrder = + shuffleOrder.cloneAndInsert( + addMultipleMessage.index, addMultipleMessage.customData.size()); + addMediaSourcesInternal(addMultipleMessage.index, addMultipleMessage.customData); + scheduleListenerNotification(addMultipleMessage.actionOnCompletion); + break; + case MSG_REMOVE: + MessageData removeMessage = (MessageData) message; + shuffleOrder = shuffleOrder.cloneAndRemove(removeMessage.index); + removeMediaSourceInternal(removeMessage.index); + scheduleListenerNotification(removeMessage.actionOnCompletion); + break; + case MSG_MOVE: + MessageData moveMessage = (MessageData) message; + shuffleOrder = shuffleOrder.cloneAndRemove(moveMessage.index); + shuffleOrder = shuffleOrder.cloneAndInsert(moveMessage.customData, 1); + moveMediaSourceInternal(moveMessage.index, moveMessage.customData); + scheduleListenerNotification(moveMessage.actionOnCompletion); + break; + case MSG_CLEAR: + clearInternal(); + scheduleListenerNotification((EventDispatcher) message); + break; + case MSG_NOTIFY_LISTENER: + notifyListener(); + break; + case MSG_ON_COMPLETION: + List actionsOnCompletion = ((List) message); + for (int i = 0; i < actionsOnCompletion.size(); i++) { + actionsOnCompletion.get(i).dispatchEvent(); + } + break; + default: + throw new IllegalStateException(); + } + } + + private void scheduleListenerNotification(@Nullable EventDispatcher actionOnCompletion) { + if (!listenerNotificationScheduled) { + player.createMessage(this).setType(MSG_NOTIFY_LISTENER).send(); + listenerNotificationScheduled = true; + } + if (actionOnCompletion != null) { + pendingOnCompletionActions.add(actionOnCompletion); + } + } + + private void notifyListener() { + listenerNotificationScheduled = false; + List actionsOnCompletion = + pendingOnCompletionActions.isEmpty() + ? Collections.emptyList() + : new ArrayList<>(pendingOnCompletionActions); + pendingOnCompletionActions.clear(); + refreshSourceInfo( + new ConcatenatedTimeline( + mediaSourceHolders, windowCount, periodCount, shuffleOrder, isAtomic), + /* manifest= */ null); + if (!actionsOnCompletion.isEmpty()) { + player.createMessage(this).setType(MSG_ON_COMPLETION).setPayload(actionsOnCompletion).send(); + } + } + + private void addMediaSourceInternal(int newIndex, MediaSourceHolder newMediaSourceHolder) { + if (newIndex > 0) { + MediaSourceHolder previousHolder = mediaSourceHolders.get(newIndex - 1); + newMediaSourceHolder.reset( + newIndex, + previousHolder.firstWindowIndexInChild + previousHolder.timeline.getWindowCount(), + previousHolder.firstPeriodIndexInChild + previousHolder.timeline.getPeriodCount()); + } else { + newMediaSourceHolder.reset( + newIndex, /* firstWindowIndexInChild= */ 0, /* firstPeriodIndexInChild= */ 0); + } + correctOffsets( + newIndex, + /* childIndexUpdate= */ 1, + newMediaSourceHolder.timeline.getWindowCount(), + newMediaSourceHolder.timeline.getPeriodCount()); + mediaSourceHolders.add(newIndex, newMediaSourceHolder); + prepareChildSource(newMediaSourceHolder, newMediaSourceHolder.mediaSource); + } + + private void addMediaSourcesInternal( + int index, Collection mediaSourceHolders) { + for (MediaSourceHolder mediaSourceHolder : mediaSourceHolders) { + addMediaSourceInternal(index++, mediaSourceHolder); + } + } + + private void updateMediaSourceInternal(MediaSourceHolder mediaSourceHolder, Timeline timeline) { + if (mediaSourceHolder == null) { + throw new IllegalArgumentException(); + } + DeferredTimeline deferredTimeline = mediaSourceHolder.timeline; + if (deferredTimeline.getTimeline() == timeline) { + return; + } + int windowOffsetUpdate = timeline.getWindowCount() - deferredTimeline.getWindowCount(); + int periodOffsetUpdate = timeline.getPeriodCount() - deferredTimeline.getPeriodCount(); + if (windowOffsetUpdate != 0 || periodOffsetUpdate != 0) { + correctOffsets( + mediaSourceHolder.childIndex + 1, + /* childIndexUpdate= */ 0, + windowOffsetUpdate, + periodOffsetUpdate); + } + mediaSourceHolder.timeline = deferredTimeline.cloneWithNewTimeline(timeline); + if (!mediaSourceHolder.isPrepared && !timeline.isEmpty()) { + timeline.getWindow(/* windowIndex= */ 0, window); + long defaultPeriodPositionUs = + window.getPositionInFirstPeriodUs() + window.getDefaultPositionUs(); + for (int i = 0; i < mediaSourceHolder.activeMediaPeriods.size(); i++) { + DeferredMediaPeriod deferredMediaPeriod = mediaSourceHolder.activeMediaPeriods.get(i); + deferredMediaPeriod.setDefaultPreparePositionUs(defaultPeriodPositionUs); + deferredMediaPeriod.createPeriod(); + } + mediaSourceHolder.isPrepared = true; + } + scheduleListenerNotification(/* actionOnCompletion= */ null); + } + + private void clearInternal() { + for (int index = mediaSourceHolders.size() - 1; index >= 0; index--) { + removeMediaSourceInternal(index); + } + } + + private void removeMediaSourceInternal(int index) { + MediaSourceHolder holder = mediaSourceHolders.remove(index); + Timeline oldTimeline = holder.timeline; + correctOffsets( + index, + /* childIndexUpdate= */ -1, + -oldTimeline.getWindowCount(), + -oldTimeline.getPeriodCount()); + holder.isRemoved = true; + if (holder.activeMediaPeriods.isEmpty()) { + releaseChildSource(holder); + } + } + + private void moveMediaSourceInternal(int currentIndex, int newIndex) { + int startIndex = Math.min(currentIndex, newIndex); + int endIndex = Math.max(currentIndex, newIndex); + int windowOffset = mediaSourceHolders.get(startIndex).firstWindowIndexInChild; + int periodOffset = mediaSourceHolders.get(startIndex).firstPeriodIndexInChild; + mediaSourceHolders.add(newIndex, mediaSourceHolders.remove(currentIndex)); + for (int i = startIndex; i <= endIndex; i++) { + MediaSourceHolder holder = mediaSourceHolders.get(i); + holder.firstWindowIndexInChild = windowOffset; + holder.firstPeriodIndexInChild = periodOffset; + windowOffset += holder.timeline.getWindowCount(); + periodOffset += holder.timeline.getPeriodCount(); + } + } + + private void correctOffsets( + int startIndex, int childIndexUpdate, int windowOffsetUpdate, int periodOffsetUpdate) { + windowCount += windowOffsetUpdate; + periodCount += periodOffsetUpdate; + for (int i = startIndex; i < mediaSourceHolders.size(); i++) { + mediaSourceHolders.get(i).childIndex += childIndexUpdate; + mediaSourceHolders.get(i).firstWindowIndexInChild += windowOffsetUpdate; + mediaSourceHolders.get(i).firstPeriodIndexInChild += periodOffsetUpdate; + } + } + + private int findMediaSourceHolderByPeriodIndex(int periodIndex) { + query.firstPeriodIndexInChild = periodIndex; + int index = Collections.binarySearch(mediaSourceHolders, query); + if (index < 0) { + return -index - 2; + } + while (index < mediaSourceHolders.size() - 1 + && mediaSourceHolders.get(index + 1).firstPeriodIndexInChild == periodIndex) { + index++; + } + return index; + } + + /** Data class to hold playlist media sources together with meta data needed to process them. */ + /* package */ static final class MediaSourceHolder implements Comparable { + + public final MediaSource mediaSource; + public final int uid; + + public DeferredTimeline timeline; + public int childIndex; + public int firstWindowIndexInChild; + public int firstPeriodIndexInChild; + public boolean isPrepared; + public boolean isRemoved; + public List activeMediaPeriods; + + public MediaSourceHolder(MediaSource mediaSource) { + this.mediaSource = mediaSource; + this.uid = System.identityHashCode(this); + this.timeline = new DeferredTimeline(); + this.activeMediaPeriods = new ArrayList<>(); + } + + public void reset(int childIndex, int firstWindowIndexInChild, int firstPeriodIndexInChild) { + this.childIndex = childIndex; + this.firstWindowIndexInChild = firstWindowIndexInChild; + this.firstPeriodIndexInChild = firstPeriodIndexInChild; + this.isPrepared = false; + this.isRemoved = false; + this.activeMediaPeriods.clear(); + } + + @Override + public int compareTo(@NonNull MediaSourceHolder other) { + return this.firstPeriodIndexInChild - other.firstPeriodIndexInChild; + } + } + + /** Can be used to dispatch a runnable on the thread the object was created on. */ + private static final class EventDispatcher { + + public final Handler eventHandler; + public final Runnable runnable; + + public EventDispatcher(Runnable runnable) { + this.runnable = runnable; + this.eventHandler = + new Handler(Looper.myLooper() != null ? Looper.myLooper() : Looper.getMainLooper()); + } + + public void dispatchEvent() { + eventHandler.post(runnable); + } + } + + /** Message used to post actions from app thread to playback thread. */ + private static final class MessageData { + + public final int index; + public final T customData; + public final @Nullable EventDispatcher actionOnCompletion; + + public MessageData(int index, T customData, @Nullable Runnable actionOnCompletion) { + this.index = index; + this.actionOnCompletion = + actionOnCompletion != null ? new EventDispatcher(actionOnCompletion) : null; + this.customData = customData; + } + } + + /** Timeline exposing concatenated timelines of playlist media sources. */ private static final class ConcatenatedTimeline extends AbstractConcatenatedTimeline { + private final int windowCount; + private final int periodCount; + private final int[] firstPeriodInChildIndices; + private final int[] firstWindowInChildIndices; private final Timeline[] timelines; - private final int[] sourcePeriodOffsets; - private final int[] sourceWindowOffsets; + private final int[] uids; + private final SparseIntArray childIndexByUid; - public ConcatenatedTimeline(Timeline[] timelines, boolean isAtomic, ShuffleOrder shuffleOrder) { + public ConcatenatedTimeline( + Collection mediaSourceHolders, + int windowCount, + int periodCount, + ShuffleOrder shuffleOrder, + boolean isAtomic) { super(isAtomic, shuffleOrder); - int[] sourcePeriodOffsets = new int[timelines.length]; - int[] sourceWindowOffsets = new int[timelines.length]; - long periodCount = 0; - int windowCount = 0; - for (int i = 0; i < timelines.length; i++) { - Timeline timeline = timelines[i]; - periodCount += timeline.getPeriodCount(); - Assertions.checkState(periodCount <= Integer.MAX_VALUE, - "ConcatenatingMediaSource children contain too many periods"); - sourcePeriodOffsets[i] = (int) periodCount; - windowCount += timeline.getWindowCount(); - sourceWindowOffsets[i] = windowCount; + this.windowCount = windowCount; + this.periodCount = periodCount; + int childCount = mediaSourceHolders.size(); + firstPeriodInChildIndices = new int[childCount]; + firstWindowInChildIndices = new int[childCount]; + timelines = new Timeline[childCount]; + uids = new int[childCount]; + childIndexByUid = new SparseIntArray(); + int index = 0; + for (MediaSourceHolder mediaSourceHolder : mediaSourceHolders) { + timelines[index] = mediaSourceHolder.timeline; + firstPeriodInChildIndices[index] = mediaSourceHolder.firstPeriodIndexInChild; + firstWindowInChildIndices[index] = mediaSourceHolder.firstWindowIndexInChild; + uids[index] = mediaSourceHolder.uid; + childIndexByUid.put(uids[index], index++); } - this.timelines = timelines; - this.sourcePeriodOffsets = sourcePeriodOffsets; - this.sourceWindowOffsets = sourceWindowOffsets; - } - - @Override - public int getWindowCount() { - return sourceWindowOffsets[sourceWindowOffsets.length - 1]; - } - - @Override - public int getPeriodCount() { - return sourcePeriodOffsets[sourcePeriodOffsets.length - 1]; } @Override protected int getChildIndexByPeriodIndex(int periodIndex) { - return Util.binarySearchFloor(sourcePeriodOffsets, periodIndex + 1, false, false) + 1; + return Util.binarySearchFloor(firstPeriodInChildIndices, periodIndex + 1, false, false); } @Override protected int getChildIndexByWindowIndex(int windowIndex) { - return Util.binarySearchFloor(sourceWindowOffsets, windowIndex + 1, false, false) + 1; + return Util.binarySearchFloor(firstWindowInChildIndices, windowIndex + 1, false, false); } @Override @@ -218,7 +771,8 @@ public final class ConcatenatingMediaSource extends CompositeMediaSource 0 + ? timeline.getPeriod(0, period, true).uid + : replacedId); + } + + public Timeline getTimeline() { + return timeline; + } + + @Override + public Period getPeriod(int periodIndex, Period period, boolean setIds) { + timeline.getPeriod(periodIndex, period, setIds); + if (Util.areEqual(period.uid, replacedId)) { + period.uid = DUMMY_ID; + } + return period; + } + + @Override + public int getIndexOfPeriod(Object uid) { + return timeline.getIndexOfPeriod(DUMMY_ID.equals(uid) ? replacedId : uid); + } + } + + /** Dummy placeholder timeline with one dynamic window with a period of indeterminate duration. */ + private static final class DummyTimeline extends Timeline { + + @Override + public int getWindowCount() { + return 1; + } + + @Override + public Window getWindow( + int windowIndex, Window window, boolean setTag, long defaultPositionProjectionUs) { + return window.set( + /* tag= */ null, + /* presentationStartTimeMs= */ C.TIME_UNSET, + /* windowStartTimeMs= */ C.TIME_UNSET, + /* isSeekable= */ false, + // Dynamic window to indicate pending timeline updates. + /* isDynamic= */ true, + // Position can't be projected yet as the default position is still unknown. + /* defaultPositionUs= */ defaultPositionProjectionUs > 0 ? C.TIME_UNSET : 0, + /* durationUs= */ C.TIME_UNSET, + /* firstPeriodIndex= */ 0, + /* lastPeriodIndex= */ 0, + /* positionInFirstPeriodUs= */ 0); + } + + @Override + public int getPeriodCount() { + return 1; + } + + @Override + public Period getPeriod(int periodIndex, Period period, boolean setIds) { + return period.set( + /* id= */ null, + /* uid= */ null, + /* windowIndex= */ 0, + /* durationUs = */ C.TIME_UNSET, + /* positionInWindowUs= */ 0); + } + + @Override + public int getIndexOfPeriod(Object uid) { + return uid == null ? 0 : C.INDEX_UNSET; + } + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DefaultMediaSourceEventListener.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DefaultMediaSourceEventListener.java new file mode 100644 index 0000000000..fd7c037fb9 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DefaultMediaSourceEventListener.java @@ -0,0 +1,92 @@ +/* + * 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; + +import android.support.annotation.Nullable; +import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; +import java.io.IOException; + +/** + * A {@link MediaSourceEventListener} allowing selective overrides. All methods are implemented as + * no-ops. + */ +public abstract class DefaultMediaSourceEventListener implements MediaSourceEventListener { + + @Override + public void onMediaPeriodCreated(int windowIndex, MediaPeriodId mediaPeriodId) { + // Do nothing. + } + + @Override + public void onMediaPeriodReleased(int windowIndex, MediaPeriodId mediaPeriodId) { + // Do nothing. + } + + @Override + public void onLoadStarted( + int windowIndex, + @Nullable MediaPeriodId mediaPeriodId, + LoadEventInfo loadEventInfo, + MediaLoadData mediaLoadData) { + // Do nothing. + } + + @Override + public void onLoadCompleted( + int windowIndex, + @Nullable MediaPeriodId mediaPeriodId, + LoadEventInfo loadEventInfo, + MediaLoadData mediaLoadData) { + // Do nothing. + } + + @Override + public void onLoadCanceled( + int windowIndex, + @Nullable MediaPeriodId mediaPeriodId, + LoadEventInfo loadEventInfo, + MediaLoadData mediaLoadData) { + // Do nothing. + } + + @Override + public void onLoadError( + int windowIndex, + @Nullable MediaPeriodId mediaPeriodId, + LoadEventInfo loadEventInfo, + MediaLoadData mediaLoadData, + IOException error, + boolean wasCanceled) { + // Do nothing. + } + + @Override + public void onReadingStarted(int windowIndex, MediaPeriodId mediaPeriodId) { + // Do nothing. + } + + @Override + public void onUpstreamDiscarded( + int windowIndex, @Nullable MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData) { + // Do nothing. + } + + @Override + public void onDownstreamFormatChanged( + int windowIndex, @Nullable MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData) { + // Do nothing. + } +} 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 e13a563d50..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 @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.source; import android.support.annotation.Nullable; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.trackselection.TrackSelection; @@ -36,12 +37,12 @@ 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; + public final MediaPeriodId id; - private final MediaPeriodId id; private final Allocator allocator; private MediaPeriod mediaPeriod; @@ -49,6 +50,7 @@ public final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callb private long preparePositionUs; private @Nullable PrepareErrorListener listener; private boolean notifiedPrepareError; + private long preparePositionOverrideUs; /** * Creates a new deferred media period. @@ -61,6 +63,7 @@ public final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callb this.id = id; this.allocator = allocator; this.mediaSource = mediaSource; + preparePositionOverrideUs = C.TIME_UNSET; } /** @@ -74,6 +77,25 @@ public final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callb this.listener = listener; } + /** + * Sets the default prepare position at which to prepare the media period. This value is only used + * if the call to {@link MediaPeriod#prepare(Callback, long)} is being deferred and the call was + * made with a (presumably default) prepare position of 0. + * + *

    Note that this will override an intentional seek to zero in the corresponding non-seekable + * timeline window. This is unlikely to be a problem as a non-zero default position usually only + * occurs for live playbacks and seeking to zero in a live window would cause + * BehindLiveWindowExceptions anyway. + * + * @param defaultPreparePositionUs The actual default prepare position, in microseconds. + */ + public void setDefaultPreparePositionUs(long defaultPreparePositionUs) { + if (preparePositionUs == 0 && defaultPreparePositionUs != 0) { + preparePositionOverrideUs = defaultPreparePositionUs; + preparePositionUs = defaultPreparePositionUs; + } + } + /** * Calls {@link MediaSource#createPeriod(MediaPeriodId, Allocator)} on the wrapped source then * prepares it if {@link #prepare(Callback, long)} has been called. Call {@link #releasePeriod()} @@ -118,7 +140,7 @@ public final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callb } if (!notifiedPrepareError) { notifiedPrepareError = true; - listener.onPrepareError(e); + listener.onPrepareError(id, e); } } } @@ -131,6 +153,10 @@ public final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callb @Override public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { + if (preparePositionOverrideUs != C.TIME_UNSET && positionUs == 0) { + positionUs = preparePositionOverrideUs; + preparePositionOverrideUs = C.TIME_UNSET; + } return mediaPeriod.selectTracks(selections, mayRetainStreamFlags, streams, streamResetFlags, positionUs); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java index f52c1bfd0f..37313fd1ab 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java @@ -15,827 +15,26 @@ */ package com.google.android.exoplayer2.source; -import android.os.Handler; -import android.os.Looper; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.util.SparseIntArray; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.ExoPlaybackException; -import com.google.android.exoplayer2.ExoPlayer; -import com.google.android.exoplayer2.PlayerMessage; -import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.source.DynamicConcatenatingMediaSource.MediaSourceHolder; -import com.google.android.exoplayer2.source.ShuffleOrder.DefaultShuffleOrder; -import com.google.android.exoplayer2.upstream.Allocator; -import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.Util; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.IdentityHashMap; -import java.util.List; -import java.util.Map; +/** @deprecated Use {@link ConcatenatingMediaSource} instead. */ +@Deprecated +public final class DynamicConcatenatingMediaSource extends ConcatenatingMediaSource { -/** - * Concatenates multiple {@link MediaSource}s. The list of {@link MediaSource}s can be modified - * during playback. Access to this class is thread-safe. - */ -public final class DynamicConcatenatingMediaSource extends CompositeMediaSource - implements PlayerMessage.Target { + /** @deprecated Use {@link ConcatenatingMediaSource#ConcatenatingMediaSource()} instead. */ + @Deprecated + public DynamicConcatenatingMediaSource() {} - private static final int MSG_ADD = 0; - private static final int MSG_ADD_MULTIPLE = 1; - private static final int MSG_REMOVE = 2; - private static final int MSG_MOVE = 3; - private static final int MSG_ON_COMPLETION = 4; - - // Accessed on the app thread. - private final List mediaSourcesPublic; - - // Accessed on the playback thread. - private final List mediaSourceHolders; - private final MediaSourceHolder query; - private final Map mediaSourceByMediaPeriod; - private final List deferredMediaPeriods; - private final boolean isAtomic; - - private ExoPlayer player; - private Listener listener; - private ShuffleOrder shuffleOrder; - private boolean preventListenerNotification; - private int windowCount; - private int periodCount; - - /** - * Creates a new dynamic concatenating media source. - */ - public DynamicConcatenatingMediaSource() { - this(/* isAtomic= */ false, new DefaultShuffleOrder(0)); - } - - /** - * Creates a new dynamic concatenating media source. - * - * @param isAtomic Whether the concatenating media source will be treated as atomic, i.e., treated - * as a single item for repeating and shuffling. - */ + /** @deprecated Use {@link ConcatenatingMediaSource#ConcatenatingMediaSource(boolean)} instead. */ + @Deprecated public DynamicConcatenatingMediaSource(boolean isAtomic) { - this(isAtomic, new DefaultShuffleOrder(0)); + super(isAtomic); } /** - * Creates a new dynamic concatenating media source with a custom shuffle order. - * - * @param isAtomic Whether the concatenating media source will be treated as atomic, i.e., treated - * as a single item for repeating and shuffling. - * @param shuffleOrder The {@link ShuffleOrder} to use when shuffling the child media sources. - * This shuffle order must be empty. + * @deprecated Use {@link ConcatenatingMediaSource#ConcatenatingMediaSource(boolean, + * ShuffleOrder)} instead. */ + @Deprecated public DynamicConcatenatingMediaSource(boolean isAtomic, ShuffleOrder shuffleOrder) { - this.shuffleOrder = shuffleOrder; - this.mediaSourceByMediaPeriod = new IdentityHashMap<>(); - this.mediaSourcesPublic = new ArrayList<>(); - this.mediaSourceHolders = new ArrayList<>(); - this.deferredMediaPeriods = new ArrayList<>(1); - this.query = new MediaSourceHolder(null, null, -1, -1, -1); - this.isAtomic = isAtomic; - } - - /** - * Appends a {@link MediaSource} to the playlist. - *

    - * Note: {@link MediaSource} instances are not designed to be re-used. If you want to add the same - * piece of media multiple times, use a new instance each time. - * - * @param mediaSource The {@link MediaSource} to be added to the list. - */ - public synchronized void addMediaSource(MediaSource mediaSource) { - addMediaSource(mediaSourcesPublic.size(), mediaSource, null); - } - - /** - * Appends a {@link MediaSource} to the playlist and executes a custom action on completion. - *

    - * Note: {@link MediaSource} instances are not designed to be re-used. If you want to add the same - * piece of media multiple times, use a new instance each time. - * - * @param mediaSource The {@link MediaSource} to be added to the list. - * @param actionOnCompletion A {@link Runnable} which is executed immediately after the media - * source has been added to the playlist. - */ - public synchronized void addMediaSource(MediaSource mediaSource, - @Nullable Runnable actionOnCompletion) { - addMediaSource(mediaSourcesPublic.size(), mediaSource, actionOnCompletion); - } - - /** - * Adds a {@link MediaSource} to the playlist. - *

    - * Note: {@link MediaSource} instances are not designed to be re-used. If you want to add the same - * piece of media multiple times, use a new instance each time. - * - * @param index The index at which the new {@link MediaSource} will be inserted. This index must - * be in the range of 0 <= index <= {@link #getSize()}. - * @param mediaSource The {@link MediaSource} to be added to the list. - */ - public synchronized void addMediaSource(int index, MediaSource mediaSource) { - addMediaSource(index, mediaSource, null); - } - - /** - * Adds a {@link MediaSource} to the playlist and executes a custom action on completion. - *

    - * Note: {@link MediaSource} instances are not designed to be re-used. If you want to add the same - * piece of media multiple times, use a new instance each time. - * - * @param index The index at which the new {@link MediaSource} will be inserted. This index must - * be in the range of 0 <= index <= {@link #getSize()}. - * @param mediaSource The {@link MediaSource} to be added to the list. - * @param actionOnCompletion A {@link Runnable} which is executed immediately after the media - * source has been added to the playlist. - */ - public synchronized void addMediaSource(int index, MediaSource mediaSource, - @Nullable Runnable actionOnCompletion) { - Assertions.checkNotNull(mediaSource); - Assertions.checkArgument(!mediaSourcesPublic.contains(mediaSource)); - mediaSourcesPublic.add(index, mediaSource); - if (player != null) { - player - .createMessage(this) - .setType(MSG_ADD) - .setPayload(new MessageData<>(index, mediaSource, actionOnCompletion)) - .send(); - } else if (actionOnCompletion != null) { - actionOnCompletion.run(); - } - } - - /** - * Appends multiple {@link MediaSource}s to the playlist. - *

    - * Note: {@link MediaSource} instances are not designed to be re-used. If you want to add the same - * piece of media multiple times, use a new instance each time. - * - * @param mediaSources A collection of {@link MediaSource}s to be added to the list. The media - * sources are added in the order in which they appear in this collection. - */ - public synchronized void addMediaSources(Collection mediaSources) { - addMediaSources(mediaSourcesPublic.size(), mediaSources, null); - } - - /** - * Appends multiple {@link MediaSource}s to the playlist and executes a custom action on - * completion. - *

    - * Note: {@link MediaSource} instances are not designed to be re-used. If you want to add the same - * piece of media multiple times, use a new instance each time. - * - * @param mediaSources A collection of {@link MediaSource}s to be added to the list. The media - * sources are added in the order in which they appear in this collection. - * @param actionOnCompletion A {@link Runnable} which is executed immediately after the media - * sources have been added to the playlist. - */ - public synchronized void addMediaSources(Collection mediaSources, - @Nullable Runnable actionOnCompletion) { - addMediaSources(mediaSourcesPublic.size(), mediaSources, actionOnCompletion); - } - - /** - * Adds multiple {@link MediaSource}s to the playlist. - *

    - * Note: {@link MediaSource} instances are not designed to be re-used. If you want to add the same - * piece of media multiple times, use a new instance each time. - * - * @param index The index at which the new {@link MediaSource}s will be inserted. This index must - * be in the range of 0 <= index <= {@link #getSize()}. - * @param mediaSources A collection of {@link MediaSource}s to be added to the list. The media - * sources are added in the order in which they appear in this collection. - */ - public synchronized void addMediaSources(int index, Collection mediaSources) { - addMediaSources(index, mediaSources, null); - } - - /** - * Adds multiple {@link MediaSource}s to the playlist and executes a custom action on completion. - *

    - * Note: {@link MediaSource} instances are not designed to be re-used. If you want to add the same - * piece of media multiple times, use a new instance each time. - * - * @param index The index at which the new {@link MediaSource}s will be inserted. This index must - * be in the range of 0 <= index <= {@link #getSize()}. - * @param mediaSources A collection of {@link MediaSource}s to be added to the list. The media - * sources are added in the order in which they appear in this collection. - * @param actionOnCompletion A {@link Runnable} which is executed immediately after the media - * sources have been added to the playlist. - */ - public synchronized void addMediaSources(int index, Collection mediaSources, - @Nullable Runnable actionOnCompletion) { - for (MediaSource mediaSource : mediaSources) { - Assertions.checkNotNull(mediaSource); - Assertions.checkArgument(!mediaSourcesPublic.contains(mediaSource)); - } - mediaSourcesPublic.addAll(index, mediaSources); - if (player != null && !mediaSources.isEmpty()) { - player - .createMessage(this) - .setType(MSG_ADD_MULTIPLE) - .setPayload(new MessageData<>(index, mediaSources, actionOnCompletion)) - .send(); - } else if (actionOnCompletion != null){ - actionOnCompletion.run(); - } - } - - /** - * Removes a {@link MediaSource} from the playlist. - *

    - * Note: {@link MediaSource} instances are not designed to be re-used, and so the instance being - * removed should not be re-added. If you want to move the instance use - * {@link #moveMediaSource(int, int)} instead. - * - * @param index The index at which the media source will be removed. This index must be in the - * range of 0 <= index < {@link #getSize()}. - */ - public synchronized void removeMediaSource(int index) { - removeMediaSource(index, null); - } - - /** - * Removes a {@link MediaSource} from the playlist and executes a custom action on completion. - *

    - * Note: {@link MediaSource} instances are not designed to be re-used, and so the instance being - * removed should not be re-added. If you want to move the instance use - * {@link #moveMediaSource(int, int)} instead. - * - * @param index The index at which the media source will be removed. This index must be in the - * range of 0 <= index < {@link #getSize()}. - * @param actionOnCompletion A {@link Runnable} which is executed immediately after the media - * source has been removed from the playlist. - */ - public synchronized void removeMediaSource(int index, @Nullable Runnable actionOnCompletion) { - mediaSourcesPublic.remove(index); - if (player != null) { - player - .createMessage(this) - .setType(MSG_REMOVE) - .setPayload(new MessageData<>(index, null, actionOnCompletion)) - .send(); - } else if (actionOnCompletion != null) { - actionOnCompletion.run(); - } - } - - /** - * Moves an existing {@link MediaSource} within the playlist. - * - * @param currentIndex The current index of the media source in the playlist. This index must be - * in the range of 0 <= index < {@link #getSize()}. - * @param newIndex The target index of the media source in the playlist. This index must be in the - * range of 0 <= index < {@link #getSize()}. - */ - public synchronized void moveMediaSource(int currentIndex, int newIndex) { - moveMediaSource(currentIndex, newIndex, null); - } - - /** - * Moves an existing {@link MediaSource} within the playlist and executes a custom action on - * completion. - * - * @param currentIndex The current index of the media source in the playlist. This index must be - * in the range of 0 <= index < {@link #getSize()}. - * @param newIndex The target index of the media source in the playlist. This index must be in the - * range of 0 <= index < {@link #getSize()}. - * @param actionOnCompletion A {@link Runnable} which is executed immediately after the media - * source has been moved. - */ - public synchronized void moveMediaSource(int currentIndex, int newIndex, - @Nullable Runnable actionOnCompletion) { - if (currentIndex == newIndex) { - return; - } - mediaSourcesPublic.add(newIndex, mediaSourcesPublic.remove(currentIndex)); - if (player != null) { - player - .createMessage(this) - .setType(MSG_MOVE) - .setPayload(new MessageData<>(currentIndex, newIndex, actionOnCompletion)) - .send(); - } else if (actionOnCompletion != null) { - actionOnCompletion.run(); - } - } - - /** - * Returns the number of media sources in the playlist. - */ - public synchronized int getSize() { - return mediaSourcesPublic.size(); - } - - /** - * Returns the {@link MediaSource} at a specified index. - * - * @param index An index in the range of 0 <= index <= {@link #getSize()}. - * @return The {@link MediaSource} at this index. - */ - public synchronized MediaSource getMediaSource(int index) { - return mediaSourcesPublic.get(index); - } - - @Override - public synchronized void prepareSource(ExoPlayer player, boolean isTopLevelSource, - Listener listener) { - super.prepareSource(player, isTopLevelSource, listener); - this.player = player; - this.listener = listener; - preventListenerNotification = true; - shuffleOrder = shuffleOrder.cloneAndInsert(0, mediaSourcesPublic.size()); - addMediaSourcesInternal(0, mediaSourcesPublic); - preventListenerNotification = false; - maybeNotifyListener(null); - } - - @Override - public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { - int mediaSourceHolderIndex = findMediaSourceHolderByPeriodIndex(id.periodIndex); - MediaSourceHolder holder = mediaSourceHolders.get(mediaSourceHolderIndex); - MediaPeriodId idInSource = id.copyWithPeriodIndex( - id.periodIndex - holder.firstPeriodIndexInChild); - MediaPeriod mediaPeriod; - if (!holder.isPrepared) { - mediaPeriod = new DeferredMediaPeriod(holder.mediaSource, idInSource, allocator); - deferredMediaPeriods.add((DeferredMediaPeriod) mediaPeriod); - } else { - mediaPeriod = holder.mediaSource.createPeriod(idInSource, allocator); - } - mediaSourceByMediaPeriod.put(mediaPeriod, holder); - holder.activeMediaPeriods++; - return mediaPeriod; - } - - @Override - public void releasePeriod(MediaPeriod mediaPeriod) { - MediaSourceHolder holder = mediaSourceByMediaPeriod.remove(mediaPeriod); - if (mediaPeriod instanceof DeferredMediaPeriod) { - deferredMediaPeriods.remove(mediaPeriod); - ((DeferredMediaPeriod) mediaPeriod).releasePeriod(); - } else { - holder.mediaSource.releasePeriod(mediaPeriod); - } - holder.activeMediaPeriods--; - if (holder.activeMediaPeriods == 0 && holder.isRemoved) { - releaseChildSource(holder); - } - } - - @Override - public void releaseSource() { - super.releaseSource(); - mediaSourceHolders.clear(); - player = null; - listener = null; - shuffleOrder = shuffleOrder.cloneAndClear(); - windowCount = 0; - periodCount = 0; - } - - @Override - protected void onChildSourceInfoRefreshed( - MediaSourceHolder mediaSourceHolder, - MediaSource mediaSource, - Timeline timeline, - @Nullable Object manifest) { - updateMediaSourceInternal(mediaSourceHolder, timeline); - } - - @Override - @SuppressWarnings("unchecked") - public void handleMessage(int messageType, Object message) throws ExoPlaybackException { - if (messageType == MSG_ON_COMPLETION) { - ((EventDispatcher) message).dispatchEvent(); - return; - } - preventListenerNotification = true; - EventDispatcher actionOnCompletion; - switch (messageType) { - case MSG_ADD: { - MessageData messageData = (MessageData) message; - shuffleOrder = shuffleOrder.cloneAndInsert(messageData.index, 1); - addMediaSourceInternal(messageData.index, messageData.customData); - actionOnCompletion = messageData.actionOnCompletion; - break; - } - case MSG_ADD_MULTIPLE: { - MessageData> messageData = - (MessageData>) message; - shuffleOrder = shuffleOrder.cloneAndInsert(messageData.index, - messageData.customData.size()); - addMediaSourcesInternal(messageData.index, messageData.customData); - actionOnCompletion = messageData.actionOnCompletion; - break; - } - case MSG_REMOVE: { - MessageData messageData = (MessageData) message; - shuffleOrder = shuffleOrder.cloneAndRemove(messageData.index); - removeMediaSourceInternal(messageData.index); - actionOnCompletion = messageData.actionOnCompletion; - break; - } - case MSG_MOVE: { - MessageData messageData = (MessageData) message; - shuffleOrder = shuffleOrder.cloneAndRemove(messageData.index); - shuffleOrder = shuffleOrder.cloneAndInsert(messageData.customData, 1); - moveMediaSourceInternal(messageData.index, messageData.customData); - actionOnCompletion = messageData.actionOnCompletion; - break; - } - default: { - throw new IllegalStateException(); - } - } - preventListenerNotification = false; - maybeNotifyListener(actionOnCompletion); - } - - private void maybeNotifyListener(@Nullable EventDispatcher actionOnCompletion) { - if (!preventListenerNotification) { - listener.onSourceInfoRefreshed( - this, - new ConcatenatedTimeline( - mediaSourceHolders, windowCount, periodCount, shuffleOrder, isAtomic), - null); - if (actionOnCompletion != null) { - player.createMessage(this).setType(MSG_ON_COMPLETION).setPayload(actionOnCompletion).send(); - } - } - } - - private void addMediaSourceInternal(int newIndex, MediaSource newMediaSource) { - final MediaSourceHolder newMediaSourceHolder; - DeferredTimeline newTimeline = new DeferredTimeline(); - if (newIndex > 0) { - MediaSourceHolder previousHolder = mediaSourceHolders.get(newIndex - 1); - newMediaSourceHolder = - new MediaSourceHolder( - newMediaSource, - newTimeline, - newIndex, - previousHolder.firstWindowIndexInChild + previousHolder.timeline.getWindowCount(), - previousHolder.firstPeriodIndexInChild + previousHolder.timeline.getPeriodCount()); - } else { - newMediaSourceHolder = new MediaSourceHolder(newMediaSource, newTimeline, 0, 0, 0); - } - correctOffsets( - newIndex, - /* childIndexUpdate= */ 1, - newTimeline.getWindowCount(), - newTimeline.getPeriodCount()); - mediaSourceHolders.add(newIndex, newMediaSourceHolder); - prepareChildSource(newMediaSourceHolder, newMediaSourceHolder.mediaSource); - } - - private void addMediaSourcesInternal(int index, Collection mediaSources) { - for (MediaSource mediaSource : mediaSources) { - addMediaSourceInternal(index++, mediaSource); - } - } - - private void updateMediaSourceInternal(MediaSourceHolder mediaSourceHolder, Timeline timeline) { - if (mediaSourceHolder == null) { - throw new IllegalArgumentException(); - } - DeferredTimeline deferredTimeline = mediaSourceHolder.timeline; - if (deferredTimeline.getTimeline() == timeline) { - return; - } - int windowOffsetUpdate = timeline.getWindowCount() - deferredTimeline.getWindowCount(); - int periodOffsetUpdate = timeline.getPeriodCount() - deferredTimeline.getPeriodCount(); - if (windowOffsetUpdate != 0 || periodOffsetUpdate != 0) { - correctOffsets( - mediaSourceHolder.childIndex + 1, - /* childIndexUpdate= */ 0, - windowOffsetUpdate, - periodOffsetUpdate); - } - mediaSourceHolder.timeline = deferredTimeline.cloneWithNewTimeline(timeline); - if (!mediaSourceHolder.isPrepared) { - for (int i = deferredMediaPeriods.size() - 1; i >= 0; i--) { - if (deferredMediaPeriods.get(i).mediaSource == mediaSourceHolder.mediaSource) { - deferredMediaPeriods.get(i).createPeriod(); - deferredMediaPeriods.remove(i); - } - } - } - mediaSourceHolder.isPrepared = true; - maybeNotifyListener(null); - } - - private void removeMediaSourceInternal(int index) { - MediaSourceHolder holder = mediaSourceHolders.get(index); - mediaSourceHolders.remove(index); - Timeline oldTimeline = holder.timeline; - correctOffsets( - index, - /* childIndexUpdate= */ -1, - -oldTimeline.getWindowCount(), - -oldTimeline.getPeriodCount()); - holder.isRemoved = true; - if (holder.activeMediaPeriods == 0) { - releaseChildSource(holder); - } - } - - private void moveMediaSourceInternal(int currentIndex, int newIndex) { - int startIndex = Math.min(currentIndex, newIndex); - int endIndex = Math.max(currentIndex, newIndex); - int windowOffset = mediaSourceHolders.get(startIndex).firstWindowIndexInChild; - int periodOffset = mediaSourceHolders.get(startIndex).firstPeriodIndexInChild; - mediaSourceHolders.add(newIndex, mediaSourceHolders.remove(currentIndex)); - for (int i = startIndex; i <= endIndex; i++) { - MediaSourceHolder holder = mediaSourceHolders.get(i); - holder.firstWindowIndexInChild = windowOffset; - holder.firstPeriodIndexInChild = periodOffset; - windowOffset += holder.timeline.getWindowCount(); - periodOffset += holder.timeline.getPeriodCount(); - } - } - - private void correctOffsets( - int startIndex, int childIndexUpdate, int windowOffsetUpdate, int periodOffsetUpdate) { - windowCount += windowOffsetUpdate; - periodCount += periodOffsetUpdate; - for (int i = startIndex; i < mediaSourceHolders.size(); i++) { - mediaSourceHolders.get(i).childIndex += childIndexUpdate; - mediaSourceHolders.get(i).firstWindowIndexInChild += windowOffsetUpdate; - mediaSourceHolders.get(i).firstPeriodIndexInChild += periodOffsetUpdate; - } - } - - private int findMediaSourceHolderByPeriodIndex(int periodIndex) { - query.firstPeriodIndexInChild = periodIndex; - int index = Collections.binarySearch(mediaSourceHolders, query); - if (index < 0) { - return -index - 2; - } - while (index < mediaSourceHolders.size() - 1 - && mediaSourceHolders.get(index + 1).firstPeriodIndexInChild == periodIndex) { - index++; - } - return index; - } - - /** Data class to hold playlist media sources together with meta data needed to process them. */ - /* package */ static final class MediaSourceHolder implements Comparable { - - public final MediaSource mediaSource; - public final int uid; - - public DeferredTimeline timeline; - public int childIndex; - public int firstWindowIndexInChild; - public int firstPeriodIndexInChild; - public boolean isPrepared; - public boolean isRemoved; - public int activeMediaPeriods; - - public MediaSourceHolder( - MediaSource mediaSource, - DeferredTimeline timeline, - int childIndex, - int window, - int period) { - this.mediaSource = mediaSource; - this.timeline = timeline; - this.childIndex = childIndex; - this.firstWindowIndexInChild = window; - this.firstPeriodIndexInChild = period; - this.uid = System.identityHashCode(this); - } - - @Override - public int compareTo(@NonNull MediaSourceHolder other) { - return this.firstPeriodIndexInChild - other.firstPeriodIndexInChild; - } - } - - /** - * Can be used to dispatch a runnable on the thread the object was created on. - */ - private static final class EventDispatcher { - - public final Handler eventHandler; - public final Runnable runnable; - - public EventDispatcher(Runnable runnable) { - this.runnable = runnable; - this.eventHandler = new Handler(Looper.myLooper() != null ? Looper.myLooper() - : Looper.getMainLooper()); - } - - public void dispatchEvent() { - eventHandler.post(runnable); - } - - } - - /** Message used to post actions from app thread to playback thread. */ - private static final class MessageData { - - public final int index; - public final T customData; - public final @Nullable EventDispatcher actionOnCompletion; - - public MessageData(int index, T customData, @Nullable Runnable actionOnCompletion) { - this.index = index; - this.actionOnCompletion = actionOnCompletion != null - ? new EventDispatcher(actionOnCompletion) : null; - this.customData = customData; - } - - } - - /** - * Timeline exposing concatenated timelines of playlist media sources. - */ - private static final class ConcatenatedTimeline extends AbstractConcatenatedTimeline { - - private final int windowCount; - private final int periodCount; - private final int[] firstPeriodInChildIndices; - private final int[] firstWindowInChildIndices; - private final Timeline[] timelines; - private final int[] uids; - private final SparseIntArray childIndexByUid; - - public ConcatenatedTimeline( - Collection mediaSourceHolders, - int windowCount, - int periodCount, - ShuffleOrder shuffleOrder, - boolean isAtomic) { - super(isAtomic, shuffleOrder); - this.windowCount = windowCount; - this.periodCount = periodCount; - int childCount = mediaSourceHolders.size(); - firstPeriodInChildIndices = new int[childCount]; - firstWindowInChildIndices = new int[childCount]; - timelines = new Timeline[childCount]; - uids = new int[childCount]; - childIndexByUid = new SparseIntArray(); - int index = 0; - for (MediaSourceHolder mediaSourceHolder : mediaSourceHolders) { - timelines[index] = mediaSourceHolder.timeline; - firstPeriodInChildIndices[index] = mediaSourceHolder.firstPeriodIndexInChild; - firstWindowInChildIndices[index] = mediaSourceHolder.firstWindowIndexInChild; - uids[index] = mediaSourceHolder.uid; - childIndexByUid.put(uids[index], index++); - } - } - - @Override - protected int getChildIndexByPeriodIndex(int periodIndex) { - return Util.binarySearchFloor(firstPeriodInChildIndices, periodIndex + 1, false, false); - } - - @Override - protected int getChildIndexByWindowIndex(int windowIndex) { - return Util.binarySearchFloor(firstWindowInChildIndices, windowIndex + 1, false, false); - } - - @Override - protected int getChildIndexByChildUid(Object childUid) { - if (!(childUid instanceof Integer)) { - return C.INDEX_UNSET; - } - int index = childIndexByUid.get((int) childUid, -1); - return index == -1 ? C.INDEX_UNSET : index; - } - - @Override - protected Timeline getTimelineByChildIndex(int childIndex) { - return timelines[childIndex]; - } - - @Override - protected int getFirstPeriodIndexByChildIndex(int childIndex) { - return firstPeriodInChildIndices[childIndex]; - } - - @Override - protected int getFirstWindowIndexByChildIndex(int childIndex) { - return firstWindowInChildIndices[childIndex]; - } - - @Override - protected Object getChildUidByChildIndex(int childIndex) { - return uids[childIndex]; - } - - @Override - public int getWindowCount() { - return windowCount; - } - - @Override - public int getPeriodCount() { - return periodCount; - } - - } - - /** - * Timeline used as placeholder for an unprepared media source. After preparation, a copy of the - * DeferredTimeline is used to keep the originally assigned first period ID. - */ - private static final class DeferredTimeline extends ForwardingTimeline { - - private static final Object DUMMY_ID = new Object(); - private static final Period period = new Period(); - private static final DummyTimeline dummyTimeline = new DummyTimeline(); - - private final Object replacedId; - - public DeferredTimeline() { - this(dummyTimeline, /* replacedId= */ null); - } - - private DeferredTimeline(Timeline timeline, Object replacedId) { - super(timeline); - this.replacedId = replacedId; - } - - public DeferredTimeline cloneWithNewTimeline(Timeline timeline) { - return new DeferredTimeline( - timeline, - replacedId == null && timeline.getPeriodCount() > 0 - ? timeline.getPeriod(0, period, true).uid - : replacedId); - } - - public Timeline getTimeline() { - return timeline; - } - - @Override - public Period getPeriod(int periodIndex, Period period, boolean setIds) { - timeline.getPeriod(periodIndex, period, setIds); - if (Util.areEqual(period.uid, replacedId)) { - period.uid = DUMMY_ID; - } - return period; - } - - @Override - public int getIndexOfPeriod(Object uid) { - return timeline.getIndexOfPeriod(DUMMY_ID.equals(uid) ? replacedId : uid); - } - } - - /** Dummy placeholder timeline with one dynamic window with a period of indeterminate duration. */ - private static final class DummyTimeline extends Timeline { - - @Override - public int getWindowCount() { - return 1; - } - - @Override - public Window getWindow(int windowIndex, Window window, boolean setIds, - long defaultPositionProjectionUs) { - // Dynamic window to indicate pending timeline updates. - return window.set( - /* id= */ null, - /* presentationStartTimeMs= */ C.TIME_UNSET, - /* windowStartTimeMs= */ C.TIME_UNSET, - /* isSeekable= */ false, - /* isDynamic= */ true, - /* defaultPositionUs= */ 0, - /* durationUs= */ C.TIME_UNSET, - /* firstPeriodIndex= */ 0, - /* lastPeriodIndex= */ 0, - /* positionInFirstPeriodUs= */ 0); - } - - @Override - public int getPeriodCount() { - return 1; - } - - @Override - public Period getPeriod(int periodIndex, Period period, boolean setIds) { - return period.set( - /* id= */ null, - /* uid= */ null, - /* windowIndex= */ 0, - /* durationUs = */ C.TIME_UNSET, - /* positionInWindowUs= */ C.TIME_UNSET); - } - - @Override - public int getIndexOfPeriod(Object uid) { - return uid == null ? 0 : C.INDEX_UNSET; - } + super(isAtomic, shuffleOrder); } } - diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java index c771188e3b..ed27a24350 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java @@ -100,6 +100,7 @@ import java.util.Arrays; private boolean seenFirstTrackSelection; private boolean notifyDiscontinuity; + private boolean notifiedReadingStarted; private int enabledTrackCount; private TrackGroupArray tracks; private long durationUs; @@ -176,6 +177,7 @@ import java.util.Arrays; minLoadableRetryCount == ExtractorMediaSource.MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA ? ExtractorMediaSource.DEFAULT_MIN_LOADABLE_RETRY_COUNT_ON_DEMAND : minLoadableRetryCount; + eventDispatcher.mediaPeriodCreated(); } public void release() { @@ -189,6 +191,7 @@ import java.util.Arrays; loader.release(this); handler.removeCallbacksAndMessages(null); released = true; + eventDispatcher.mediaPeriodReleased(); } @Override @@ -319,6 +322,10 @@ import java.util.Arrays; @Override public long readDiscontinuity() { + if (!notifiedReadingStarted) { + eventDispatcher.readingStarted(); + notifiedReadingStarted = true; + } if (notifyDiscontinuity && (loadingFinished || getExtractedSamplesCount() > extractedSamplesCountAtStartOfLoad)) { notifyDiscontinuity = false; @@ -524,8 +531,8 @@ import java.util.Arrays; } @Override - public int onLoadError(ExtractingLoadable loadable, long elapsedRealtimeMs, - long loadDurationMs, IOException error) { + public @Loader.RetryAction int onLoadError( + ExtractingLoadable loadable, long elapsedRealtimeMs, long loadDurationMs, IOException error) { boolean isErrorFatal = isLoadableExceptionFatal(error); eventDispatcher.loadError( loadable.dataSpec, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java index 14453653af..c4a0487bd9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java @@ -20,31 +20,29 @@ import android.os.Handler; import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; -import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorsFactory; -import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.ads.AdsMediaSource; 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; /** * Provides one period that loads data from a {@link Uri} and extracted using an {@link Extractor}. - *

    - * If the possible input stream container formats are known, pass a factory that instantiates - * extractors for them to the constructor. Otherwise, pass a {@link DefaultExtractorsFactory} to - * use the default extractors. When reading a new stream, the first {@link Extractor} in the array - * of extractors created by the factory that returns {@code true} from {@link Extractor#sniff} will - * be used to extract samples from the input stream. - *

    - * Note that the built-in extractors for AAC, MPEG PS/TS and FLV streams do not support seeking. + * + *

    If the possible input stream container formats are known, pass a factory that instantiates + * extractors for them to the constructor. Otherwise, pass a {@link DefaultExtractorsFactory} to use + * the default extractors. When reading a new stream, the first {@link Extractor} in the array of + * extractors created by the factory that returns {@code true} from {@link Extractor#sniff} will be + * used to extract samples from the input stream. + * + *

    Note that the built-in extractors for AAC, MPEG PS/TS and FLV streams do not support seeking. */ -public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPeriod.Listener { +public final class ExtractorMediaSource extends BaseMediaSource + implements ExtractorMediaPeriod.Listener { /** * Listener of {@link ExtractorMediaSource} events. * @@ -96,11 +94,10 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe private final DataSource.Factory dataSourceFactory; private final ExtractorsFactory extractorsFactory; private final int minLoadableRetryCount; - private final EventDispatcher eventDispatcher; private final String customCacheKey; private final int continueLoadingCheckIntervalBytes; + private final @Nullable Object tag; - private MediaSource.Listener sourceListener; private long timelineDurationUs; private boolean timelineIsSeekable; @@ -111,6 +108,7 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe private @Nullable ExtractorsFactory extractorsFactory; private @Nullable String customCacheKey; + private @Nullable Object tag; private int minLoadableRetryCount; private int continueLoadingCheckIntervalBytes; private boolean isCreateCalled; @@ -157,6 +155,21 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe return this; } + /** + * Sets a tag for the media source which will be published in the {@link + * com.google.android.exoplayer2.Timeline} of the source as {@link + * com.google.android.exoplayer2.Timeline.Window#tag}. + * + * @param tag A tag for the media source. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. + */ + public Factory setTag(Object tag) { + Assertions.checkState(!isCreateCalled); + this.tag = tag; + return this; + } + /** * Sets the minimum number of times to retry if a loading error occurs. The default value is * {@link #MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA}. @@ -188,35 +201,40 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe return this; } - /** - * Returns a new {@link ExtractorMediaSource} using the current parameters. Media source events - * will not be delivered. - * - * @param uri The {@link Uri}. - * @return The new {@link ExtractorMediaSource}. - */ - public ExtractorMediaSource createMediaSource(Uri uri) { - return createMediaSource(uri, null, null); - } - /** * Returns a new {@link ExtractorMediaSource} using the current parameters. * * @param uri The {@link Uri}. - * @param eventHandler A handler for events. - * @param eventListener A listener of events. * @return The new {@link ExtractorMediaSource}. */ @Override - public ExtractorMediaSource createMediaSource( - Uri uri, @Nullable Handler eventHandler, @Nullable MediaSourceEventListener eventListener) { + public ExtractorMediaSource createMediaSource(Uri uri) { isCreateCalled = true; if (extractorsFactory == null) { extractorsFactory = new DefaultExtractorsFactory(); } - return new ExtractorMediaSource(uri, dataSourceFactory, extractorsFactory, - minLoadableRetryCount, eventHandler, eventListener, customCacheKey, - continueLoadingCheckIntervalBytes); + return new ExtractorMediaSource( + uri, + dataSourceFactory, + extractorsFactory, + minLoadableRetryCount, + customCacheKey, + continueLoadingCheckIntervalBytes, + tag); + } + + /** + * @deprecated Use {@link #createMediaSource(Uri)} and {@link #addEventListener(Handler, + * MediaSourceEventListener)} instead. + */ + @Deprecated + public ExtractorMediaSource createMediaSource( + Uri uri, @Nullable Handler eventHandler, @Nullable MediaSourceEventListener eventListener) { + ExtractorMediaSource mediaSource = createMediaSource(uri); + if (eventHandler != null && eventListener != null) { + mediaSource.addEventListener(eventHandler, eventListener); + } + return mediaSource; } @Override @@ -299,10 +317,12 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe dataSourceFactory, extractorsFactory, minLoadableRetryCount, - eventHandler, - eventListener == null ? null : new EventListenerWrapper(eventListener), customCacheKey, - continueLoadingCheckIntervalBytes); + continueLoadingCheckIntervalBytes, + /* tag= */ null); + if (eventListener != null && eventHandler != null) { + addEventListener(eventHandler, new EventListenerWrapper(eventListener)); + } } private ExtractorMediaSource( @@ -310,23 +330,22 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe DataSource.Factory dataSourceFactory, ExtractorsFactory extractorsFactory, int minLoadableRetryCount, - @Nullable Handler eventHandler, - @Nullable MediaSourceEventListener eventListener, @Nullable String customCacheKey, - int continueLoadingCheckIntervalBytes) { + int continueLoadingCheckIntervalBytes, + @Nullable Object tag) { this.uri = uri; this.dataSourceFactory = dataSourceFactory; this.extractorsFactory = extractorsFactory; this.minLoadableRetryCount = minLoadableRetryCount; - this.eventDispatcher = new EventDispatcher(eventHandler, eventListener); this.customCacheKey = customCacheKey; this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes; + this.timelineDurationUs = C.TIME_UNSET; + this.tag = tag; } @Override - public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { - sourceListener = listener; - notifySourceInfoRefreshed(C.TIME_UNSET, false); + public void prepareSourceInternal(ExoPlayer player, boolean isTopLevelSource) { + notifySourceInfoRefreshed(timelineDurationUs, /* isSeekable= */ false); } @Override @@ -342,7 +361,7 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe dataSourceFactory.createDataSource(), extractorsFactory.createExtractors(), minLoadableRetryCount, - eventDispatcher, + createEventDispatcher(id), this, allocator, customCacheKey, @@ -355,8 +374,8 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe } @Override - public void releaseSource() { - sourceListener = null; + public void releaseSourceInternal() { + // Do nothing. } // ExtractorMediaPeriod.Listener implementation. @@ -378,98 +397,32 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe timelineDurationUs = durationUs; timelineIsSeekable = isSeekable; // TODO: Make timeline dynamic until its duration is known. This is non-trivial. See b/69703223. - sourceListener.onSourceInfoRefreshed(this, - new SinglePeriodTimeline(timelineDurationUs, timelineIsSeekable, false), null); + refreshSourceInfo( + new SinglePeriodTimeline( + timelineDurationUs, timelineIsSeekable, /* isDynamic= */ false, tag), + /* manifest= */ null); } /** * Wraps a deprecated {@link EventListener}, invoking its callback from the equivalent callback in * {@link MediaSourceEventListener}. */ - private static final class EventListenerWrapper implements MediaSourceEventListener { + private static final class EventListenerWrapper extends DefaultMediaSourceEventListener { private final EventListener eventListener; public EventListenerWrapper(EventListener eventListener) { this.eventListener = Assertions.checkNotNull(eventListener); } - @Override - public void onLoadStarted( - DataSpec dataSpec, - int dataType, - int trackType, - Format trackFormat, - int trackSelectionReason, - Object trackSelectionData, - long mediaStartTimeMs, - long mediaEndTimeMs, - long elapsedRealtimeMs) { - // Do nothing. - } - - @Override - public void onLoadCompleted( - DataSpec dataSpec, - int dataType, - int trackType, - Format trackFormat, - int trackSelectionReason, - Object trackSelectionData, - long mediaStartTimeMs, - long mediaEndTimeMs, - long elapsedRealtimeMs, - long loadDurationMs, - long bytesLoaded) { - // Do nothing. - } - - @Override - public void onLoadCanceled( - DataSpec dataSpec, - int dataType, - int trackType, - Format trackFormat, - int trackSelectionReason, - Object trackSelectionData, - long mediaStartTimeMs, - long mediaEndTimeMs, - long elapsedRealtimeMs, - long loadDurationMs, - long bytesLoaded) { - // Do nothing. - } - @Override public void onLoadError( - DataSpec dataSpec, - int dataType, - int trackType, - Format trackFormat, - int trackSelectionReason, - Object trackSelectionData, - long mediaStartTimeMs, - long mediaEndTimeMs, - long elapsedRealtimeMs, - long loadDurationMs, - long bytesLoaded, + int windowIndex, + @Nullable MediaPeriodId mediaPeriodId, + LoadEventInfo loadEventInfo, + MediaLoadData mediaLoadData, IOException error, boolean wasCanceled) { eventListener.onLoadError(error); } - - @Override - public void onUpstreamDiscarded(int trackType, long mediaStartTimeMs, long mediaEndTimeMs) { - // Do nothing. - } - - @Override - public void onDownstreamFormatChanged( - int trackType, - Format trackFormat, - int trackSelectionReason, - Object trackSelectionData, - long mediaTimeMs) { - // Do nothing. - } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ForwardingTimeline.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ForwardingTimeline.java index cfa5cec387..c7ab7615d8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ForwardingTimeline.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ForwardingTimeline.java @@ -57,9 +57,9 @@ public abstract class ForwardingTimeline extends Timeline { } @Override - public Window getWindow(int windowIndex, Window window, boolean setIds, - long defaultPositionProjectionUs) { - return timeline.getWindow(windowIndex, window, setIds, defaultPositionProjectionUs); + public Window getWindow( + int windowIndex, Window window, boolean setTag, long defaultPositionProjectionUs) { + return timeline.getWindow(windowIndex, window, setTag, defaultPositionProjectionUs); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java index e2ef4eb5fa..774074b016 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java @@ -36,7 +36,6 @@ public final class LoopingMediaSource extends CompositeMediaSource { private final int loopCount; private int childPeriodCount; - private Listener listener; /** * Loops the provided source indefinitely. Note that it is usually better to use @@ -61,9 +60,8 @@ public final class LoopingMediaSource extends CompositeMediaSource { } @Override - public void prepareSource(ExoPlayer player, boolean isTopLevelSource, final Listener listener) { - super.prepareSource(player, isTopLevelSource, listener); - this.listener = listener; + public void prepareSourceInternal(ExoPlayer player, boolean isTopLevelSource) { + super.prepareSourceInternal(player, isTopLevelSource); prepareChildSource(/* id= */ null, childSource); } @@ -81,9 +79,8 @@ public final class LoopingMediaSource extends CompositeMediaSource { } @Override - public void releaseSource() { - super.releaseSource(); - listener = null; + public void releaseSourceInternal() { + super.releaseSourceInternal(); childPeriodCount = 0; } @@ -95,7 +92,7 @@ public final class LoopingMediaSource extends CompositeMediaSource { loopCount != Integer.MAX_VALUE ? new LoopingTimeline(timeline, loopCount) : new InfinitelyLoopingTimeline(timeline); - listener.onSourceInfoRefreshed(this, loopingTimeline, manifest); + refreshSourceInfo(loopingTimeline, manifest); } private static final class LoopingTimeline extends AbstractConcatenatedTimeline { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java index a5b2314d78..997f94bbfe 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java @@ -53,8 +53,9 @@ public interface MediaPeriod extends SequenceableLoader { * {@link #maybeThrowPrepareError()} will throw an {@link IOException}. * *

    If preparation succeeds and results in a source timeline change (e.g. the period duration - * becoming known), {@link MediaSource.Listener#onSourceInfoRefreshed(MediaSource, Timeline, - * Object)} will be called before {@code callback.onPrepared}. + * becoming known), {@link + * MediaSource.SourceInfoRefreshListener#onSourceInfoRefreshed(MediaSource, Timeline, Object)} + * will be called before {@code callback.onPrepared}. * * @param callback Callback to receive updates from this period, including being notified when * preparation completes. @@ -66,7 +67,7 @@ public interface MediaPeriod extends SequenceableLoader { * Throws an error that's preventing the period from becoming prepared. Does nothing if no such * error exists. * - *

    This method should only be called before the period has completed preparation. + *

    This method is only called before the period has completed preparation. * * @throws IOException The underlying error. */ @@ -75,7 +76,7 @@ public interface MediaPeriod extends SequenceableLoader { /** * Returns the {@link TrackGroup}s exposed by the period. * - *

    This method should only be called after the period has been prepared. + *

    This method is only called after the period has been prepared. * * @return The {@link TrackGroup}s. */ @@ -92,7 +93,7 @@ public interface MediaPeriod extends SequenceableLoader { * corresponding flag in {@code streamResetFlags} will be set to true. This flag will also be set * if a new sample stream is created. * - *

    This method should only be called after the period has been prepared. + *

    This method is only called after the period has been prepared. * * @param selections The renderer track selections. * @param mayRetainStreamFlags Flags indicating whether the existing sample stream can be retained @@ -116,7 +117,7 @@ public interface MediaPeriod extends SequenceableLoader { /** * Discards buffered media up to the specified position. * - *

    This method should only be called after the period has been prepared. + *

    This method is only called after the period has been prepared. * * @param positionUs The position in microseconds. * @param toKeyframe If true then for each track discards samples up to the keyframe before or at @@ -130,7 +131,8 @@ public interface MediaPeriod extends SequenceableLoader { *

    After this method has returned a value other than {@link C#TIME_UNSET}, all {@link * SampleStream}s provided by the period are guaranteed to start from a key frame. * - *

    This method should only be called after the period has been prepared. + *

    This method is only called after the period has been prepared and before reading from any + * {@link SampleStream}s provided by the period. * * @return If a discontinuity was read then the playback position in microseconds after the * discontinuity. Else {@link C#TIME_UNSET}. @@ -143,7 +145,7 @@ public interface MediaPeriod extends SequenceableLoader { *

    After this method has been called, all {@link SampleStream}s provided by the period are * guaranteed to start from a key frame. * - *

    This method should only be called when at least one track is selected. + *

    This method is only called when at least one track is selected. * * @param positionUs The seek position in microseconds. * @return The actual position to which the period was seeked, in microseconds. @@ -154,7 +156,7 @@ public interface MediaPeriod extends SequenceableLoader { * Returns the position to which a seek will be performed, given the specified seek position and * {@link SeekParameters}. * - *

    This method should only be called after the period has been prepared. + *

    This method is only called after the period has been prepared. * * @param positionUs The seek position in microseconds. * @param seekParameters Parameters that control how the seek is performed. Implementations may @@ -168,7 +170,7 @@ public interface MediaPeriod extends SequenceableLoader { /** * Returns an estimate of the position up to which data is buffered for the enabled tracks. * - *

    This method should only be called when at least one track is selected. + *

    This method is only called when at least one track is selected. * * @return An estimate of the absolute position in microseconds up to which data is buffered, or * {@link C#TIME_END_OF_SOURCE} if the track is fully buffered. @@ -179,8 +181,8 @@ public interface MediaPeriod extends SequenceableLoader { /** * Returns the next load time, or {@link C#TIME_END_OF_SOURCE} if loading has finished. * - *

    This method should only be called after the period has been prepared. It may be called when - * no tracks are selected. + *

    This method is only called after the period has been prepared. It may be called when no + * tracks are selected. */ @Override long getNextLoadPositionUs(); @@ -207,7 +209,7 @@ public interface MediaPeriod extends SequenceableLoader { /** * Re-evaluates the buffer given the playback position. * - *

    This method should only be called after the period has been prepared. + *

    This method is only called after the period has been prepared. * *

    A period may choose to discard buffered media so that it can be re-buffered in a different * quality. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java index 02bd0cdbc7..f8c2f8b3e1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.source; +import android.os.Handler; import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; @@ -25,25 +26,26 @@ import java.io.IOException; /** * Defines and provides media to be played by an {@link ExoPlayer}. A MediaSource has two main * responsibilities: + * *

      *
    • To provide the player with a {@link Timeline} defining the structure of its media, and to - * provide a new timeline whenever the structure of the media changes. The MediaSource provides - * these timelines by calling {@link Listener#onSourceInfoRefreshed} on the {@link Listener} - * passed to {@link #prepareSource(ExoPlayer, boolean, Listener)}.
    • + * provide a new timeline whenever the structure of the media changes. The MediaSource + * provides these timelines by calling {@link SourceInfoRefreshListener#onSourceInfoRefreshed} + * on the {@link SourceInfoRefreshListener}s passed to {@link #prepareSource(ExoPlayer, + * boolean, SourceInfoRefreshListener)}. *
    • To provide {@link MediaPeriod} instances for the periods in its timeline. MediaPeriods are - * obtained by calling {@link #createPeriod(MediaPeriodId, Allocator)}, and provide a way for the - * player to load and read the media.
    • + * obtained by calling {@link #createPeriod(MediaPeriodId, Allocator)}, and provide a way for + * the player to load and read the media. *
    - * All methods are called on the player's internal playback thread, as described in the - * {@link ExoPlayer} Javadoc. They should not be called directly from application code. Instances - * should not be re-used, meaning they should be passed to {@link ExoPlayer#prepare} at most once. + * + * All methods are called on the player's internal playback thread, as described in the {@link + * ExoPlayer} Javadoc. They should not be called directly from application code. Instances can be + * re-used, but only for one {@link ExoPlayer} instance simultaneously. */ public interface MediaSource { - /** - * Listener for source events. - */ - interface Listener { + /** Listener for source events. */ + interface SourceInfoRefreshListener { /** * Called when manifest and/or timeline has been refreshed. @@ -170,21 +172,43 @@ public interface MediaSource { } - String MEDIA_SOURCE_REUSED_ERROR_MESSAGE = "MediaSource instances are not allowed to be reused."; + /** + * Adds a {@link MediaSourceEventListener} to the list of listeners which are notified of media + * source events. + * + * @param handler A handler on the which listener events will be posted. + * @param eventListener The listener to be added. + */ + void addEventListener(Handler handler, MediaSourceEventListener eventListener); /** - * Starts preparation of the source. - *

    - * Should not be called directly from application code. + * Removes a {@link MediaSourceEventListener} from the list of listeners which are notified of + * media source events. + * + * @param eventListener The listener to be removed. + */ + void removeEventListener(MediaSourceEventListener eventListener); + + /** + * Starts source preparation if not yet started, and adds a listener for timeline and/or manifest + * updates. + * + *

    Should not be called directly from application code. + * + *

    The listener will be also be notified if the source already has a timeline and/or manifest. + * + *

    For each call to this method, a call to {@link #releaseSource(SourceInfoRefreshListener)} is + * needed to remove the listener and to release the source if no longer required. * * @param player The player for which this source is being prepared. - * @param isTopLevelSource Whether this source has been passed directly to - * {@link ExoPlayer#prepare(MediaSource)} or - * {@link ExoPlayer#prepare(MediaSource, boolean, boolean)}. If {@code false}, this source is - * being prepared by another source (e.g. {@link ConcatenatingMediaSource}) for composition. - * @param listener The listener for source events. + * @param isTopLevelSource Whether this source has been passed directly to {@link + * ExoPlayer#prepare(MediaSource)} or {@link ExoPlayer#prepare(MediaSource, boolean, + * boolean)}. If {@code false}, this source is being prepared by another source (e.g. {@link + * ConcatenatingMediaSource}) for composition. + * @param listener The listener to be added. */ - void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener); + void prepareSource( + ExoPlayer player, boolean isTopLevelSource, SourceInfoRefreshListener listener); /** * Throws any pending error encountered while loading or refreshing source information. @@ -216,10 +240,12 @@ public interface MediaSource { void releasePeriod(MediaPeriod mediaPeriod); /** - * Releases the source. - *

    - * Should not be called directly from application code. + * Removes a listener for timeline and/or manifest updates and releases the source if no longer + * required. + * + *

    Should not be called directly from application code. + * + * @param listener The listener to be removed. */ - void releaseSource(); - + void releaseSource(SourceInfoRefreshListener listener); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceEventListener.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceEventListener.java index 9fc2572b55..9d1ba10866 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceEventListener.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceEventListener.java @@ -16,118 +16,183 @@ package com.google.android.exoplayer2.source; import android.os.Handler; +import android.os.Looper; import android.os.SystemClock; +import android.support.annotation.CheckResult; import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; +import java.util.concurrent.CopyOnWriteArrayList; /** Interface for callbacks to be notified of {@link MediaSource} events. */ public interface MediaSourceEventListener { + + /** Media source load event information. */ + final class LoadEventInfo { + + /** Defines the data being loaded. */ + public final DataSpec dataSpec; + /** The value of {@link SystemClock#elapsedRealtime} at the time of the load event. */ + public final long elapsedRealtimeMs; + /** The duration of the load up to the event time. */ + public final long loadDurationMs; + /** The number of bytes that were loaded up to the event time. */ + public final long bytesLoaded; + + /** + * Creates load event info. + * + * @param dataSpec Defines the data being loaded. + * @param elapsedRealtimeMs The value of {@link SystemClock#elapsedRealtime} at the time of the + * load event. + * @param loadDurationMs The duration of the load up to the event time. + * @param bytesLoaded The number of bytes that were loaded up to the event time. + */ + public LoadEventInfo( + DataSpec dataSpec, long elapsedRealtimeMs, long loadDurationMs, long bytesLoaded) { + this.dataSpec = dataSpec; + this.elapsedRealtimeMs = elapsedRealtimeMs; + this.loadDurationMs = loadDurationMs; + this.bytesLoaded = bytesLoaded; + } + } + + /** Descriptor for data being loaded or selected by a media source. */ + final class MediaLoadData { + + /** One of the {@link C} {@code DATA_TYPE_*} constants defining the type of data. */ + public final int dataType; + /** + * One of the {@link C} {@code TRACK_TYPE_*} constants if the data corresponds to media of a + * specific type. {@link C#TRACK_TYPE_UNKNOWN} otherwise. + */ + public final int trackType; + /** + * The format of the track to which the data belongs. Null if the data does not belong to a + * specific track. + */ + public final @Nullable Format trackFormat; + /** + * One of the {@link C} {@code SELECTION_REASON_*} constants if the data belongs to a track. + * {@link C#SELECTION_REASON_UNKNOWN} otherwise. + */ + public final int trackSelectionReason; + /** + * Optional data associated with the selection of the track to which the data belongs. Null if + * the data does not belong to a track. + */ + public final @Nullable Object trackSelectionData; + /** + * The start time of the media, or {@link C#TIME_UNSET} if the data does not belong to a + * specific media period. + */ + public final long mediaStartTimeMs; + /** + * The end time of the media, or {@link C#TIME_UNSET} if the data does not belong to a specific + * media period or the end time is unknown. + */ + public final long mediaEndTimeMs; + + /** + * Creates media load data. + * + * @param dataType One of the {@link C} {@code DATA_TYPE_*} constants defining the type of data. + * @param trackType One of the {@link C} {@code TRACK_TYPE_*} constants if the data corresponds + * to media of a specific type. {@link C#TRACK_TYPE_UNKNOWN} otherwise. + * @param trackFormat The format of the track to which the data belongs. Null if the data does + * not belong to a track. + * @param trackSelectionReason One of the {@link C} {@code SELECTION_REASON_*} constants if the + * data belongs to a track. {@link C#SELECTION_REASON_UNKNOWN} otherwise. + * @param trackSelectionData Optional data associated with the selection of the track to which + * the data belongs. Null if the data does not belong to a track. + * @param mediaStartTimeMs The start time of the media, or {@link C#TIME_UNSET} if the data does + * not belong to a specific media period. + * @param mediaEndTimeMs The end time of the media, or {@link C#TIME_UNSET} if the data does not + * belong to a specific media period or the end time is unknown. + */ + public MediaLoadData( + int dataType, + int trackType, + @Nullable Format trackFormat, + int trackSelectionReason, + @Nullable Object trackSelectionData, + long mediaStartTimeMs, + long mediaEndTimeMs) { + this.dataType = dataType; + this.trackType = trackType; + this.trackFormat = trackFormat; + this.trackSelectionReason = trackSelectionReason; + this.trackSelectionData = trackSelectionData; + this.mediaStartTimeMs = mediaStartTimeMs; + this.mediaEndTimeMs = mediaEndTimeMs; + } + } + + /** + * Called when a media period is created by the media source. + * + * @param windowIndex The window index in the timeline this media period belongs to. + * @param mediaPeriodId The {@link MediaPeriodId} of the created media period. + */ + void onMediaPeriodCreated(int windowIndex, MediaPeriodId mediaPeriodId); + + /** + * Called when a media period is released by the media source. + * + * @param windowIndex The window index in the timeline this media period belongs to. + * @param mediaPeriodId The {@link MediaPeriodId} of the released media period. + */ + void onMediaPeriodReleased(int windowIndex, MediaPeriodId mediaPeriodId); + /** * Called when a load begins. * - * @param dataSpec Defines the data being loaded. - * @param dataType One of the {@link C} {@code DATA_TYPE_*} constants defining the type of data - * being loaded. - * @param trackType One of the {@link C} {@code TRACK_TYPE_*} constants if the data corresponds to - * media of a specific type. {@link C#TRACK_TYPE_UNKNOWN} otherwise. - * @param trackFormat The format of the track to which the data belongs. Null if the data does not - * belong to a track. - * @param trackSelectionReason One of the {@link C} {@code SELECTION_REASON_*} constants if the - * data belongs to a track. {@link C#SELECTION_REASON_UNKNOWN} otherwise. - * @param trackSelectionData Optional data associated with the selection of the track to which the - * data belongs. Null if the data does not belong to a track. - * @param mediaStartTimeMs The start time of the media being loaded, or {@link C#TIME_UNSET} if - * the load is not for media data. - * @param mediaEndTimeMs The end time of the media being loaded, or {@link C#TIME_UNSET} if the - * load is not for media data or the end time is unknown. - * @param elapsedRealtimeMs The value of {@link SystemClock#elapsedRealtime} when the load began. + * @param windowIndex The window index in the timeline of the media source this load belongs to. + * @param mediaPeriodId The {@link MediaPeriodId} this load belongs to. Null if the load does not + * belong to a specific media period. + * @param loadEventInfo The {@link LoadEventInfo} defining the load event. + * @param mediaLoadData The {@link MediaLoadData} defining the data being loaded. */ void onLoadStarted( - DataSpec dataSpec, - int dataType, - int trackType, - Format trackFormat, - int trackSelectionReason, - Object trackSelectionData, - long mediaStartTimeMs, - long mediaEndTimeMs, - long elapsedRealtimeMs); + int windowIndex, + @Nullable MediaPeriodId mediaPeriodId, + LoadEventInfo loadEventInfo, + MediaLoadData mediaLoadData); /** * Called when a load ends. * - * @param dataSpec Defines the data being loaded. - * @param dataType One of the {@link C} {@code DATA_TYPE_*} constants defining the type of data - * being loaded. - * @param trackType One of the {@link C} {@code TRACK_TYPE_*} constants if the data corresponds to - * media of a specific type. {@link C#TRACK_TYPE_UNKNOWN} otherwise. - * @param trackFormat The format of the track to which the data belongs. Null if the data does not - * belong to a track. - * @param trackSelectionReason One of the {@link C} {@code SELECTION_REASON_*} constants if the - * data belongs to a track. {@link C#SELECTION_REASON_UNKNOWN} otherwise. - * @param trackSelectionData Optional data associated with the selection of the track to which the - * data belongs. Null if the data does not belong to a track. - * @param mediaStartTimeMs The start time of the media being loaded, or {@link C#TIME_UNSET} if - * the load is not for media data. - * @param mediaEndTimeMs The end time of the media being loaded, or {@link C#TIME_UNSET} if the - * load is not for media data. - * @param elapsedRealtimeMs The value of {@link SystemClock#elapsedRealtime} when the load ended. - * @param loadDurationMs The duration of the load. - * @param bytesLoaded The number of bytes that were loaded. + * @param windowIndex The window index in the timeline of the media source this load belongs to. + * @param mediaPeriodId The {@link MediaPeriodId} this load belongs to. Null if the load does not + * belong to a specific media period. + * @param loadEventInfo The {@link LoadEventInfo} defining the load event. + * @param mediaLoadData The {@link MediaLoadData} defining the data being loaded. */ void onLoadCompleted( - DataSpec dataSpec, - int dataType, - int trackType, - Format trackFormat, - int trackSelectionReason, - Object trackSelectionData, - long mediaStartTimeMs, - long mediaEndTimeMs, - long elapsedRealtimeMs, - long loadDurationMs, - long bytesLoaded); + int windowIndex, + @Nullable MediaPeriodId mediaPeriodId, + LoadEventInfo loadEventInfo, + MediaLoadData mediaLoadData); /** * Called when a load is canceled. * - * @param dataSpec Defines the data being loaded. - * @param dataType One of the {@link C} {@code DATA_TYPE_*} constants defining the type of data - * being loaded. - * @param trackType One of the {@link C} {@code TRACK_TYPE_*} constants if the data corresponds to - * media of a specific type. {@link C#TRACK_TYPE_UNKNOWN} otherwise. - * @param trackFormat The format of the track to which the data belongs. Null if the data does not - * belong to a track. - * @param trackSelectionReason One of the {@link C} {@code SELECTION_REASON_*} constants if the - * data belongs to a track. {@link C#SELECTION_REASON_UNKNOWN} otherwise. - * @param trackSelectionData Optional data associated with the selection of the track to which the - * data belongs. Null if the data does not belong to a track. - * @param mediaStartTimeMs The start time of the media being loaded, or {@link C#TIME_UNSET} if - * the load is not for media data. - * @param mediaEndTimeMs The end time of the media being loaded, or {@link C#TIME_UNSET} if the - * load is not for media data. - * @param elapsedRealtimeMs The value of {@link SystemClock#elapsedRealtime} when the load was - * canceled. - * @param loadDurationMs The duration of the load up to the point at which it was canceled. - * @param bytesLoaded The number of bytes that were loaded prior to cancelation. + * @param windowIndex The window index in the timeline of the media source this load belongs to. + * @param mediaPeriodId The {@link MediaPeriodId} this load belongs to. Null if the load does not + * belong to a specific media period. + * @param loadEventInfo The {@link LoadEventInfo} defining the load event. + * @param mediaLoadData The {@link MediaLoadData} defining the data being loaded. */ void onLoadCanceled( - DataSpec dataSpec, - int dataType, - int trackType, - Format trackFormat, - int trackSelectionReason, - Object trackSelectionData, - long mediaStartTimeMs, - long mediaEndTimeMs, - long elapsedRealtimeMs, - long loadDurationMs, - long bytesLoaded); + int windowIndex, + @Nullable MediaPeriodId mediaPeriodId, + LoadEventInfo loadEventInfo, + MediaLoadData mediaLoadData); /** * Called when a load error occurs. @@ -143,97 +208,156 @@ public interface MediaSourceEventListener { * such behavior). This method is called to provide the application with an opportunity to log the * error if it wishes to do so. * - * @param dataSpec Defines the data being loaded. - * @param dataType One of the {@link C} {@code DATA_TYPE_*} constants defining the type of data - * being loaded. - * @param trackType One of the {@link C} {@code TRACK_TYPE_*} constants if the data corresponds to - * media of a specific type. {@link C#TRACK_TYPE_UNKNOWN} otherwise. - * @param trackFormat The format of the track to which the data belongs. Null if the data does not - * belong to a track. - * @param trackSelectionReason One of the {@link C} {@code SELECTION_REASON_*} constants if the - * data belongs to a track. {@link C#SELECTION_REASON_UNKNOWN} otherwise. - * @param trackSelectionData Optional data associated with the selection of the track to which the - * data belongs. Null if the data does not belong to a track. - * @param mediaStartTimeMs The start time of the media being loaded, or {@link C#TIME_UNSET} if - * the load is not for media data. - * @param mediaEndTimeMs The end time of the media being loaded, or {@link C#TIME_UNSET} if the - * load is not for media data. - * @param elapsedRealtimeMs The value of {@link SystemClock#elapsedRealtime} when the error - * occurred. - * @param loadDurationMs The duration of the load up to the point at which the error occurred. - * @param bytesLoaded The number of bytes that were loaded prior to the error. + * @param windowIndex The window index in the timeline of the media source this load belongs to. + * @param mediaPeriodId The {@link MediaPeriodId} this load belongs to. Null if the load does not + * belong to a specific media period. + * @param loadEventInfo The {@link LoadEventInfo} defining the load event. + * @param mediaLoadData The {@link MediaLoadData} defining the data being loaded. * @param error The load error. * @param wasCanceled Whether the load was canceled as a result of the error. */ void onLoadError( - DataSpec dataSpec, - int dataType, - int trackType, - Format trackFormat, - int trackSelectionReason, - Object trackSelectionData, - long mediaStartTimeMs, - long mediaEndTimeMs, - long elapsedRealtimeMs, - long loadDurationMs, - long bytesLoaded, + int windowIndex, + @Nullable MediaPeriodId mediaPeriodId, + LoadEventInfo loadEventInfo, + MediaLoadData mediaLoadData, IOException error, boolean wasCanceled); + /** + * Called when a media period is first being read from. + * + * @param windowIndex The window index in the timeline this media period belongs to. + * @param mediaPeriodId The {@link MediaPeriodId} of the media period being read from. + */ + void onReadingStarted(int windowIndex, MediaPeriodId mediaPeriodId); + /** * Called when data is removed from the back of a media buffer, typically so that it can be * re-buffered in a different format. * - * @param trackType The type of the media. One of the {@link C} {@code TRACK_TYPE_*} constants. - * @param mediaStartTimeMs The start time of the media being discarded. - * @param mediaEndTimeMs The end time of the media being discarded. + * @param windowIndex The window index in the timeline of the media source this load belongs to. + * @param mediaPeriodId The {@link MediaPeriodId} the media belongs to. + * @param mediaLoadData The {@link MediaLoadData} defining the media being discarded. */ - void onUpstreamDiscarded(int trackType, long mediaStartTimeMs, long mediaEndTimeMs); + void onUpstreamDiscarded( + int windowIndex, MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData); /** * Called when a downstream format change occurs (i.e. when the format of the media being read * from one or more {@link SampleStream}s provided by the source changes). * - * @param trackType The type of the media. One of the {@link C} {@code TRACK_TYPE_*} constants. - * @param trackFormat The format of the track to which the data belongs. Null if the data does not - * belong to a track. - * @param trackSelectionReason One of the {@link C} {@code SELECTION_REASON_*} constants if the - * data belongs to a track. {@link C#SELECTION_REASON_UNKNOWN} otherwise. - * @param trackSelectionData Optional data associated with the selection of the track to which the - * data belongs. Null if the data does not belong to a track. - * @param mediaTimeMs The media time at which the change occurred. + * @param windowIndex The window index in the timeline of the media source this load belongs to. + * @param mediaPeriodId The {@link MediaPeriodId} the media belongs to. + * @param mediaLoadData The {@link MediaLoadData} defining the newly selected downstream data. */ void onDownstreamFormatChanged( - int trackType, - Format trackFormat, - int trackSelectionReason, - Object trackSelectionData, - long mediaTimeMs); + int windowIndex, @Nullable MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData); - /** Dispatches events to a {@link MediaSourceEventListener}. */ + /** Dispatches events to {@link MediaSourceEventListener}s. */ final class EventDispatcher { - @Nullable private final Handler handler; - @Nullable private final MediaSourceEventListener listener; + /** The timeline window index reported with the events. */ + public final int windowIndex; + /** The {@link MediaPeriodId} reported with the events. */ + public final @Nullable MediaPeriodId mediaPeriodId; + + private final CopyOnWriteArrayList listenerAndHandlers; private final long mediaTimeOffsetMs; - public EventDispatcher(@Nullable Handler handler, @Nullable MediaSourceEventListener listener) { - this(handler, listener, 0); + /** Creates an event dispatcher. */ + public EventDispatcher() { + this( + /* listenerAndHandlers= */ new CopyOnWriteArrayList(), + /* windowIndex= */ 0, + /* mediaPeriodId= */ null, + /* mediaTimeOffsetMs= */ 0); } - public EventDispatcher( - @Nullable Handler handler, - @Nullable MediaSourceEventListener listener, + private EventDispatcher( + CopyOnWriteArrayList listenerAndHandlers, + int windowIndex, + @Nullable MediaPeriodId mediaPeriodId, long mediaTimeOffsetMs) { - this.handler = listener != null ? Assertions.checkNotNull(handler) : null; - this.listener = listener; + this.listenerAndHandlers = listenerAndHandlers; + this.windowIndex = windowIndex; + this.mediaPeriodId = mediaPeriodId; this.mediaTimeOffsetMs = mediaTimeOffsetMs; } - public EventDispatcher copyWithMediaTimeOffsetMs(long mediaTimeOffsetMs) { - return new EventDispatcher(handler, listener, mediaTimeOffsetMs); + /** + * Creates a view of the event dispatcher with pre-configured window index, media period id, and + * media time offset. + * + * @param windowIndex The timeline window index to be reported with the events. + * @param mediaPeriodId The {@link MediaPeriodId} to be reported with the events. + * @param mediaTimeOffsetMs The offset to be added to all media times, in milliseconds. + * @return A view of the event dispatcher with the pre-configured parameters. + */ + @CheckResult + public EventDispatcher withParameters( + int windowIndex, @Nullable MediaPeriodId mediaPeriodId, long mediaTimeOffsetMs) { + return new EventDispatcher( + listenerAndHandlers, windowIndex, mediaPeriodId, mediaTimeOffsetMs); } + /** + * Adds a listener to the event dispatcher. + * + * @param handler A handler on the which listener events will be posted. + * @param eventListener The listener to be added. + */ + public void addEventListener(Handler handler, MediaSourceEventListener eventListener) { + Assertions.checkArgument(handler != null && eventListener != null); + listenerAndHandlers.add(new ListenerAndHandler(handler, eventListener)); + } + + /** + * Removes a listener from the event dispatcher. + * + * @param eventListener The listener to be removed. + */ + public void removeEventListener(MediaSourceEventListener eventListener) { + for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) { + if (listenerAndHandler.listener == eventListener) { + listenerAndHandlers.remove(listenerAndHandler); + } + } + } + + /** Dispatches {@link #onMediaPeriodCreated(int, MediaPeriodId)}. */ + public void mediaPeriodCreated() { + Assertions.checkState(mediaPeriodId != null); + for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) { + final MediaSourceEventListener listener = listenerAndHandler.listener; + postOrRun( + listenerAndHandler.handler, + new Runnable() { + @Override + public void run() { + listener.onMediaPeriodCreated(windowIndex, mediaPeriodId); + } + }); + } + } + + /** Dispatches {@link #onMediaPeriodReleased(int, MediaPeriodId)}. */ + public void mediaPeriodReleased() { + Assertions.checkState(mediaPeriodId != null); + for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) { + final MediaSourceEventListener listener = listenerAndHandler.listener; + postOrRun( + listenerAndHandler.handler, + new Runnable() { + @Override + public void run() { + listener.onMediaPeriodReleased(windowIndex, mediaPeriodId); + } + }); + } + } + + /** Dispatches {@link #onLoadStarted(int, MediaPeriodId, LoadEventInfo, MediaLoadData)}. */ public void loadStarted(DataSpec dataSpec, int dataType, long elapsedRealtimeMs) { loadStarted( dataSpec, @@ -247,36 +371,46 @@ public interface MediaSourceEventListener { elapsedRealtimeMs); } + /** Dispatches {@link #onLoadStarted(int, MediaPeriodId, LoadEventInfo, MediaLoadData)}. */ public void loadStarted( - final DataSpec dataSpec, - final int dataType, - final int trackType, - final Format trackFormat, - final int trackSelectionReason, - final Object trackSelectionData, - final long mediaStartTimeUs, - final long mediaEndTimeUs, - final long elapsedRealtimeMs) { - if (listener != null && handler != null) { - handler.post( + DataSpec dataSpec, + int dataType, + int trackType, + @Nullable Format trackFormat, + int trackSelectionReason, + @Nullable Object trackSelectionData, + long mediaStartTimeUs, + long mediaEndTimeUs, + long elapsedRealtimeMs) { + loadStarted( + new LoadEventInfo( + dataSpec, elapsedRealtimeMs, /* loadDurationMs= */ 0, /* bytesLoaded= */ 0), + new MediaLoadData( + dataType, + trackType, + trackFormat, + trackSelectionReason, + trackSelectionData, + adjustMediaTime(mediaStartTimeUs), + adjustMediaTime(mediaEndTimeUs))); + } + + /** Dispatches {@link #onLoadStarted(int, MediaPeriodId, LoadEventInfo, MediaLoadData)}. */ + public void loadStarted(final LoadEventInfo loadEventInfo, final MediaLoadData mediaLoadData) { + for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) { + final MediaSourceEventListener listener = listenerAndHandler.listener; + postOrRun( + listenerAndHandler.handler, new Runnable() { @Override public void run() { - listener.onLoadStarted( - dataSpec, - dataType, - trackType, - trackFormat, - trackSelectionReason, - trackSelectionData, - adjustMediaTime(mediaStartTimeUs), - adjustMediaTime(mediaEndTimeUs), - elapsedRealtimeMs); + listener.onLoadStarted(windowIndex, mediaPeriodId, loadEventInfo, mediaLoadData); } }); } } + /** Dispatches {@link #onLoadCompleted(int, MediaPeriodId, LoadEventInfo, MediaLoadData)}. */ public void loadCompleted( DataSpec dataSpec, int dataType, @@ -297,40 +431,48 @@ public interface MediaSourceEventListener { bytesLoaded); } + /** Dispatches {@link #onLoadCompleted(int, MediaPeriodId, LoadEventInfo, MediaLoadData)}. */ public void loadCompleted( - final DataSpec dataSpec, - final int dataType, - final int trackType, - final Format trackFormat, - final int trackSelectionReason, - final Object trackSelectionData, - final long mediaStartTimeUs, - final long mediaEndTimeUs, - final long elapsedRealtimeMs, - final long loadDurationMs, - final long bytesLoaded) { - if (listener != null && handler != null) { - handler.post( + DataSpec dataSpec, + int dataType, + int trackType, + @Nullable Format trackFormat, + int trackSelectionReason, + @Nullable Object trackSelectionData, + long mediaStartTimeUs, + long mediaEndTimeUs, + long elapsedRealtimeMs, + long loadDurationMs, + long bytesLoaded) { + loadCompleted( + new LoadEventInfo(dataSpec, elapsedRealtimeMs, loadDurationMs, bytesLoaded), + new MediaLoadData( + dataType, + trackType, + trackFormat, + trackSelectionReason, + trackSelectionData, + adjustMediaTime(mediaStartTimeUs), + adjustMediaTime(mediaEndTimeUs))); + } + + /** Dispatches {@link #onLoadCompleted(int, MediaPeriodId, LoadEventInfo, MediaLoadData)}. */ + public void loadCompleted( + final LoadEventInfo loadEventInfo, final MediaLoadData mediaLoadData) { + for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) { + final MediaSourceEventListener listener = listenerAndHandler.listener; + postOrRun( + listenerAndHandler.handler, new Runnable() { @Override public void run() { - listener.onLoadCompleted( - dataSpec, - dataType, - trackType, - trackFormat, - trackSelectionReason, - trackSelectionData, - adjustMediaTime(mediaStartTimeUs), - adjustMediaTime(mediaEndTimeUs), - elapsedRealtimeMs, - loadDurationMs, - bytesLoaded); + listener.onLoadCompleted(windowIndex, mediaPeriodId, loadEventInfo, mediaLoadData); } }); } } + /** Dispatches {@link #onLoadCanceled(int, MediaPeriodId, LoadEventInfo, MediaLoadData)}. */ public void loadCanceled( DataSpec dataSpec, int dataType, @@ -351,40 +493,50 @@ public interface MediaSourceEventListener { bytesLoaded); } + /** Dispatches {@link #onLoadCanceled(int, MediaPeriodId, LoadEventInfo, MediaLoadData)}. */ public void loadCanceled( - final DataSpec dataSpec, - final int dataType, - final int trackType, - final Format trackFormat, - final int trackSelectionReason, - final Object trackSelectionData, - final long mediaStartTimeUs, - final long mediaEndTimeUs, - final long elapsedRealtimeMs, - final long loadDurationMs, - final long bytesLoaded) { - if (listener != null && handler != null) { - handler.post( + DataSpec dataSpec, + int dataType, + int trackType, + @Nullable Format trackFormat, + int trackSelectionReason, + @Nullable Object trackSelectionData, + long mediaStartTimeUs, + long mediaEndTimeUs, + long elapsedRealtimeMs, + long loadDurationMs, + long bytesLoaded) { + loadCanceled( + new LoadEventInfo(dataSpec, elapsedRealtimeMs, loadDurationMs, bytesLoaded), + new MediaLoadData( + dataType, + trackType, + trackFormat, + trackSelectionReason, + trackSelectionData, + adjustMediaTime(mediaStartTimeUs), + adjustMediaTime(mediaEndTimeUs))); + } + + /** Dispatches {@link #onLoadCanceled(int, MediaPeriodId, LoadEventInfo, MediaLoadData)}. */ + public void loadCanceled(final LoadEventInfo loadEventInfo, final MediaLoadData mediaLoadData) { + for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) { + final MediaSourceEventListener listener = listenerAndHandler.listener; + postOrRun( + listenerAndHandler.handler, new Runnable() { @Override public void run() { - listener.onLoadCanceled( - dataSpec, - dataType, - trackType, - trackFormat, - trackSelectionReason, - trackSelectionData, - adjustMediaTime(mediaStartTimeUs), - adjustMediaTime(mediaEndTimeUs), - elapsedRealtimeMs, - loadDurationMs, - bytesLoaded); + listener.onLoadCanceled(windowIndex, mediaPeriodId, loadEventInfo, mediaLoadData); } }); } } + /** + * Dispatches {@link #onLoadError(int, MediaPeriodId, LoadEventInfo, MediaLoadData, IOException, + * boolean)}. + */ public void loadError( DataSpec dataSpec, int dataType, @@ -409,75 +561,133 @@ public interface MediaSourceEventListener { wasCanceled); } + /** + * Dispatches {@link #onLoadError(int, MediaPeriodId, LoadEventInfo, MediaLoadData, IOException, + * boolean)}. + */ public void loadError( - final DataSpec dataSpec, - final int dataType, - final int trackType, - final Format trackFormat, - final int trackSelectionReason, - final Object trackSelectionData, - final long mediaStartTimeUs, - final long mediaEndTimeUs, - final long elapsedRealtimeMs, - final long loadDurationMs, - final long bytesLoaded, + DataSpec dataSpec, + int dataType, + int trackType, + @Nullable Format trackFormat, + int trackSelectionReason, + @Nullable Object trackSelectionData, + long mediaStartTimeUs, + long mediaEndTimeUs, + long elapsedRealtimeMs, + long loadDurationMs, + long bytesLoaded, + IOException error, + boolean wasCanceled) { + loadError( + new LoadEventInfo(dataSpec, elapsedRealtimeMs, loadDurationMs, bytesLoaded), + new MediaLoadData( + dataType, + trackType, + trackFormat, + trackSelectionReason, + trackSelectionData, + adjustMediaTime(mediaStartTimeUs), + adjustMediaTime(mediaEndTimeUs)), + error, + wasCanceled); + } + + /** + * Dispatches {@link #onLoadError(int, MediaPeriodId, LoadEventInfo, MediaLoadData, IOException, + * boolean)}. + */ + public void loadError( + final LoadEventInfo loadEventInfo, + final MediaLoadData mediaLoadData, final IOException error, final boolean wasCanceled) { - if (listener != null && handler != null) { - handler.post( + for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) { + final MediaSourceEventListener listener = listenerAndHandler.listener; + postOrRun( + listenerAndHandler.handler, new Runnable() { @Override public void run() { listener.onLoadError( - dataSpec, - dataType, - trackType, - trackFormat, - trackSelectionReason, - trackSelectionData, - adjustMediaTime(mediaStartTimeUs), - adjustMediaTime(mediaEndTimeUs), - elapsedRealtimeMs, - loadDurationMs, - bytesLoaded, - error, - wasCanceled); + windowIndex, mediaPeriodId, loadEventInfo, mediaLoadData, error, wasCanceled); } }); } } - public void upstreamDiscarded( - final int trackType, final long mediaStartTimeUs, final long mediaEndTimeUs) { - if (listener != null && handler != null) { - handler.post( + /** Dispatches {@link #onReadingStarted(int, MediaPeriodId)}. */ + public void readingStarted() { + Assertions.checkState(mediaPeriodId != null); + for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) { + final MediaSourceEventListener listener = listenerAndHandler.listener; + postOrRun( + listenerAndHandler.handler, new Runnable() { @Override public void run() { - listener.onUpstreamDiscarded( - trackType, adjustMediaTime(mediaStartTimeUs), adjustMediaTime(mediaEndTimeUs)); + listener.onReadingStarted(windowIndex, mediaPeriodId); } }); } } + /** Dispatches {@link #onUpstreamDiscarded(int, MediaPeriodId, MediaLoadData)}. */ + public void upstreamDiscarded(int trackType, long mediaStartTimeUs, long mediaEndTimeUs) { + upstreamDiscarded( + new MediaLoadData( + C.DATA_TYPE_MEDIA, + trackType, + /* trackFormat= */ null, + C.SELECTION_REASON_ADAPTIVE, + /* trackSelectionData= */ null, + adjustMediaTime(mediaStartTimeUs), + adjustMediaTime(mediaEndTimeUs))); + } + + /** Dispatches {@link #onUpstreamDiscarded(int, MediaPeriodId, MediaLoadData)}. */ + public void upstreamDiscarded(final MediaLoadData mediaLoadData) { + for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) { + final MediaSourceEventListener listener = listenerAndHandler.listener; + postOrRun( + listenerAndHandler.handler, + new Runnable() { + @Override + public void run() { + listener.onUpstreamDiscarded(windowIndex, mediaPeriodId, mediaLoadData); + } + }); + } + } + + /** Dispatches {@link #onDownstreamFormatChanged(int, MediaPeriodId, MediaLoadData)}. */ public void downstreamFormatChanged( - final int trackType, - final Format trackFormat, - final int trackSelectionReason, - final Object trackSelectionData, - final long mediaTimeUs) { - if (listener != null && handler != null) { - handler.post( + int trackType, + @Nullable Format trackFormat, + int trackSelectionReason, + @Nullable Object trackSelectionData, + long mediaTimeUs) { + downstreamFormatChanged( + new MediaLoadData( + C.DATA_TYPE_MEDIA, + trackType, + trackFormat, + trackSelectionReason, + trackSelectionData, + adjustMediaTime(mediaTimeUs), + /* mediaEndTimeMs= */ C.TIME_UNSET)); + } + + /** Dispatches {@link #onDownstreamFormatChanged(int, MediaPeriodId, MediaLoadData)}. */ + public void downstreamFormatChanged(final MediaLoadData mediaLoadData) { + for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) { + final MediaSourceEventListener listener = listenerAndHandler.listener; + postOrRun( + listenerAndHandler.handler, new Runnable() { @Override public void run() { - listener.onDownstreamFormatChanged( - trackType, - trackFormat, - trackSelectionReason, - trackSelectionData, - adjustMediaTime(mediaTimeUs)); + listener.onDownstreamFormatChanged(windowIndex, mediaPeriodId, mediaLoadData); } }); } @@ -487,5 +697,24 @@ public interface MediaSourceEventListener { long mediaTimeMs = C.usToMs(mediaTimeUs); return mediaTimeMs == C.TIME_UNSET ? C.TIME_UNSET : mediaTimeOffsetMs + mediaTimeMs; } + + private void postOrRun(Handler handler, Runnable runnable) { + if (handler.getLooper() == Looper.myLooper()) { + runnable.run(); + } else { + handler.post(runnable); + } + } + + private static final class ListenerAndHandler { + + public final Handler handler; + public final MediaSourceEventListener listener; + + public ListenerAndHandler(Handler handler, MediaSourceEventListener listener) { + this.handler = handler; + this.listener = listener; + } + } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java index cc0c63ef41..a4fc8c6b00 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java @@ -21,6 +21,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; import java.util.ArrayList; +import java.util.Collections; import java.util.IdentityHashMap; /** @@ -32,9 +33,9 @@ import java.util.IdentityHashMap; private final IdentityHashMap streamPeriodIndices; private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; + private final ArrayList childrenPendingPreparation; private Callback callback; - private int pendingChildPrepareCount; private TrackGroupArray trackGroups; private MediaPeriod[] enabledPeriods; @@ -44,13 +45,16 @@ import java.util.IdentityHashMap; MediaPeriod... periods) { this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory; this.periods = periods; + childrenPendingPreparation = new ArrayList<>(); + compositeSequenceableLoader = + compositeSequenceableLoaderFactory.createCompositeSequenceableLoader(); streamPeriodIndices = new IdentityHashMap<>(); } @Override public void prepare(Callback callback, long positionUs) { this.callback = callback; - pendingChildPrepareCount = periods.length; + Collections.addAll(childrenPendingPreparation, periods); for (MediaPeriod period : periods) { period.prepare(this, positionUs); } @@ -104,7 +108,7 @@ import java.util.IdentityHashMap; if (i == 0) { positionUs = selectPositionUs; } else if (selectPositionUs != positionUs) { - throw new IllegalStateException("Children enabled at different positions"); + throw new IllegalStateException("Children enabled at different positions."); } boolean periodEnabled = false; for (int j = 0; j < selections.length; j++) { @@ -147,7 +151,16 @@ import java.util.IdentityHashMap; @Override public boolean continueLoading(long positionUs) { - return compositeSequenceableLoader.continueLoading(positionUs); + if (!childrenPendingPreparation.isEmpty()) { + // Preparation is still going on. + int childrenPendingPreparationSize = childrenPendingPreparation.size(); + for (int i = 0; i < childrenPendingPreparationSize; i++) { + childrenPendingPreparation.get(i).continueLoading(positionUs); + } + return false; + } else { + return compositeSequenceableLoader.continueLoading(positionUs); + } } @Override @@ -161,7 +174,7 @@ import java.util.IdentityHashMap; // Periods other than the first one are not allowed to report discontinuities. for (int i = 1; i < periods.length; i++) { if (periods[i].readDiscontinuity() != C.TIME_UNSET) { - throw new IllegalStateException("Child reported discontinuity"); + throw new IllegalStateException("Child reported discontinuity."); } } // It must be possible to seek enabled periods to the new position, if there is one. @@ -169,7 +182,7 @@ import java.util.IdentityHashMap; for (MediaPeriod enabledPeriod : enabledPeriods) { if (enabledPeriod != periods[0] && enabledPeriod.seekToUs(positionUs) != positionUs) { - throw new IllegalStateException("Children seeked to different positions"); + throw new IllegalStateException("Unexpected child seekToUs result."); } } } @@ -187,7 +200,7 @@ import java.util.IdentityHashMap; // Additional periods must seek to the same position. for (int i = 1; i < enabledPeriods.length; i++) { if (enabledPeriods[i].seekToUs(positionUs) != positionUs) { - throw new IllegalStateException("Children seeked to different positions"); + throw new IllegalStateException("Unexpected child seekToUs result."); } } return positionUs; @@ -201,8 +214,9 @@ import java.util.IdentityHashMap; // MediaPeriod.Callback implementation @Override - public void onPrepared(MediaPeriod ignored) { - if (--pendingChildPrepareCount > 0) { + public void onPrepared(MediaPeriod preparedPeriod) { + childrenPendingPreparation.remove(preparedPeriod); + if (!childrenPendingPreparation.isEmpty()) { return; } int totalTrackGroupCount = 0; @@ -224,10 +238,6 @@ import java.util.IdentityHashMap; @Override public void onContinueLoadingRequested(MediaPeriod ignored) { - if (trackGroups == null) { - // Still preparing. - return; - } callback.onContinueLoadingRequested(this); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java index a738cb1893..f9bf86081f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java @@ -70,7 +70,6 @@ public final class MergingMediaSource extends CompositeMediaSource { private final ArrayList pendingTimelineSources; private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; - private Listener listener; private Timeline primaryTimeline; private Object primaryManifest; private int periodCount; @@ -98,9 +97,8 @@ public final class MergingMediaSource extends CompositeMediaSource { } @Override - public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { - super.prepareSource(player, isTopLevelSource, listener); - this.listener = listener; + public void prepareSourceInternal(ExoPlayer player, boolean isTopLevelSource) { + super.prepareSourceInternal(player, isTopLevelSource); for (int i = 0; i < mediaSources.length; i++) { prepareChildSource(i, mediaSources[i]); } @@ -132,9 +130,8 @@ public final class MergingMediaSource extends CompositeMediaSource { } @Override - public void releaseSource() { - super.releaseSource(); - listener = null; + public void releaseSourceInternal() { + super.releaseSourceInternal(); primaryTimeline = null; primaryManifest = null; periodCount = PERIOD_COUNT_UNSET; @@ -158,7 +155,7 @@ public final class MergingMediaSource extends CompositeMediaSource { primaryManifest = manifest; } if (pendingTimelineSources.isEmpty()) { - listener.onSourceInfoRefreshed(this, primaryTimeline, primaryManifest); + refreshSourceInfo(primaryTimeline, primaryManifest); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java index 9cce67f68c..0bddd482ac 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.source; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.util.Assertions; @@ -24,7 +25,7 @@ import com.google.android.exoplayer2.util.Assertions; */ public final class SinglePeriodTimeline extends Timeline { - private static final Object ID = new Object(); + private static final Object UID = new Object(); private final long presentationStartTimeMs; private final long windowStartTimeMs; @@ -34,6 +35,7 @@ public final class SinglePeriodTimeline extends Timeline { private final long windowDefaultStartPositionUs; private final boolean isSeekable; private final boolean isDynamic; + private final @Nullable Object tag; /** * Creates a timeline containing a single period and a window that spans it. @@ -43,7 +45,27 @@ public final class SinglePeriodTimeline extends Timeline { * @param isDynamic Whether the window may change when the timeline is updated. */ public SinglePeriodTimeline(long durationUs, boolean isSeekable, boolean isDynamic) { - this(durationUs, durationUs, 0, 0, isSeekable, isDynamic); + this(durationUs, isSeekable, isDynamic, /* tag= */ null); + } + + /** + * Creates a timeline containing a single period and a window that spans it. + * + * @param durationUs The duration of the period, in microseconds. + * @param isSeekable Whether seeking is supported within the period. + * @param isDynamic Whether the window may change when the timeline is updated. + * @param tag A tag used for {@link Timeline.Window#tag}. + */ + public SinglePeriodTimeline( + long durationUs, boolean isSeekable, boolean isDynamic, @Nullable Object tag) { + this( + durationUs, + durationUs, + /* windowPositionInPeriodUs= */ 0, + /* windowDefaultStartPositionUs= */ 0, + isSeekable, + isDynamic, + tag); } /** @@ -58,12 +80,26 @@ public final class SinglePeriodTimeline extends Timeline { * which to begin playback, in microseconds. * @param isSeekable Whether seeking is supported within the window. * @param isDynamic Whether the window may change when the timeline is updated. + * @param tag A tag used for {@link Timeline.Window#tag}. */ - public SinglePeriodTimeline(long periodDurationUs, long windowDurationUs, - long windowPositionInPeriodUs, long windowDefaultStartPositionUs, boolean isSeekable, - boolean isDynamic) { - this(C.TIME_UNSET, C.TIME_UNSET, periodDurationUs, windowDurationUs, windowPositionInPeriodUs, - windowDefaultStartPositionUs, isSeekable, isDynamic); + public SinglePeriodTimeline( + long periodDurationUs, + long windowDurationUs, + long windowPositionInPeriodUs, + long windowDefaultStartPositionUs, + boolean isSeekable, + boolean isDynamic, + @Nullable Object tag) { + this( + /* presentationStartTimeMs= */ C.TIME_UNSET, + /* windowStartTimeMs= */ C.TIME_UNSET, + periodDurationUs, + windowDurationUs, + windowPositionInPeriodUs, + windowDefaultStartPositionUs, + isSeekable, + isDynamic, + tag); } /** @@ -81,10 +117,18 @@ public final class SinglePeriodTimeline extends Timeline { * which to begin playback, in microseconds. * @param isSeekable Whether seeking is supported within the window. * @param isDynamic Whether the window may change when the timeline is updated. + * @param tag A tag used for {@link Timeline.Window#tag}. */ - public SinglePeriodTimeline(long presentationStartTimeMs, long windowStartTimeMs, - long periodDurationUs, long windowDurationUs, long windowPositionInPeriodUs, - long windowDefaultStartPositionUs, boolean isSeekable, boolean isDynamic) { + public SinglePeriodTimeline( + long presentationStartTimeMs, + long windowStartTimeMs, + long periodDurationUs, + long windowDurationUs, + long windowPositionInPeriodUs, + long windowDefaultStartPositionUs, + boolean isSeekable, + boolean isDynamic, + @Nullable Object tag) { this.presentationStartTimeMs = presentationStartTimeMs; this.windowStartTimeMs = windowStartTimeMs; this.periodDurationUs = periodDurationUs; @@ -93,6 +137,7 @@ public final class SinglePeriodTimeline extends Timeline { this.windowDefaultStartPositionUs = windowDefaultStartPositionUs; this.isSeekable = isSeekable; this.isDynamic = isDynamic; + this.tag = tag; } @Override @@ -101,10 +146,10 @@ public final class SinglePeriodTimeline extends Timeline { } @Override - public Window getWindow(int windowIndex, Window window, boolean setIds, - long defaultPositionProjectionUs) { + public Window getWindow( + int windowIndex, Window window, boolean setTag, long defaultPositionProjectionUs) { Assertions.checkIndex(windowIndex, 0, 1); - Object id = setIds ? ID : null; + Object tag = setTag ? this.tag : null; long windowDefaultStartPositionUs = this.windowDefaultStartPositionUs; if (isDynamic && defaultPositionProjectionUs != 0) { if (windowDurationUs == C.TIME_UNSET) { @@ -118,8 +163,17 @@ public final class SinglePeriodTimeline extends Timeline { } } } - return window.set(id, presentationStartTimeMs, windowStartTimeMs, isSeekable, isDynamic, - windowDefaultStartPositionUs, windowDurationUs, 0, 0, windowPositionInPeriodUs); + return window.set( + tag, + presentationStartTimeMs, + windowStartTimeMs, + isSeekable, + isDynamic, + windowDefaultStartPositionUs, + windowDurationUs, + 0, + 0, + windowPositionInPeriodUs); } @Override @@ -130,13 +184,13 @@ public final class SinglePeriodTimeline extends Timeline { @Override public Period getPeriod(int periodIndex, Period period, boolean setIds) { Assertions.checkIndex(periodIndex, 0, 1); - Object id = setIds ? ID : null; - return period.set(id, id, 0, periodDurationUs, -windowPositionInPeriodUs); + Object uid = setIds ? UID : null; + return period.set(/* id= */ null, uid, 0, periodDurationUs, -windowPositionInPeriodUs); } @Override public int getIndexOfPeriod(Object uid) { - return ID.equals(uid) ? 0 : C.INDEX_UNSET; + return UID.equals(uid) ? 0 : C.INDEX_UNSET; } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java index 36e5d910c4..0a089e5b7c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java @@ -56,6 +56,7 @@ import java.util.Arrays; /* package */ final Format format; /* package */ final boolean treatLoadErrorsAsEndOfStream; + /* package */ boolean notifiedReadingStarted; /* package */ boolean loadingFinished; /* package */ boolean loadingSucceeded; /* package */ byte[] sampleData; @@ -80,10 +81,12 @@ import java.util.Arrays; tracks = new TrackGroupArray(new TrackGroup(format)); sampleStreams = new ArrayList<>(); loader = new Loader("Loader:SingleSampleMediaPeriod"); + eventDispatcher.mediaPeriodCreated(); } public void release() { loader.release(); + eventDispatcher.mediaPeriodReleased(); } @Override @@ -154,6 +157,10 @@ import java.util.Arrays; @Override public long readDiscontinuity() { + if (!notifiedReadingStarted) { + eventDispatcher.readingStarted(); + notifiedReadingStarted = true; + } return C.TIME_UNSET; } @@ -221,8 +228,8 @@ import java.util.Arrays; } @Override - public int onLoadError(SourceLoadable loadable, long elapsedRealtimeMs, long loadDurationMs, - IOException error) { + public @Loader.RetryAction int onLoadError( + SourceLoadable loadable, long elapsedRealtimeMs, long loadDurationMs, IOException error) { errorCount++; boolean cancel = treatLoadErrorsAsEndOfStream && errorCount >= minLoadableRetryCount; eventDispatcher.loadError( diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java index b92085d15e..2c651bef59 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java @@ -21,7 +21,6 @@ import android.support.annotation.Nullable; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; @@ -31,7 +30,7 @@ import java.io.IOException; /** * Loads data at a given {@link Uri} as a single sample belonging to a single {@link MediaPeriod}. */ -public final class SingleSampleMediaSource implements MediaSource { +public final class SingleSampleMediaSource extends BaseMediaSource { /** * Listener of {@link SingleSampleMediaSource} events. @@ -59,6 +58,7 @@ public final class SingleSampleMediaSource implements MediaSource { private int minLoadableRetryCount; private boolean treatLoadErrorsAsEndOfStream; private boolean isCreateCalled; + private @Nullable Object tag; /** * Creates a factory for {@link SingleSampleMediaSource}s. @@ -71,6 +71,20 @@ public final class SingleSampleMediaSource implements MediaSource { this.minLoadableRetryCount = DEFAULT_MIN_LOADABLE_RETRY_COUNT; } + /** + * Sets a tag for the media source which will be published in the {@link Timeline} of the source + * as {@link Timeline.Window#tag}. + * + * @param tag A tag for the media source. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. + */ + public Factory setTag(Object tag) { + Assertions.checkState(!isCreateCalled); + this.tag = tag; + return this; + } + /** * Sets the minimum number of times to retry if a loading error occurs. The default value is * {@link #DEFAULT_MIN_LOADABLE_RETRY_COUNT}. @@ -102,8 +116,7 @@ public final class SingleSampleMediaSource implements MediaSource { } /** - * Returns a new {@link ExtractorMediaSource} using the current parameters. Media source events - * will not be delivered. + * Returns a new {@link ExtractorMediaSource} using the current parameters. * * @param uri The {@link Uri}. * @param format The {@link Format} of the media stream. @@ -111,25 +124,6 @@ public final class SingleSampleMediaSource implements MediaSource { * @return The new {@link ExtractorMediaSource}. */ public SingleSampleMediaSource createMediaSource(Uri uri, Format format, long durationUs) { - return createMediaSource(uri, format, durationUs, null, null); - } - - /** - * Returns a new {@link SingleSampleMediaSource} using the current parameters. - * - * @param uri The {@link Uri}. - * @param format The {@link Format} of the media stream. - * @param durationUs The duration of the media stream in microseconds. - * @param eventHandler A handler for events. - * @param eventListener A listener of events., Format format, long durationUs - * @return The newly built {@link SingleSampleMediaSource}. - */ - public SingleSampleMediaSource createMediaSource( - Uri uri, - Format format, - long durationUs, - @Nullable Handler eventHandler, - @Nullable MediaSourceEventListener eventListener) { isCreateCalled = true; return new SingleSampleMediaSource( uri, @@ -137,9 +131,26 @@ public final class SingleSampleMediaSource implements MediaSource { format, durationUs, minLoadableRetryCount, - eventHandler, - eventListener, - treatLoadErrorsAsEndOfStream); + treatLoadErrorsAsEndOfStream, + tag); + } + + /** + * @deprecated Use {@link #createMediaSource(Uri, Format, long)} and {@link + * #addEventListener(Handler, MediaSourceEventListener)} instead. + */ + @Deprecated + public SingleSampleMediaSource createMediaSource( + Uri uri, + Format format, + long durationUs, + @Nullable Handler eventHandler, + @Nullable MediaSourceEventListener eventListener) { + SingleSampleMediaSource mediaSource = createMediaSource(uri, format, durationUs); + if (eventHandler != null && eventListener != null) { + mediaSource.addEventListener(eventHandler, eventListener); + } + return mediaSource; } } @@ -153,7 +164,6 @@ public final class SingleSampleMediaSource implements MediaSource { private final DataSource.Factory dataSourceFactory; private final Format format; private final long durationUs; - private final MediaSourceEventListener.EventDispatcher eventDispatcher; private final int minLoadableRetryCount; private final boolean treatLoadErrorsAsEndOfStream; private final Timeline timeline; @@ -188,7 +198,14 @@ public final class SingleSampleMediaSource implements MediaSource { Format format, long durationUs, int minLoadableRetryCount) { - this(uri, dataSourceFactory, format, durationUs, minLoadableRetryCount, null, null, false); + this( + uri, + dataSourceFactory, + format, + durationUs, + minLoadableRetryCount, + /* treatLoadErrorsAsEndOfStream= */ false, + /* tag= */ null); } /** @@ -223,9 +240,11 @@ public final class SingleSampleMediaSource implements MediaSource { format, durationUs, minLoadableRetryCount, - eventHandler, - eventListener == null ? null : new EventListenerWrapper(eventListener, eventSourceId), - treatLoadErrorsAsEndOfStream); + treatLoadErrorsAsEndOfStream, + /* tag= */ null); + if (eventHandler != null && eventListener != null) { + addEventListener(eventHandler, new EventListenerWrapper(eventListener, eventSourceId)); + } } private SingleSampleMediaSource( @@ -234,24 +253,23 @@ public final class SingleSampleMediaSource implements MediaSource { Format format, long durationUs, int minLoadableRetryCount, - Handler eventHandler, - MediaSourceEventListener eventListener, - boolean treatLoadErrorsAsEndOfStream) { + boolean treatLoadErrorsAsEndOfStream, + @Nullable Object tag) { this.dataSourceFactory = dataSourceFactory; this.format = format; this.durationUs = durationUs; this.minLoadableRetryCount = minLoadableRetryCount; this.treatLoadErrorsAsEndOfStream = treatLoadErrorsAsEndOfStream; - this.eventDispatcher = new EventDispatcher(eventHandler, eventListener); dataSpec = new DataSpec(uri); - timeline = new SinglePeriodTimeline(durationUs, true, false); + timeline = + new SinglePeriodTimeline(durationUs, /* isSeekable= */ true, /* isDynamic= */ false, tag); } // MediaSource implementation. @Override - public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { - listener.onSourceInfoRefreshed(this, timeline, null); + public void prepareSourceInternal(ExoPlayer player, boolean isTopLevelSource) { + refreshSourceInfo(timeline, /* manifest= */ null); } @Override @@ -268,7 +286,7 @@ public final class SingleSampleMediaSource implements MediaSource { format, durationUs, minLoadableRetryCount, - eventDispatcher, + createEventDispatcher(id), treatLoadErrorsAsEndOfStream); } @@ -278,7 +296,7 @@ public final class SingleSampleMediaSource implements MediaSource { } @Override - public void releaseSource() { + public void releaseSourceInternal() { // Do nothing. } @@ -286,7 +304,7 @@ public final class SingleSampleMediaSource implements MediaSource { * Wraps a deprecated {@link EventListener}, invoking its callback from the equivalent callback in * {@link MediaSourceEventListener}. */ - private static final class EventListenerWrapper implements MediaSourceEventListener { + private static final class EventListenerWrapper extends DefaultMediaSourceEventListener { private final EventListener eventListener; private final int eventSourceId; @@ -296,83 +314,15 @@ public final class SingleSampleMediaSource implements MediaSource { this.eventSourceId = eventSourceId; } - @Override - public void onLoadStarted( - DataSpec dataSpec, - int dataType, - int trackType, - Format trackFormat, - int trackSelectionReason, - Object trackSelectionData, - long mediaStartTimeMs, - long mediaEndTimeMs, - long elapsedRealtimeMs) { - // Do nothing. - } - - @Override - public void onLoadCompleted( - DataSpec dataSpec, - int dataType, - int trackType, - Format trackFormat, - int trackSelectionReason, - Object trackSelectionData, - long mediaStartTimeMs, - long mediaEndTimeMs, - long elapsedRealtimeMs, - long loadDurationMs, - long bytesLoaded) { - // Do nothing. - } - - @Override - public void onLoadCanceled( - DataSpec dataSpec, - int dataType, - int trackType, - Format trackFormat, - int trackSelectionReason, - Object trackSelectionData, - long mediaStartTimeMs, - long mediaEndTimeMs, - long elapsedRealtimeMs, - long loadDurationMs, - long bytesLoaded) { - // Do nothing. - } - @Override public void onLoadError( - DataSpec dataSpec, - int dataType, - int trackType, - Format trackFormat, - int trackSelectionReason, - Object trackSelectionData, - long mediaStartTimeMs, - long mediaEndTimeMs, - long elapsedRealtimeMs, - long loadDurationMs, - long bytesLoaded, + int windowIndex, + @Nullable MediaPeriodId mediaPeriodId, + LoadEventInfo loadEventInfo, + MediaLoadData mediaLoadData, IOException error, boolean wasCanceled) { eventListener.onLoadError(eventSourceId, error); } - - @Override - public void onUpstreamDiscarded(int trackType, long mediaStartTimeMs, long mediaEndTimeMs) { - // Do nothing. - } - - @Override - public void onDownstreamFormatChanged( - int trackType, - Format trackFormat, - int trackSelectionReason, - Object trackSelectionData, - long mediaTimeMs) { - // Do nothing. - } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/TrackGroup.java b/library/core/src/main/java/com/google/android/exoplayer2/source/TrackGroup.java index 06410d5426..2e5b259a88 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/TrackGroup.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/TrackGroup.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.source; +import android.os.Parcel; +import android.os.Parcelable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.util.Assertions; @@ -24,12 +26,12 @@ import java.util.Arrays; // does not apply. /** * Defines a group of tracks exposed by a {@link MediaPeriod}. - *

    - * A {@link MediaPeriod} is only able to provide one {@link SampleStream} corresponding to a group - * at any given time, however this {@link SampleStream} may adapt between multiple tracks within the - * group. + * + *

    A {@link MediaPeriod} is only able to provide one {@link SampleStream} corresponding to a + * group at any given time, however this {@link SampleStream} may adapt between multiple tracks + * within the group. */ -public final class TrackGroup { +public final class TrackGroup implements Parcelable { /** * The number of tracks in the group. @@ -50,6 +52,14 @@ public final class TrackGroup { this.length = formats.length; } + /* package */ TrackGroup(Parcel in) { + length = in.readInt(); + formats = new Format[length]; + for (int i = 0; i < length; i++) { + formats[i] = in.readParcelable(Format.class.getClassLoader()); + } + } + /** * Returns the format of the track at a given index. * @@ -97,4 +107,32 @@ public final class TrackGroup { return length == other.length && Arrays.equals(formats, other.formats); } + // Parcelable implementation. + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(length); + for (int i = 0; i < length; i++) { + dest.writeParcelable(formats[i], 0); + } + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + + @Override + public TrackGroup createFromParcel(Parcel in) { + return new TrackGroup(in); + } + + @Override + public TrackGroup[] newArray(int size) { + return new TrackGroup[size]; + } + }; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/TrackGroupArray.java b/library/core/src/main/java/com/google/android/exoplayer2/source/TrackGroupArray.java index fb28da581c..72afa3463e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/TrackGroupArray.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/TrackGroupArray.java @@ -15,13 +15,13 @@ */ package com.google.android.exoplayer2.source; +import android.os.Parcel; +import android.os.Parcelable; import com.google.android.exoplayer2.C; import java.util.Arrays; -/** - * An array of {@link TrackGroup}s exposed by a {@link MediaPeriod}. - */ -public final class TrackGroupArray { +/** An array of {@link TrackGroup}s exposed by a {@link MediaPeriod}. */ +public final class TrackGroupArray implements Parcelable { /** * The empty array. @@ -46,6 +46,14 @@ public final class TrackGroupArray { this.length = trackGroups.length; } + /* package */ TrackGroupArray(Parcel in) { + length = in.readInt(); + trackGroups = new TrackGroup[length]; + for (int i = 0; i < length; i++) { + trackGroups[i] = in.readParcelable(TrackGroup.class.getClassLoader()); + } + } + /** * Returns the group at a given index. * @@ -101,4 +109,32 @@ public final class TrackGroupArray { return length == other.length && Arrays.equals(trackGroups, other.trackGroups); } + // Parcelable implementation. + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(length); + for (int i = 0; i < length; i++) { + dest.writeParcelable(trackGroups[i], 0); + } + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + + @Override + public TrackGroupArray createFromParcel(Parcel in) { + return new TrackGroupArray(in); + } + + @Override + public TrackGroupArray[] newArray(int size) { + return new TrackGroupArray[size]; + } + }; } 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 64bab7ed96..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; @@ -31,10 +31,15 @@ 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; @@ -51,13 +56,9 @@ public final class AdsMediaSource extends CompositeMediaSource { * Creates a new {@link MediaSource} for loading the ad media with the specified {@code uri}. * * @param uri The URI of the media or manifest to play. - * @param handler A handler for listener events. May be null if delivery of events is not - * required. - * @param listener A listener for events. May be null if delivery of events is not required. * @return The new media source. */ - MediaSource createMediaSource( - Uri uri, @Nullable Handler handler, @Nullable MediaSourceEventListener listener); + MediaSource createMediaSource(Uri uri); /** * Returns the content types supported by media sources created by this factory. Each element @@ -69,8 +70,76 @@ public final class AdsMediaSource extends CompositeMediaSource { int[] getSupportedTypes(); } - /** Listener for ads media source events. */ - public interface EventListener extends MediaSourceEventListener { + /** + * 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 { /** * Called if there was an error loading one or more ads. The loader will skip the problematic @@ -119,7 +188,6 @@ public final class AdsMediaSource extends CompositeMediaSource { private AdPlaybackState adPlaybackState; private MediaSource[][] adGroupMediaSources; private long[][] adDurationsUs; - private MediaSource.Listener listener; /** * Constructs a new source that inserts ads linearly with the content specified by {@code @@ -137,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, @@ -154,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, @@ -181,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, @@ -204,11 +307,10 @@ public final class AdsMediaSource extends CompositeMediaSource { } @Override - public void prepareSource(final ExoPlayer player, boolean isTopLevelSource, Listener listener) { - super.prepareSource(player, isTopLevelSource, listener); + public void prepareSourceInternal(final ExoPlayer player, boolean isTopLevelSource) { + super.prepareSourceInternal(player, isTopLevelSource); Assertions.checkArgument(isTopLevelSource); final ComponentListener componentListener = new ComponentListener(); - this.listener = listener; this.componentListener = componentListener; prepareChildSource(new MediaPeriodId(/* periodIndex= */ 0), contentMediaSource); mainHandler.post(new Runnable() { @@ -224,11 +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, eventHandler, eventListener); - int oldAdCount = adGroupMediaSources[id.adGroupIndex].length; + MediaSource adMediaSource = adMediaSourceFactory.createMediaSource(adUri); + int oldAdCount = adGroupMediaSources[adGroupIndex].length; if (adIndexInAdGroup >= oldAdCount) { int adCount = adIndexInAdGroup + 1; adGroupMediaSources[adGroupIndex] = @@ -247,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(); @@ -276,8 +377,8 @@ public final class AdsMediaSource extends CompositeMediaSource { } @Override - public void releaseSource() { - super.releaseSource(); + public void releaseSourceInternal() { + super.releaseSourceInternal(); componentListener.release(); componentListener = null; deferredMediaPeriodByAdMediaSource.clear(); @@ -286,7 +387,6 @@ public final class AdsMediaSource extends CompositeMediaSource { adPlaybackState = null; adGroupMediaSources = new MediaSource[0][]; adDurationsUs = new long[0][]; - listener = null; mainHandler.post(new Runnable() { @Override public void run() { @@ -310,6 +410,14 @@ public final class AdsMediaSource extends CompositeMediaSource { } } + @Override + protected @Nullable MediaPeriodId getMediaPeriodIdForChildMediaPeriodId( + MediaPeriodId childId, MediaPeriodId mediaPeriodId) { + // The child id for the content period is just a dummy without window sequence number. That's + // why we need to forward the reported mediaPeriodId in this case. + return childId.isAd() ? childId : mediaPeriodId; + } + // Internal methods. private void onAdPlaybackState(AdPlaybackState adPlaybackState) { @@ -350,7 +458,7 @@ public final class AdsMediaSource extends CompositeMediaSource { adPlaybackState.adGroupCount == 0 ? contentTimeline : new SinglePeriodAdTimeline(contentTimeline, adPlaybackState); - listener.onSourceInfoRefreshed(this, timeline, contentManifest); + refreshSourceInfo(timeline, contentManifest); } } @@ -358,6 +466,7 @@ public final class AdsMediaSource extends CompositeMediaSource { private final class ComponentListener implements AdsLoader.EventListener { private final Handler playerHandler; + private volatile boolean released; /** @@ -425,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); + } } } }); @@ -465,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/ads/SinglePeriodAdTimeline.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/SinglePeriodAdTimeline.java index ec0d6cb2fe..b0c245b706 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/SinglePeriodAdTimeline.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/SinglePeriodAdTimeline.java @@ -55,9 +55,9 @@ import com.google.android.exoplayer2.util.Assertions; } @Override - public Window getWindow(int windowIndex, Window window, boolean setIds, - long defaultPositionProjectionUs) { - window = super.getWindow(windowIndex, window, setIds, defaultPositionProjectionUs); + public Window getWindow( + int windowIndex, Window window, boolean setTag, long defaultPositionProjectionUs) { + window = super.getWindow(windowIndex, window, setTag, defaultPositionProjectionUs); if (window.durationUs == C.TIME_UNSET) { window.durationUs = adPlaybackState.contentDurationUs; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunk.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunk.java index c8ebc02434..e3eae2b4d8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunk.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunk.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.source.chunk; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; @@ -24,6 +25,12 @@ import com.google.android.exoplayer2.upstream.DataSpec; */ public abstract class BaseMediaChunk extends MediaChunk { + /** + * The media time from which output will begin, or {@link C#TIME_UNSET} if the whole chunk should + * be output. + */ + public final long seekTimeUs; + private BaseMediaChunkOutput output; private int[] firstSampleIndices; @@ -35,6 +42,8 @@ public abstract class BaseMediaChunk extends MediaChunk { * @param trackSelectionData See {@link #trackSelectionData}. * @param startTimeUs The start time of the media contained by the chunk, in microseconds. * @param endTimeUs The end time of the media contained by the chunk, in microseconds. + * @param seekTimeUs The media time from which output will begin, or {@link C#TIME_UNSET} if the + * whole chunk should be output. * @param chunkIndex The index of the chunk. */ public BaseMediaChunk( @@ -45,9 +54,11 @@ public abstract class BaseMediaChunk extends MediaChunk { Object trackSelectionData, long startTimeUs, long endTimeUs, + long seekTimeUs, long chunkIndex) { super(dataSource, dataSpec, trackFormat, trackSelectionReason, trackSelectionData, startTimeUs, endTimeUs, chunkIndex); + this.seekTimeUs = seekTimeUs; } /** 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/ChunkExtractorWrapper.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java index 17eb30dee9..f043571b69 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java @@ -96,18 +96,23 @@ public final class ChunkExtractorWrapper implements ExtractorOutput { } /** - * Initializes the wrapper to output to {@link TrackOutput}s provided by the specified - * {@link TrackOutputProvider}, and configures the extractor to receive data from a new chunk. + * Initializes the wrapper to output to {@link TrackOutput}s provided by the specified {@link + * TrackOutputProvider}, and configures the extractor to receive data from a new chunk. * * @param trackOutputProvider The provider of {@link TrackOutput}s that will receive sample data. + * @param seekTimeUs The seek position within the new chunk, or {@link C#TIME_UNSET} to output the + * whole chunk. */ - public void init(TrackOutputProvider trackOutputProvider) { + public void init(TrackOutputProvider trackOutputProvider, long seekTimeUs) { this.trackOutputProvider = trackOutputProvider; if (!extractorInitialized) { extractor.init(this); + if (seekTimeUs != C.TIME_UNSET) { + extractor.seek(/* position= */ 0, seekTimeUs); + } extractorInitialized = true; } else { - extractor.seek(0, 0); + extractor.seek(/* position= */ 0, seekTimeUs == C.TIME_UNSET ? 0 : seekTimeUs); for (int i = 0; i < bindingTrackOutputs.size(); i++) { bindingTrackOutputs.valueAt(i).bind(trackOutputProvider); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java index 7096c84c5e..6cda68bac9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java @@ -242,7 +242,7 @@ public class ChunkSampleStream implements SampleStream, S for (int i = 0; i < mediaChunks.size(); i++) { BaseMediaChunk mediaChunk = mediaChunks.get(i); long mediaChunkStartTimeUs = mediaChunk.startTimeUs; - if (mediaChunkStartTimeUs == positionUs) { + if (mediaChunkStartTimeUs == positionUs && mediaChunk.seekTimeUs == C.TIME_UNSET) { seekToMediaChunk = mediaChunk; break; } else if (mediaChunkStartTimeUs > positionUs) { @@ -409,8 +409,8 @@ public class ChunkSampleStream implements SampleStream, S } @Override - public int onLoadError(Chunk loadable, long elapsedRealtimeMs, long loadDurationMs, - IOException error) { + public @Loader.RetryAction int onLoadError( + Chunk loadable, long elapsedRealtimeMs, long loadDurationMs, IOException error) { long bytesLoaded = loadable.bytesLoaded(); boolean isMediaChunk = isMediaChunk(loadable); int lastChunkIndex = mediaChunks.size() - 1; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java index b43c69b63a..ed73cf2588 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.source.chunk; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.DefaultExtractorInput; import com.google.android.exoplayer2.extractor.Extractor; @@ -46,6 +47,8 @@ public class ContainerMediaChunk extends BaseMediaChunk { * @param trackSelectionData See {@link #trackSelectionData}. * @param startTimeUs The start time of the media contained by the chunk, in microseconds. * @param endTimeUs The end time of the media contained by the chunk, in microseconds. + * @param seekTimeUs The media time from which output will begin, or {@link C#TIME_UNSET} if the + * whole chunk should be output. * @param chunkIndex The index of the chunk. * @param chunkCount The number of chunks in the underlying media that are spanned by this * instance. Normally equal to one, but may be larger if multiple chunks as defined by the @@ -61,12 +64,21 @@ public class ContainerMediaChunk extends BaseMediaChunk { Object trackSelectionData, long startTimeUs, long endTimeUs, + long seekTimeUs, long chunkIndex, int chunkCount, long sampleOffsetUs, ChunkExtractorWrapper extractorWrapper) { - super(dataSource, dataSpec, trackFormat, trackSelectionReason, trackSelectionData, startTimeUs, - endTimeUs, chunkIndex); + super( + dataSource, + dataSpec, + trackFormat, + trackSelectionReason, + trackSelectionData, + startTimeUs, + endTimeUs, + seekTimeUs, + chunkIndex); this.chunkCount = chunkCount; this.sampleOffsetUs = sampleOffsetUs; this.extractorWrapper = extractorWrapper; @@ -111,7 +123,8 @@ public class ContainerMediaChunk extends BaseMediaChunk { // Configure the output and set it as the target for the extractor wrapper. BaseMediaChunkOutput output = getOutput(); output.setSampleOffsetUs(sampleOffsetUs); - extractorWrapper.init(output); + extractorWrapper.init( + output, seekTimeUs == C.TIME_UNSET ? 0 : (seekTimeUs - sampleOffsetUs)); } // Load and decode the sample data. try { 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 4acf0b8525..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); @@ -78,7 +83,7 @@ public final class InitializationChunk extends Chunk { ExtractorInput input = new DefaultExtractorInput(dataSource, loadDataSpec.absoluteStreamPosition, dataSource.open(loadDataSpec)); if (bytesLoaded == 0) { - extractorWrapper.init(null); + extractorWrapper.init(/* trackOutputProvider= */ null, C.TIME_UNSET); } // Load and decode the initialization data. try { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/SingleSampleMediaChunk.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/SingleSampleMediaChunk.java index 87a90bc285..bd2363ede1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/SingleSampleMediaChunk.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/SingleSampleMediaChunk.java @@ -61,8 +61,16 @@ public final class SingleSampleMediaChunk extends BaseMediaChunk { long chunkIndex, int trackType, Format sampleFormat) { - super(dataSource, dataSpec, trackFormat, trackSelectionReason, trackSelectionData, startTimeUs, - endTimeUs, chunkIndex); + super( + dataSource, + dataSpec, + trackFormat, + trackSelectionReason, + trackSelectionData, + startTimeUs, + endTimeUs, + C.TIME_UNSET, + chunkIndex); this.trackType = trackType; this.sampleFormat = sampleFormat; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsDecoder.java index 6d60da7d81..1e45595144 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsDecoder.java @@ -25,6 +25,8 @@ import com.google.android.exoplayer2.util.Util; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.zip.DataFormatException; +import java.util.zip.Inflater; /** A {@link SimpleSubtitleDecoder} for PGS subtitles. */ public final class PgsDecoder extends SimpleSubtitleDecoder { @@ -34,9 +36,15 @@ public final class PgsDecoder extends SimpleSubtitleDecoder { private static final int SECTION_TYPE_IDENTIFIER = 0x16; private static final int SECTION_TYPE_END = 0x80; + private static final byte INFLATE_HEADER = 0x78; + private final ParsableByteArray buffer; private final CueBuilder cueBuilder; + private Inflater inflater; + private byte[] inflatedData; + private int inflatedDataSize; + public PgsDecoder() { super("PgsDecoder"); buffer = new ParsableByteArray(); @@ -45,7 +53,11 @@ public final class PgsDecoder extends SimpleSubtitleDecoder { @Override protected Subtitle decode(byte[] data, int size, boolean reset) throws SubtitleDecoderException { - buffer.reset(data, size); + if (maybeInflateData(data, size)) { + buffer.reset(inflatedData, inflatedDataSize); + } else { + buffer.reset(data, size); + } cueBuilder.reset(); ArrayList cues = new ArrayList<>(); while (buffer.bytesLeft() >= 3) { @@ -57,6 +69,34 @@ public final class PgsDecoder extends SimpleSubtitleDecoder { return new PgsSubtitle(Collections.unmodifiableList(cues)); } + private boolean maybeInflateData(byte[] data, int size) { + if (size == 0 || data[0] != INFLATE_HEADER) { + return false; + } + if (inflater == null) { + inflater = new Inflater(); + inflatedData = new byte[size]; + } + inflatedDataSize = 0; + inflater.setInput(data, 0, size); + try { + while (!inflater.finished() && !inflater.needsDictionary() && !inflater.needsInput()) { + if (inflatedDataSize == inflatedData.length) { + inflatedData = Arrays.copyOf(inflatedData, inflatedData.length * 2); + } + inflatedDataSize += + inflater.inflate( + inflatedData, inflatedDataSize, inflatedData.length - inflatedDataSize); + } + return inflater.finished(); + } catch (DataFormatException e) { + // Assume data is not compressed. + return false; + } finally { + inflater.reset(); + } + } + private static Cue readNextSection(ParsableByteArray buffer, CueBuilder cueBuilder) { int limit = buffer.limit(); int sectionType = buffer.readUnsignedByte(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java index 973155c2e3..b28dc6ca6f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java @@ -36,7 +36,6 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { public static final class Factory implements TrackSelection.Factory { private final BandwidthMeter bandwidthMeter; - private final int maxInitialBitrate; private final int minDurationForQualityIncreaseMs; private final int maxDurationForQualityDecreaseMs; private final int minDurationToRetainAfterDiscardMs; @@ -51,7 +50,6 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { public Factory(BandwidthMeter bandwidthMeter) { this( bandwidthMeter, - DEFAULT_MAX_INITIAL_BITRATE, DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS, DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS, DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS, @@ -63,26 +61,26 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { /** * @param bandwidthMeter Provides an estimate of the currently available bandwidth. - * @param maxInitialBitrate The maximum bitrate in bits per second that should be assumed - * when a bandwidth estimate is unavailable. - * @param minDurationForQualityIncreaseMs The minimum duration of buffered data required for - * the selected track to switch to one of higher quality. - * @param maxDurationForQualityDecreaseMs The maximum duration of buffered data required for - * the selected track to switch to one of lower quality. + * @param minDurationForQualityIncreaseMs The minimum duration of buffered data required for the + * selected track to switch to one of higher quality. + * @param maxDurationForQualityDecreaseMs The maximum duration of buffered data required for the + * selected track to switch to one of lower quality. * @param minDurationToRetainAfterDiscardMs When switching to a track of significantly higher * quality, the selection may indicate that media already buffered at the lower quality can * be discarded to speed up the switch. This is the minimum duration of media that must be * retained at the lower quality. * @param bandwidthFraction The fraction of the available bandwidth that the selection should - * consider available for use. Setting to a value less than 1 is recommended to account - * for inaccuracies in the bandwidth estimator. + * consider available for use. Setting to a value less than 1 is recommended to account for + * inaccuracies in the bandwidth estimator. */ - public Factory(BandwidthMeter bandwidthMeter, int maxInitialBitrate, - int minDurationForQualityIncreaseMs, int maxDurationForQualityDecreaseMs, - int minDurationToRetainAfterDiscardMs, float bandwidthFraction) { + public Factory( + BandwidthMeter bandwidthMeter, + int minDurationForQualityIncreaseMs, + int maxDurationForQualityDecreaseMs, + int minDurationToRetainAfterDiscardMs, + float bandwidthFraction) { this( bandwidthMeter, - maxInitialBitrate, minDurationForQualityIncreaseMs, maxDurationForQualityDecreaseMs, minDurationToRetainAfterDiscardMs, @@ -93,9 +91,7 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { } /** - * @param bandwidthMeter Provides an estimate of the currently available bandwidth. - * @param maxInitialBitrate The maximum bitrate in bits per second that should be assumed when a - * bandwidth estimate is unavailable. + * @param bandwidthMeter Provides an estimate of the currently available bandwidth.. * @param minDurationForQualityIncreaseMs The minimum duration of buffered data required for the * selected track to switch to one of higher quality. * @param maxDurationForQualityDecreaseMs The maximum duration of buffered data required for the @@ -121,7 +117,6 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { */ public Factory( BandwidthMeter bandwidthMeter, - int maxInitialBitrate, int minDurationForQualityIncreaseMs, int maxDurationForQualityDecreaseMs, int minDurationToRetainAfterDiscardMs, @@ -130,7 +125,6 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { long minTimeBetweenBufferReevaluationMs, Clock clock) { this.bandwidthMeter = bandwidthMeter; - this.maxInitialBitrate = maxInitialBitrate; this.minDurationForQualityIncreaseMs = minDurationForQualityIncreaseMs; this.maxDurationForQualityDecreaseMs = maxDurationForQualityDecreaseMs; this.minDurationToRetainAfterDiscardMs = minDurationToRetainAfterDiscardMs; @@ -147,7 +141,6 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { group, tracks, bandwidthMeter, - maxInitialBitrate, minDurationForQualityIncreaseMs, maxDurationForQualityDecreaseMs, minDurationToRetainAfterDiscardMs, @@ -156,10 +149,8 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { minTimeBetweenBufferReevaluationMs, clock); } - } - public static final int DEFAULT_MAX_INITIAL_BITRATE = 800000; public static final int DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS = 10000; public static final int DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS = 25000; public static final int DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS = 25000; @@ -168,7 +159,6 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { public static final long DEFAULT_MIN_TIME_BETWEEN_BUFFER_REEVALUTATION_MS = 2000; private final BandwidthMeter bandwidthMeter; - private final int maxInitialBitrate; private final long minDurationForQualityIncreaseUs; private final long maxDurationForQualityDecreaseUs; private final long minDurationToRetainAfterDiscardUs; @@ -194,7 +184,6 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { group, tracks, bandwidthMeter, - DEFAULT_MAX_INITIAL_BITRATE, DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS, DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS, DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS, @@ -209,8 +198,6 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { * @param tracks The indices of the selected tracks within the {@link TrackGroup}. Must not be * empty. May be in any order. * @param bandwidthMeter Provides an estimate of the currently available bandwidth. - * @param maxInitialBitrate The maximum bitrate in bits per second that should be assumed when a - * bandwidth estimate is unavailable. * @param minDurationForQualityIncreaseMs The minimum duration of buffered data required for the * selected track to switch to one of higher quality. * @param maxDurationForQualityDecreaseMs The maximum duration of buffered data required for the @@ -237,7 +224,6 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { TrackGroup group, int[] tracks, BandwidthMeter bandwidthMeter, - int maxInitialBitrate, long minDurationForQualityIncreaseMs, long maxDurationForQualityDecreaseMs, long minDurationToRetainAfterDiscardMs, @@ -247,7 +233,6 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { Clock clock) { super(group, tracks); this.bandwidthMeter = bandwidthMeter; - this.maxInitialBitrate = maxInitialBitrate; this.minDurationForQualityIncreaseUs = minDurationForQualityIncreaseMs * 1000L; this.maxDurationForQualityDecreaseUs = maxDurationForQualityDecreaseMs * 1000L; this.minDurationToRetainAfterDiscardUs = minDurationToRetainAfterDiscardMs * 1000L; @@ -369,9 +354,7 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { * Long#MIN_VALUE} to ignore blacklisting. */ private int determineIdealSelectedIndex(long nowMs) { - long bitrateEstimate = bandwidthMeter.getBitrateEstimate(); - long effectiveBitrate = bitrateEstimate == BandwidthMeter.NO_ESTIMATE - ? maxInitialBitrate : (long) (bitrateEstimate * bandwidthFraction); + long effectiveBitrate = (long) (bandwidthMeter.getBitrateEstimate() * bandwidthFraction); int lowestBitrateNonBlacklistedIndex = 0; for (int i = 0; i < length; i++) { if (nowMs == Long.MIN_VALUE || !isBlacklisted(i, nowMs)) { @@ -393,5 +376,4 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { ? (long) (availableDurationUs * bufferedFractionToLiveEdgeForQualityIncrease) : minDurationForQualityIncreaseUs; } - } 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 509e86345e..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 @@ -17,61 +17,138 @@ package com.google.android.exoplayer2.trackselection; import android.content.Context; import android.graphics.Point; +import android.os.Parcel; +import android.os.Parcelable; import android.support.annotation.NonNull; import android.text.TextUtils; +import android.util.Pair; +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; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.RendererCapabilities; +import com.google.android.exoplayer2.RendererConfiguration; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; 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; +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: - *
    - * {@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);}
    - * 
    + * + * 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
    + * trackSelector.setParameters(
    + *     trackSelector
    + *         .buildUponParameters()
    + *         .setMaxVideoSizeSd()
    + *         .setPreferredAudioLanguage("deu"));
    + * }
    + * * There are several benefits to using constraint based track selection instead of specific track * overrides: + * *
      *
    • You can specify constraints before knowing what tracks the media provides. This can - * simplify track selection code (e.g. you don't have to listen for changes in the available - * tracks before configuring the selector).
    • + * simplify track selection code (e.g. you don't have to listen for changes in the available + * tracks before configuring the selector). *
    • Constraints can be applied consistently across all periods in a complex piece of media, - * even if those periods contain different tracks. In contrast, a specific track override is only - * applied to periods whose tracks match those for which the override was set.
    • + * even if those periods contain different tracks. In contrast, a specific track override is + * only applied to periods whose tracks match those for which the override was set. *
    * - *

    Track overrides, disabling renderers and tunneling

    - * This selector extends {@link MappingTrackSelector}, and so inherits its support for setting - * specific track overrides, disabling renderers and configuring tunneled media playback. See - * {@link MappingTrackSelector} for details. + *

    Tunneling

    * - *

    Extending this class

    - * This class is designed to be extensible by developers who wish to customize its behavior but do - * not wish to implement their own {@link MappingTrackSelector} or {@link TrackSelector} from - * scratch. + * Tunneled playback can be enabled in cases where the combination of renderers and selected tracks + * support it. Tunneled playback is enabled by passing an audio session ID to {@link + * ParametersBuilder#setTunnelingAudioSessionId(int)}. */ public class DefaultTrackSelector extends MappingTrackSelector { @@ -80,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; @@ -95,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}. @@ -108,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; @@ -123,6 +206,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { viewportWidth = initialValues.viewportWidth; viewportHeight = initialValues.viewportHeight; viewportOrientationMayChange = initialValues.viewportOrientationMayChange; + tunnelingAudioSessionId = initialValues.tunnelingAudioSessionId; } /** @@ -297,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, @@ -316,15 +523,22 @@ 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}. - */ - public static final class Parameters { + /** Constraint parameters for {@link DefaultTrackSelector}. */ + public static final class Parameters implements Parcelable { /** * An instance with default values: @@ -347,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. @@ -422,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, @@ -439,10 +665,13 @@ public class DefaultTrackSelector extends MappingTrackSelector { true, Integer.MAX_VALUE, Integer.MAX_VALUE, - true); + true, + C.AUDIO_SESSION_ID_UNSET); } - private Parameters( + /* package */ Parameters( + SparseArray> selectionOverrides, + SparseBooleanArray rendererDisabledFlags, String preferredAudioLanguage, String preferredTextLanguage, boolean selectUndeterminedTextLanguage, @@ -457,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; @@ -473,6 +705,62 @@ 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); + this.disabledTextTrackSelectionFlags = in.readInt(); + this.forceLowestBitrate = Util.readBoolean(in); + this.allowMixedMimeAdaptiveness = Util.readBoolean(in); + this.allowNonSeamlessAdaptiveness = Util.readBoolean(in); + this.maxVideoWidth = in.readInt(); + this.maxVideoHeight = in.readInt(); + this.maxVideoBitrate = in.readInt(); + this.exceedVideoConstraintsIfNecessary = Util.readBoolean(in); + this.exceedRendererCapabilitiesIfNecessary = Util.readBoolean(in); + 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; } /** @@ -504,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 @@ -523,11 +814,222 @@ 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; } + // Parcelable implementation. + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + writeSelectionOverridesToParcel(dest, selectionOverrides); + dest.writeSparseBooleanArray(rendererDisabledFlags); + dest.writeString(preferredAudioLanguage); + dest.writeString(preferredTextLanguage); + Util.writeBoolean(dest, selectUndeterminedTextLanguage); + dest.writeInt(disabledTextTrackSelectionFlags); + Util.writeBoolean(dest, forceLowestBitrate); + Util.writeBoolean(dest, allowMixedMimeAdaptiveness); + Util.writeBoolean(dest, allowNonSeamlessAdaptiveness); + dest.writeInt(maxVideoWidth); + dest.writeInt(maxVideoHeight); + dest.writeInt(maxVideoBitrate); + Util.writeBoolean(dest, exceedVideoConstraintsIfNecessary); + Util.writeBoolean(dest, exceedRendererCapabilitiesIfNecessary); + dest.writeInt(viewportWidth); + dest.writeInt(viewportHeight); + Util.writeBoolean(dest, viewportOrientationMayChange); + dest.writeInt(tunnelingAudioSessionId); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + + @Override + public Parameters createFromParcel(Parcel in) { + return new Parameters(in); + } + + @Override + public Parameters[] newArray(int size) { + 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 final class SelectionOverride implements Parcelable { + + public final int groupIndex; + public final int[] tracks; + public final int length; + + /** + * @param groupIndex The overriding track group index. + * @param tracks The overriding track indices within the track group. + */ + public SelectionOverride(int groupIndex, int... tracks) { + this.groupIndex = groupIndex; + this.tracks = Arrays.copyOf(tracks, tracks.length); + this.length = tracks.length; + Arrays.sort(this.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. */ + public boolean containsTrack(int track) { + for (int overrideTrack : tracks) { + if (overrideTrack == track) { + return true; + } + } + 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]; + } + }; } /** @@ -540,7 +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 AtomicReference parametersReference; /** * Constructs an instance that does not support adaptive track selection. @@ -568,124 +1070,302 @@ public class DefaultTrackSelector extends MappingTrackSelector { */ public DefaultTrackSelector(TrackSelection.Factory adaptiveTrackSelectionFactory) { this.adaptiveTrackSelectionFactory = adaptiveTrackSelectionFactory; - paramsReference = new AtomicReference<>(Parameters.DEFAULT); + 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(); + } + + /** 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) { + setParameters(buildUponParameters().setRendererDisabled(rendererIndex, disabled)); + } + + /** @deprecated Use {@link Parameters#getRendererDisabled(int)}. * */ + @Deprecated + public final boolean getRendererDisabled(int rendererIndex) { + return getParameters().getRendererDisabled(rendererIndex); + } + + /** + * @deprecated Use {@link ParametersBuilder#setSelectionOverride(int, TrackGroupArray, + * SelectionOverride)}. + */ + @Deprecated + public final void setSelectionOverride( + int rendererIndex, TrackGroupArray groups, SelectionOverride override) { + setParameters(buildUponParameters().setSelectionOverride(rendererIndex, groups, override)); + } + + /** @deprecated Use {@link Parameters#hasSelectionOverride(int, TrackGroupArray)}. * */ + @Deprecated + public final boolean hasSelectionOverride(int rendererIndex, TrackGroupArray groups) { + return getParameters().hasSelectionOverride(rendererIndex, groups); + } + + /** @deprecated Use {@link Parameters#getSelectionOverride(int, TrackGroupArray)}. */ + @Deprecated + public final SelectionOverride getSelectionOverride(int rendererIndex, TrackGroupArray groups) { + return getParameters().getSelectionOverride(rendererIndex, groups); + } + + /** @deprecated Use {@link ParametersBuilder#clearSelectionOverride(int, TrackGroupArray)}. */ + @Deprecated + public final void clearSelectionOverride(int rendererIndex, TrackGroupArray groups) { + setParameters(buildUponParameters().clearSelectionOverride(rendererIndex, groups)); + } + + /** @deprecated Use {@link ParametersBuilder#clearSelectionOverrides(int)}. */ + @Deprecated + public final void clearSelectionOverrides(int rendererIndex) { + setParameters(buildUponParameters().clearSelectionOverrides(rendererIndex)); + } + + /** @deprecated Use {@link ParametersBuilder#clearSelectionOverrides()}. */ + @Deprecated + public final void clearSelectionOverrides() { + setParameters(buildUponParameters().clearSelectionOverrides()); + } + + /** @deprecated Use {@link ParametersBuilder#setTunnelingAudioSessionId(int)}. */ + @Deprecated + public void setTunnelingAudioSessionId(int tunnelingAudioSessionId) { + setParameters(buildUponParameters().setTunnelingAudioSessionId(tunnelingAudioSessionId)); } // MappingTrackSelector implementation. @Override - protected TrackSelection[] selectTracks(RendererCapabilities[] rendererCapabilities, - TrackGroupArray[] rendererTrackGroupArrays, int[][][] rendererFormatSupports) + protected final Pair selectTracks( + MappedTrackInfo mappedTrackInfo, + int[][][] rendererFormatSupports, + int[] rendererMixedMimeTypeAdaptationSupports) throws ExoPlaybackException { - // Make a track selection for each renderer. - int rendererCount = rendererCapabilities.length; + Parameters params = parametersReference.get(); + int rendererCount = mappedTrackInfo.getRendererCount(); + TrackSelection[] rendererTrackSelections = + selectAllTracks( + mappedTrackInfo, + rendererFormatSupports, + rendererMixedMimeTypeAdaptationSupports, + params); + + // Apply track disabling and overriding. + for (int i = 0; i < rendererCount; i++) { + if (params.getRendererDisabled(i)) { + rendererTrackSelections[i] = null; + } else { + 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[rendererCount]; + for (int i = 0; i < rendererCount; i++) { + boolean forceRendererDisabled = params.getRendererDisabled(i); + boolean rendererEnabled = + !forceRendererDisabled + && (mappedTrackInfo.getRendererType(i) == C.TRACK_TYPE_NONE + || rendererTrackSelections[i] != null); + rendererConfigurations[i] = rendererEnabled ? RendererConfiguration.DEFAULT : null; + } + + // Configure audio and video renderers to use tunneling if appropriate. + maybeConfigureRenderersForTunneling( + mappedTrackInfo, + rendererFormatSupports, + rendererConfigurations, + rendererTrackSelections, + params.tunnelingAudioSessionId); + + return Pair.create(rendererConfigurations, rendererTrackSelections); + } + + // Track selection 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 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( + MappedTrackInfo mappedTrackInfo, + int[][][] rendererFormatSupports, + int[] rendererMixedMimeTypeAdaptationSupports, + Parameters params) + throws ExoPlaybackException { + 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], - rendererTrackGroupArrays[i], rendererFormatSupports[i], params, - adaptiveTrackSelectionFactory); + rendererTrackSelections[i] = + selectVideoTrack( + mappedTrackInfo.getTrackGroups(i), + rendererFormatSupports[i], + rendererMixedMimeTypeAdaptationSupports[i], + params, + adaptiveTrackSelectionFactory); selectedVideoTracks = rendererTrackSelections[i] != null; } - seenVideoRendererWithMappedTracks |= rendererTrackGroupArrays[i].length > 0; + seenVideoRendererWithMappedTracks |= mappedTrackInfo.getTrackGroups(i).length > 0; } } 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; case C.TRACK_TYPE_AUDIO: if (!selectedAudioTracks) { - rendererTrackSelections[i] = selectAudioTrack(rendererTrackGroupArrays[i], - rendererFormatSupports[i], params, - seenVideoRendererWithMappedTracks ? null : adaptiveTrackSelectionFactory); + rendererTrackSelections[i] = + selectAudioTrack( + mappedTrackInfo.getTrackGroups(i), + rendererFormatSupports[i], + rendererMixedMimeTypeAdaptationSupports[i], + params, + seenVideoRendererWithMappedTracks ? null : adaptiveTrackSelectionFactory); selectedAudioTracks = rendererTrackSelections[i] != null; } break; case C.TRACK_TYPE_TEXT: if (!selectedTextTracks) { - rendererTrackSelections[i] = selectTextTrack(rendererTrackGroupArrays[i], - rendererFormatSupports[i], params); + rendererTrackSelections[i] = + selectTextTrack( + mappedTrackInfo.getTrackGroups(i), rendererFormatSupports[i], params); selectedTextTracks = rendererTrackSelections[i] != null; } break; default: - rendererTrackSelections[i] = selectOtherTrack(rendererCapabilities[i].getTrackType(), - rendererTrackGroupArrays[i], rendererFormatSupports[i], params); + rendererTrackSelections[i] = + selectOtherTrack( + trackType, mappedTrackInfo.getTrackGroups(i), rendererFormatSupports[i], params); break; } } + return rendererTrackSelections; } // Video track selection implementation. /** - * Called by {@link #selectTracks(RendererCapabilities[], TrackGroupArray[], int[][][])} 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. * @return The {@link TrackSelection} for the renderer, or null if no selection was made. * @throws ExoPlaybackException If an error occurs while selecting the tracks. */ - protected TrackSelection selectVideoTrack(RendererCapabilities rendererCapabilities, - TrackGroupArray groups, int[][] formatSupport, Parameters params, - TrackSelection.Factory adaptiveTrackSelectionFactory) throws ExoPlaybackException { + protected TrackSelection selectVideoTrack( + TrackGroupArray groups, + 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], @@ -778,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; @@ -789,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)) { @@ -839,44 +1519,36 @@ public class DefaultTrackSelector extends MappingTrackSelector { : new FixedTrackSelection(selectedGroup, selectedTrackIndex); } - /** - * Compares two format values for order. A known value is considered greater than - * {@link Format#NO_VALUE}. - * - * @param first The first value. - * @param second The second value. - * @return A negative integer if the first value is less than the second. Zero if they are equal. - * A positive integer if the first value is greater than the second. - */ - private static int compareFormatValues(int first, int second) { - return first == Format.NO_VALUE ? (second == Format.NO_VALUE ? 0 : -1) - : (second == Format.NO_VALUE ? 1 : (first - second)); - } - // Audio track selection implementation. /** - * Called by {@link #selectTracks(RendererCapabilities[], TrackGroupArray[], int[][][])} 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. * @return The {@link TrackSelection} for the renderer, or null if no selection was made. * @throws ExoPlaybackException If an error occurs while selecting the tracks. */ - protected TrackSelection selectAudioTrack(TrackGroupArray groups, int[][] formatSupport, - Parameters params, TrackSelection.Factory adaptiveTrackSelectionFactory) + protected TrackSelection selectAudioTrack( + TrackGroupArray groups, + int[][] formatSupports, + int mixedMimeTypeAdaptationSupports, + Parameters params, + TrackSelection.Factory adaptiveTrackSelectionFactory) throws ExoPlaybackException { int selectedTrackIndex = C.INDEX_UNSET; int selectedGroupIndex = C.INDEX_UNSET; 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)) { @@ -899,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); @@ -964,8 +1637,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { // Text track selection implementation. /** - * Called by {@link #selectTracks(RendererCapabilities[], TrackGroupArray[], int[][][])} 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 @@ -974,8 +1647,9 @@ public class DefaultTrackSelector extends MappingTrackSelector { * @return The {@link TrackSelection} for the renderer, or null if no selection was made. * @throws ExoPlaybackException If an error occurs while selecting the tracks. */ - protected TrackSelection selectTextTrack(TrackGroupArray groups, int[][] formatSupport, - Parameters params) throws ExoPlaybackException { + protected TrackSelection selectTextTrack( + TrackGroupArray groups, int[][] formatSupport, Parameters params) + throws ExoPlaybackException { TrackGroup selectedGroup = null; int selectedTrackIndex = 0; int selectedTrackScore = 0; @@ -1035,8 +1709,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { // General track selection methods. /** - * Called by {@link #selectTracks(RendererCapabilities[], TrackGroupArray[], int[][][])} 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. @@ -1046,8 +1720,9 @@ public class DefaultTrackSelector extends MappingTrackSelector { * @return The {@link TrackSelection} for the renderer, or null if no selection was made. * @throws ExoPlaybackException If an error occurs while selecting the tracks. */ - protected TrackSelection selectOtherTrack(int trackType, TrackGroupArray groups, - int[][] formatSupport, Parameters params) throws ExoPlaybackException { + protected TrackSelection selectOtherTrack( + int trackType, TrackGroupArray groups, int[][] formatSupport, Parameters params) + throws ExoPlaybackException { TrackGroup selectedGroup = null; int selectedTrackIndex = 0; int selectedTrackScore = 0; @@ -1075,6 +1750,108 @@ public class DefaultTrackSelector extends MappingTrackSelector { : new FixedTrackSelection(selectedGroup, selectedTrackIndex); } + // Utility methods. + + /** + * Determines whether tunneling should be enabled, replacing {@link RendererConfiguration}s in + * {@code rendererConfigurations} with configurations that enable tunneling on the appropriate + * renderers if so. + * + * @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. + * @param tunnelingAudioSessionId The audio session id to use when tunneling, or {@link + * C#AUDIO_SESSION_ID_UNSET} if tunneling should not be enabled. + */ + private static void maybeConfigureRenderersForTunneling( + MappedTrackInfo mappedTrackInfo, + int[][][] renderererFormatSupports, + RendererConfiguration[] rendererConfigurations, + TrackSelection[] trackSelections, + int tunnelingAudioSessionId) { + if (tunnelingAudioSessionId == C.AUDIO_SESSION_ID_UNSET) { + return; + } + // Check whether we can enable tunneling. To enable tunneling we require exactly one audio and + // one video renderer to support tunneling and have a selection. + int tunnelingAudioRendererIndex = -1; + int tunnelingVideoRendererIndex = -1; + boolean enableTunneling = true; + 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( + renderererFormatSupports[i], mappedTrackInfo.getTrackGroups(i), trackSelection)) { + if (rendererType == C.TRACK_TYPE_AUDIO) { + if (tunnelingAudioRendererIndex != -1) { + enableTunneling = false; + break; + } else { + tunnelingAudioRendererIndex = i; + } + } else { + if (tunnelingVideoRendererIndex != -1) { + enableTunneling = false; + break; + } else { + tunnelingVideoRendererIndex = i; + } + } + } + } + } + enableTunneling &= tunnelingAudioRendererIndex != -1 && tunnelingVideoRendererIndex != -1; + if (enableTunneling) { + RendererConfiguration tunnelingRendererConfiguration = + new RendererConfiguration(tunnelingAudioSessionId); + rendererConfigurations[tunnelingAudioRendererIndex] = tunnelingRendererConfiguration; + rendererConfigurations[tunnelingVideoRendererIndex] = tunnelingRendererConfiguration; + } + } + + /** + * Returns whether a renderer supports tunneling for a {@link TrackSelection}. + * + * @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[][] 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 = formatSupports[trackGroupIndex][selection.getIndexInTrackGroup(i)]; + if ((trackFormatSupport & RendererCapabilities.TUNNELING_SUPPORT_MASK) + != RendererCapabilities.TUNNELING_SUPPORTED) { + return false; + } + } + return true; + } + + /** + * Compares two format values for order. A known value is considered greater than {@link + * Format#NO_VALUE}. + * + * @param first The first value. + * @param second The second value. + * @return A negative integer if the first value is less than the second. Zero if they are equal. + * A positive integer if the first value is greater than the second. + */ + private static int compareFormatValues(int first, int second) { + return first == Format.NO_VALUE + ? (second == Format.NO_VALUE ? 0 : -1) + : (second == Format.NO_VALUE ? 1 : (first - second)); + } + /** * Applies the {@link RendererCapabilities#FORMAT_SUPPORT_MASK} to a value obtained from * {@link RendererCapabilities#supportsFormat(Format)}, returning true if the result is @@ -1118,8 +1895,6 @@ public class DefaultTrackSelector extends MappingTrackSelector { && TextUtils.equals(language, Util.normalizeLanguageCode(format.language)); } - // Viewport size util methods. - private static List getViewportFilteredTrackIndices(TrackGroup group, int viewportWidth, int viewportHeight, boolean orientationMayChange) { // Initially include all indices. 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 5d120990fc..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 @@ -15,64 +15,24 @@ */ package com.google.android.exoplayer2.trackselection; -import android.content.Context; -import android.util.SparseArray; -import android.util.SparseBooleanArray; +import android.support.annotation.IntDef; +import android.util.Pair; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; -import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.RendererConfiguration; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.util.Util; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; /** * Base class for {@link TrackSelector}s that first establish a mapping between {@link TrackGroup}s * and {@link Renderer}s, and then from that mapping create a {@link TrackSelection} for each * renderer. - * - *

    Track overrides

    - * Mapping track selectors support 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(trackSelectionFactory, groupIndex, trackIndices));}
    - * 
    - * where {@code trackSelectionFactory} is a {@link TrackSelection.Factory} for generating concrete - * {@link TrackSelection} instances for the override. It's also possible to pass {@code null} as the - * selection override if you don't want any tracks to 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. */ public abstract class MappingTrackSelector extends TrackSelector { @@ -81,54 +41,87 @@ public abstract class MappingTrackSelector extends TrackSelector { */ public static final class MappedTrackInfo { - /** - * The renderer does not have any associated tracks. - */ + /** Levels of renderer support. Higher numerical values indicate higher levels of support. */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + RENDERER_SUPPORT_NO_TRACKS, + RENDERER_SUPPORT_UNSUPPORTED_TRACKS, + RENDERER_SUPPORT_EXCEEDS_CAPABILITIES_TRACKS, + RENDERER_SUPPORT_PLAYABLE_TRACKS + }) + @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]; } /** @@ -138,20 +131,20 @@ 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 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 #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 getRendererSupport(int rendererIndex) { + 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; @@ -171,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)); } @@ -191,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)) { @@ -249,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; @@ -265,88 +270,39 @@ 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(); + } + + /** Returns {@link TrackGroup}s not mapped to any renderer. */ + public TrackGroupArray getUnmappedTrackGroups() { + return unmappedTrackGroups; } } - /** - * A track selection override. - */ - public static final class SelectionOverride { - - 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; - this.groupIndex = groupIndex; - this.tracks = tracks; - this.length = tracks.length; - } - - /** - * 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); - } - - /** - * Returns whether this override contains the specified track index. - */ - public boolean containsTrack(int track) { - for (int overrideTrack : tracks) { - if (overrideTrack == track) { - return true; - } - } - return false; - } - - } - - private final SparseArray> selectionOverrides; - private final SparseBooleanArray rendererDisabledFlags; - private int tunnelingAudioSessionId; - private MappedTrackInfo currentMappedTrackInfo; - public MappingTrackSelector() { - selectionOverrides = new SparseArray<>(); - rendererDisabledFlags = new SparseBooleanArray(); - tunnelingAudioSessionId = C.AUDIO_SESSION_ID_UNSET; - } - /** * Returns the mapping information for the currently active track selection, or null if no * selection is currently active. @@ -355,158 +311,13 @@ public abstract class MappingTrackSelector extends TrackSelector { return currentMappedTrackInfo; } - /** - * 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 void setRendererDisabled(int rendererIndex, boolean disabled) { - if (rendererDisabledFlags.get(rendererIndex) == disabled) { - // The disabled flag is unchanged. - return; - } - rendererDisabledFlags.put(rendererIndex, disabled); - invalidate(); - } - - /** - * 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); - } - - /** - * 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 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(); - } - - /** - * 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; - } - - /** - * 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 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(); - } - - /** - * Clears all track selection overrides for the specified renderer. - * - * @param rendererIndex The renderer index. - */ - public final void clearSelectionOverrides(int rendererIndex) { - Map overrides = selectionOverrides.get(rendererIndex); - if (overrides == null || overrides.isEmpty()) { - // Nothing to clear. - return; - } - selectionOverrides.remove(rendererIndex); - invalidate(); - } - - /** - * Clears all track selection overrides for all renderers. - */ - public final void clearSelectionOverrides() { - if (selectionOverrides.size() == 0) { - // Nothing to clear. - return; - } - selectionOverrides.clear(); - invalidate(); - } - - /** - * 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 void setTunnelingAudioSessionId(int tunnelingAudioSessionId) { - if (this.tunnelingAudioSessionId != tunnelingAudioSessionId) { - this.tunnelingAudioSessionId = tunnelingAudioSessionId; - invalidate(); - } - } - // TrackSelector implementation. + @Override + public final void onSelectionActivated(Object info) { + currentMappedTrackInfo = (MappedTrackInfo) info; + } + @Override public final TrackSelectorResult selectTracks(RendererCapabilities[] rendererCapabilities, TrackGroupArray trackGroups) throws ExoPlaybackException { @@ -522,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. @@ -551,81 +363,46 @@ 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)); - - TrackSelection[] trackSelections = selectTracks(rendererCapabilities, rendererTrackGroupArrays, - rendererFormatSupports); - - // Apply track disabling and overriding. - for (int i = 0; i < rendererCapabilities.length; i++) { - if (rendererDisabledFlags.get(i)) { - trackSelections[i] = null; - } else { - TrackGroupArray rendererTrackGroup = rendererTrackGroupArrays[i]; - if (hasSelectionOverride(i, rendererTrackGroup)) { - SelectionOverride override = selectionOverrides.get(i).get(rendererTrackGroup); - trackSelections[i] = override == null ? null - : override.createTrackSelection(rendererTrackGroup); - } - } - } - - boolean[] rendererEnabled = determineEnabledRenderers(rendererCapabilities, trackSelections); + // 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, rendererFormatSupports, - unassociatedTrackGroupArray); + MappedTrackInfo mappedTrackInfo = + new MappedTrackInfo( + rendererTrackTypes, + rendererTrackGroupArrays, + rendererMixedMimeTypeAdaptationSupports, + rendererFormatSupports, + unmappedTrackGroupArray); - // Initialize the renderer configurations to the default configuration for all renderers with - // selections, and null otherwise. - RendererConfiguration[] rendererConfigurations = - new RendererConfiguration[rendererCapabilities.length]; - for (int i = 0; i < rendererCapabilities.length; i++) { - rendererConfigurations[i] = rendererEnabled[i] ? RendererConfiguration.DEFAULT : null; - } - // Configure audio and video renderers to use tunneling if appropriate. - maybeConfigureRenderersForTunneling(rendererCapabilities, rendererTrackGroupArrays, - rendererFormatSupports, rendererConfigurations, trackSelections, tunnelingAudioSessionId); - - return new TrackSelectorResult(trackGroups, rendererEnabled, - new TrackSelectionArray(trackSelections), mappedTrackInfo, rendererConfigurations); - } - - private boolean[] determineEnabledRenderers(RendererCapabilities[] rendererCapabilities, - TrackSelection[] trackSelections) { - boolean[] rendererEnabled = new boolean[trackSelections.length]; - for (int i = 0; i < rendererEnabled.length; i++) { - boolean forceRendererDisabled = rendererDisabledFlags.get(i); - rendererEnabled[i] = !forceRendererDisabled - && (rendererCapabilities[i].getTrackType() == C.TRACK_TYPE_NONE - || trackSelections[i] != null); - } - return rendererEnabled; - } - - @Override - public final void onSelectionActivated(Object info) { - currentMappedTrackInfo = (MappedTrackInfo) info; + Pair result = + selectTracks( + mappedTrackInfo, rendererFormatSupports, rendererMixedMimeTypeAdaptationSupports); + return new TrackSelectorResult(result.first, result.second, mappedTrackInfo); } /** - * Given an array of renderer capabilities and the {@link TrackGroupArray}s mapped to each of - * them, provides a {@link TrackSelection} per renderer. + * Given mapped track information, returns a track selection and configuration for each renderer. * - * @param rendererCapabilities The {@link RendererCapabilities} of the renderers for which - * {@link TrackSelection}s are to be generated. - * @param rendererTrackGroupArrays The {@link TrackGroupArray}s mapped to each of the renderers. + * @param mappedTrackInfo Mapped track information. * @param rendererFormatSupports The result of {@link RendererCapabilities#supportsFormat} for - * each mapped track, indexed by renderer index, track group index and track index (in that - * order). + * 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 + * RendererCapabilities#getTrackType()} is {@link C#TRACK_TYPE_NONE}. * @throws ExoPlaybackException If an error occurs while selecting the tracks. */ - protected abstract TrackSelection[] selectTracks(RendererCapabilities[] rendererCapabilities, - TrackGroupArray[] rendererTrackGroupArrays, int[][][] rendererFormatSupports) + protected abstract Pair selectTracks( + MappedTrackInfo mappedTrackInfo, + int[][][] rendererFormatSupports, + int[] rendererMixedMimeTypeAdaptationSupport) throws ExoPlaybackException; /** @@ -694,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++) { @@ -707,92 +484,4 @@ public abstract class MappingTrackSelector extends TrackSelector { return mixedMimeTypeAdaptationSupport; } - /** - * Determines whether tunneling should be enabled, replacing {@link RendererConfiguration}s in - * {@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 rendererTrackGroupArrays An array of {@link TrackGroupArray}s where each entry - * corresponds to the renderer of equal index in {@code renderers}. - * @param rendererFormatSupports Maps every available track to a specific level of support as - * defined by the renderer {@code FORMAT_*} constants. - * @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. - * @param tunnelingAudioSessionId The audio session id to use when tunneling, or - * {@link C#AUDIO_SESSION_ID_UNSET} if tunneling should not be enabled. - */ - private static void maybeConfigureRenderersForTunneling( - RendererCapabilities[] rendererCapabilities, TrackGroupArray[] rendererTrackGroupArrays, - int[][][] rendererFormatSupports, RendererConfiguration[] rendererConfigurations, - TrackSelection[] trackSelections, int tunnelingAudioSessionId) { - if (tunnelingAudioSessionId == C.AUDIO_SESSION_ID_UNSET) { - return; - } - // Check whether we can enable tunneling. To enable tunneling we require exactly one audio and - // one video renderer to support tunneling and have a selection. - int tunnelingAudioRendererIndex = -1; - int tunnelingVideoRendererIndex = -1; - boolean enableTunneling = true; - for (int i = 0; i < rendererCapabilities.length; i++) { - int rendererType = rendererCapabilities[i].getTrackType(); - TrackSelection trackSelection = trackSelections[i]; - if ((rendererType == C.TRACK_TYPE_AUDIO || rendererType == C.TRACK_TYPE_VIDEO) - && trackSelection != null) { - if (rendererSupportsTunneling(rendererFormatSupports[i], rendererTrackGroupArrays[i], - trackSelection)) { - if (rendererType == C.TRACK_TYPE_AUDIO) { - if (tunnelingAudioRendererIndex != -1) { - enableTunneling = false; - break; - } else { - tunnelingAudioRendererIndex = i; - } - } else { - if (tunnelingVideoRendererIndex != -1) { - enableTunneling = false; - break; - } else { - tunnelingVideoRendererIndex = i; - } - } - } - } - } - enableTunneling &= tunnelingAudioRendererIndex != -1 && tunnelingVideoRendererIndex != -1; - if (enableTunneling) { - RendererConfiguration tunnelingRendererConfiguration = - new RendererConfiguration(tunnelingAudioSessionId); - rendererConfigurations[tunnelingAudioRendererIndex] = tunnelingRendererConfiguration; - rendererConfigurations[tunnelingVideoRendererIndex] = tunnelingRendererConfiguration; - } - } - - /** - * Returns whether a renderer supports tunneling for a {@link TrackSelection}. - * - * @param formatSupport 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) { - 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)]; - if ((trackFormatSupport & RendererCapabilities.TUNNELING_SUPPORT_MASK) - != RendererCapabilities.TUNNELING_SUPPORTED) { - return false; - } - } - return true; - } - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionArray.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionArray.java index 809e571f10..2d457750e4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionArray.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionArray.java @@ -17,14 +17,10 @@ package com.google.android.exoplayer2.trackselection; import java.util.Arrays; -/** - * The result of a {@link TrackSelector} operation. - */ +/** An array of {@link TrackSelection}s. */ public final class TrackSelectionArray { - /** - * The number of selections in the result. Greater than or equal to zero. - */ + /** The length of this array. */ public final int length; private final TrackSelection[] trackSelections; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java index 68adc32395..882d98764e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java @@ -16,7 +16,6 @@ package com.google.android.exoplayer2.trackselection; import com.google.android.exoplayer2.RendererConfiguration; -import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.util.Util; /** @@ -24,14 +23,13 @@ import com.google.android.exoplayer2.util.Util; */ public final class TrackSelectorResult { + /** The number of selections in the result. Greater than or equal to zero. */ + public final int length; /** - * The track groups that were provided to the {@link TrackSelector}. + * A {@link RendererConfiguration} for each renderer. A null entry indicates the corresponding + * renderer should be disabled. */ - public final TrackGroupArray groups; - /** - * An array containing whether each renderer is enabled after the track selection operation. - */ - public final boolean[] renderersEnabled; + public final RendererConfiguration[] rendererConfigurations; /** * A {@link TrackSelectionArray} containing the track selection for each renderer. */ @@ -41,29 +39,25 @@ public final class TrackSelectorResult { * should the selections be activated. */ public final Object info; - /** - * A {@link RendererConfiguration} for each enabled renderer, to be used with the selections. - */ - public final RendererConfiguration[] rendererConfigurations; /** - * @param groups The track groups provided to the {@link TrackSelector}. - * @param renderersEnabled An array containing whether each renderer is enabled after the track - * selection operation. + * @param rendererConfigurations A {@link RendererConfiguration} for each renderer. A null entry + * indicates the corresponding renderer should be disabled. * @param selections A {@link TrackSelectionArray} containing the selection for each renderer. - * @param info An opaque object that will be returned to - * {@link TrackSelector#onSelectionActivated(Object)} should the selection be activated. - * @param rendererConfigurations A {@link RendererConfiguration} for each enabled renderer, - * to be used with the selections. + * @param info An opaque object that will be returned to {@link + * TrackSelector#onSelectionActivated(Object)} should the selection be activated. */ - public TrackSelectorResult(TrackGroupArray groups, boolean[] renderersEnabled, - TrackSelectionArray selections, Object info, - RendererConfiguration[] rendererConfigurations) { - this.groups = groups; - this.renderersEnabled = renderersEnabled; - this.selections = selections; - this.info = info; + public TrackSelectorResult( + RendererConfiguration[] rendererConfigurations, TrackSelection[] selections, Object info) { this.rendererConfigurations = rendererConfigurations; + this.selections = new TrackSelectionArray(selections); + this.info = info; + length = rendererConfigurations.length; + } + + /** Returns whether the renderer at the specified index is enabled. */ + public boolean isRendererEnabled(int index) { + return rendererConfigurations[index] != null; } /** @@ -100,9 +94,8 @@ public final class TrackSelectorResult { if (other == null) { return false; } - return renderersEnabled[index] == other.renderersEnabled[index] - && Util.areEqual(selections.get(index), other.selections.get(index)) - && Util.areEqual(rendererConfigurations[index], other.rendererConfigurations[index]); + return Util.areEqual(rendererConfigurations[index], other.rendererConfigurations[index]) + && Util.areEqual(selections.get(index), other.selections.get(index)); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/BandwidthMeter.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/BandwidthMeter.java index 21bdddf9b8..0a3fb967a8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/BandwidthMeter.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/BandwidthMeter.java @@ -27,28 +27,17 @@ public interface BandwidthMeter { /** * Called periodically to indicate that bytes have been transferred. - *

    - * Note: The estimated bitrate is typically derived from more information than just - * {@code bytes} and {@code elapsedMs}. + * + *

    Note: The estimated bitrate is typically derived from more information than just {@code + * bytes} and {@code elapsedMs}. * * @param elapsedMs The time taken to transfer the bytes, in milliseconds. * @param bytes The number of bytes transferred. - * @param bitrate The estimated bitrate in bits/sec, or {@link #NO_ESTIMATE} if an estimate is - * not available. + * @param bitrate The estimated bitrate in bits/sec. */ void onBandwidthSample(int elapsedMs, long bytes, long bitrate); - } - /** - * Indicates no bandwidth estimate is available. - */ - long NO_ESTIMATE = -1; - - /** - * Returns the estimated bandwidth in bits/sec, or {@link #NO_ESTIMATE} if an estimate is not - * available. - */ + /** Returns the estimated bandwidth in bits/sec. */ long getBitrateEstimate(); - } 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/DefaultBandwidthMeter.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultBandwidthMeter.java index db04b2580e..f32965619a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultBandwidthMeter.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultBandwidthMeter.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.upstream; import android.os.Handler; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Clock; import com.google.android.exoplayer2.util.SlidingPercentile; @@ -26,16 +27,94 @@ import com.google.android.exoplayer2.util.SlidingPercentile; */ public final class DefaultBandwidthMeter implements BandwidthMeter, TransferListener { - /** - * The default maximum weight for the sliding window. - */ - public static final int DEFAULT_MAX_WEIGHT = 2000; + /** Default initial bitrate estimate in bits per second. */ + public static final long DEFAULT_INITIAL_BITRATE_ESTIMATE = 1_000_000; + + /** Default maximum weight for the sliding window. */ + public static final int DEFAULT_SLIDING_WINDOW_MAX_WEIGHT = 2000; + + /** Builder for a bandwidth meter. */ + public static final class Builder { + + private @Nullable Handler eventHandler; + private @Nullable EventListener eventListener; + private long initialBitrateEstimate; + private int slidingWindowMaxWeight; + private Clock clock; + + /** Creates a builder with default parameters and without listener. */ + public Builder() { + initialBitrateEstimate = DEFAULT_INITIAL_BITRATE_ESTIMATE; + slidingWindowMaxWeight = DEFAULT_SLIDING_WINDOW_MAX_WEIGHT; + clock = Clock.DEFAULT; + } + + /** + * Sets an event listener for new bandwidth estimates. + * + * @param eventHandler A handler for events. + * @param eventListener A listener of events. + * @return This builder. + * @throws IllegalArgumentException If the event handler or listener are null. + */ + public Builder setEventListener(Handler eventHandler, EventListener eventListener) { + Assertions.checkArgument(eventHandler != null && eventListener != null); + this.eventHandler = eventHandler; + this.eventListener = eventListener; + return this; + } + + /** + * Sets the maximum weight for the sliding window. + * + * @param slidingWindowMaxWeight The maximum weight for the sliding window. + * @return This builder. + */ + public Builder setSlidingWindowMaxWeight(int slidingWindowMaxWeight) { + this.slidingWindowMaxWeight = slidingWindowMaxWeight; + return this; + } + + /** + * Sets the initial bitrate estimate in bits per second that should be assumed when a bandwidth + * estimate is unavailable. + * + * @param initialBitrateEstimate The initial bitrate estimate in bits per second. + * @return This builder. + */ + public Builder setInitialBitrateEstimate(long initialBitrateEstimate) { + this.initialBitrateEstimate = initialBitrateEstimate; + return this; + } + + /** + * Sets the clock used to estimate bandwidth from data transfers. Should only be set for testing + * purposes. + * + * @param clock The clock used to estimate bandwidth from data transfers. + * @return This builder. + */ + public Builder setClock(Clock clock) { + this.clock = clock; + return this; + } + + /** + * Builds the bandwidth meter. + * + * @return A bandwidth meter with the configured properties. + */ + public DefaultBandwidthMeter build() { + return new DefaultBandwidthMeter( + eventHandler, eventListener, initialBitrateEstimate, slidingWindowMaxWeight, clock); + } + } private static final int ELAPSED_MILLIS_FOR_ESTIMATE = 2000; private static final int BYTES_TRANSFERRED_FOR_ESTIMATE = 512 * 1024; - private final Handler eventHandler; - private final EventListener eventListener; + private final @Nullable Handler eventHandler; + private final @Nullable EventListener eventListener; private final SlidingPercentile slidingPercentile; private final Clock clock; @@ -47,25 +126,44 @@ public final class DefaultBandwidthMeter implements BandwidthMeter, TransferList private long totalBytesTransferred; private long bitrateEstimate; + /** Creates a bandwidth meter with default parameters. */ public DefaultBandwidthMeter() { - this(null, null); + this( + /* eventHandler= */ null, + /* eventListener= */ null, + DEFAULT_INITIAL_BITRATE_ESTIMATE, + DEFAULT_SLIDING_WINDOW_MAX_WEIGHT, + Clock.DEFAULT); } + /** @deprecated Use {@link Builder} instead. */ + @Deprecated public DefaultBandwidthMeter(Handler eventHandler, EventListener eventListener) { - this(eventHandler, eventListener, DEFAULT_MAX_WEIGHT); + this( + eventHandler, + eventListener, + DEFAULT_INITIAL_BITRATE_ESTIMATE, + DEFAULT_SLIDING_WINDOW_MAX_WEIGHT, + Clock.DEFAULT); } + /** @deprecated Use {@link Builder} instead. */ + @Deprecated public DefaultBandwidthMeter(Handler eventHandler, EventListener eventListener, int maxWeight) { - this(eventHandler, eventListener, maxWeight, Clock.DEFAULT); + this(eventHandler, eventListener, DEFAULT_INITIAL_BITRATE_ESTIMATE, maxWeight, Clock.DEFAULT); } - public DefaultBandwidthMeter(Handler eventHandler, EventListener eventListener, int maxWeight, + private DefaultBandwidthMeter( + @Nullable Handler eventHandler, + @Nullable EventListener eventListener, + long initialBitrateEstimate, + int maxWeight, Clock clock) { this.eventHandler = eventHandler; this.eventListener = eventListener; this.slidingPercentile = new SlidingPercentile(maxWeight); this.clock = clock; - bitrateEstimate = NO_ESTIMATE; + bitrateEstimate = initialBitrateEstimate; } @Override @@ -98,9 +196,7 @@ public final class DefaultBandwidthMeter implements BandwidthMeter, TransferList slidingPercentile.addSample((int) Math.sqrt(sampleBytesTransferred), bitsPerSecond); if (totalElapsedTimeMs >= ELAPSED_MILLIS_FOR_ESTIMATE || totalBytesTransferred >= BYTES_TRANSFERRED_FOR_ESTIMATE) { - float bitrateEstimateFloat = slidingPercentile.getPercentile(0.5f); - bitrateEstimate = Float.isNaN(bitrateEstimateFloat) ? NO_ESTIMATE - : (long) bitrateEstimateFloat; + bitrateEstimate = (long) slidingPercentile.getPercentile(0.5f); } } notifyBandwidthSample(sampleElapsedTimeMs, sampleBytesTransferred, bitrateEstimate); @@ -120,5 +216,4 @@ public final class DefaultBandwidthMeter implements BandwidthMeter, TransferList }); } } - } 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 599cdddeb9..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) { @@ -613,9 +613,9 @@ public class DefaultHttpDataSource implements HttpDataSource { return; } String className = inputStream.getClass().getName(); - if (className.equals("com.android.okhttp.internal.http.HttpTransport$ChunkedInputStream") - || className.equals( - "com.android.okhttp.internal.http.HttpTransport$FixedLengthInputStream")) { + if ("com.android.okhttp.internal.http.HttpTransport$ChunkedInputStream".equals(className) + || "com.android.okhttp.internal.http.HttpTransport$FixedLengthInputStream" + .equals(className)) { Class superclass = inputStream.getClass().getSuperclass(); Method unexpectedEndOfInput = superclass.getDeclaredMethod("unexpectedEndOfInput"); unexpectedEndOfInput.setAccessible(true); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/Loader.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/Loader.java index a118f10784..074fc095ea 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/Loader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/Loader.java @@ -20,12 +20,15 @@ import android.os.Handler; import android.os.Looper; import android.os.Message; import android.os.SystemClock; +import android.support.annotation.IntDef; import android.support.annotation.Nullable; import android.util.Log; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.TraceUtil; import com.google.android.exoplayer2.util.Util; import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.concurrent.ExecutorService; /** @@ -104,20 +107,20 @@ public final class Loader implements LoaderErrorThrower { /** * Called when a load encounters an error. - *

    - * Note: There is guaranteed to be a memory barrier between {@link Loadable#load()} exiting and - * this callback being called. + * + *

    Note: There is guaranteed to be a memory barrier between {@link Loadable#load()} exiting + * and this callback being called. * * @param loadable The loadable whose load has encountered an error. * @param elapsedRealtimeMs {@link SystemClock#elapsedRealtime} when the error occurred. * @param loadDurationMs The duration of the load up to the point at which the error occurred. * @param error The load error. - * @return The desired retry action. One of {@link Loader#RETRY}, - * {@link Loader#RETRY_RESET_ERROR_COUNT}, {@link Loader#DONT_RETRY} and - * {@link Loader#DONT_RETRY_FATAL}. + * @return The desired retry action. One of {@link Loader#RETRY}, {@link + * Loader#RETRY_RESET_ERROR_COUNT}, {@link Loader#DONT_RETRY} and {@link + * Loader#DONT_RETRY_FATAL}. */ + @RetryAction int onLoadError(T loadable, long elapsedRealtimeMs, long loadDurationMs, IOException error); - } /** @@ -132,6 +135,11 @@ public final class Loader implements LoaderErrorThrower { } + /** Actions that can be taken in response to a load error. */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({RETRY, RETRY_RESET_ERROR_COUNT, DONT_RETRY, DONT_RETRY_FATAL}) + public @interface RetryAction {} + public static final int RETRY = 0; public static final int RETRY_RESET_ERROR_COUNT = 1; public static final int DONT_RETRY = 2; @@ -151,22 +159,23 @@ public final class Loader implements LoaderErrorThrower { /** * Starts loading a {@link Loadable}. - *

    - * The calling thread must be a {@link Looper} thread, which is the thread on which the - * {@link Callback} will be called. + * + *

    The calling thread must be a {@link Looper} thread, which is the thread on which the {@link + * Callback} will be called. * * @param The type of the loadable. * @param loadable The {@link Loadable} to load. * @param callback A callback to be called when the load ends. - * @param defaultMinRetryCount The minimum number of times the load must be retried before - * {@link #maybeThrowError()} will propagate an error. + * @param defaultMinRetryCount The minimum number of times the load must be retried before {@link + * #maybeThrowError()} will propagate an error. * @throws IllegalStateException If the calling thread does not have an associated {@link Looper}. * @return {@link SystemClock#elapsedRealtime} when the load started. */ - public long startLoading(T loadable, Callback callback, - int defaultMinRetryCount) { + public long startLoading( + T loadable, Callback callback, int defaultMinRetryCount) { Looper looper = Looper.myLooper(); Assertions.checkState(looper != null); + fatalError = null; long startTimeMs = SystemClock.elapsedRealtime(); new LoadTask<>(looper, loadable, callback, defaultMinRetryCount, startTimeMs).start(0); return startTimeMs; 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/Cache.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/Cache.java index 171aa0878a..584939fdc7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/Cache.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/Cache.java @@ -79,6 +79,12 @@ public interface Cache { } + /** + * Releases the cache. This method must be called when the cache is no longer required. The cache + * must not be used after calling this method. + */ + void release() throws CacheException; + /** * Registers a listener to listen for changes to a given key. * @@ -238,4 +244,23 @@ public interface Cache { * com.google.android.exoplayer2.C#LENGTH_UNSET} otherwise. */ long getContentLength(String key); + + /** + * Applies {@code mutations} to the {@link ContentMetadata} for the given key. A new {@link + * CachedContent} is added if there isn't one already with the given key. + * + * @param key The cache key for the data. + * @param mutations Contains mutations to be applied to the metadata. + * @throws CacheException If an error is encountered. + */ + void applyContentMetadataMutations(String key, ContentMetadataMutations mutations) + throws CacheException; + + /** + * Returns a {@link ContentMetadata} for the given key. + * + * @param key The cache key for the data. + * @return A {@link ContentMetadata} for the given key. + */ + ContentMetadata getContentMetadata(String key); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java index 57f5a6ad93..8d310015f8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java @@ -43,6 +43,7 @@ public final class CacheDataSink implements DataSink { private final Cache cache; private final long maxCacheFileSize; private final int bufferSize; + private final boolean syncFileDescriptor; private DataSpec dataSpec; private File file; @@ -72,21 +73,49 @@ public final class CacheDataSink implements DataSink { * multiple cache files. */ public CacheDataSink(Cache cache, long maxCacheFileSize) { - this(cache, maxCacheFileSize, DEFAULT_BUFFER_SIZE); + this(cache, maxCacheFileSize, DEFAULT_BUFFER_SIZE, true); + } + + /** + * Constructs a CacheDataSink using the {@link #DEFAULT_BUFFER_SIZE}. + * + * @param cache The cache into which data should be written. + * @param maxCacheFileSize The maximum size of a cache file, in bytes. If the sink is opened for a + * {@link DataSpec} whose size exceeds this value, then the data will be fragmented into + * multiple cache files. + * @param syncFileDescriptor Whether file descriptors are sync'd when closing output streams. + */ + public CacheDataSink(Cache cache, long maxCacheFileSize, boolean syncFileDescriptor) { + this(cache, maxCacheFileSize, DEFAULT_BUFFER_SIZE, syncFileDescriptor); } /** * @param cache The cache into which data should be written. - * @param maxCacheFileSize The maximum size of a cache file, in bytes. If the sink is opened for - * a {@link DataSpec} whose size exceeds this value, then the data will be fragmented into - * multiple cache files. + * @param maxCacheFileSize The maximum size of a cache file, in bytes. If the sink is opened for a + * {@link DataSpec} whose size exceeds this value, then the data will be fragmented into + * multiple cache files. * @param bufferSize The buffer size in bytes for writing to a cache file. A zero or negative - * value disables buffering. + * value disables buffering. */ public CacheDataSink(Cache cache, long maxCacheFileSize, int bufferSize) { + this(cache, maxCacheFileSize, bufferSize, true); + } + + /** + * @param cache The cache into which data should be written. + * @param maxCacheFileSize The maximum size of a cache file, in bytes. If the sink is opened for a + * {@link DataSpec} whose size exceeds this value, then the data will be fragmented into + * multiple cache files. + * @param bufferSize The buffer size in bytes for writing to a cache file. A zero or negative + * value disables buffering. + * @param syncFileDescriptor Whether file descriptors are sync'd when closing output streams. + */ + public CacheDataSink( + Cache cache, long maxCacheFileSize, int bufferSize, boolean syncFileDescriptor) { this.cache = Assertions.checkNotNull(cache); this.maxCacheFileSize = maxCacheFileSize; this.bufferSize = bufferSize; + this.syncFileDescriptor = syncFileDescriptor; } @Override @@ -170,7 +199,9 @@ public final class CacheDataSink implements DataSink { boolean success = false; try { outputStream.flush(); - underlyingFileOutputStream.getFD().sync(); + if (syncFileDescriptor) { + underlyingFileOutputStream.getFD().sync(); + } success = true; } finally { Util.closeQuietly(outputStream); 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 c0927eb7db..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 @@ -77,6 +77,20 @@ public final class CacheDataSource implements DataSource { */ public static final int FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS = 1 << 2; + /** Reasons the cache may be ignored. */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({CACHE_IGNORED_REASON_ERROR, CACHE_IGNORED_REASON_UNSET_LENGTH}) + public @interface CacheIgnoredReason {} + + /** 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. */ + public static final int CACHE_IGNORED_REASON_UNSET_LENGTH = 1; + /** * Listener of {@link CacheDataSource} events. */ @@ -90,6 +104,12 @@ public final class CacheDataSource implements DataSource { */ void onCachedBytesRead(long cacheSizeBytes, long cachedBytesRead); + /** + * Called when the current request ignores cache. + * + * @param reason Reason cache is bypassed. + */ + void onCacheIgnored(@CacheIgnoredReason int reason); } /** Minimum number of bytes to read before checking cache for availability. */ @@ -108,6 +128,7 @@ public final class CacheDataSource implements DataSource { private DataSource currentDataSource; private boolean currentDataSpecLengthUnset; private Uri uri; + private Uri actualUri; private int flags; private String key; private long readPosition; @@ -195,12 +216,18 @@ public final class CacheDataSource implements DataSource { @Override public long open(DataSpec dataSpec) throws IOException { try { - uri = dataSpec.uri; - flags = dataSpec.flags; key = CacheUtil.getKey(dataSpec); + uri = dataSpec.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 { @@ -234,7 +261,7 @@ public final class CacheDataSource implements DataSource { } int bytesRead = currentDataSource.read(buffer, offset, readLength); if (bytesRead != C.RESULT_END_OF_INPUT) { - if (currentDataSource == cacheReadDataSource) { + if (isReadingFromCache()) { totalCachedBytesRead += bytesRead; } readPosition += bytesRead; @@ -242,7 +269,7 @@ public final class CacheDataSource implements DataSource { bytesRemaining -= bytesRead; } } else if (currentDataSpecLengthUnset) { - setBytesRemaining(0); + setNoBytesRemainingAndMaybeStoreLength(); } else if (bytesRemaining > 0 || bytesRemaining == C.LENGTH_UNSET) { closeCurrentSource(); openNextSource(false); @@ -251,7 +278,7 @@ public final class CacheDataSource implements DataSource { return bytesRead; } catch (IOException e) { if (currentDataSpecLengthUnset && isCausedByPositionOutOfRange(e)) { - setBytesRemaining(0); + setNoBytesRemainingAndMaybeStoreLength(); return C.RESULT_END_OF_INPUT; } handleBeforeThrow(e); @@ -261,12 +288,13 @@ public final class CacheDataSource implements DataSource { @Override public Uri getUri() { - return currentDataSource == upstreamDataSource ? currentDataSource.getUri() : uri; + return actualUri; } @Override public void close() throws IOException { uri = null; + actualUri = null; notifyBytesRead(); try { closeCurrentSource(); @@ -298,6 +326,7 @@ public final class CacheDataSource implements DataSource { try { nextSpan = cache.startReadWrite(key, readPosition); } catch (InterruptedException e) { + Thread.currentThread().interrupt(); throw new InterruptedIOException(); } } else { @@ -347,7 +376,7 @@ public final class CacheDataSource implements DataSource { ? readPosition + MIN_READ_BEFORE_CHECKING_CACHE : Long.MAX_VALUE; if (checkCache) { - Assertions.checkState(currentDataSource == upstreamDataSource); + Assertions.checkState(isBypassingCache()); if (nextDataSource == upstreamDataSource) { // Continue reading from upstream. return; @@ -370,9 +399,38 @@ public final class CacheDataSource implements DataSource { currentDataSource = nextDataSource; currentDataSpecLengthUnset = nextDataSpec.length == C.LENGTH_UNSET; long resolvedLength = nextDataSource.open(nextDataSpec); + + // Update bytesRemaining, actualUri and (if writing to cache) the cache metadata. + ContentMetadataMutations mutations = new ContentMetadataMutations(); if (currentDataSpecLengthUnset && resolvedLength != C.LENGTH_UNSET) { - setBytesRemaining(resolvedLength); + bytesRemaining = resolvedLength; + ContentMetadataInternal.setContentLength(mutations, readPosition + bytesRemaining); } + 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); + } + } + + 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 ? defaultUri : redirectedUri; } private static boolean isCausedByPositionOutOfRange(IOException e) { @@ -389,11 +447,16 @@ public final class CacheDataSource implements DataSource { return false; } - private void setBytesRemaining(long bytesRemaining) throws IOException { - this.bytesRemaining = bytesRemaining; - if (isWritingToCache()) { - cache.setContentLength(key, readPosition + bytesRemaining); - } + private boolean isReadingFromUpstream() { + return !isReadingFromCache(); + } + + private boolean isBypassingCache() { + return currentDataSource == upstreamDataSource; + } + + private boolean isReadingFromCache() { + return currentDataSource == cacheReadDataSource; } private boolean isWritingToCache() { @@ -417,11 +480,27 @@ public final class CacheDataSource implements DataSource { } private void handleBeforeThrow(IOException exception) { - if (currentDataSource == cacheReadDataSource || exception instanceof CacheException) { + if (isReadingFromCache() || exception instanceof CacheException) { seenCacheError = true; } } + 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/CachedContent.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContent.java index da7b4bfd60..7b0b459dd9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContent.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContent.java @@ -15,7 +15,6 @@ */ package com.google.android.exoplayer2.upstream.cache; -import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.upstream.cache.Cache.CacheException; import com.google.android.exoplayer2.util.Assertions; import java.io.DataInputStream; @@ -28,25 +27,41 @@ import java.util.TreeSet; */ /*package*/ final class CachedContent { + private static final int VERSION_METADATA_INTRODUCED = 2; + private static final int VERSION_MAX = Integer.MAX_VALUE; + /** The cache file id that uniquely identifies the original stream. */ public final int id; /** The cache key that uniquely identifies the original stream. */ public final String key; /** The cached spans of this content. */ private final TreeSet cachedSpans; - /** The length of the original stream, or {@link C#LENGTH_UNSET} if the length is unknown. */ - private long length; + /** Metadata values. */ + private DefaultContentMetadata metadata; /** Whether the content is locked. */ private boolean locked; /** * Reads an instance from a {@link DataInputStream}. * + * @param version Version of the encoded data. * @param input Input stream containing values needed to initialize CachedContent instance. * @throws IOException If an error occurs during reading values. */ - public CachedContent(DataInputStream input) throws IOException { - this(input.readInt(), input.readUTF(), input.readLong()); + public static CachedContent readFromStream(int version, DataInputStream input) + throws IOException { + int id = input.readInt(); + String key = input.readUTF(); + CachedContent cachedContent = new CachedContent(id, key); + if (version < VERSION_METADATA_INTRODUCED) { + long length = input.readLong(); + ContentMetadataMutations mutations = new ContentMetadataMutations(); + ContentMetadataInternal.setContentLength(mutations, length); + cachedContent.applyMetadataMutations(mutations); + } else { + cachedContent.metadata = DefaultContentMetadata.readFromStream(input); + } + return cachedContent; } /** @@ -54,12 +69,11 @@ import java.util.TreeSet; * * @param id The cache file id. * @param key The cache stream key. - * @param length The length of the original stream. */ - public CachedContent(int id, String key, long length) { + public CachedContent(int id, String key) { this.id = id; this.key = key; - this.length = length; + this.metadata = DefaultContentMetadata.EMPTY; this.cachedSpans = new TreeSet<>(); } @@ -72,17 +86,23 @@ import java.util.TreeSet; public void writeToStream(DataOutputStream output) throws IOException { output.writeInt(id); output.writeUTF(key); - output.writeLong(length); + metadata.writeToStream(output); } - /** Returns the length of the content. */ - public long getLength() { - return length; + /** Returns the metadata. */ + public ContentMetadata getMetadata() { + return metadata; } - /** Sets the length of the content. */ - public void setLength(long length) { - this.length = length; + /** + * Applies {@code mutations} to the metadata. + * + * @return Whether {@code mutations} changed any metadata. + */ + public boolean applyMetadataMutations(ContentMetadataMutations mutations) { + DefaultContentMetadata oldMetadata = metadata; + metadata = metadata.copyWithMutationsApplied(mutations); + return !metadata.equals(oldMetadata); } /** Returns whether the content is locked. */ @@ -192,12 +212,41 @@ import java.util.TreeSet; return false; } - /** Calculates a hash code for the header of this {@code CachedContent}. */ - public int headerHashCode() { + /** + * Calculates a hash code for the header of this {@code CachedContent} which is compatible with + * the index file with {@code version}. + */ + public int headerHashCode(int version) { int result = id; result = 31 * result + key.hashCode(); - result = 31 * result + (int) (length ^ (length >>> 32)); + if (version < VERSION_METADATA_INTRODUCED) { + long length = ContentMetadataInternal.getContentLength(metadata); + result = 31 * result + (int) (length ^ (length >>> 32)); + } else { + result = 31 * result + metadata.hashCode(); + } return result; } + @Override + public int hashCode() { + int result = headerHashCode(VERSION_MAX); + result = 31 * result + cachedSpans.hashCode(); + return result; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + CachedContent that = (CachedContent) o; + return id == that.id + && key.equals(that.key) + && cachedSpans.equals(that.cachedSpans) + && metadata.equals(that.metadata); + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java index bd97ea8880..7b5fd2c598 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java @@ -17,7 +17,6 @@ package com.google.android.exoplayer2.upstream.cache; import android.util.Log; import android.util.SparseArray; -import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.upstream.cache.Cache.CacheException; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.AtomicFile; @@ -45,14 +44,12 @@ import javax.crypto.NoSuchPaddingException; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; -/** - * This class maintains the index of cached content. - */ +/** Maintains the index of cached content. */ /*package*/ class CachedContentIndex { public static final String FILE_NAME = "cached_content_index.exi"; - private static final int VERSION = 1; + private static final int VERSION = 2; private static final int FLAG_ENCRYPTED_INDEX = 1; @@ -141,10 +138,7 @@ import javax.crypto.spec.SecretKeySpec; */ public CachedContent getOrAdd(String key) { CachedContent cachedContent = keyToContent.get(key); - if (cachedContent == null) { - cachedContent = addNew(key, C.LENGTH_UNSET); - } - return cachedContent; + return cachedContent == null ? addNew(key) : cachedContent; } /** Returns a CachedContent instance with the given key or null if there isn't one. */ @@ -203,28 +197,20 @@ import javax.crypto.spec.SecretKeySpec; } /** - * Sets the content length for the given key. A new {@link CachedContent} is added if there isn't - * one already with the given key. + * Applies {@code mutations} to the {@link ContentMetadata} for the given key. A new {@link + * CachedContent} is added if there isn't one already with the given key. */ - public void setContentLength(String key, long length) { - CachedContent cachedContent = get(key); - if (cachedContent != null) { - if (cachedContent.getLength() != length) { - cachedContent.setLength(length); - changed = true; - } - } else { - addNew(key, length); + public void applyContentMetadataMutations(String key, ContentMetadataMutations mutations) { + CachedContent cachedContent = getOrAdd(key); + if (cachedContent.applyMetadataMutations(mutations)) { + changed = true; } } - /** - * Returns the content length for the given key if one set, or {@link - * com.google.android.exoplayer2.C#LENGTH_UNSET} otherwise. - */ - public long getContentLength(String key) { + /** Returns a {@link ContentMetadata} for the given key. */ + public ContentMetadata getContentMetadata(String key) { CachedContent cachedContent = get(key); - return cachedContent == null ? C.LENGTH_UNSET : cachedContent.getLength(); + return cachedContent != null ? cachedContent.getMetadata() : DefaultContentMetadata.EMPTY; } private boolean readFile() { @@ -233,8 +219,7 @@ import javax.crypto.spec.SecretKeySpec; InputStream inputStream = new BufferedInputStream(atomicFile.openRead()); input = new DataInputStream(inputStream); int version = input.readInt(); - if (version != VERSION) { - // Currently there is no other version + if (version < 0 || version > VERSION) { return false; } @@ -259,9 +244,9 @@ import javax.crypto.spec.SecretKeySpec; int count = input.readInt(); int hashCode = 0; for (int i = 0; i < count; i++) { - CachedContent cachedContent = new CachedContent(input); + CachedContent cachedContent = CachedContent.readFromStream(version, input); add(cachedContent); - hashCode += cachedContent.headerHashCode(); + hashCode += cachedContent.headerHashCode(version); } if (input.readInt() != hashCode) { return false; @@ -312,7 +297,7 @@ import javax.crypto.spec.SecretKeySpec; int hashCode = 0; for (CachedContent cachedContent : keyToContent.values()) { cachedContent.writeToStream(output); - hashCode += cachedContent.headerHashCode(); + hashCode += cachedContent.headerHashCode(VERSION); } output.writeInt(hashCode); atomicFile.endWrite(output); @@ -326,24 +311,19 @@ import javax.crypto.spec.SecretKeySpec; } } + private CachedContent addNew(String key) { + int id = getNewId(idToKey); + CachedContent cachedContent = new CachedContent(id, key); + add(cachedContent); + changed = true; + return cachedContent; + } + private void add(CachedContent cachedContent) { keyToContent.put(cachedContent.key, cachedContent); idToKey.put(cachedContent.id, cachedContent.key); } - /** Adds the given CachedContent to the index. */ - /*package*/ void addNew(CachedContent cachedContent) { - add(cachedContent); - changed = true; - } - - private CachedContent addNew(String key, long length) { - int id = getNewId(idToKey); - CachedContent cachedContent = new CachedContent(id, key, length); - addNew(cachedContent); - return cachedContent; - } - private static Cipher getCipher() throws NoSuchPaddingException, NoSuchAlgorithmException { // Workaround for https://issuetracker.google.com/issues/36976726 if (Util.SDK_INT == 18) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/ContentMetadata.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/ContentMetadata.java new file mode 100644 index 0000000000..aacd11f915 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/ContentMetadata.java @@ -0,0 +1,58 @@ +/* + * 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.upstream.cache; + +/** + * Interface for an immutable snapshot of keyed metadata. + * + *

    Internal metadata names are prefixed with {@value #INTERNAL_METADATA_NAME_PREFIX}. Custom + * metadata names should avoid this prefix to prevent clashes. + */ +public interface ContentMetadata { + + /** Prefix of internal metadata names. */ + String INTERNAL_METADATA_NAME_PREFIX = "exo_"; + + /** + * Returns a metadata value. + * + * @param name Name of the metadata to be returned. + * @param defaultValue Value to return if the metadata doesn't exist. + * @return The metadata value. + */ + byte[] get(String name, byte[] defaultValue); + + /** + * Returns a metadata value. + * + * @param name Name of the metadata to be returned. + * @param defaultValue Value to return if the metadata doesn't exist. + * @return The metadata value. + */ + String get(String name, String defaultValue); + + /** + * Returns a metadata value. + * + * @param name Name of the metadata to be returned. + * @param defaultValue Value to return if the metadata doesn't exist. + * @return The metadata value. + */ + long get(String name, long defaultValue); + + /** Returns whether the metadata is available. */ + boolean contains(String name); +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/ContentMetadataInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/ContentMetadataInternal.java new file mode 100644 index 0000000000..3376dd6944 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/ContentMetadataInternal.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.upstream.cache; + +import android.net.Uri; +import android.support.annotation.Nullable; +import com.google.android.exoplayer2.C; + +/** Helper classes to easily access and modify internal metadata values. */ +/*package*/ final class ContentMetadataInternal { + + private static final String PREFIX = ContentMetadata.INTERNAL_METADATA_NAME_PREFIX; + private static final String METADATA_NAME_REDIRECTED_URI = PREFIX + "redir"; + private static final String METADATA_NAME_CONTENT_LENGTH = PREFIX + "len"; + + /** Returns the content length metadata, or {@link C#LENGTH_UNSET} if not set. */ + public static long getContentLength(ContentMetadata contentMetadata) { + return contentMetadata.get(METADATA_NAME_CONTENT_LENGTH, C.LENGTH_UNSET); + } + + /** Adds a mutation to set content length metadata value. */ + public static void setContentLength(ContentMetadataMutations mutations, long length) { + mutations.set(METADATA_NAME_CONTENT_LENGTH, length); + } + + /** Adds a mutation to remove content length metadata value. */ + public static void removeContentLength(ContentMetadataMutations mutations) { + mutations.remove(METADATA_NAME_CONTENT_LENGTH); + } + + /** Returns the redirected uri metadata, or {@code null} if not set. */ + public @Nullable static Uri getRedirectedUri(ContentMetadata contentMetadata) { + String redirectedUri = contentMetadata.get(METADATA_NAME_REDIRECTED_URI, (String) null); + return redirectedUri == null ? null : Uri.parse(redirectedUri); + } + + /** + * Adds a mutation to set redirected uri metadata value. Passing {@code null} as {@code uri} isn't + * allowed. + */ + public static void setRedirectedUri(ContentMetadataMutations mutations, Uri uri) { + mutations.set(METADATA_NAME_REDIRECTED_URI, uri.toString()); + } + + /** Adds a mutation to remove redirected uri metadata value. */ + public static void removeRedirectedUri(ContentMetadataMutations mutations) { + mutations.remove(METADATA_NAME_REDIRECTED_URI); + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/ContentMetadataMutations.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/ContentMetadataMutations.java new file mode 100644 index 0000000000..70154b0308 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/ContentMetadataMutations.java @@ -0,0 +1,112 @@ +/* + * 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.upstream.cache; + +import com.google.android.exoplayer2.util.Assertions; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +/** + * Defines multiple mutations on metadata value which are applied atomically. This class isn't + * thread safe. + */ +public class ContentMetadataMutations { + + private final Map editedValues; + private final List removedValues; + + /** Constructs a DefaultMetadataMutations. */ + public ContentMetadataMutations() { + editedValues = new HashMap<>(); + removedValues = new ArrayList<>(); + } + + /** + * Adds a mutation to set a metadata value. Passing {@code null} as {@code name} or {@code value} + * isn't allowed. + * + * @param name The name of the metadata value. + * @param value The value to be set. + * @return This Editor instance, for convenience. + */ + public ContentMetadataMutations set(String name, String value) { + return checkAndSet(name, value); + } + + /** + * Adds a mutation to set a metadata value. Passing {@code null} as {@code name} isn't allowed. + * + * @param name The name of the metadata value. + * @param value The value to be set. + * @return This Editor instance, for convenience. + */ + public ContentMetadataMutations set(String name, long value) { + return checkAndSet(name, value); + } + + /** + * Adds a mutation to set a metadata value. Passing {@code null} as {@code name} or {@code value} + * isn't allowed. + * + * @param name The name of the metadata value. + * @param value The value to be set. + * @return This Editor instance, for convenience. + */ + public ContentMetadataMutations set(String name, byte[] value) { + return checkAndSet(name, Arrays.copyOf(value, value.length)); + } + + /** + * Adds a mutation to remove a metadata value. + * + * @param name The name of the metadata value. + * @return This Editor instance, for convenience. + */ + public ContentMetadataMutations remove(String name) { + removedValues.add(name); + editedValues.remove(name); + return this; + } + + /** Returns a list of names of metadata values to be removed. */ + public List getRemovedValues() { + return Collections.unmodifiableList(new ArrayList<>(removedValues)); + } + + /** Returns a map of metadata name, value pairs to be set. Values are copied. */ + public Map getEditedValues() { + HashMap hashMap = new HashMap<>(editedValues); + for (Entry entry : hashMap.entrySet()) { + Object value = entry.getValue(); + if (value instanceof byte[]) { + byte[] bytes = (byte[]) value; + entry.setValue(Arrays.copyOf(bytes, bytes.length)); + } + } + return Collections.unmodifiableMap(hashMap); + } + + private ContentMetadataMutations checkAndSet(String name, Object value) { + editedValues.put(Assertions.checkNotNull(name), Assertions.checkNotNull(value)); + removedValues.remove(name); + return this; + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/DefaultContentMetadata.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/DefaultContentMetadata.java new file mode 100644 index 0000000000..b855befe00 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/DefaultContentMetadata.java @@ -0,0 +1,210 @@ +/* + * 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.upstream.cache; + +import com.google.android.exoplayer2.C; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +/** Default implementation of {@link ContentMetadata}. Values are stored as byte arrays. */ +public final class DefaultContentMetadata implements ContentMetadata { + + /** An empty DefaultContentMetadata. */ + public static final DefaultContentMetadata EMPTY = + new DefaultContentMetadata(Collections.emptyMap()); + + private static final int MAX_VALUE_LENGTH = 10 * 1024 * 1024; + private int hashCode; + + /** + * Deserializes a {@link DefaultContentMetadata} from the given input stream. + * + * @param input Input stream to read from. + * @return a {@link DefaultContentMetadata} instance. + * @throws IOException If an error occurs during reading from input. + */ + public static DefaultContentMetadata readFromStream(DataInputStream input) throws IOException { + int size = input.readInt(); + HashMap metadata = new HashMap<>(); + for (int i = 0; i < size; i++) { + String name = input.readUTF(); + int valueSize = input.readInt(); + if (valueSize < 0 || valueSize > MAX_VALUE_LENGTH) { + throw new IOException("Invalid value size: " + valueSize); + } + byte[] value = new byte[valueSize]; + input.readFully(value); + metadata.put(name, value); + } + return new DefaultContentMetadata(metadata); + } + + private final Map metadata; + + private DefaultContentMetadata(Map metadata) { + this.metadata = Collections.unmodifiableMap(metadata); + } + + /** + * Returns a copy {@link DefaultContentMetadata} with {@code mutations} applied. If {@code + * mutations} don't change anything, returns this instance. + */ + public DefaultContentMetadata copyWithMutationsApplied(ContentMetadataMutations mutations) { + Map mutatedMetadata = applyMutations(metadata, mutations); + if (isMetadataEqual(mutatedMetadata)) { + return this; + } + return new DefaultContentMetadata(mutatedMetadata); + } + + /** + * Serializes itself to a {@link DataOutputStream}. + * + * @param output Output stream to store the values. + * @throws IOException If an error occurs during writing values to output. + */ + public void writeToStream(DataOutputStream output) throws IOException { + output.writeInt(metadata.size()); + for (Entry entry : metadata.entrySet()) { + output.writeUTF(entry.getKey()); + byte[] value = entry.getValue(); + output.writeInt(value.length); + output.write(value); + } + } + + @Override + public final byte[] get(String name, byte[] defaultValue) { + if (metadata.containsKey(name)) { + byte[] bytes = metadata.get(name); + return Arrays.copyOf(bytes, bytes.length); + } else { + return defaultValue; + } + } + + @Override + public final String get(String name, String defaultValue) { + if (metadata.containsKey(name)) { + byte[] bytes = metadata.get(name); + return new String(bytes, Charset.forName(C.UTF8_NAME)); + } else { + return defaultValue; + } + } + + @Override + public final long get(String name, long defaultValue) { + if (metadata.containsKey(name)) { + byte[] bytes = metadata.get(name); + return ByteBuffer.wrap(bytes).getLong(); + } else { + return defaultValue; + } + } + + @Override + public final boolean contains(String name) { + return metadata.containsKey(name); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + return isMetadataEqual(((DefaultContentMetadata) o).metadata); + } + + private boolean isMetadataEqual(Map otherMetadata) { + if (metadata.size() != otherMetadata.size()) { + return false; + } + for (Entry entry : metadata.entrySet()) { + byte[] value = entry.getValue(); + byte[] otherValue = otherMetadata.get(entry.getKey()); + if (!Arrays.equals(value, otherValue)) { + return false; + } + } + return true; + } + + @Override + public int hashCode() { + if (hashCode == 0) { + int result = 0; + for (Entry entry : metadata.entrySet()) { + result += entry.getKey().hashCode() ^ Arrays.hashCode(entry.getValue()); + } + hashCode = result; + } + return hashCode; + } + + private static Map applyMutations( + Map otherMetadata, ContentMetadataMutations mutations) { + HashMap metadata = new HashMap<>(otherMetadata); + removeValues(metadata, mutations.getRemovedValues()); + addValues(metadata, mutations.getEditedValues()); + return metadata; + } + + private static void removeValues(HashMap metadata, List names) { + for (int i = 0; i < names.size(); i++) { + metadata.remove(names.get(i)); + } + } + + private static void addValues(HashMap metadata, Map values) { + for (String name : values.keySet()) { + Object value = values.get(name); + byte[] bytes = getBytes(value); + if (bytes.length > MAX_VALUE_LENGTH) { + throw new IllegalArgumentException( + String.format( + "The size of %s (%d) is greater than maximum allowed: %d", + name, bytes.length, MAX_VALUE_LENGTH)); + } + metadata.put(name, bytes); + } + } + + private static byte[] getBytes(Object value) { + if (value instanceof Long) { + return ByteBuffer.allocate(8).putLong((Long) value).array(); + } else if (value instanceof String) { + return ((String) value).getBytes(Charset.forName(C.UTF8_NAME)); + } else if (value instanceof byte[]) { + return (byte[]) value; + } else { + throw new IllegalArgumentException(); + } + } + +} 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 f2679fee0c..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 @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.upstream.cache; import android.os.ConditionVariable; +import android.support.annotation.NonNull; import android.util.Log; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.Assertions; @@ -28,17 +29,48 @@ import java.util.Set; import java.util.TreeSet; /** - * A {@link Cache} implementation that maintains an in-memory representation. + * A {@link Cache} implementation that maintains an in-memory representation. Note, only one + * instance of SimpleCache is allowed for a given directory at a given time. */ 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; private final HashMap> listeners; - private long totalSpace = 0; + + 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 @@ -88,10 +120,15 @@ 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; this.listeners = new HashMap<>(); + // Start cache initialization. final ConditionVariable conditionVariable = new ConditionVariable(); new Thread("SimpleCache.initialize()") { @@ -107,8 +144,23 @@ public final class SimpleCache implements Cache { conditionVariable.block(); } + @Override + public synchronized void release() throws CacheException { + if (released) { + return; + } + listeners.clear(); + try { + removeStaleSpansAndCachedContents(); + } finally { + unlockFolder(cacheDir); + released = true; + } + } + @Override public synchronized NavigableSet addListener(String key, Listener listener) { + Assertions.checkState(!released); ArrayList listenersForKey = listeners.get(key); if (listenersForKey == null) { listenersForKey = new ArrayList<>(); @@ -120,6 +172,9 @@ public final class SimpleCache implements Cache { @Override public synchronized void removeListener(String key, Listener listener) { + if (released) { + return; + } ArrayList listenersForKey = listeners.get(key); if (listenersForKey != null) { listenersForKey.remove(listener); @@ -129,8 +184,10 @@ public final class SimpleCache implements Cache { } } + @NonNull @Override public synchronized NavigableSet getCachedSpans(String key) { + Assertions.checkState(!released); CachedContent cachedContent = index.get(key); return cachedContent == null || cachedContent.isEmpty() ? new TreeSet() @@ -139,11 +196,13 @@ public final class SimpleCache implements Cache { @Override public synchronized Set getKeys() { + Assertions.checkState(!released); return new HashSet<>(index.getKeys()); } @Override public synchronized long getCacheSpace() { + Assertions.checkState(!released); return totalSpace; } @@ -167,6 +226,7 @@ public final class SimpleCache implements Cache { @Override public synchronized SimpleCacheSpan startReadWriteNonBlocking(String key, long position) throws CacheException { + Assertions.checkState(!released); SimpleCacheSpan cacheSpan = getSpan(key, position); // Read case. @@ -191,13 +251,14 @@ public final class SimpleCache implements Cache { @Override public synchronized File startFile(String key, long position, long maxLength) throws CacheException { + Assertions.checkState(!released); CachedContent cachedContent = index.get(key); Assertions.checkNotNull(cachedContent); Assertions.checkState(cachedContent.isLocked()); if (!cacheDir.exists()) { // For some reason the cache directory doesn't exist. Make a best effort to create it. - removeStaleSpansAndCachedContents(); cacheDir.mkdirs(); + removeStaleSpansAndCachedContents(); } evictor.onStartFile(this, key, position, maxLength); return SimpleCacheSpan.getCacheFile( @@ -206,6 +267,7 @@ public final class SimpleCache implements Cache { @Override public synchronized void commitFile(File file) throws CacheException { + Assertions.checkState(!released); SimpleCacheSpan span = SimpleCacheSpan.createCacheEntry(file, index); Assertions.checkState(span != null); CachedContent cachedContent = index.get(span.key); @@ -221,7 +283,7 @@ public final class SimpleCache implements Cache { return; } // Check if the span conflicts with the set content length - Long length = cachedContent.getLength(); + long length = ContentMetadataInternal.getContentLength(cachedContent.getMetadata()); if (length != C.LENGTH_UNSET) { Assertions.checkState((span.position + span.length) <= length); } @@ -232,6 +294,7 @@ public final class SimpleCache implements Cache { @Override public synchronized void releaseHoleSpan(CacheSpan holeSpan) { + Assertions.checkState(!released); CachedContent cachedContent = index.get(holeSpan.key); Assertions.checkNotNull(cachedContent); Assertions.checkState(cachedContent.isLocked()); @@ -240,6 +303,52 @@ public final class SimpleCache implements Cache { notifyAll(); } + @Override + public synchronized void removeSpan(CacheSpan span) throws CacheException { + Assertions.checkState(!released); + removeSpan(span, true); + } + + @Override + public synchronized boolean isCached(String key, long position, long length) { + Assertions.checkState(!released); + CachedContent cachedContent = index.get(key); + return cachedContent != null && cachedContent.getCachedBytesLength(position, length) >= length; + } + + @Override + public synchronized long getCachedLength(String key, long position, long length) { + Assertions.checkState(!released); + CachedContent cachedContent = index.get(key); + return cachedContent != null ? cachedContent.getCachedBytesLength(position, length) : -length; + } + + @Override + public synchronized void setContentLength(String key, long length) throws CacheException { + ContentMetadataMutations mutations = new ContentMetadataMutations(); + ContentMetadataInternal.setContentLength(mutations, length); + applyContentMetadataMutations(key, mutations); + } + + @Override + public synchronized long getContentLength(String key) { + return ContentMetadataInternal.getContentLength(getContentMetadata(key)); + } + + @Override + public synchronized void applyContentMetadataMutations( + String key, ContentMetadataMutations mutations) throws CacheException { + Assertions.checkState(!released); + index.applyContentMetadataMutations(key, mutations); + index.store(); + } + + @Override + public synchronized ContentMetadata getContentMetadata(String key) { + Assertions.checkState(!released); + return index.getContentMetadata(key); + } + /** * Returns the cache {@link SimpleCacheSpan} corresponding to the provided lookup {@link * SimpleCacheSpan}. @@ -270,9 +379,7 @@ public final class SimpleCache implements Cache { } } - /** - * Ensures that the cache's in-memory representation has been initialized. - */ + /** Ensures that the cache's in-memory representation has been initialized. */ private void initialize() { if (!cacheDir.exists()) { cacheDir.mkdirs(); @@ -289,8 +396,8 @@ public final class SimpleCache implements Cache { if (file.getName().equals(CachedContentIndex.FILE_NAME)) { continue; } - SimpleCacheSpan span = file.length() > 0 - ? SimpleCacheSpan.createCacheEntry(file, index) : null; + SimpleCacheSpan span = + file.length() > 0 ? SimpleCacheSpan.createCacheEntry(file, index) : null; if (span != null) { addSpan(span); } else { @@ -333,14 +440,9 @@ public final class SimpleCache implements Cache { } } - @Override - public synchronized void removeSpan(CacheSpan span) throws CacheException { - removeSpan(span, true); - } - /** - * Scans all of the cached spans in the in-memory representation, removing any for which files - * no longer exist. + * Scans all of the cached spans in the in-memory representation, removing any for which files no + * longer exist. */ private void removeStaleSpansAndCachedContents() throws CacheException { ArrayList spansToBeRemoved = new ArrayList<>(); @@ -389,27 +491,16 @@ public final class SimpleCache implements Cache { evictor.onSpanTouched(this, oldSpan, newSpan); } - @Override - public synchronized boolean isCached(String key, long position, long length) { - CachedContent cachedContent = index.get(key); - return cachedContent != null && cachedContent.getCachedBytesLength(position, length) >= length; + private static synchronized boolean lockFolder(File cacheDir) { + if (cacheFolderLockingDisabled) { + return true; + } + return lockedCacheDirs.add(cacheDir.getAbsoluteFile()); } - @Override - public synchronized long getCachedLength(String key, long position, long length) { - CachedContent cachedContent = index.get(key); - return cachedContent != null ? cachedContent.getCachedBytesLength(position, length) : -length; + private static synchronized void unlockFolder(File cacheDir) { + if (!cacheFolderLockingDisabled) { + lockedCacheDirs.remove(cacheDir.getAbsoluteFile()); + } } - - @Override - public synchronized void setContentLength(String key, long length) throws CacheException { - index.setContentLength(key, length); - index.store(); - } - - @Override - public synchronized long getContentLength(String key) { - return index.getContentLength(key); - } - } 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 d95f387996..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,7 +15,9 @@ */ package com.google.android.exoplayer2.util; +import android.net.NetworkInfo; import android.os.SystemClock; +import android.support.annotation.Nullable; import android.util.Log; import android.view.Surface; import com.google.android.exoplayer2.C; @@ -25,43 +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.DefaultDrmSessionManager; import com.google.android.exoplayer2.metadata.Metadata; -import com.google.android.exoplayer2.metadata.MetadataOutput; -import com.google.android.exoplayer2.metadata.emsg.EventMessage; -import com.google.android.exoplayer2.metadata.id3.ApicFrame; -import com.google.android.exoplayer2.metadata.id3.CommentFrame; -import com.google.android.exoplayer2.metadata.id3.GeobFrame; -import com.google.android.exoplayer2.metadata.id3.Id3Frame; -import com.google.android.exoplayer2.metadata.id3.PrivFrame; -import com.google.android.exoplayer2.metadata.id3.TextInformationFrame; -import com.google.android.exoplayer2.metadata.id3.UrlLinkFrame; -import com.google.android.exoplayer2.metadata.scte35.SpliceCommand; -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.upstream.DataSpec; -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, - DefaultDrmSessionManager.EventListener { +public class EventLogger implements AnalyticsListener { private static final String TAG = "EventLogger"; private static final int MAX_TIMELINE_ITEM_LINES = 3; @@ -73,378 +55,424 @@ 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) { - Log.d(TAG, "loading [" + isLoading + "]"); + public void onLoadingChanged(EventTime eventTime, boolean isLoading) { + logd(eventTime, "loading", Boolean.toString(isLoading)); } @Override - public void onPlayerStateChanged(boolean playWhenReady, int state) { - Log.d(TAG, "state [" + getSessionTimeString() + ", " + playWhenReady + ", " - + getStateString(state) + "]"); + public void onPlayerStateChanged(EventTime eventTime, boolean playWhenReady, int state) { + logd(eventTime, "state", playWhenReady + ", " + getStateString(state)); } @Override - public void onRepeatModeChanged(@Player.RepeatMode int repeatMode) { - Log.d(TAG, "repeatMode [" + getRepeatModeString(repeatMode) + "]"); + public void onRepeatModeChanged(EventTime eventTime, @Player.RepeatMode int repeatMode) { + logd(eventTime, "repeatMode", getRepeatModeString(repeatMode)); } @Override - public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { - Log.d(TAG, "shuffleModeEnabled [" + shuffleModeEnabled + "]"); + public void onShuffleModeChanged(EventTime eventTime, boolean shuffleModeEnabled) { + logd(eventTime, "shuffleModeEnabled", Boolean.toString(shuffleModeEnabled)); } @Override - public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { - Log.d(TAG, "positionDiscontinuity [" + getDiscontinuityReasonString(reason) + "]"); + public void onPositionDiscontinuity(EventTime eventTime, @Player.DiscontinuityReason int reason) { + logd(eventTime, "positionDiscontinuity", getDiscontinuityReasonString(reason)); } @Override - public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { - Log.d(TAG, "playbackParameters " + String.format( - "[speed=%.2f, pitch=%.2f]", playbackParameters.speed, playbackParameters.pitch)); + public void onSeekStarted(EventTime eventTime) { + logd(eventTime, "seekStarted"); } @Override - public void onTimelineChanged(Timeline timeline, Object manifest, - @Player.TimelineChangeReason int reason) { - int periodCount = timeline.getPeriodCount(); - int windowCount = timeline.getWindowCount(); - Log.d(TAG, "timelineChanged [periodCount=" + periodCount + ", windowCount=" + windowCount - + ", reason=" + getTimelineChangeReasonString(reason)); + public void onPlaybackParametersChanged( + EventTime eventTime, PlaybackParameters playbackParameters) { + logd( + eventTime, + "playbackParameters", + Util.formatInvariant( + "speed=%.2f, pitch=%.2f, skipSilence=%s", + playbackParameters.speed, playbackParameters.pitch, playbackParameters.skipSilence)); + } + + @Override + public void onTimelineChanged(EventTime eventTime, @Player.TimelineChangeReason int reason) { + int periodCount = eventTime.timeline.getPeriodCount(); + int windowCount = eventTime.timeline.getWindowCount(); + logd( + "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); - Log.d(TAG, " " + "period [" + getTimeString(period.getDurationMs()) + "]"); + eventTime.timeline.getPeriod(i, period); + logd(" " + "period [" + getTimeString(period.getDurationMs()) + "]"); } if (periodCount > MAX_TIMELINE_ITEM_LINES) { - Log.d(TAG, " ..."); + logd(" ..."); } for (int i = 0; i < Math.min(windowCount, MAX_TIMELINE_ITEM_LINES); i++) { - timeline.getWindow(i, window); - Log.d(TAG, " " + "window [" + getTimeString(window.getDurationMs()) + ", " - + window.isSeekable + ", " + window.isDynamic + "]"); + eventTime.timeline.getWindow(i, window); + logd( + " " + + "window [" + + getTimeString(window.getDurationMs()) + + ", " + + window.isSeekable + + ", " + + window.isDynamic + + "]"); } if (windowCount > MAX_TIMELINE_ITEM_LINES) { - Log.d(TAG, " ..."); + logd(" ..."); } - Log.d(TAG, "]"); + logd("]"); } @Override - public void onPlayerError(ExoPlaybackException e) { - Log.e(TAG, "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) { - Log.d(TAG, "Tracks []"); + logd(eventTime, "tracksChanged", "[]"); return; } - Log.d(TAG, "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) { - Log.d(TAG, " Renderer:" + rendererIndex + " ["); + logd(" Renderer:" + rendererIndex + " ["); for (int groupIndex = 0; groupIndex < rendererTrackGroups.length; groupIndex++) { TrackGroup trackGroup = rendererTrackGroups.get(groupIndex); - String adaptiveSupport = getAdaptiveSupportString(trackGroup.length, - mappedTrackInfo.getAdaptiveSupport(rendererIndex, groupIndex, false)); - Log.d(TAG, " Group:" + groupIndex + ", adaptive_supported=" + adaptiveSupport + " ["); + String adaptiveSupport = + getAdaptiveSupportString( + trackGroup.length, + mappedTrackInfo.getAdaptiveSupport(rendererIndex, groupIndex, false)); + logd(" Group:" + groupIndex + ", adaptive_supported=" + adaptiveSupport + " ["); for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { String status = getTrackStatusString(trackSelection, trackGroup, trackIndex); - String formatSupport = getFormatSupportString( - mappedTrackInfo.getTrackFormatSupport(rendererIndex, groupIndex, trackIndex)); - Log.d(TAG, " " + status + " Track:" + trackIndex + ", " - + Format.toLogString(trackGroup.getFormat(trackIndex)) - + ", supported=" + formatSupport); + String formatSupport = + getFormatSupportString( + mappedTrackInfo.getTrackSupport(rendererIndex, groupIndex, trackIndex)); + logd( + " " + + status + + " Track:" + + trackIndex + + ", " + + Format.toLogString(trackGroup.getFormat(trackIndex)) + + ", supported=" + + formatSupport); } - Log.d(TAG, " ]"); + logd(" ]"); } // Log metadata for at most one of the tracks selected for the renderer. if (trackSelection != null) { for (int selectionIndex = 0; selectionIndex < trackSelection.length(); selectionIndex++) { Metadata metadata = trackSelection.getFormat(selectionIndex).metadata; if (metadata != null) { - Log.d(TAG, " Metadata ["); + logd(" Metadata ["); printMetadata(metadata, " "); - Log.d(TAG, " ]"); + logd(" ]"); break; } } } - Log.d(TAG, " ]"); + logd(" ]"); } } // Log tracks not associated with a renderer. - TrackGroupArray unassociatedTrackGroups = mappedTrackInfo.getUnassociatedTrackGroups(); + TrackGroupArray unassociatedTrackGroups = mappedTrackInfo.getUnmappedTrackGroups(); if (unassociatedTrackGroups.length > 0) { - Log.d(TAG, " Renderer:None ["); + logd(" Renderer:None ["); for (int groupIndex = 0; groupIndex < unassociatedTrackGroups.length; groupIndex++) { - Log.d(TAG, " Group:" + groupIndex + " ["); + logd(" Group:" + groupIndex + " ["); TrackGroup trackGroup = unassociatedTrackGroups.get(groupIndex); for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { String status = getTrackStatusString(false); - String formatSupport = getFormatSupportString( - RendererCapabilities.FORMAT_UNSUPPORTED_TYPE); - Log.d(TAG, " " + status + " Track:" + trackIndex + ", " - + Format.toLogString(trackGroup.getFormat(trackIndex)) - + ", supported=" + formatSupport); + String formatSupport = + getFormatSupportString(RendererCapabilities.FORMAT_UNSUPPORTED_TYPE); + logd( + " " + + status + + " Track:" + + trackIndex + + ", " + + Format.toLogString(trackGroup.getFormat(trackIndex)) + + ", supported=" + + formatSupport); } - Log.d(TAG, " ]"); + logd(" ]"); } - Log.d(TAG, " ]"); + logd(" ]"); } - Log.d(TAG, "]"); + logd("]"); } @Override - public void onSeekProcessed() { - Log.d(TAG, "seekProcessed"); + public void onSeekProcessed(EventTime eventTime) { + logd(eventTime, "seekProcessed"); } - // MetadataOutput - @Override - public void onMetadata(Metadata metadata) { - Log.d(TAG, "onMetadata ["); + public void onMetadata(EventTime eventTime, Metadata metadata) { + logd("metadata [" + getEventTimeString(eventTime) + ", "); printMetadata(metadata, " "); - Log.d(TAG, "]"); - } - - // AudioRendererEventListener - - @Override - public void onAudioEnabled(DecoderCounters counters) { - Log.d(TAG, "audioEnabled [" + getSessionTimeString() + "]"); + logd("]"); } @Override - public void onAudioSessionId(int audioSessionId) { - Log.d(TAG, "audioSessionId [" + audioSessionId + "]"); + public void onDecoderEnabled(EventTime eventTime, int trackType, DecoderCounters counters) { + logd(eventTime, "decoderEnabled", getTrackTypeString(trackType)); } @Override - public void onAudioDecoderInitialized(String decoderName, long elapsedRealtimeMs, - long initializationDurationMs) { - Log.d(TAG, "audioDecoderInitialized [" + getSessionTimeString() + ", " + decoderName + "]"); + public void onAudioSessionId(EventTime eventTime, int audioSessionId) { + logd(eventTime, "audioSessionId", Integer.toString(audioSessionId)); } @Override - public void onAudioInputFormatChanged(Format format) { - Log.d(TAG, "audioFormatChanged [" + getSessionTimeString() + ", " + Format.toLogString(format) - + "]"); + public void onDecoderInitialized( + EventTime eventTime, int trackType, String decoderName, long initializationDurationMs) { + logd(eventTime, "decoderInitialized", getTrackTypeString(trackType) + ", " + decoderName); } @Override - public void onAudioDisabled(DecoderCounters counters) { - Log.d(TAG, "audioDisabled [" + getSessionTimeString() + "]"); + public void onDecoderInputFormatChanged(EventTime eventTime, int trackType, Format format) { + logd( + eventTime, + "decoderInputFormatChanged", + getTrackTypeString(trackType) + ", " + Format.toLogString(format)); } @Override - public void onAudioSinkUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) { - printInternalError("audioTrackUnderrun [" + bufferSize + ", " + bufferSizeMs + ", " - + elapsedSinceLastFeedMs + "]", null); - } - - // VideoRendererEventListener - - @Override - public void onVideoEnabled(DecoderCounters counters) { - Log.d(TAG, "videoEnabled [" + getSessionTimeString() + "]"); + public void onDecoderDisabled(EventTime eventTime, int trackType, DecoderCounters counters) { + logd(eventTime, "decoderDisabled", getTrackTypeString(trackType)); } @Override - public void onVideoDecoderInitialized(String decoderName, long elapsedRealtimeMs, - long initializationDurationMs) { - Log.d(TAG, "videoDecoderInitialized [" + getSessionTimeString() + ", " + decoderName + "]"); + public void onAudioUnderrun( + EventTime eventTime, int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) { + loge( + eventTime, + "audioTrackUnderrun", + bufferSize + ", " + bufferSizeMs + ", " + elapsedSinceLastFeedMs + "]", + null); } @Override - public void onVideoInputFormatChanged(Format format) { - Log.d(TAG, "videoFormatChanged [" + getSessionTimeString() + ", " + Format.toLogString(format) - + "]"); + public void onDroppedVideoFrames(EventTime eventTime, int count, long elapsedMs) { + logd(eventTime, "droppedFrames", Integer.toString(count)); } @Override - public void onVideoDisabled(DecoderCounters counters) { - Log.d(TAG, "videoDisabled [" + getSessionTimeString() + "]"); - } - - @Override - public void onDroppedFrames(int count, long elapsed) { - Log.d(TAG, "droppedFrames [" + getSessionTimeString() + ", " + count + "]"); - } - - @Override - public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, + public void onVideoSizeChanged( + EventTime eventTime, + int width, + int height, + int unappliedRotationDegrees, float pixelWidthHeightRatio) { - Log.d(TAG, "videoSizeChanged [" + width + ", " + height + "]"); + logd(eventTime, "videoSizeChanged", width + ", " + height); } @Override - public void onRenderedFirstFrame(Surface surface) { - Log.d(TAG, "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() { - Log.d(TAG, "drmKeysRestored [" + getSessionTimeString() + "]"); + public void onMediaPeriodCreated(EventTime eventTime) { + logd(eventTime, "mediaPeriodCreated"); } @Override - public void onDrmKeysRemoved() { - Log.d(TAG, "drmKeysRemoved [" + getSessionTimeString() + "]"); + public void onMediaPeriodReleased(EventTime eventTime) { + logd(eventTime, "mediaPeriodReleased"); } - @Override - public void onDrmKeysLoaded() { - Log.d(TAG, "drmKeysLoaded [" + getSessionTimeString() + "]"); - } - - // MediaSourceEventListener - @Override public void onLoadStarted( - DataSpec dataSpec, - int dataType, - int trackType, - Format trackFormat, - int trackSelectionReason, - Object trackSelectionData, - long mediaStartTimeMs, - long mediaEndTimeMs, - long elapsedRealtimeMs) { + EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) { // Do nothing. } @Override - public void onLoadError(DataSpec dataSpec, int dataType, int trackType, Format trackFormat, - int trackSelectionReason, Object trackSelectionData, long mediaStartTimeMs, - long mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs, long bytesLoaded, - IOException error, boolean wasCanceled) { - printInternalError("loadError", error); + public void onLoadError( + EventTime eventTime, + LoadEventInfo loadEventInfo, + MediaLoadData mediaLoadData, + IOException error, + boolean wasCanceled) { + printInternalError(eventTime, "loadError", error); } @Override - public void onLoadCanceled(DataSpec dataSpec, int dataType, int trackType, Format trackFormat, - int trackSelectionReason, Object trackSelectionData, long mediaStartTimeMs, - long mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs, long bytesLoaded) { + public void onLoadCanceled( + EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) { // Do nothing. } @Override - public void onLoadCompleted(DataSpec dataSpec, int dataType, int trackType, Format trackFormat, - int trackSelectionReason, Object trackSelectionData, long mediaStartTimeMs, - long mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs, long bytesLoaded) { + public void onLoadCompleted( + EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) { // Do nothing. } @Override - public void onUpstreamDiscarded(int trackType, long mediaStartTimeMs, long mediaEndTimeMs) { + 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 onDownstreamFormatChanged(int trackType, Format trackFormat, int trackSelectionReason, - Object trackSelectionData, long mediaTimeMs) { - // Do nothing. - } - - // AdsMediaSource.EventListener - - @Override - public void onAdLoadError(IOException error) { - printInternalError("adLoadError", error); + public void onViewportSizeChange(EventTime eventTime, int width, int height) { + logd(eventTime, "viewportSizeChanged", width + ", " + height); } @Override - public void onInternalAdLoadError(RuntimeException error) { - printInternalError("internalAdLoadError", error); + public void onNetworkTypeChanged(EventTime eventTime, @Nullable NetworkInfo networkInfo) { + logd(eventTime, "networkTypeChanged", networkInfo == null ? "none" : networkInfo.toString()); } @Override - public void onAdClicked() { - // Do nothing. + public void onUpstreamDiscarded(EventTime eventTime, MediaLoadData mediaLoadData) { + logd(eventTime, "upstreamDiscarded", Format.toLogString(mediaLoadData.trackFormat)); } @Override - public void onAdTapped() { - // Do nothing. + public void onDownstreamFormatChanged(EventTime eventTime, MediaLoadData mediaLoadData) { + logd(eventTime, "downstreamFormatChanged", Format.toLogString(mediaLoadData.trackFormat)); + } + + @Override + 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"); + } + + /** + * Logs a debug message. + * + * @param msg The message to log. + */ + protected void logd(String msg) { + Log.d(TAG, msg); + } + + /** + * Logs an error message and exception. + * + * @param msg The message to log. + * @param tr The exception to log. + */ + protected void loge(String msg, Throwable tr) { + Log.e(TAG, msg, tr); } // Internal methods - private void printInternalError(String type, Exception e) { - Log.e(TAG, "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) { for (int i = 0; i < metadata.length(); i++) { - Metadata.Entry entry = metadata.get(i); - if (entry instanceof TextInformationFrame) { - TextInformationFrame textInformationFrame = (TextInformationFrame) entry; - Log.d(TAG, prefix + String.format("%s: value=%s", textInformationFrame.id, - textInformationFrame.value)); - } else if (entry instanceof UrlLinkFrame) { - UrlLinkFrame urlLinkFrame = (UrlLinkFrame) entry; - Log.d(TAG, prefix + String.format("%s: url=%s", urlLinkFrame.id, urlLinkFrame.url)); - } else if (entry instanceof PrivFrame) { - PrivFrame privFrame = (PrivFrame) entry; - Log.d(TAG, prefix + String.format("%s: owner=%s", privFrame.id, privFrame.owner)); - } else if (entry instanceof GeobFrame) { - GeobFrame geobFrame = (GeobFrame) entry; - Log.d(TAG, prefix + String.format("%s: mimeType=%s, filename=%s, description=%s", - geobFrame.id, geobFrame.mimeType, geobFrame.filename, geobFrame.description)); - } else if (entry instanceof ApicFrame) { - ApicFrame apicFrame = (ApicFrame) entry; - Log.d(TAG, prefix + String.format("%s: mimeType=%s, description=%s", - apicFrame.id, apicFrame.mimeType, apicFrame.description)); - } else if (entry instanceof CommentFrame) { - CommentFrame commentFrame = (CommentFrame) entry; - Log.d(TAG, prefix + String.format("%s: language=%s, description=%s", commentFrame.id, - commentFrame.language, commentFrame.description)); - } else if (entry instanceof Id3Frame) { - Id3Frame id3Frame = (Id3Frame) entry; - Log.d(TAG, prefix + String.format("%s", id3Frame.id)); - } else if (entry instanceof EventMessage) { - EventMessage eventMessage = (EventMessage) entry; - Log.d(TAG, prefix + String.format("EMSG: scheme=%s, id=%d, value=%s", - eventMessage.schemeIdUri, eventMessage.id, eventMessage.value)); - } else if (entry instanceof SpliceCommand) { - String description = - String.format("SCTE-35 splice command: type=%s.", entry.getClass().getSimpleName()); - Log.d(TAG, prefix + description); - } + logd(prefix + metadata.get(i)); } } - 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) { @@ -454,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 "?"; } @@ -555,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/Util.java b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java index e01dcc6f3a..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,14 +18,18 @@ 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; import android.graphics.Point; 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; @@ -127,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. @@ -160,7 +180,7 @@ public final class Util { */ public static boolean isLocalFileUri(Uri uri) { String scheme = uri.getScheme(); - return TextUtils.isEmpty(scheme) || scheme.equals("file"); + return TextUtils.isEmpty(scheme) || "file".equals(scheme); } /** @@ -171,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); } @@ -205,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. * @@ -251,6 +285,28 @@ public final class Util { } } + /** + * Reads an integer from a {@link Parcel} and interprets it as a boolean, with 0 mapping to false + * and all other values mapping to true. + * + * @param parcel The {@link Parcel} to read from. + * @return The read value. + */ + public static boolean readBoolean(Parcel parcel) { + return parcel.readInt() != 0; + } + + /** + * Writes a boolean to a {@link Parcel}. The boolean is written as an integer with value 1 (true) + * or 0 (false). + * + * @param parcel The {@link Parcel} to write to. + * @param value The value to write. + */ + public static void writeBoolean(Parcel parcel, boolean value) { + parcel.writeInt(value ? 1 : 0); + } + /** * Returns a normalized RFC 639-2/T code for {@code language}. * @@ -316,6 +372,15 @@ public final class Util { return text == null ? null : text.toUpperCase(Locale.US); } + /** + * Formats a string using {@link Locale#US}. + * + * @see String#format(String, Object...) + */ + public static String formatInvariant(String format, Object... args) { + return String.format(Locale.US, format, args); + } + /** * Divides a {@code numerator} by a {@code denominator}, returning the ceiled result. * @@ -639,7 +704,7 @@ public final class Util { } else { timezoneShift = ((Integer.parseInt(matcher.group(12)) * 60 + Integer.parseInt(matcher.group(13)))); - if (matcher.group(11).equals("-")) { + if ("-".equals(matcher.group(11))) { timezoneShift *= -1; } } @@ -951,6 +1016,20 @@ public final class Util { } } + /** + * Returns whether {@code encoding} is one of the PCM encodings. + * + * @param encoding The encoding of the audio data. + * @return Whether the encoding is one of the PCM encodings. + */ + public static boolean isEncodingPcm(@C.Encoding int encoding) { + return encoding == C.ENCODING_PCM_8BIT + || encoding == C.ENCODING_PCM_16BIT + || encoding == C.ENCODING_PCM_24BIT + || encoding == C.ENCODING_PCM_32BIT + || encoding == C.ENCODING_PCM_FLOAT; + } + /** * Returns whether {@code encoding} is high resolution (> 16-bit) integer PCM. * @@ -1089,6 +1168,20 @@ public final class Util { } } + /** + * Makes a best guess to infer the type from a {@link Uri}. + * + * @param uri The {@link Uri}. + * @param overrideExtension If not null, used to infer the type. + * @return The content type. + */ + @C.ContentType + public static int inferContentType(Uri uri, String overrideExtension) { + return TextUtils.isEmpty(overrideExtension) + ? inferContentType(uri) + : inferContentType("." + overrideExtension); + } + /** * Makes a best guess to infer the type from a {@link Uri}. * diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/ColorInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/video/ColorInfo.java index 14e40f8605..a983a0a6a3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/ColorInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/ColorInfo.java @@ -19,6 +19,8 @@ import android.os.Parcel; import android.os.Parcelable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.util.Util; + import java.util.Arrays; /** @@ -77,7 +79,7 @@ public final class ColorInfo implements Parcelable { colorSpace = in.readInt(); colorRange = in.readInt(); colorTransfer = in.readInt(); - boolean hasHdrStaticInfo = in.readInt() != 0; + boolean hasHdrStaticInfo = Util.readBoolean(in); hdrStaticInfo = hasHdrStaticInfo ? in.createByteArray() : null; } @@ -126,7 +128,7 @@ public final class ColorInfo implements Parcelable { dest.writeInt(colorSpace); dest.writeInt(colorRange); dest.writeInt(colorTransfer); - dest.writeInt(hdrStaticInfo != null ? 1 : 0); + Util.writeBoolean(dest, hdrStaticInfo != null); if (hdrStaticInfo != null) { dest.writeByteArray(hdrStaticInfo); } 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 1519bfdd2b..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 @@ -44,6 +44,7 @@ import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer; import com.google.android.exoplayer2.mediacodec.MediaCodecSelector; import com.google.android.exoplayer2.mediacodec.MediaCodecUtil; import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException; +import com.google.android.exoplayer2.mediacodec.MediaFormatUtil; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.TraceUtil; @@ -90,8 +91,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { private final int maxDroppedFramesToNotify; private final boolean deviceNeedsAutoFrcWorkaround; private final long[] pendingOutputStreamOffsetsUs; + private final long[] pendingOutputStreamSwitchTimesUs; - private Format[] streamFormats; private CodecMaxValues codecMaxValues; private boolean codecNeedsSetOutputSurfaceWorkaround; @@ -100,6 +101,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { @C.VideoScalingMode private int scalingMode; private boolean renderedFirstFrame; + private long initialPositionUs; private long joiningDeadlineMs; private long droppedFrameAccumulationStartTimeMs; private int droppedFrames; @@ -122,6 +124,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { private int tunnelingAudioSessionId; /* package */ OnFrameRenderedListenerV23 tunnelingOnFrameRenderedListener; + private long lastInputTimeUs; private long outputStreamOffsetUs; private int pendingOutputStreamOffsetCount; @@ -141,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); } /** @@ -158,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); } /** @@ -193,7 +209,9 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { eventDispatcher = new EventDispatcher(eventHandler, eventListener); deviceNeedsAutoFrcWorkaround = deviceNeedsAutoFrcWorkaround(); pendingOutputStreamOffsetsUs = new long[MAX_PENDING_OUTPUT_STREAM_OFFSET_COUNT]; + pendingOutputStreamSwitchTimesUs = new long[MAX_PENDING_OUTPUT_STREAM_OFFSET_COUNT]; outputStreamOffsetUs = C.TIME_UNSET; + lastInputTimeUs = C.TIME_UNSET; joiningDeadlineMs = C.TIME_UNSET; currentWidth = Format.NO_VALUE; currentHeight = Format.NO_VALUE; @@ -258,7 +276,6 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { @Override protected void onStreamChanged(Format[] formats, long offsetUs) throws ExoPlaybackException { - streamFormats = formats; if (outputStreamOffsetUs == C.TIME_UNSET) { outputStreamOffsetUs = offsetUs; } else { @@ -269,6 +286,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { pendingOutputStreamOffsetCount++; } pendingOutputStreamOffsetsUs[pendingOutputStreamOffsetCount - 1] = offsetUs; + pendingOutputStreamSwitchTimesUs[pendingOutputStreamOffsetCount - 1] = lastInputTimeUs; } super.onStreamChanged(formats, offsetUs); } @@ -277,7 +295,9 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException { super.onPositionReset(positionUs, joining); clearRenderedFirstFrame(); + initialPositionUs = C.TIME_UNSET; consecutiveDroppedFrameCount = 0; + lastInputTimeUs = C.TIME_UNSET; if (pendingOutputStreamOffsetCount != 0) { outputStreamOffsetUs = pendingOutputStreamOffsetsUs[pendingOutputStreamOffsetCount - 1]; pendingOutputStreamOffsetCount = 0; @@ -331,6 +351,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { currentPixelWidthHeightRatio = Format.NO_VALUE; pendingPixelWidthHeightRatio = Format.NO_VALUE; outputStreamOffsetUs = C.TIME_UNSET; + lastInputTimeUs = C.TIME_UNSET; pendingOutputStreamOffsetCount = 0; clearReportedVideoSize(); clearRenderedFirstFrame(); @@ -353,7 +374,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { scalingMode = (Integer) message; MediaCodec codec = getCodec(); if (codec != null) { - setVideoScalingMode(codec, scalingMode); + codec.setVideoScalingMode(scalingMode); } } else { super.handleMessage(messageType, message); @@ -376,7 +397,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { // We only need to update the codec if the surface has changed. if (this.surface != surface) { this.surface = surface; - int state = getState(); + @State int state = getState(); if (state == STATE_ENABLED || state == STATE_STARTED) { MediaCodec codec = getCodec(); if (Util.SDK_INT >= 23 && codec != null && surface != null @@ -416,7 +437,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { @Override protected void configureCodec(MediaCodecInfo codecInfo, MediaCodec codec, Format format, MediaCrypto crypto) throws DecoderQueryException { - codecMaxValues = getCodecMaxValues(codecInfo, format, streamFormats); + codecMaxValues = getCodecMaxValues(codecInfo, format, getStreamFormats()); MediaFormat mediaFormat = getMediaFormat(format, codecMaxValues, deviceNeedsAutoFrcWorkaround, tunnelingAudioSessionId); if (surface == null) { @@ -432,6 +453,20 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } } + @Override + protected @KeepCodecResult int canKeepCodec( + 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) { + return oldFormat.initializationDataEquals(newFormat) + ? KEEP_CODEC_RESULT_YES_WITHOUT_RECONFIGURATION + : KEEP_CODEC_RESULT_YES_WITH_RECONFIGURATION; + } + return KEEP_CODEC_RESULT_NO; + } + @CallSuper @Override protected void releaseCodec() { @@ -467,8 +502,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { protected void onInputFormatChanged(Format newFormat) throws ExoPlaybackException { super.onInputFormatChanged(newFormat); eventDispatcher.inputFormatChanged(newFormat); - pendingPixelWidthHeightRatio = getPixelWidthHeightRatio(newFormat); - pendingRotationDegrees = getRotationDegrees(newFormat); + pendingPixelWidthHeightRatio = newFormat.pixelWidthHeightRatio; + pendingRotationDegrees = newFormat.rotationDegrees; } /** @@ -480,6 +515,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { @Override protected void onQueueInputBuffer(DecoderInputBuffer buffer) { buffersInCodecCount++; + lastInputTimeUs = Math.max(buffer.timeUs, lastInputTimeUs); if (Util.SDK_INT < 23 && tunneling) { maybeNotifyRenderedFirstFrame(); } @@ -512,28 +548,17 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { currentUnappliedRotationDegrees = pendingRotationDegrees; } // Must be applied each time the output format changes. - setVideoScalingMode(codec, scalingMode); - } - - @Override - protected boolean canReconfigureCodec(MediaCodec codec, boolean codecIsAdaptive, - Format oldFormat, Format newFormat) { - return areAdaptationCompatible(codecIsAdaptive, oldFormat, newFormat) - && newFormat.width <= codecMaxValues.width && newFormat.height <= codecMaxValues.height - && getMaxInputSize(newFormat) <= codecMaxValues.inputSize; + codec.setVideoScalingMode(scalingMode); } @Override protected boolean processOutputBuffer(long positionUs, long elapsedRealtimeUs, MediaCodec codec, ByteBuffer buffer, int bufferIndex, int bufferFlags, long bufferPresentationTimeUs, boolean shouldSkip) throws ExoPlaybackException { - while (pendingOutputStreamOffsetCount != 0 - && bufferPresentationTimeUs >= pendingOutputStreamOffsetsUs[0]) { - outputStreamOffsetUs = pendingOutputStreamOffsetsUs[0]; - pendingOutputStreamOffsetCount--; - System.arraycopy(pendingOutputStreamOffsetsUs, 1, pendingOutputStreamOffsetsUs, 0, - pendingOutputStreamOffsetCount); + if (initialPositionUs == C.TIME_UNSET) { + initialPositionUs = positionUs; } + long presentationTimeUs = bufferPresentationTimeUs - outputStreamOffsetUs; if (shouldSkip) { @@ -564,7 +589,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { return true; } - if (!isStarted) { + if (!isStarted || positionUs == initialPositionUs) { return false; } @@ -628,6 +653,23 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { @Override protected void onProcessedOutputBuffer(long presentationTimeUs) { buffersInCodecCount--; + while (pendingOutputStreamOffsetCount != 0 + && presentationTimeUs >= pendingOutputStreamSwitchTimesUs[0]) { + outputStreamOffsetUs = pendingOutputStreamOffsetsUs[0]; + pendingOutputStreamOffsetCount--; + System.arraycopy( + pendingOutputStreamOffsetsUs, + /* srcPos= */ 1, + pendingOutputStreamOffsetsUs, + /* destPos= */ 0, + pendingOutputStreamOffsetCount); + System.arraycopy( + pendingOutputStreamSwitchTimesUs, + /* srcPos= */ 1, + pendingOutputStreamSwitchTimesUs, + /* destPos= */ 0, + pendingOutputStreamOffsetCount); + } } /** @@ -879,6 +921,51 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { mediaFormat.setInteger(MediaFormat.KEY_AUDIO_SESSION_ID, tunnelingAudioSessionId); } + /** + * Returns the framework {@link MediaFormat} that should be used to configure the decoder. + * + * @param format The format of media. + * @param codecMaxValues Codec max values that should be used when configuring the decoder. + * @param deviceNeedsAutoFrcWorkaround Whether the device is known to enable frame-rate conversion + * logic that negatively impacts ExoPlayer. + * @param tunnelingAudioSessionId The audio session id to use for tunneling, or {@link + * C#AUDIO_SESSION_ID_UNSET} if tunneling should not be enabled. + * @return The framework {@link MediaFormat} that should be used to configure the decoder. + */ + @SuppressLint("InlinedApi") + protected MediaFormat getMediaFormat( + Format format, + CodecMaxValues codecMaxValues, + boolean deviceNeedsAutoFrcWorkaround, + int tunnelingAudioSessionId) { + MediaFormat mediaFormat = new MediaFormat(); + // Set format parameters that should always be set. + mediaFormat.setString(MediaFormat.KEY_MIME, format.sampleMimeType); + mediaFormat.setInteger(MediaFormat.KEY_WIDTH, format.width); + mediaFormat.setInteger(MediaFormat.KEY_HEIGHT, format.height); + MediaFormatUtil.setCsdBuffers(mediaFormat, format.initializationData); + // Set format parameters that may be unset. + MediaFormatUtil.maybeSetFloat(mediaFormat, MediaFormat.KEY_FRAME_RATE, format.frameRate); + MediaFormatUtil.maybeSetInteger(mediaFormat, MediaFormat.KEY_ROTATION, format.rotationDegrees); + MediaFormatUtil.maybeSetColorInfo(mediaFormat, format.colorInfo); + // Set codec max values. + mediaFormat.setInteger(MediaFormat.KEY_MAX_WIDTH, codecMaxValues.width); + mediaFormat.setInteger(MediaFormat.KEY_MAX_HEIGHT, codecMaxValues.height); + MediaFormatUtil.maybeSetInteger( + mediaFormat, MediaFormat.KEY_MAX_INPUT_SIZE, codecMaxValues.inputSize); + // Set codec configuration values. + if (Util.SDK_INT >= 23) { + mediaFormat.setInteger(MediaFormat.KEY_PRIORITY, 0 /* realtime priority */); + } + if (deviceNeedsAutoFrcWorkaround) { + mediaFormat.setInteger("auto-frc", 0); + } + if (tunnelingAudioSessionId != C.AUDIO_SESSION_ID_UNSET) { + configureTunnelingV21(mediaFormat, tunnelingAudioSessionId); + } + 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}. @@ -889,8 +976,9 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { * @return Suitable {@link CodecMaxValues}. * @throws DecoderQueryException If an error occurs querying {@code codecInfo}. */ - protected CodecMaxValues getCodecMaxValues(MediaCodecInfo codecInfo, Format format, - Format[] streamFormats) throws DecoderQueryException { + protected CodecMaxValues getCodecMaxValues( + MediaCodecInfo codecInfo, Format format, Format[] streamFormats) + throws DecoderQueryException { int maxWidth = format.width; int maxHeight = format.height; int maxInputSize = getMaxInputSize(format); @@ -902,8 +990,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { boolean haveUnknownDimensions = false; for (Format streamFormat : streamFormats) { if (areAdaptationCompatible(codecInfo.adaptive, format, streamFormat)) { - haveUnknownDimensions |= (streamFormat.width == Format.NO_VALUE - || streamFormat.height == Format.NO_VALUE); + 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)); @@ -915,44 +1003,14 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { if (codecMaxSize != null) { maxWidth = Math.max(maxWidth, codecMaxSize.x); maxHeight = Math.max(maxHeight, codecMaxSize.y); - maxInputSize = Math.max(maxInputSize, - getMaxInputSize(format.sampleMimeType, maxWidth, maxHeight)); + 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 when - * playing media in the specified input format. - * - * @param format The format of input media. - * @param codecMaxValues The codec's maximum supported values. - * @param deviceNeedsAutoFrcWorkaround Whether the device is known to enable frame-rate conversion - * logic that negatively impacts ExoPlayer. - * @param tunnelingAudioSessionId The audio session id to use for tunneling, or - * {@link C#AUDIO_SESSION_ID_UNSET} if tunneling should not be enabled. - * @return The framework {@link MediaFormat} that should be used to configure the decoder. - */ - @SuppressLint("InlinedApi") - protected MediaFormat getMediaFormat(Format format, CodecMaxValues codecMaxValues, - boolean deviceNeedsAutoFrcWorkaround, int tunnelingAudioSessionId) { - MediaFormat frameworkMediaFormat = getMediaFormatForPlayback(format); - frameworkMediaFormat.setInteger(MediaFormat.KEY_MAX_WIDTH, codecMaxValues.width); - frameworkMediaFormat.setInteger(MediaFormat.KEY_MAX_HEIGHT, codecMaxValues.height); - if (codecMaxValues.inputSize != Format.NO_VALUE) { - frameworkMediaFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, codecMaxValues.inputSize); - } - if (deviceNeedsAutoFrcWorkaround) { - frameworkMediaFormat.setInteger("auto-frc", 0); - } - if (tunnelingAudioSessionId != C.AUDIO_SESSION_ID_UNSET) { - configureTunnelingV21(frameworkMediaFormat, tunnelingAudioSessionId); - } - return frameworkMediaFormat; - } - /** * 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 @@ -1070,8 +1128,21 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { return (maxPixels * 3) / (2 * minCompressionRatio); } - private static void setVideoScalingMode(MediaCodec codec, int scalingMode) { - codec.setVideoScalingMode(scalingMode); + /** + * Returns whether a codec with suitable {@link CodecMaxValues} will support adaptation between + * two {@link Format}s. + * + * @param codecIsAdaptive Whether the codec supports seamless resolution switches. + * @param first The first format. + * @param second The second format. + * @return Whether the codec will support adaptation between the two {@link Format}s. + */ + private static boolean areAdaptationCompatible( + boolean codecIsAdaptive, Format first, Format second) { + return first.sampleMimeType.equals(second.sampleMimeType) + && first.rotationDegrees == second.rotationDegrees + && (codecIsAdaptive || (first.width == second.width && first.height == second.height)) + && Util.areEqual(first.colorInfo, second.colorInfo); } /** @@ -1103,8 +1174,10 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { // https://github.com/google/ExoPlayer/issues/3355, // https://github.com/google/ExoPlayer/issues/3439, // https://github.com/google/ExoPlayer/issues/3724, - // https://github.com/google/ExoPlayer/issues/3835 and - // https://github.com/google/ExoPlayer/issues/4006. + // https://github.com/google/ExoPlayer/issues/3835, + // https://github.com/google/ExoPlayer/issues/4006, + // https://github.com/google/ExoPlayer/issues/4084, + // https://github.com/google/ExoPlayer/issues/4104. return (("deb".equals(Util.DEVICE) // Nexus 7 (2013) || "flo".equals(Util.DEVICE) // Nexus 7 (2013) || "mido".equals(Util.DEVICE) // Redmi Note 4 @@ -1116,35 +1189,14 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { || Util.DEVICE.startsWith("panell_") // Motorola Moto C Plus || "F3311".equals(Util.DEVICE) // Sony Xperia E5 || "M5c".equals(Util.DEVICE) // Meizu M5C + || "QM16XE_U".equals(Util.DEVICE) // Philips QM163E || "A7010a48".equals(Util.DEVICE)) // Lenovo K4 Note && "OMX.MTK.VIDEO.DECODER.AVC".equals(name)) || (("ALE-L21".equals(Util.MODEL) // Huawei P8 Lite || "CAM-L21".equals(Util.MODEL)) // Huawei Y6II - && "OMX.k3.video.decoder.avc".equals(name)); - } - - /** - * Returns whether a codec with suitable {@link CodecMaxValues} will support adaptation between - * two {@link Format}s. - * - * @param codecIsAdaptive Whether the codec supports seamless resolution switches. - * @param first The first format. - * @param second The second format. - * @return Whether the codec will support adaptation between the two {@link Format}s. - */ - private static boolean areAdaptationCompatible(boolean codecIsAdaptive, Format first, - Format second) { - return first.sampleMimeType.equals(second.sampleMimeType) - && getRotationDegrees(first) == getRotationDegrees(second) - && (codecIsAdaptive || (first.width == second.width && first.height == second.height)); - } - - private static float getPixelWidthHeightRatio(Format format) { - return format.pixelWidthHeightRatio == Format.NO_VALUE ? 1 : format.pixelWidthHeightRatio; - } - - private static int getRotationDegrees(Format format) { - return format.rotationDegrees == Format.NO_VALUE ? 0 : format.rotationDegrees; + && "OMX.k3.video.decoder.avc".equals(name)) + || (("HUAWEI VNS-L21".equals(Util.MODEL)) // Huawei P9 Lite + && "OMX.IMG.MSVDX.Decoder.AVC".equals(name)); } protected static final class CodecMaxValues { diff --git a/library/core/src/test/AndroidManifest.xml b/library/core/src/test/AndroidManifest.xml index f2a4cd6647..2cf0313256 100644 --- a/library/core/src/test/AndroidManifest.xml +++ b/library/core/src/test/AndroidManifest.xml @@ -14,9 +14,4 @@ limitations under the License. --> - - - - - + 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/assets/flv/sample.flv.0.dump b/library/core/src/test/assets/flv/sample.flv.0.dump index f4502749f5..098311a310 100644 --- a/library/core/src/test/assets/flv/sample.flv.0.dump +++ b/library/core/src/test/assets/flv/sample.flv.0.dump @@ -13,13 +13,13 @@ track 8: width = -1 height = -1 frameRate = -1.0 - rotationDegrees = -1 - pixelWidthHeightRatio = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 channelCount = 1 sampleRate = 44100 pcmEncoding = -1 - encoderDelay = -1 - encoderPadding = -1 + encoderDelay = 0 + encoderPadding = 0 subsampleOffsetUs = 9223372036854775807 selectionFlags = 0 language = null @@ -218,13 +218,13 @@ track 9: width = 1080 height = 720 frameRate = -1.0 - rotationDegrees = -1 + rotationDegrees = 0 pixelWidthHeightRatio = 1.0 channelCount = -1 sampleRate = -1 pcmEncoding = -1 - encoderDelay = -1 - encoderPadding = -1 + encoderDelay = 0 + encoderPadding = 0 subsampleOffsetUs = 9223372036854775807 selectionFlags = 0 language = null diff --git a/library/core/src/test/assets/mkv/sample.mkv.0.dump b/library/core/src/test/assets/mkv/sample.mkv.0.dump index 009ff55c23..847799396d 100644 --- a/library/core/src/test/assets/mkv/sample.mkv.0.dump +++ b/library/core/src/test/assets/mkv/sample.mkv.0.dump @@ -13,13 +13,13 @@ track 1: width = 1080 height = 720 frameRate = -1.0 - rotationDegrees = -1 + rotationDegrees = 0 pixelWidthHeightRatio = 1.0 channelCount = -1 sampleRate = -1 pcmEncoding = -1 - encoderDelay = -1 - encoderPadding = -1 + encoderDelay = 0 + encoderPadding = 0 subsampleOffsetUs = 9223372036854775807 selectionFlags = 0 language = null @@ -159,13 +159,13 @@ track 2: width = -1 height = -1 frameRate = -1.0 - rotationDegrees = -1 - pixelWidthHeightRatio = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 channelCount = 1 sampleRate = 44100 pcmEncoding = -1 - encoderDelay = -1 - encoderPadding = -1 + encoderDelay = 0 + encoderPadding = 0 subsampleOffsetUs = 9223372036854775807 selectionFlags = 1 language = und diff --git a/library/core/src/test/assets/mkv/sample.mkv.1.dump b/library/core/src/test/assets/mkv/sample.mkv.1.dump index 91396e81b8..5caa638437 100644 --- a/library/core/src/test/assets/mkv/sample.mkv.1.dump +++ b/library/core/src/test/assets/mkv/sample.mkv.1.dump @@ -13,13 +13,13 @@ track 1: width = 1080 height = 720 frameRate = -1.0 - rotationDegrees = -1 + rotationDegrees = 0 pixelWidthHeightRatio = 1.0 channelCount = -1 sampleRate = -1 pcmEncoding = -1 - encoderDelay = -1 - encoderPadding = -1 + encoderDelay = 0 + encoderPadding = 0 subsampleOffsetUs = 9223372036854775807 selectionFlags = 0 language = null @@ -127,13 +127,13 @@ track 2: width = -1 height = -1 frameRate = -1.0 - rotationDegrees = -1 - pixelWidthHeightRatio = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 channelCount = 1 sampleRate = 44100 pcmEncoding = -1 - encoderDelay = -1 - encoderPadding = -1 + encoderDelay = 0 + encoderPadding = 0 subsampleOffsetUs = 9223372036854775807 selectionFlags = 1 language = und diff --git a/library/core/src/test/assets/mkv/sample.mkv.2.dump b/library/core/src/test/assets/mkv/sample.mkv.2.dump index 5c56dcc8af..de4e2a58bf 100644 --- a/library/core/src/test/assets/mkv/sample.mkv.2.dump +++ b/library/core/src/test/assets/mkv/sample.mkv.2.dump @@ -13,13 +13,13 @@ track 1: width = 1080 height = 720 frameRate = -1.0 - rotationDegrees = -1 + rotationDegrees = 0 pixelWidthHeightRatio = 1.0 channelCount = -1 sampleRate = -1 pcmEncoding = -1 - encoderDelay = -1 - encoderPadding = -1 + encoderDelay = 0 + encoderPadding = 0 subsampleOffsetUs = 9223372036854775807 selectionFlags = 0 language = null @@ -83,13 +83,13 @@ track 2: width = -1 height = -1 frameRate = -1.0 - rotationDegrees = -1 - pixelWidthHeightRatio = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 channelCount = 1 sampleRate = 44100 pcmEncoding = -1 - encoderDelay = -1 - encoderPadding = -1 + encoderDelay = 0 + encoderPadding = 0 subsampleOffsetUs = 9223372036854775807 selectionFlags = 1 language = und diff --git a/library/core/src/test/assets/mkv/sample.mkv.3.dump b/library/core/src/test/assets/mkv/sample.mkv.3.dump index cf5a0199fc..6034c54dec 100644 --- a/library/core/src/test/assets/mkv/sample.mkv.3.dump +++ b/library/core/src/test/assets/mkv/sample.mkv.3.dump @@ -13,13 +13,13 @@ track 1: width = 1080 height = 720 frameRate = -1.0 - rotationDegrees = -1 + rotationDegrees = 0 pixelWidthHeightRatio = 1.0 channelCount = -1 sampleRate = -1 pcmEncoding = -1 - encoderDelay = -1 - encoderPadding = -1 + encoderDelay = 0 + encoderPadding = 0 subsampleOffsetUs = 9223372036854775807 selectionFlags = 0 language = null @@ -39,13 +39,13 @@ track 2: width = -1 height = -1 frameRate = -1.0 - rotationDegrees = -1 - pixelWidthHeightRatio = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 channelCount = 1 sampleRate = 44100 pcmEncoding = -1 - encoderDelay = -1 - encoderPadding = -1 + encoderDelay = 0 + encoderPadding = 0 subsampleOffsetUs = 9223372036854775807 selectionFlags = 1 language = und diff --git a/library/core/src/test/assets/mkv/subsample_encrypted_altref.webm.0.dump b/library/core/src/test/assets/mkv/subsample_encrypted_altref.webm.0.dump index 62a270eb9e..89a7514784 100644 --- a/library/core/src/test/assets/mkv/subsample_encrypted_altref.webm.0.dump +++ b/library/core/src/test/assets/mkv/subsample_encrypted_altref.webm.0.dump @@ -13,13 +13,13 @@ track 1: width = 360 height = 240 frameRate = -1.0 - rotationDegrees = -1 + rotationDegrees = 0 pixelWidthHeightRatio = 1.0 channelCount = -1 sampleRate = -1 pcmEncoding = -1 - encoderDelay = -1 - encoderPadding = -1 + encoderDelay = 0 + encoderPadding = 0 subsampleOffsetUs = 9223372036854775807 selectionFlags = 0 language = null diff --git a/library/core/src/test/assets/mkv/subsample_encrypted_noaltref.webm.0.dump b/library/core/src/test/assets/mkv/subsample_encrypted_noaltref.webm.0.dump index 43e5eed5d1..1caa3f9f27 100644 --- a/library/core/src/test/assets/mkv/subsample_encrypted_noaltref.webm.0.dump +++ b/library/core/src/test/assets/mkv/subsample_encrypted_noaltref.webm.0.dump @@ -13,13 +13,13 @@ track 1: width = 360 height = 240 frameRate = -1.0 - rotationDegrees = -1 + rotationDegrees = 0 pixelWidthHeightRatio = 1.0 channelCount = -1 sampleRate = -1 pcmEncoding = -1 - encoderDelay = -1 - encoderPadding = -1 + encoderDelay = 0 + encoderPadding = 0 subsampleOffsetUs = 9223372036854775807 selectionFlags = 0 language = null diff --git a/library/core/src/test/assets/mp3/bear.mp3.0.dump b/library/core/src/test/assets/mp3/bear.mp3.0.dump index b12a68a60b..5c8700fed1 100644 --- a/library/core/src/test/assets/mp3/bear.mp3.0.dump +++ b/library/core/src/test/assets/mp3/bear.mp3.0.dump @@ -13,8 +13,8 @@ track 0: width = -1 height = -1 frameRate = -1.0 - rotationDegrees = -1 - pixelWidthHeightRatio = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 channelCount = 2 sampleRate = 48000 pcmEncoding = -1 diff --git a/library/core/src/test/assets/mp3/bear.mp3.1.dump b/library/core/src/test/assets/mp3/bear.mp3.1.dump index abf5b10415..c2f37973b7 100644 --- a/library/core/src/test/assets/mp3/bear.mp3.1.dump +++ b/library/core/src/test/assets/mp3/bear.mp3.1.dump @@ -13,8 +13,8 @@ track 0: width = -1 height = -1 frameRate = -1.0 - rotationDegrees = -1 - pixelWidthHeightRatio = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 channelCount = 2 sampleRate = 48000 pcmEncoding = -1 diff --git a/library/core/src/test/assets/mp3/bear.mp3.2.dump b/library/core/src/test/assets/mp3/bear.mp3.2.dump index 813f61b7fc..543cf44cc0 100644 --- a/library/core/src/test/assets/mp3/bear.mp3.2.dump +++ b/library/core/src/test/assets/mp3/bear.mp3.2.dump @@ -13,8 +13,8 @@ track 0: width = -1 height = -1 frameRate = -1.0 - rotationDegrees = -1 - pixelWidthHeightRatio = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 channelCount = 2 sampleRate = 48000 pcmEncoding = -1 diff --git a/library/core/src/test/assets/mp3/bear.mp3.3.dump b/library/core/src/test/assets/mp3/bear.mp3.3.dump index 9a0207bd53..a87b7d6d37 100644 --- a/library/core/src/test/assets/mp3/bear.mp3.3.dump +++ b/library/core/src/test/assets/mp3/bear.mp3.3.dump @@ -13,8 +13,8 @@ track 0: width = -1 height = -1 frameRate = -1.0 - rotationDegrees = -1 - pixelWidthHeightRatio = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 channelCount = 2 sampleRate = 48000 pcmEncoding = -1 diff --git a/library/core/src/test/assets/mp3/play-trimmed.mp3.0.dump b/library/core/src/test/assets/mp3/play-trimmed.mp3.0.dump index 435360dfed..96b0cd259c 100644 --- a/library/core/src/test/assets/mp3/play-trimmed.mp3.0.dump +++ b/library/core/src/test/assets/mp3/play-trimmed.mp3.0.dump @@ -13,13 +13,13 @@ track 0: width = -1 height = -1 frameRate = -1.0 - rotationDegrees = -1 - pixelWidthHeightRatio = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 channelCount = 2 sampleRate = 44100 pcmEncoding = -1 - encoderDelay = -1 - encoderPadding = -1 + encoderDelay = 0 + encoderPadding = 0 subsampleOffsetUs = 9223372036854775807 selectionFlags = 0 language = null diff --git a/library/core/src/test/assets/mp3/play-trimmed.mp3.1.dump b/library/core/src/test/assets/mp3/play-trimmed.mp3.1.dump index 435360dfed..96b0cd259c 100644 --- a/library/core/src/test/assets/mp3/play-trimmed.mp3.1.dump +++ b/library/core/src/test/assets/mp3/play-trimmed.mp3.1.dump @@ -13,13 +13,13 @@ track 0: width = -1 height = -1 frameRate = -1.0 - rotationDegrees = -1 - pixelWidthHeightRatio = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 channelCount = 2 sampleRate = 44100 pcmEncoding = -1 - encoderDelay = -1 - encoderPadding = -1 + encoderDelay = 0 + encoderPadding = 0 subsampleOffsetUs = 9223372036854775807 selectionFlags = 0 language = null diff --git a/library/core/src/test/assets/mp3/play-trimmed.mp3.2.dump b/library/core/src/test/assets/mp3/play-trimmed.mp3.2.dump index 435360dfed..96b0cd259c 100644 --- a/library/core/src/test/assets/mp3/play-trimmed.mp3.2.dump +++ b/library/core/src/test/assets/mp3/play-trimmed.mp3.2.dump @@ -13,13 +13,13 @@ track 0: width = -1 height = -1 frameRate = -1.0 - rotationDegrees = -1 - pixelWidthHeightRatio = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 channelCount = 2 sampleRate = 44100 pcmEncoding = -1 - encoderDelay = -1 - encoderPadding = -1 + encoderDelay = 0 + encoderPadding = 0 subsampleOffsetUs = 9223372036854775807 selectionFlags = 0 language = null diff --git a/library/core/src/test/assets/mp3/play-trimmed.mp3.3.dump b/library/core/src/test/assets/mp3/play-trimmed.mp3.3.dump index 435360dfed..96b0cd259c 100644 --- a/library/core/src/test/assets/mp3/play-trimmed.mp3.3.dump +++ b/library/core/src/test/assets/mp3/play-trimmed.mp3.3.dump @@ -13,13 +13,13 @@ track 0: width = -1 height = -1 frameRate = -1.0 - rotationDegrees = -1 - pixelWidthHeightRatio = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 channelCount = 2 sampleRate = 44100 pcmEncoding = -1 - encoderDelay = -1 - encoderPadding = -1 + encoderDelay = 0 + encoderPadding = 0 subsampleOffsetUs = 9223372036854775807 selectionFlags = 0 language = null diff --git a/library/core/src/test/assets/mp3/play-trimmed.mp3.unklen.dump b/library/core/src/test/assets/mp3/play-trimmed.mp3.unklen.dump index 6b49619b50..d28cca025b 100644 --- a/library/core/src/test/assets/mp3/play-trimmed.mp3.unklen.dump +++ b/library/core/src/test/assets/mp3/play-trimmed.mp3.unklen.dump @@ -13,13 +13,13 @@ track 0: width = -1 height = -1 frameRate = -1.0 - rotationDegrees = -1 - pixelWidthHeightRatio = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 channelCount = 2 sampleRate = 44100 pcmEncoding = -1 - encoderDelay = -1 - encoderPadding = -1 + encoderDelay = 0 + encoderPadding = 0 subsampleOffsetUs = 9223372036854775807 selectionFlags = 0 language = null diff --git a/library/core/src/test/assets/mp4/sample.mp4.0.dump b/library/core/src/test/assets/mp4/sample.mp4.0.dump index 77708b16df..efc804d48b 100644 --- a/library/core/src/test/assets/mp4/sample.mp4.0.dump +++ b/library/core/src/test/assets/mp4/sample.mp4.0.dump @@ -18,8 +18,8 @@ track 0: channelCount = -1 sampleRate = -1 pcmEncoding = -1 - encoderDelay = -1 - encoderPadding = -1 + encoderDelay = 0 + encoderPadding = 0 subsampleOffsetUs = 9223372036854775807 selectionFlags = 0 language = null @@ -159,13 +159,13 @@ track 1: width = -1 height = -1 frameRate = -1.0 - rotationDegrees = -1 - pixelWidthHeightRatio = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 channelCount = 1 sampleRate = 44100 pcmEncoding = -1 - encoderDelay = -1 - encoderPadding = -1 + encoderDelay = 0 + encoderPadding = 0 subsampleOffsetUs = 9223372036854775807 selectionFlags = 0 language = und diff --git a/library/core/src/test/assets/mp4/sample.mp4.1.dump b/library/core/src/test/assets/mp4/sample.mp4.1.dump index 30ed21ef98..10104b5e81 100644 --- a/library/core/src/test/assets/mp4/sample.mp4.1.dump +++ b/library/core/src/test/assets/mp4/sample.mp4.1.dump @@ -18,8 +18,8 @@ track 0: channelCount = -1 sampleRate = -1 pcmEncoding = -1 - encoderDelay = -1 - encoderPadding = -1 + encoderDelay = 0 + encoderPadding = 0 subsampleOffsetUs = 9223372036854775807 selectionFlags = 0 language = null @@ -159,13 +159,13 @@ track 1: width = -1 height = -1 frameRate = -1.0 - rotationDegrees = -1 - pixelWidthHeightRatio = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 channelCount = 1 sampleRate = 44100 pcmEncoding = -1 - encoderDelay = -1 - encoderPadding = -1 + encoderDelay = 0 + encoderPadding = 0 subsampleOffsetUs = 9223372036854775807 selectionFlags = 0 language = und diff --git a/library/core/src/test/assets/mp4/sample.mp4.2.dump b/library/core/src/test/assets/mp4/sample.mp4.2.dump index 640d92722c..8af96be673 100644 --- a/library/core/src/test/assets/mp4/sample.mp4.2.dump +++ b/library/core/src/test/assets/mp4/sample.mp4.2.dump @@ -18,8 +18,8 @@ track 0: channelCount = -1 sampleRate = -1 pcmEncoding = -1 - encoderDelay = -1 - encoderPadding = -1 + encoderDelay = 0 + encoderPadding = 0 subsampleOffsetUs = 9223372036854775807 selectionFlags = 0 language = null @@ -159,13 +159,13 @@ track 1: width = -1 height = -1 frameRate = -1.0 - rotationDegrees = -1 - pixelWidthHeightRatio = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 channelCount = 1 sampleRate = 44100 pcmEncoding = -1 - encoderDelay = -1 - encoderPadding = -1 + encoderDelay = 0 + encoderPadding = 0 subsampleOffsetUs = 9223372036854775807 selectionFlags = 0 language = und diff --git a/library/core/src/test/assets/mp4/sample.mp4.3.dump b/library/core/src/test/assets/mp4/sample.mp4.3.dump index b4fd4a0b02..f1259661ed 100644 --- a/library/core/src/test/assets/mp4/sample.mp4.3.dump +++ b/library/core/src/test/assets/mp4/sample.mp4.3.dump @@ -18,8 +18,8 @@ track 0: channelCount = -1 sampleRate = -1 pcmEncoding = -1 - encoderDelay = -1 - encoderPadding = -1 + encoderDelay = 0 + encoderPadding = 0 subsampleOffsetUs = 9223372036854775807 selectionFlags = 0 language = null @@ -159,13 +159,13 @@ track 1: width = -1 height = -1 frameRate = -1.0 - rotationDegrees = -1 - pixelWidthHeightRatio = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 channelCount = 1 sampleRate = 44100 pcmEncoding = -1 - encoderDelay = -1 - encoderPadding = -1 + encoderDelay = 0 + encoderPadding = 0 subsampleOffsetUs = 9223372036854775807 selectionFlags = 0 language = und diff --git a/library/core/src/test/assets/mp4/sample_fragmented.mp4.0.dump b/library/core/src/test/assets/mp4/sample_fragmented.mp4.0.dump index ec2cb7b8ce..faa8a015ca 100644 --- a/library/core/src/test/assets/mp4/sample_fragmented.mp4.0.dump +++ b/library/core/src/test/assets/mp4/sample_fragmented.mp4.0.dump @@ -18,8 +18,8 @@ track 0: channelCount = -1 sampleRate = -1 pcmEncoding = -1 - encoderDelay = -1 - encoderPadding = -1 + encoderDelay = 0 + encoderPadding = 0 subsampleOffsetUs = 9223372036854775807 selectionFlags = 0 language = null @@ -159,13 +159,13 @@ track 1: width = -1 height = -1 frameRate = -1.0 - rotationDegrees = -1 - pixelWidthHeightRatio = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 channelCount = 1 sampleRate = 44100 pcmEncoding = -1 - encoderDelay = -1 - encoderPadding = -1 + encoderDelay = 0 + encoderPadding = 0 subsampleOffsetUs = 9223372036854775807 selectionFlags = 0 language = und diff --git a/library/core/src/test/assets/mp4/sample_fragmented_seekable.mp4 b/library/core/src/test/assets/mp4/sample_fragmented_seekable.mp4 new file mode 100644 index 0000000000..592aa725b2 Binary files /dev/null and b/library/core/src/test/assets/mp4/sample_fragmented_seekable.mp4 differ diff --git a/library/core/src/test/assets/mp4/sample_fragmented_seekable.mp4.0.dump b/library/core/src/test/assets/mp4/sample_fragmented_seekable.mp4.0.dump new file mode 100644 index 0000000000..04e2f6f0a0 --- /dev/null +++ b/library/core/src/test/assets/mp4/sample_fragmented_seekable.mp4.0.dump @@ -0,0 +1,361 @@ +seekMap: + isSeekable = true + duration = 1067733 + getPosition(0) = [[timeUs=66733, position=1325]] +numberOfTracks = 2 +track 0: + format: + bitrate = -1 + id = 1 + containerMimeType = null + sampleMimeType = video/avc + maxInputSize = -1 + width = 1080 + height = 720 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = -1 + sampleRate = -1 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = null + drmInitData = - + initializationData: + data = length 29, hash 4746B5D9 + data = length 10, hash 7A0D0F2B + total output bytes = 85933 + sample count = 30 + sample 0: + time = 66000 + flags = 1 + data = length 38070, hash B58E1AEE + sample 1: + time = 199000 + flags = 0 + data = length 8340, hash 8AC449FF + sample 2: + time = 132000 + flags = 0 + data = length 1295, hash C0DA5090 + sample 3: + time = 100000 + flags = 0 + data = length 469, hash D6E0A200 + sample 4: + time = 166000 + flags = 0 + data = length 564, hash E5F56C5B + sample 5: + time = 332000 + flags = 0 + data = length 6075, hash 8756E49E + sample 6: + time = 266000 + flags = 0 + data = length 847, hash DCC2B618 + sample 7: + time = 233000 + flags = 0 + data = length 455, hash B9CCE047 + sample 8: + time = 299000 + flags = 0 + data = length 467, hash 69806D94 + sample 9: + time = 466000 + flags = 0 + data = length 4549, hash 3944F501 + sample 10: + time = 399000 + flags = 0 + data = length 1087, hash 491BF106 + sample 11: + time = 367000 + flags = 0 + data = length 380, hash 5FED016A + sample 12: + time = 433000 + flags = 0 + data = length 455, hash 8A0610 + sample 13: + time = 599000 + flags = 0 + data = length 5190, hash B9031D8 + sample 14: + time = 533000 + flags = 0 + data = length 1071, hash 684E7DC8 + sample 15: + time = 500000 + flags = 0 + data = length 653, hash 8494F326 + sample 16: + time = 566000 + flags = 0 + data = length 485, hash 2CCC85F4 + sample 17: + time = 733000 + flags = 0 + data = length 4884, hash D16B6A96 + sample 18: + time = 666000 + flags = 0 + data = length 997, hash 164FF210 + sample 19: + time = 633000 + flags = 0 + data = length 640, hash F664125B + sample 20: + time = 700000 + flags = 0 + data = length 491, hash B5930C7C + sample 21: + time = 866000 + flags = 0 + data = length 2989, hash 92CF4FCF + sample 22: + time = 800000 + flags = 0 + data = length 838, hash 294A3451 + sample 23: + time = 767000 + flags = 0 + data = length 544, hash FCCE2DE6 + sample 24: + time = 833000 + flags = 0 + data = length 329, hash A654FFA1 + sample 25: + time = 1000000 + flags = 0 + data = length 1517, hash 5F7EBF8B + sample 26: + time = 933000 + flags = 0 + data = length 803, hash 7A5C4C1D + sample 27: + time = 900000 + flags = 0 + data = length 415, hash B31BBC3B + sample 28: + time = 967000 + flags = 0 + data = length 415, hash 850DFEA3 + sample 29: + time = 1033000 + flags = 0 + data = length 619, hash AB5E56CA +track 1: + format: + bitrate = -1 + id = 2 + containerMimeType = null + sampleMimeType = audio/mp4a-latm + maxInputSize = -1 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 1 + sampleRate = 44100 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = und + drmInitData = - + initializationData: + data = length 5, hash 2B7623A + total output bytes = 18257 + sample count = 46 + sample 0: + time = 0 + flags = 1 + data = length 18, hash 96519432 + sample 1: + time = 23000 + flags = 1 + data = length 4, hash EE9DF + sample 2: + time = 46000 + flags = 1 + data = length 4, hash EEDBF + sample 3: + time = 69000 + flags = 1 + data = length 157, hash E2F078F4 + sample 4: + time = 92000 + flags = 1 + data = length 371, hash B9471F94 + sample 5: + time = 116000 + flags = 1 + data = length 373, hash 2AB265CB + sample 6: + time = 139000 + flags = 1 + data = length 402, hash 1295477C + sample 7: + time = 162000 + flags = 1 + data = length 455, hash 2D8146C8 + sample 8: + time = 185000 + flags = 1 + data = length 434, hash F2C5D287 + sample 9: + time = 208000 + flags = 1 + data = length 450, hash 84143FCD + sample 10: + time = 232000 + flags = 1 + data = length 429, hash EF769D50 + sample 11: + time = 255000 + flags = 1 + data = length 450, hash EC3DE692 + sample 12: + time = 278000 + flags = 1 + data = length 447, hash 3E519E13 + sample 13: + time = 301000 + flags = 1 + data = length 457, hash 1E4F23A0 + sample 14: + time = 325000 + flags = 1 + data = length 447, hash A439EA97 + sample 15: + time = 348000 + flags = 1 + data = length 456, hash 1E9034C6 + sample 16: + time = 371000 + flags = 1 + data = length 398, hash 99DB7345 + sample 17: + time = 394000 + flags = 1 + data = length 474, hash 3F05F10A + sample 18: + time = 417000 + flags = 1 + data = length 416, hash C105EE09 + sample 19: + time = 441000 + flags = 1 + data = length 454, hash 5FDBE458 + sample 20: + time = 464000 + flags = 1 + data = length 438, hash 41A93AC3 + sample 21: + time = 487000 + flags = 1 + data = length 443, hash 10FDA652 + sample 22: + time = 510000 + flags = 1 + data = length 412, hash 1F791E25 + sample 23: + time = 534000 + flags = 1 + data = length 482, hash A6D983D + sample 24: + time = 557000 + flags = 1 + data = length 386, hash BED7392F + sample 25: + time = 580000 + flags = 1 + data = length 463, hash 5309F8C9 + sample 26: + time = 603000 + flags = 1 + data = length 394, hash 21C7321F + sample 27: + time = 626000 + flags = 1 + data = length 489, hash 71B4730D + sample 28: + time = 650000 + flags = 1 + data = length 403, hash D9C6DE89 + sample 29: + time = 673000 + flags = 1 + data = length 447, hash 9B14B73B + sample 30: + time = 696000 + flags = 1 + data = length 439, hash 4760D35B + sample 31: + time = 719000 + flags = 1 + data = length 463, hash 1601F88D + sample 32: + time = 743000 + flags = 1 + data = length 423, hash D4AE6773 + sample 33: + time = 766000 + flags = 1 + data = length 497, hash A3C674D3 + sample 34: + time = 789000 + flags = 1 + data = length 419, hash D3734A1F + sample 35: + time = 812000 + flags = 1 + data = length 474, hash DFB41F9 + sample 36: + time = 835000 + flags = 1 + data = length 413, hash 53E7CB9F + sample 37: + time = 859000 + flags = 1 + data = length 445, hash D15B0E39 + sample 38: + time = 882000 + flags = 1 + data = length 453, hash 77ED81E4 + sample 39: + time = 905000 + flags = 1 + data = length 545, hash 3321AEB9 + sample 40: + time = 928000 + flags = 1 + data = length 317, hash F557D0E + sample 41: + time = 952000 + flags = 1 + data = length 537, hash ED58CF7B + sample 42: + time = 975000 + flags = 1 + data = length 458, hash 51CDAA10 + sample 43: + time = 998000 + flags = 1 + data = length 465, hash CBA1EFD7 + sample 44: + time = 1021000 + flags = 1 + data = length 446, hash D6735B8A + sample 45: + time = 1044000 + flags = 1 + data = length 10, hash A453EEBE +tracksEnded = true diff --git a/library/core/src/test/assets/mp4/sample_fragmented_seekable.mp4.1.dump b/library/core/src/test/assets/mp4/sample_fragmented_seekable.mp4.1.dump new file mode 100644 index 0000000000..48a7623a7d --- /dev/null +++ b/library/core/src/test/assets/mp4/sample_fragmented_seekable.mp4.1.dump @@ -0,0 +1,301 @@ +seekMap: + isSeekable = true + duration = 1067733 + getPosition(0) = [[timeUs=66733, position=1325]] +numberOfTracks = 2 +track 0: + format: + bitrate = -1 + id = 1 + containerMimeType = null + sampleMimeType = video/avc + maxInputSize = -1 + width = 1080 + height = 720 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = -1 + sampleRate = -1 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = null + drmInitData = - + initializationData: + data = length 29, hash 4746B5D9 + data = length 10, hash 7A0D0F2B + total output bytes = 85933 + sample count = 30 + sample 0: + time = 66000 + flags = 1 + data = length 38070, hash B58E1AEE + sample 1: + time = 199000 + flags = 0 + data = length 8340, hash 8AC449FF + sample 2: + time = 132000 + flags = 0 + data = length 1295, hash C0DA5090 + sample 3: + time = 100000 + flags = 0 + data = length 469, hash D6E0A200 + sample 4: + time = 166000 + flags = 0 + data = length 564, hash E5F56C5B + sample 5: + time = 332000 + flags = 0 + data = length 6075, hash 8756E49E + sample 6: + time = 266000 + flags = 0 + data = length 847, hash DCC2B618 + sample 7: + time = 233000 + flags = 0 + data = length 455, hash B9CCE047 + sample 8: + time = 299000 + flags = 0 + data = length 467, hash 69806D94 + sample 9: + time = 466000 + flags = 0 + data = length 4549, hash 3944F501 + sample 10: + time = 399000 + flags = 0 + data = length 1087, hash 491BF106 + sample 11: + time = 367000 + flags = 0 + data = length 380, hash 5FED016A + sample 12: + time = 433000 + flags = 0 + data = length 455, hash 8A0610 + sample 13: + time = 599000 + flags = 0 + data = length 5190, hash B9031D8 + sample 14: + time = 533000 + flags = 0 + data = length 1071, hash 684E7DC8 + sample 15: + time = 500000 + flags = 0 + data = length 653, hash 8494F326 + sample 16: + time = 566000 + flags = 0 + data = length 485, hash 2CCC85F4 + sample 17: + time = 733000 + flags = 0 + data = length 4884, hash D16B6A96 + sample 18: + time = 666000 + flags = 0 + data = length 997, hash 164FF210 + sample 19: + time = 633000 + flags = 0 + data = length 640, hash F664125B + sample 20: + time = 700000 + flags = 0 + data = length 491, hash B5930C7C + sample 21: + time = 866000 + flags = 0 + data = length 2989, hash 92CF4FCF + sample 22: + time = 800000 + flags = 0 + data = length 838, hash 294A3451 + sample 23: + time = 767000 + flags = 0 + data = length 544, hash FCCE2DE6 + sample 24: + time = 833000 + flags = 0 + data = length 329, hash A654FFA1 + sample 25: + time = 1000000 + flags = 0 + data = length 1517, hash 5F7EBF8B + sample 26: + time = 933000 + flags = 0 + data = length 803, hash 7A5C4C1D + sample 27: + time = 900000 + flags = 0 + data = length 415, hash B31BBC3B + sample 28: + time = 967000 + flags = 0 + data = length 415, hash 850DFEA3 + sample 29: + time = 1033000 + flags = 0 + data = length 619, hash AB5E56CA +track 1: + format: + bitrate = -1 + id = 2 + containerMimeType = null + sampleMimeType = audio/mp4a-latm + maxInputSize = -1 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 1 + sampleRate = 44100 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = und + drmInitData = - + initializationData: + data = length 5, hash 2B7623A + total output bytes = 13359 + sample count = 31 + sample 0: + time = 348000 + flags = 1 + data = length 456, hash 1E9034C6 + sample 1: + time = 371000 + flags = 1 + data = length 398, hash 99DB7345 + sample 2: + time = 394000 + flags = 1 + data = length 474, hash 3F05F10A + sample 3: + time = 417000 + flags = 1 + data = length 416, hash C105EE09 + sample 4: + time = 441000 + flags = 1 + data = length 454, hash 5FDBE458 + sample 5: + time = 464000 + flags = 1 + data = length 438, hash 41A93AC3 + sample 6: + time = 487000 + flags = 1 + data = length 443, hash 10FDA652 + sample 7: + time = 510000 + flags = 1 + data = length 412, hash 1F791E25 + sample 8: + time = 534000 + flags = 1 + data = length 482, hash A6D983D + sample 9: + time = 557000 + flags = 1 + data = length 386, hash BED7392F + sample 10: + time = 580000 + flags = 1 + data = length 463, hash 5309F8C9 + sample 11: + time = 603000 + flags = 1 + data = length 394, hash 21C7321F + sample 12: + time = 626000 + flags = 1 + data = length 489, hash 71B4730D + sample 13: + time = 650000 + flags = 1 + data = length 403, hash D9C6DE89 + sample 14: + time = 673000 + flags = 1 + data = length 447, hash 9B14B73B + sample 15: + time = 696000 + flags = 1 + data = length 439, hash 4760D35B + sample 16: + time = 719000 + flags = 1 + data = length 463, hash 1601F88D + sample 17: + time = 743000 + flags = 1 + data = length 423, hash D4AE6773 + sample 18: + time = 766000 + flags = 1 + data = length 497, hash A3C674D3 + sample 19: + time = 789000 + flags = 1 + data = length 419, hash D3734A1F + sample 20: + time = 812000 + flags = 1 + data = length 474, hash DFB41F9 + sample 21: + time = 835000 + flags = 1 + data = length 413, hash 53E7CB9F + sample 22: + time = 859000 + flags = 1 + data = length 445, hash D15B0E39 + sample 23: + time = 882000 + flags = 1 + data = length 453, hash 77ED81E4 + sample 24: + time = 905000 + flags = 1 + data = length 545, hash 3321AEB9 + sample 25: + time = 928000 + flags = 1 + data = length 317, hash F557D0E + sample 26: + time = 952000 + flags = 1 + data = length 537, hash ED58CF7B + sample 27: + time = 975000 + flags = 1 + data = length 458, hash 51CDAA10 + sample 28: + time = 998000 + flags = 1 + data = length 465, hash CBA1EFD7 + sample 29: + time = 1021000 + flags = 1 + data = length 446, hash D6735B8A + sample 30: + time = 1044000 + flags = 1 + data = length 10, hash A453EEBE +tracksEnded = true diff --git a/library/core/src/test/assets/mp4/sample_fragmented_seekable.mp4.2.dump b/library/core/src/test/assets/mp4/sample_fragmented_seekable.mp4.2.dump new file mode 100644 index 0000000000..7522891e14 --- /dev/null +++ b/library/core/src/test/assets/mp4/sample_fragmented_seekable.mp4.2.dump @@ -0,0 +1,241 @@ +seekMap: + isSeekable = true + duration = 1067733 + getPosition(0) = [[timeUs=66733, position=1325]] +numberOfTracks = 2 +track 0: + format: + bitrate = -1 + id = 1 + containerMimeType = null + sampleMimeType = video/avc + maxInputSize = -1 + width = 1080 + height = 720 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = -1 + sampleRate = -1 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = null + drmInitData = - + initializationData: + data = length 29, hash 4746B5D9 + data = length 10, hash 7A0D0F2B + total output bytes = 85933 + sample count = 30 + sample 0: + time = 66000 + flags = 1 + data = length 38070, hash B58E1AEE + sample 1: + time = 199000 + flags = 0 + data = length 8340, hash 8AC449FF + sample 2: + time = 132000 + flags = 0 + data = length 1295, hash C0DA5090 + sample 3: + time = 100000 + flags = 0 + data = length 469, hash D6E0A200 + sample 4: + time = 166000 + flags = 0 + data = length 564, hash E5F56C5B + sample 5: + time = 332000 + flags = 0 + data = length 6075, hash 8756E49E + sample 6: + time = 266000 + flags = 0 + data = length 847, hash DCC2B618 + sample 7: + time = 233000 + flags = 0 + data = length 455, hash B9CCE047 + sample 8: + time = 299000 + flags = 0 + data = length 467, hash 69806D94 + sample 9: + time = 466000 + flags = 0 + data = length 4549, hash 3944F501 + sample 10: + time = 399000 + flags = 0 + data = length 1087, hash 491BF106 + sample 11: + time = 367000 + flags = 0 + data = length 380, hash 5FED016A + sample 12: + time = 433000 + flags = 0 + data = length 455, hash 8A0610 + sample 13: + time = 599000 + flags = 0 + data = length 5190, hash B9031D8 + sample 14: + time = 533000 + flags = 0 + data = length 1071, hash 684E7DC8 + sample 15: + time = 500000 + flags = 0 + data = length 653, hash 8494F326 + sample 16: + time = 566000 + flags = 0 + data = length 485, hash 2CCC85F4 + sample 17: + time = 733000 + flags = 0 + data = length 4884, hash D16B6A96 + sample 18: + time = 666000 + flags = 0 + data = length 997, hash 164FF210 + sample 19: + time = 633000 + flags = 0 + data = length 640, hash F664125B + sample 20: + time = 700000 + flags = 0 + data = length 491, hash B5930C7C + sample 21: + time = 866000 + flags = 0 + data = length 2989, hash 92CF4FCF + sample 22: + time = 800000 + flags = 0 + data = length 838, hash 294A3451 + sample 23: + time = 767000 + flags = 0 + data = length 544, hash FCCE2DE6 + sample 24: + time = 833000 + flags = 0 + data = length 329, hash A654FFA1 + sample 25: + time = 1000000 + flags = 0 + data = length 1517, hash 5F7EBF8B + sample 26: + time = 933000 + flags = 0 + data = length 803, hash 7A5C4C1D + sample 27: + time = 900000 + flags = 0 + data = length 415, hash B31BBC3B + sample 28: + time = 967000 + flags = 0 + data = length 415, hash 850DFEA3 + sample 29: + time = 1033000 + flags = 0 + data = length 619, hash AB5E56CA +track 1: + format: + bitrate = -1 + id = 2 + containerMimeType = null + sampleMimeType = audio/mp4a-latm + maxInputSize = -1 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 1 + sampleRate = 44100 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = und + drmInitData = - + initializationData: + data = length 5, hash 2B7623A + total output bytes = 6804 + sample count = 16 + sample 0: + time = 696000 + flags = 1 + data = length 439, hash 4760D35B + sample 1: + time = 719000 + flags = 1 + data = length 463, hash 1601F88D + sample 2: + time = 743000 + flags = 1 + data = length 423, hash D4AE6773 + sample 3: + time = 766000 + flags = 1 + data = length 497, hash A3C674D3 + sample 4: + time = 789000 + flags = 1 + data = length 419, hash D3734A1F + sample 5: + time = 812000 + flags = 1 + data = length 474, hash DFB41F9 + sample 6: + time = 835000 + flags = 1 + data = length 413, hash 53E7CB9F + sample 7: + time = 859000 + flags = 1 + data = length 445, hash D15B0E39 + sample 8: + time = 882000 + flags = 1 + data = length 453, hash 77ED81E4 + sample 9: + time = 905000 + flags = 1 + data = length 545, hash 3321AEB9 + sample 10: + time = 928000 + flags = 1 + data = length 317, hash F557D0E + sample 11: + time = 952000 + flags = 1 + data = length 537, hash ED58CF7B + sample 12: + time = 975000 + flags = 1 + data = length 458, hash 51CDAA10 + sample 13: + time = 998000 + flags = 1 + data = length 465, hash CBA1EFD7 + sample 14: + time = 1021000 + flags = 1 + data = length 446, hash D6735B8A + sample 15: + time = 1044000 + flags = 1 + data = length 10, hash A453EEBE +tracksEnded = true diff --git a/library/core/src/test/assets/mp4/sample_fragmented_seekable.mp4.3.dump b/library/core/src/test/assets/mp4/sample_fragmented_seekable.mp4.3.dump new file mode 100644 index 0000000000..afd24e40ce --- /dev/null +++ b/library/core/src/test/assets/mp4/sample_fragmented_seekable.mp4.3.dump @@ -0,0 +1,181 @@ +seekMap: + isSeekable = true + duration = 1067733 + getPosition(0) = [[timeUs=66733, position=1325]] +numberOfTracks = 2 +track 0: + format: + bitrate = -1 + id = 1 + containerMimeType = null + sampleMimeType = video/avc + maxInputSize = -1 + width = 1080 + height = 720 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = -1 + sampleRate = -1 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = null + drmInitData = - + initializationData: + data = length 29, hash 4746B5D9 + data = length 10, hash 7A0D0F2B + total output bytes = 85933 + sample count = 30 + sample 0: + time = 66000 + flags = 1 + data = length 38070, hash B58E1AEE + sample 1: + time = 199000 + flags = 0 + data = length 8340, hash 8AC449FF + sample 2: + time = 132000 + flags = 0 + data = length 1295, hash C0DA5090 + sample 3: + time = 100000 + flags = 0 + data = length 469, hash D6E0A200 + sample 4: + time = 166000 + flags = 0 + data = length 564, hash E5F56C5B + sample 5: + time = 332000 + flags = 0 + data = length 6075, hash 8756E49E + sample 6: + time = 266000 + flags = 0 + data = length 847, hash DCC2B618 + sample 7: + time = 233000 + flags = 0 + data = length 455, hash B9CCE047 + sample 8: + time = 299000 + flags = 0 + data = length 467, hash 69806D94 + sample 9: + time = 466000 + flags = 0 + data = length 4549, hash 3944F501 + sample 10: + time = 399000 + flags = 0 + data = length 1087, hash 491BF106 + sample 11: + time = 367000 + flags = 0 + data = length 380, hash 5FED016A + sample 12: + time = 433000 + flags = 0 + data = length 455, hash 8A0610 + sample 13: + time = 599000 + flags = 0 + data = length 5190, hash B9031D8 + sample 14: + time = 533000 + flags = 0 + data = length 1071, hash 684E7DC8 + sample 15: + time = 500000 + flags = 0 + data = length 653, hash 8494F326 + sample 16: + time = 566000 + flags = 0 + data = length 485, hash 2CCC85F4 + sample 17: + time = 733000 + flags = 0 + data = length 4884, hash D16B6A96 + sample 18: + time = 666000 + flags = 0 + data = length 997, hash 164FF210 + sample 19: + time = 633000 + flags = 0 + data = length 640, hash F664125B + sample 20: + time = 700000 + flags = 0 + data = length 491, hash B5930C7C + sample 21: + time = 866000 + flags = 0 + data = length 2989, hash 92CF4FCF + sample 22: + time = 800000 + flags = 0 + data = length 838, hash 294A3451 + sample 23: + time = 767000 + flags = 0 + data = length 544, hash FCCE2DE6 + sample 24: + time = 833000 + flags = 0 + data = length 329, hash A654FFA1 + sample 25: + time = 1000000 + flags = 0 + data = length 1517, hash 5F7EBF8B + sample 26: + time = 933000 + flags = 0 + data = length 803, hash 7A5C4C1D + sample 27: + time = 900000 + flags = 0 + data = length 415, hash B31BBC3B + sample 28: + time = 967000 + flags = 0 + data = length 415, hash 850DFEA3 + sample 29: + time = 1033000 + flags = 0 + data = length 619, hash AB5E56CA +track 1: + format: + bitrate = -1 + id = 2 + containerMimeType = null + sampleMimeType = audio/mp4a-latm + maxInputSize = -1 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 1 + sampleRate = 44100 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = und + drmInitData = - + initializationData: + data = length 5, hash 2B7623A + total output bytes = 10 + sample count = 1 + sample 0: + time = 1044000 + flags = 1 + data = length 10, hash A453EEBE +tracksEnded = true diff --git a/library/core/src/test/assets/mp4/sample_fragmented_sei.mp4.0.dump b/library/core/src/test/assets/mp4/sample_fragmented_sei.mp4.0.dump index ae012055fe..87f2cc6714 100644 --- a/library/core/src/test/assets/mp4/sample_fragmented_sei.mp4.0.dump +++ b/library/core/src/test/assets/mp4/sample_fragmented_sei.mp4.0.dump @@ -18,8 +18,8 @@ track 0: channelCount = -1 sampleRate = -1 pcmEncoding = -1 - encoderDelay = -1 - encoderPadding = -1 + encoderDelay = 0 + encoderPadding = 0 subsampleOffsetUs = 9223372036854775807 selectionFlags = 0 language = null @@ -159,13 +159,13 @@ track 1: width = -1 height = -1 frameRate = -1.0 - rotationDegrees = -1 - pixelWidthHeightRatio = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 channelCount = 1 sampleRate = 44100 pcmEncoding = -1 - encoderDelay = -1 - encoderPadding = -1 + encoderDelay = 0 + encoderPadding = 0 subsampleOffsetUs = 9223372036854775807 selectionFlags = 0 language = und @@ -368,13 +368,13 @@ track 3: width = -1 height = -1 frameRate = -1.0 - rotationDegrees = -1 - pixelWidthHeightRatio = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 channelCount = -1 sampleRate = -1 pcmEncoding = -1 - encoderDelay = -1 - encoderPadding = -1 + encoderDelay = 0 + encoderPadding = 0 subsampleOffsetUs = 9223372036854775807 selectionFlags = 0 language = null diff --git a/library/core/src/test/assets/ogg/bear.opus.0.dump b/library/core/src/test/assets/ogg/bear.opus.0.dump index 643972b836..f8eadb16fa 100644 --- a/library/core/src/test/assets/ogg/bear.opus.0.dump +++ b/library/core/src/test/assets/ogg/bear.opus.0.dump @@ -13,13 +13,13 @@ track 0: width = -1 height = -1 frameRate = -1.0 - rotationDegrees = -1 - pixelWidthHeightRatio = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 channelCount = 2 sampleRate = 48000 pcmEncoding = -1 - encoderDelay = -1 - encoderPadding = -1 + encoderDelay = 0 + encoderPadding = 0 subsampleOffsetUs = 9223372036854775807 selectionFlags = 0 language = null diff --git a/library/core/src/test/assets/ogg/bear.opus.1.dump b/library/core/src/test/assets/ogg/bear.opus.1.dump index 8df1563d90..593116a22e 100644 --- a/library/core/src/test/assets/ogg/bear.opus.1.dump +++ b/library/core/src/test/assets/ogg/bear.opus.1.dump @@ -13,13 +13,13 @@ track 0: width = -1 height = -1 frameRate = -1.0 - rotationDegrees = -1 - pixelWidthHeightRatio = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 channelCount = 2 sampleRate = 48000 pcmEncoding = -1 - encoderDelay = -1 - encoderPadding = -1 + encoderDelay = 0 + encoderPadding = 0 subsampleOffsetUs = 9223372036854775807 selectionFlags = 0 language = null diff --git a/library/core/src/test/assets/ogg/bear.opus.2.dump b/library/core/src/test/assets/ogg/bear.opus.2.dump index bed4c46d9c..beabde35c8 100644 --- a/library/core/src/test/assets/ogg/bear.opus.2.dump +++ b/library/core/src/test/assets/ogg/bear.opus.2.dump @@ -13,13 +13,13 @@ track 0: width = -1 height = -1 frameRate = -1.0 - rotationDegrees = -1 - pixelWidthHeightRatio = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 channelCount = 2 sampleRate = 48000 pcmEncoding = -1 - encoderDelay = -1 - encoderPadding = -1 + encoderDelay = 0 + encoderPadding = 0 subsampleOffsetUs = 9223372036854775807 selectionFlags = 0 language = null diff --git a/library/core/src/test/assets/ogg/bear.opus.3.dump b/library/core/src/test/assets/ogg/bear.opus.3.dump index 8a9c99250e..d0f3e2948b 100644 --- a/library/core/src/test/assets/ogg/bear.opus.3.dump +++ b/library/core/src/test/assets/ogg/bear.opus.3.dump @@ -13,13 +13,13 @@ track 0: width = -1 height = -1 frameRate = -1.0 - rotationDegrees = -1 - pixelWidthHeightRatio = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 channelCount = 2 sampleRate = 48000 pcmEncoding = -1 - encoderDelay = -1 - encoderPadding = -1 + encoderDelay = 0 + encoderPadding = 0 subsampleOffsetUs = 9223372036854775807 selectionFlags = 0 language = null diff --git a/library/core/src/test/assets/ogg/bear.opus.unklen.dump b/library/core/src/test/assets/ogg/bear.opus.unklen.dump index 5d2c84b047..ec8f8b8665 100644 --- a/library/core/src/test/assets/ogg/bear.opus.unklen.dump +++ b/library/core/src/test/assets/ogg/bear.opus.unklen.dump @@ -13,13 +13,13 @@ track 0: width = -1 height = -1 frameRate = -1.0 - rotationDegrees = -1 - pixelWidthHeightRatio = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 channelCount = 2 sampleRate = 48000 pcmEncoding = -1 - encoderDelay = -1 - encoderPadding = -1 + encoderDelay = 0 + encoderPadding = 0 subsampleOffsetUs = 9223372036854775807 selectionFlags = 0 language = null diff --git a/library/core/src/test/assets/ogg/bear_flac.ogg.0.dump b/library/core/src/test/assets/ogg/bear_flac.ogg.0.dump index ff22bb2d3e..dbe97c02bd 100644 --- a/library/core/src/test/assets/ogg/bear_flac.ogg.0.dump +++ b/library/core/src/test/assets/ogg/bear_flac.ogg.0.dump @@ -13,13 +13,13 @@ track 0: width = -1 height = -1 frameRate = -1.0 - rotationDegrees = -1 - pixelWidthHeightRatio = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 channelCount = 2 sampleRate = 48000 pcmEncoding = -1 - encoderDelay = -1 - encoderPadding = -1 + encoderDelay = 0 + encoderPadding = 0 subsampleOffsetUs = 9223372036854775807 selectionFlags = 0 language = null diff --git a/library/core/src/test/assets/ogg/bear_flac.ogg.1.dump b/library/core/src/test/assets/ogg/bear_flac.ogg.1.dump index 50110149fd..d1246a3e64 100644 --- a/library/core/src/test/assets/ogg/bear_flac.ogg.1.dump +++ b/library/core/src/test/assets/ogg/bear_flac.ogg.1.dump @@ -13,13 +13,13 @@ track 0: width = -1 height = -1 frameRate = -1.0 - rotationDegrees = -1 - pixelWidthHeightRatio = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 channelCount = 2 sampleRate = 48000 pcmEncoding = -1 - encoderDelay = -1 - encoderPadding = -1 + encoderDelay = 0 + encoderPadding = 0 subsampleOffsetUs = 9223372036854775807 selectionFlags = 0 language = null diff --git a/library/core/src/test/assets/ogg/bear_flac.ogg.2.dump b/library/core/src/test/assets/ogg/bear_flac.ogg.2.dump index 483ae36721..ec0336309a 100644 --- a/library/core/src/test/assets/ogg/bear_flac.ogg.2.dump +++ b/library/core/src/test/assets/ogg/bear_flac.ogg.2.dump @@ -13,13 +13,13 @@ track 0: width = -1 height = -1 frameRate = -1.0 - rotationDegrees = -1 - pixelWidthHeightRatio = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 channelCount = 2 sampleRate = 48000 pcmEncoding = -1 - encoderDelay = -1 - encoderPadding = -1 + encoderDelay = 0 + encoderPadding = 0 subsampleOffsetUs = 9223372036854775807 selectionFlags = 0 language = null diff --git a/library/core/src/test/assets/ogg/bear_flac.ogg.3.dump b/library/core/src/test/assets/ogg/bear_flac.ogg.3.dump index a47407e63d..1e3254a9fc 100644 --- a/library/core/src/test/assets/ogg/bear_flac.ogg.3.dump +++ b/library/core/src/test/assets/ogg/bear_flac.ogg.3.dump @@ -13,13 +13,13 @@ track 0: width = -1 height = -1 frameRate = -1.0 - rotationDegrees = -1 - pixelWidthHeightRatio = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 channelCount = 2 sampleRate = 48000 pcmEncoding = -1 - encoderDelay = -1 - encoderPadding = -1 + encoderDelay = 0 + encoderPadding = 0 subsampleOffsetUs = 9223372036854775807 selectionFlags = 0 language = null diff --git a/library/core/src/test/assets/ogg/bear_flac.ogg.unklen.dump b/library/core/src/test/assets/ogg/bear_flac.ogg.unklen.dump index ff22bb2d3e..dbe97c02bd 100644 --- a/library/core/src/test/assets/ogg/bear_flac.ogg.unklen.dump +++ b/library/core/src/test/assets/ogg/bear_flac.ogg.unklen.dump @@ -13,13 +13,13 @@ track 0: width = -1 height = -1 frameRate = -1.0 - rotationDegrees = -1 - pixelWidthHeightRatio = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 channelCount = 2 sampleRate = 48000 pcmEncoding = -1 - encoderDelay = -1 - encoderPadding = -1 + encoderDelay = 0 + encoderPadding = 0 subsampleOffsetUs = 9223372036854775807 selectionFlags = 0 language = null diff --git a/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.0.dump b/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.0.dump index 32f350efcb..cce7bf2450 100644 --- a/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.0.dump +++ b/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.0.dump @@ -13,13 +13,13 @@ track 0: width = -1 height = -1 frameRate = -1.0 - rotationDegrees = -1 - pixelWidthHeightRatio = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 channelCount = 2 sampleRate = 48000 pcmEncoding = -1 - encoderDelay = -1 - encoderPadding = -1 + encoderDelay = 0 + encoderPadding = 0 subsampleOffsetUs = 9223372036854775807 selectionFlags = 0 language = null diff --git a/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.1.dump b/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.1.dump index 3082e8faca..ac36a48412 100644 --- a/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.1.dump +++ b/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.1.dump @@ -13,13 +13,13 @@ track 0: width = -1 height = -1 frameRate = -1.0 - rotationDegrees = -1 - pixelWidthHeightRatio = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 channelCount = 2 sampleRate = 48000 pcmEncoding = -1 - encoderDelay = -1 - encoderPadding = -1 + encoderDelay = 0 + encoderPadding = 0 subsampleOffsetUs = 9223372036854775807 selectionFlags = 0 language = null diff --git a/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.2.dump b/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.2.dump index b574409f70..dae0d878fa 100644 --- a/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.2.dump +++ b/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.2.dump @@ -13,13 +13,13 @@ track 0: width = -1 height = -1 frameRate = -1.0 - rotationDegrees = -1 - pixelWidthHeightRatio = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 channelCount = 2 sampleRate = 48000 pcmEncoding = -1 - encoderDelay = -1 - encoderPadding = -1 + encoderDelay = 0 + encoderPadding = 0 subsampleOffsetUs = 9223372036854775807 selectionFlags = 0 language = null diff --git a/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.3.dump b/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.3.dump index f411596b44..c9570ab58e 100644 --- a/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.3.dump +++ b/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.3.dump @@ -13,13 +13,13 @@ track 0: width = -1 height = -1 frameRate = -1.0 - rotationDegrees = -1 - pixelWidthHeightRatio = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 channelCount = 2 sampleRate = 48000 pcmEncoding = -1 - encoderDelay = -1 - encoderPadding = -1 + encoderDelay = 0 + encoderPadding = 0 subsampleOffsetUs = 9223372036854775807 selectionFlags = 0 language = null diff --git a/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.unklen.dump b/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.unklen.dump index bdfe90277d..7a3e7ef5ac 100644 --- a/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.unklen.dump +++ b/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.unklen.dump @@ -13,13 +13,13 @@ track 0: width = -1 height = -1 frameRate = -1.0 - rotationDegrees = -1 - pixelWidthHeightRatio = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 channelCount = 2 sampleRate = 48000 pcmEncoding = -1 - encoderDelay = -1 - encoderPadding = -1 + encoderDelay = 0 + encoderPadding = 0 subsampleOffsetUs = 9223372036854775807 selectionFlags = 0 language = null diff --git a/library/core/src/test/assets/ogg/bear_vorbis.ogg.0.dump b/library/core/src/test/assets/ogg/bear_vorbis.ogg.0.dump index dd129ce9dc..138e13c54d 100644 --- a/library/core/src/test/assets/ogg/bear_vorbis.ogg.0.dump +++ b/library/core/src/test/assets/ogg/bear_vorbis.ogg.0.dump @@ -13,13 +13,13 @@ track 0: width = -1 height = -1 frameRate = -1.0 - rotationDegrees = -1 - pixelWidthHeightRatio = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 channelCount = 2 sampleRate = 48000 pcmEncoding = -1 - encoderDelay = -1 - encoderPadding = -1 + encoderDelay = 0 + encoderPadding = 0 subsampleOffsetUs = 9223372036854775807 selectionFlags = 0 language = null diff --git a/library/core/src/test/assets/ogg/bear_vorbis.ogg.1.dump b/library/core/src/test/assets/ogg/bear_vorbis.ogg.1.dump index 4fb8a74d92..6b37dfb6cf 100644 --- a/library/core/src/test/assets/ogg/bear_vorbis.ogg.1.dump +++ b/library/core/src/test/assets/ogg/bear_vorbis.ogg.1.dump @@ -13,13 +13,13 @@ track 0: width = -1 height = -1 frameRate = -1.0 - rotationDegrees = -1 - pixelWidthHeightRatio = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 channelCount = 2 sampleRate = 48000 pcmEncoding = -1 - encoderDelay = -1 - encoderPadding = -1 + encoderDelay = 0 + encoderPadding = 0 subsampleOffsetUs = 9223372036854775807 selectionFlags = 0 language = null diff --git a/library/core/src/test/assets/ogg/bear_vorbis.ogg.2.dump b/library/core/src/test/assets/ogg/bear_vorbis.ogg.2.dump index fad8f33d77..9620979357 100644 --- a/library/core/src/test/assets/ogg/bear_vorbis.ogg.2.dump +++ b/library/core/src/test/assets/ogg/bear_vorbis.ogg.2.dump @@ -13,13 +13,13 @@ track 0: width = -1 height = -1 frameRate = -1.0 - rotationDegrees = -1 - pixelWidthHeightRatio = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 channelCount = 2 sampleRate = 48000 pcmEncoding = -1 - encoderDelay = -1 - encoderPadding = -1 + encoderDelay = 0 + encoderPadding = 0 subsampleOffsetUs = 9223372036854775807 selectionFlags = 0 language = null diff --git a/library/core/src/test/assets/ogg/bear_vorbis.ogg.3.dump b/library/core/src/test/assets/ogg/bear_vorbis.ogg.3.dump index 49dca02220..18d869030d 100644 --- a/library/core/src/test/assets/ogg/bear_vorbis.ogg.3.dump +++ b/library/core/src/test/assets/ogg/bear_vorbis.ogg.3.dump @@ -13,13 +13,13 @@ track 0: width = -1 height = -1 frameRate = -1.0 - rotationDegrees = -1 - pixelWidthHeightRatio = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 channelCount = 2 sampleRate = 48000 pcmEncoding = -1 - encoderDelay = -1 - encoderPadding = -1 + encoderDelay = 0 + encoderPadding = 0 subsampleOffsetUs = 9223372036854775807 selectionFlags = 0 language = null diff --git a/library/core/src/test/assets/ogg/bear_vorbis.ogg.unklen.dump b/library/core/src/test/assets/ogg/bear_vorbis.ogg.unklen.dump index 756be42854..2686f740db 100644 --- a/library/core/src/test/assets/ogg/bear_vorbis.ogg.unklen.dump +++ b/library/core/src/test/assets/ogg/bear_vorbis.ogg.unklen.dump @@ -13,13 +13,13 @@ track 0: width = -1 height = -1 frameRate = -1.0 - rotationDegrees = -1 - pixelWidthHeightRatio = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 channelCount = 2 sampleRate = 48000 pcmEncoding = -1 - encoderDelay = -1 - encoderPadding = -1 + encoderDelay = 0 + encoderPadding = 0 subsampleOffsetUs = 9223372036854775807 selectionFlags = 0 language = null diff --git a/library/core/src/test/assets/rawcc/sample.rawcc.0.dump b/library/core/src/test/assets/rawcc/sample.rawcc.0.dump index 130be06ceb..adeaaf6a37 100644 --- a/library/core/src/test/assets/rawcc/sample.rawcc.0.dump +++ b/library/core/src/test/assets/rawcc/sample.rawcc.0.dump @@ -13,13 +13,13 @@ track 0: width = -1 height = -1 frameRate = -1.0 - rotationDegrees = -1 - pixelWidthHeightRatio = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 channelCount = -1 sampleRate = -1 pcmEncoding = -1 - encoderDelay = -1 - encoderPadding = -1 + encoderDelay = 0 + encoderPadding = 0 subsampleOffsetUs = 9223372036854775807 selectionFlags = 0 language = null diff --git a/library/core/src/test/assets/ts/sample.ac3.0.dump b/library/core/src/test/assets/ts/sample.ac3.0.dump index 46028638fe..a1d29a77dc 100644 --- a/library/core/src/test/assets/ts/sample.ac3.0.dump +++ b/library/core/src/test/assets/ts/sample.ac3.0.dump @@ -13,13 +13,13 @@ track 0: width = -1 height = -1 frameRate = -1.0 - rotationDegrees = -1 - pixelWidthHeightRatio = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 channelCount = 6 sampleRate = 48000 pcmEncoding = -1 - encoderDelay = -1 - encoderPadding = -1 + encoderDelay = 0 + encoderPadding = 0 subsampleOffsetUs = 9223372036854775807 selectionFlags = 0 language = null diff --git a/library/core/src/test/assets/ts/sample.adts.0.dump b/library/core/src/test/assets/ts/sample.adts.0.dump index 132859a00e..93d7b776c0 100644 --- a/library/core/src/test/assets/ts/sample.adts.0.dump +++ b/library/core/src/test/assets/ts/sample.adts.0.dump @@ -13,13 +13,13 @@ track 0: width = -1 height = -1 frameRate = -1.0 - rotationDegrees = -1 - pixelWidthHeightRatio = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 channelCount = 1 sampleRate = 44100 pcmEncoding = -1 - encoderDelay = -1 - encoderPadding = -1 + encoderDelay = 0 + encoderPadding = 0 subsampleOffsetUs = 9223372036854775807 selectionFlags = 0 language = null @@ -614,13 +614,13 @@ track 1: width = -1 height = -1 frameRate = -1.0 - rotationDegrees = -1 - pixelWidthHeightRatio = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 channelCount = -1 sampleRate = -1 pcmEncoding = -1 - encoderDelay = -1 - encoderPadding = -1 + encoderDelay = 0 + encoderPadding = 0 subsampleOffsetUs = 9223372036854775807 selectionFlags = 0 language = null diff --git a/library/core/src/test/assets/ts/sample.ps.0.dump b/library/core/src/test/assets/ts/sample.ps.0.dump index e833201692..dda6de8ab4 100644 --- a/library/core/src/test/assets/ts/sample.ps.0.dump +++ b/library/core/src/test/assets/ts/sample.ps.0.dump @@ -13,13 +13,13 @@ track 192: width = -1 height = -1 frameRate = -1.0 - rotationDegrees = -1 - pixelWidthHeightRatio = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 channelCount = 1 sampleRate = 44100 pcmEncoding = -1 - encoderDelay = -1 - encoderPadding = -1 + encoderDelay = 0 + encoderPadding = 0 subsampleOffsetUs = 9223372036854775807 selectionFlags = 0 language = null @@ -53,13 +53,13 @@ track 224: width = 640 height = 426 frameRate = -1.0 - rotationDegrees = -1 + rotationDegrees = 0 pixelWidthHeightRatio = 1.0 channelCount = -1 sampleRate = -1 pcmEncoding = -1 - encoderDelay = -1 - encoderPadding = -1 + encoderDelay = 0 + encoderPadding = 0 subsampleOffsetUs = 9223372036854775807 selectionFlags = 0 language = null diff --git a/library/core/src/test/assets/ts/sample.ts.0.dump b/library/core/src/test/assets/ts/sample.ts.0.dump index 39b1565289..a74268a702 100644 --- a/library/core/src/test/assets/ts/sample.ts.0.dump +++ b/library/core/src/test/assets/ts/sample.ts.0.dump @@ -13,13 +13,13 @@ track 256: width = 640 height = 426 frameRate = -1.0 - rotationDegrees = -1 + rotationDegrees = 0 pixelWidthHeightRatio = 1.0 channelCount = -1 sampleRate = -1 pcmEncoding = -1 - encoderDelay = -1 - encoderPadding = -1 + encoderDelay = 0 + encoderPadding = 0 subsampleOffsetUs = 9223372036854775807 selectionFlags = 0 language = null @@ -46,13 +46,13 @@ track 257: width = -1 height = -1 frameRate = -1.0 - rotationDegrees = -1 - pixelWidthHeightRatio = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 channelCount = 1 sampleRate = 44100 pcmEncoding = -1 - encoderDelay = -1 - encoderPadding = -1 + encoderDelay = 0 + encoderPadding = 0 subsampleOffsetUs = 9223372036854775807 selectionFlags = 0 language = und diff --git a/library/core/src/test/assets/wav/sample.wav.0.dump b/library/core/src/test/assets/wav/sample.wav.0.dump index 32f9d495d2..a6c46f75fc 100644 --- a/library/core/src/test/assets/wav/sample.wav.0.dump +++ b/library/core/src/test/assets/wav/sample.wav.0.dump @@ -13,13 +13,13 @@ track 0: width = -1 height = -1 frameRate = -1.0 - rotationDegrees = -1 - pixelWidthHeightRatio = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 channelCount = 1 sampleRate = 44100 pcmEncoding = 2 - encoderDelay = -1 - encoderPadding = -1 + encoderDelay = 0 + encoderPadding = 0 subsampleOffsetUs = 9223372036854775807 selectionFlags = 0 language = null diff --git a/library/core/src/test/assets/wav/sample.wav.1.dump b/library/core/src/test/assets/wav/sample.wav.1.dump index d4758e65b5..3cc70dc71f 100644 --- a/library/core/src/test/assets/wav/sample.wav.1.dump +++ b/library/core/src/test/assets/wav/sample.wav.1.dump @@ -13,13 +13,13 @@ track 0: width = -1 height = -1 frameRate = -1.0 - rotationDegrees = -1 - pixelWidthHeightRatio = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 channelCount = 1 sampleRate = 44100 pcmEncoding = 2 - encoderDelay = -1 - encoderPadding = -1 + encoderDelay = 0 + encoderPadding = 0 subsampleOffsetUs = 9223372036854775807 selectionFlags = 0 language = null diff --git a/library/core/src/test/assets/wav/sample.wav.2.dump b/library/core/src/test/assets/wav/sample.wav.2.dump index ea33c62423..07ce135dfa 100644 --- a/library/core/src/test/assets/wav/sample.wav.2.dump +++ b/library/core/src/test/assets/wav/sample.wav.2.dump @@ -13,13 +13,13 @@ track 0: width = -1 height = -1 frameRate = -1.0 - rotationDegrees = -1 - pixelWidthHeightRatio = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 channelCount = 1 sampleRate = 44100 pcmEncoding = 2 - encoderDelay = -1 - encoderPadding = -1 + encoderDelay = 0 + encoderPadding = 0 subsampleOffsetUs = 9223372036854775807 selectionFlags = 0 language = null diff --git a/library/core/src/test/assets/wav/sample.wav.3.dump b/library/core/src/test/assets/wav/sample.wav.3.dump index de0d8f22d0..82ed95ad60 100644 --- a/library/core/src/test/assets/wav/sample.wav.3.dump +++ b/library/core/src/test/assets/wav/sample.wav.3.dump @@ -13,13 +13,13 @@ track 0: width = -1 height = -1 frameRate = -1.0 - rotationDegrees = -1 - pixelWidthHeightRatio = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 channelCount = 1 sampleRate = 44100 pcmEncoding = 2 - encoderDelay = -1 - encoderPadding = -1 + encoderDelay = 0 + encoderPadding = 0 subsampleOffsetUs = 9223372036854775807 selectionFlags = 0 language = null diff --git a/library/core/src/test/java/com/google/android/exoplayer2/DefaultLoadControlTest.java b/library/core/src/test/java/com/google/android/exoplayer2/DefaultLoadControlTest.java new file mode 100644 index 0000000000..b066cc263a --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/DefaultLoadControlTest.java @@ -0,0 +1,127 @@ +/* + * 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; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.android.exoplayer2.DefaultLoadControl.Builder; +import com.google.android.exoplayer2.upstream.DefaultAllocator; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +/** Unit tests for {@link DefaultLoadControl}. */ +@RunWith(RobolectricTestRunner.class) +public class DefaultLoadControlTest { + + private static final float SPEED = 1f; + private static final long MIN_BUFFER_US = C.msToUs(DefaultLoadControl.DEFAULT_MIN_BUFFER_MS); + private static final long MAX_BUFFER_US = C.msToUs(DefaultLoadControl.DEFAULT_MAX_BUFFER_MS); + private static final int TARGET_BUFFER_BYTES = C.DEFAULT_BUFFER_SEGMENT_SIZE * 2; + + private Builder builder; + private DefaultAllocator allocator; + private DefaultLoadControl loadControl; + + @Before + public void setUp() throws Exception { + builder = new Builder(); + allocator = new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE); + } + + @Test + public void testShouldContinueLoading_untilMaxBufferExceeded() { + createDefaultLoadControl(); + assertThat(loadControl.shouldContinueLoading(/* bufferedDurationUs= */ 0, SPEED)).isTrue(); + assertThat(loadControl.shouldContinueLoading(MIN_BUFFER_US, SPEED)).isTrue(); + assertThat(loadControl.shouldContinueLoading(MAX_BUFFER_US, SPEED)).isTrue(); + assertThat(loadControl.shouldContinueLoading(MAX_BUFFER_US + 1, SPEED)).isFalse(); + } + + @Test + public void testShouldNotContinueLoadingOnceBufferingStopped_untilBelowMinBuffer() { + createDefaultLoadControl(); + assertThat(loadControl.shouldContinueLoading(MAX_BUFFER_US + 1, SPEED)).isFalse(); + assertThat(loadControl.shouldContinueLoading(MAX_BUFFER_US, SPEED)).isFalse(); + assertThat(loadControl.shouldContinueLoading(MIN_BUFFER_US, SPEED)).isFalse(); + assertThat(loadControl.shouldContinueLoading(MIN_BUFFER_US - 1, SPEED)).isTrue(); + } + + @Test + public void testShouldContinueLoadingWithTargetBufferBytesReached_untilMinBufferReached() { + createDefaultLoadControl(); + makeSureTargetBufferBytesReached(); + + assertThat(loadControl.shouldContinueLoading(/* bufferedDurationUs= */ 0, SPEED)).isTrue(); + assertThat(loadControl.shouldContinueLoading(MIN_BUFFER_US - 1, SPEED)).isTrue(); + assertThat(loadControl.shouldContinueLoading(MIN_BUFFER_US, SPEED)).isFalse(); + assertThat(loadControl.shouldContinueLoading(MAX_BUFFER_US + 1, SPEED)).isFalse(); + } + + @Test + public void testShouldNeverContinueLoading_ifMaxBufferReachedAndNotPrioritizeTimeOverSize() { + builder.setPrioritizeTimeOverSizeThresholds(false); + createDefaultLoadControl(); + // Put loadControl in buffering state. + assertThat(loadControl.shouldContinueLoading(/* bufferedDurationUs= */ 0, SPEED)).isTrue(); + makeSureTargetBufferBytesReached(); + + assertThat(loadControl.shouldContinueLoading(/* bufferedDurationUs= */ 0, SPEED)).isFalse(); + assertThat(loadControl.shouldContinueLoading(MIN_BUFFER_US, SPEED)).isFalse(); + assertThat(loadControl.shouldContinueLoading(MAX_BUFFER_US + 1, SPEED)).isFalse(); + } + + @Test + public void testShouldContinueLoadingWithMinBufferReached_inFastPlayback() { + createDefaultLoadControl(); + + // At normal playback speed, we stop buffering when the buffer reaches the minimum. + assertThat(loadControl.shouldContinueLoading(MIN_BUFFER_US, SPEED)).isFalse(); + + // At double playback speed, we continue loading. + assertThat(loadControl.shouldContinueLoading(MIN_BUFFER_US, /* playbackSpeed= */ 2f)).isTrue(); + } + + @Test + public void testShouldNotContinueLoadingWithMaxBufferReached_inFastPlayback() { + createDefaultLoadControl(); + + assertThat(loadControl.shouldContinueLoading(MAX_BUFFER_US + 1, /* playbackSpeed= */ 100f)) + .isFalse(); + } + + @Test + public void testStartsPlayback_whenMinBufferSizeReached() { + createDefaultLoadControl(); + assertThat(loadControl.shouldStartPlayback(MIN_BUFFER_US, SPEED, /* rebuffering= */ false)) + .isTrue(); + } + + private void createDefaultLoadControl() { + builder.setAllocator(allocator); + builder.setTargetBufferBytes(TARGET_BUFFER_BYTES); + loadControl = builder.createDefaultLoadControl(); + loadControl.onTracksSelected(new Renderer[0], null, null); + } + + private void makeSureTargetBufferBytesReached() { + while (allocator.getTotalBytesAllocated() < TARGET_BUFFER_BYTES) { + allocator.allocate(); + } + } + +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/DefaultMediaClockTest.java b/library/core/src/test/java/com/google/android/exoplayer2/DefaultMediaClockTest.java index ece22dc02a..d2d032870e 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/DefaultMediaClockTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/DefaultMediaClockTest.java @@ -40,7 +40,7 @@ public class DefaultMediaClockTest { private static final long TEST_POSITION_US = 123456789012345678L; private static final long SLEEP_TIME_MS = 1_000; private static final PlaybackParameters TEST_PLAYBACK_PARAMETERS = - new PlaybackParameters(2.0f, 1.0f); + new PlaybackParameters(/* speed= */ 2f); @Mock private PlaybackParameterListener listener; private FakeClock fakeClock; diff --git a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java index b1ddcdb207..ed91f6651c 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -25,6 +25,7 @@ import com.google.android.exoplayer2.Timeline.Window; import com.google.android.exoplayer2.source.ConcatenatingMediaSource; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; +import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.ads.AdPlaybackState; @@ -180,8 +181,7 @@ public final class ExoPlayerTest { // media clock position will be the start of the timeline until the stream is set to be // final, at which point it jumps to the end of the timeline allowing the playing period // to advance. - // TODO: Avoid hard-coding ExoPlayerImplInternal.RENDERER_TIMESTAMP_OFFSET_US. - return isCurrentStreamFinal() ? 60000030 : 60000000; + return isCurrentStreamFinal() ? 30 : 0; } @Override @@ -228,9 +228,9 @@ public final class ExoPlayerTest { MediaSource secondSource = new FakeMediaSource(timeline, new Object(), Builder.VIDEO_FORMAT) { @Override - public synchronized void prepareSource( - ExoPlayer player, boolean isTopLevelSource, Listener listener) { - super.prepareSource(player, isTopLevelSource, listener); + public synchronized void prepareSourceInternal( + ExoPlayer player, boolean isTopLevelSource) { + super.prepareSourceInternal(player, isTopLevelSource); // We've queued a source info refresh on the playback thread's event queue. Allow the // test thread to prepare the player with the third source, and block this thread (the // playback thread) until the test thread's call to prepare() has returned. @@ -573,8 +573,11 @@ public final class ExoPlayerTest { new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT) { @Override protected FakeMediaPeriod createFakeMediaPeriod( - MediaPeriodId id, TrackGroupArray trackGroupArray, Allocator allocator) { - FakeMediaPeriod mediaPeriod = new FakeMediaPeriod(trackGroupArray); + MediaPeriodId id, + TrackGroupArray trackGroupArray, + Allocator allocator, + EventDispatcher eventDispatcher) { + FakeMediaPeriod mediaPeriod = new FakeMediaPeriod(trackGroupArray, eventDispatcher); mediaPeriod.setSeekToUsOffset(10); return mediaPeriod; } @@ -604,8 +607,11 @@ public final class ExoPlayerTest { new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT) { @Override protected FakeMediaPeriod createFakeMediaPeriod( - MediaPeriodId id, TrackGroupArray trackGroupArray, Allocator allocator) { - FakeMediaPeriod mediaPeriod = new FakeMediaPeriod(trackGroupArray); + MediaPeriodId id, + TrackGroupArray trackGroupArray, + Allocator allocator, + EventDispatcher eventDispatcher) { + FakeMediaPeriod mediaPeriod = new FakeMediaPeriod(trackGroupArray, eventDispatcher); mediaPeriod.setDiscontinuityPositionUs(10); return mediaPeriod; } @@ -626,8 +632,11 @@ public final class ExoPlayerTest { new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT) { @Override protected FakeMediaPeriod createFakeMediaPeriod( - MediaPeriodId id, TrackGroupArray trackGroupArray, Allocator allocator) { - FakeMediaPeriod mediaPeriod = new FakeMediaPeriod(trackGroupArray); + MediaPeriodId id, + TrackGroupArray trackGroupArray, + Allocator allocator, + EventDispatcher eventDispatcher) { + FakeMediaPeriod mediaPeriod = new FakeMediaPeriod(trackGroupArray, eventDispatcher); mediaPeriod.setDiscontinuityPositionUs(0); return mediaPeriod; } @@ -659,7 +668,7 @@ public final class ExoPlayerTest { .start() .blockUntilEnded(TIMEOUT_MS); - List createdTrackSelections = trackSelector.getSelectedTrackSelections(); + List createdTrackSelections = trackSelector.getAllTrackSelections(); int numSelectionsEnabled = 0; // Assert that all tracks selection are disabled at the end of the playback. for (FakeTrackSelection trackSelection : createdTrackSelections) { @@ -667,9 +676,7 @@ public final class ExoPlayerTest { numSelectionsEnabled += trackSelection.enableCount; } // There are 2 renderers, and track selections are made once (1 period). - // Track selections are not reused, so there are 2 track selections made. assertThat(createdTrackSelections).hasSize(2); - // There should be 2 track selections enabled in total. assertThat(numSelectionsEnabled).isEqualTo(2); } @@ -690,7 +697,7 @@ public final class ExoPlayerTest { .start() .blockUntilEnded(TIMEOUT_MS); - List createdTrackSelections = trackSelector.getSelectedTrackSelections(); + List createdTrackSelections = trackSelector.getAllTrackSelections(); int numSelectionsEnabled = 0; // Assert that all tracks selection are disabled at the end of the playback. for (FakeTrackSelection trackSelection : createdTrackSelections) { @@ -698,9 +705,7 @@ public final class ExoPlayerTest { numSelectionsEnabled += trackSelection.enableCount; } // There are 2 renderers, and track selections are made twice (2 periods). - // Track selections are not reused, so there are 4 track selections made. assertThat(createdTrackSelections).hasSize(4); - // There should be 4 track selections enabled in total. assertThat(numSelectionsEnabled).isEqualTo(4); } @@ -717,13 +722,7 @@ public final class ExoPlayerTest { new ActionSchedule.Builder("testChangeTrackSelection") .pause() .waitForPlaybackState(Player.STATE_READY) - .executeRunnable( - new Runnable() { - @Override - public void run() { - trackSelector.setRendererDisabled(0, true); - } - }) + .disableRenderer(0) .play() .build(); @@ -736,23 +735,21 @@ public final class ExoPlayerTest { .start() .blockUntilEnded(TIMEOUT_MS); - List createdTrackSelections = trackSelector.getSelectedTrackSelections(); + List createdTrackSelections = trackSelector.getAllTrackSelections(); int numSelectionsEnabled = 0; // Assert that all tracks selection are disabled at the end of the playback. for (FakeTrackSelection trackSelection : createdTrackSelections) { assertThat(trackSelection.isEnabled).isFalse(); numSelectionsEnabled += trackSelection.enableCount; } - // There are 2 renderers, and track selections are made twice. - // Track selections are not reused, so there are 4 track selections made. + // There are 2 renderers, and track selections are made twice. The second time one renderer is + // disabled, so only one out of the two track selections is enabled. assertThat(createdTrackSelections).hasSize(4); - // Initially there are 2 track selections enabled. - // The second time one renderer is disabled, so only 1 track selection should be enabled. assertThat(numSelectionsEnabled).isEqualTo(3); } @Test - public void testAllActivatedTrackSelectionAreReleasedWhenTrackSelectionsAreUsed() + public void testAllActivatedTrackSelectionAreReleasedWhenTrackSelectionsAreReused() throws Exception { Timeline timeline = new FakeTimeline(/* windowCount= */ 1); MediaSource mediaSource = @@ -764,13 +761,7 @@ public final class ExoPlayerTest { new ActionSchedule.Builder("testReuseTrackSelection") .pause() .waitForPlaybackState(Player.STATE_READY) - .executeRunnable( - new Runnable() { - @Override - public void run() { - trackSelector.setRendererDisabled(0, true); - } - }) + .disableRenderer(0) .play() .build(); @@ -783,18 +774,17 @@ public final class ExoPlayerTest { .start() .blockUntilEnded(TIMEOUT_MS); - List createdTrackSelections = trackSelector.getSelectedTrackSelections(); + List createdTrackSelections = trackSelector.getAllTrackSelections(); int numSelectionsEnabled = 0; // Assert that all tracks selection are disabled at the end of the playback. for (FakeTrackSelection trackSelection : createdTrackSelections) { assertThat(trackSelection.isEnabled).isFalse(); numSelectionsEnabled += trackSelection.enableCount; } - // There are 2 renderers, and track selections are made twice. - // TrackSelections are reused, so there are only 2 track selections made for 2 renderers. + // There are 2 renderers, and track selections are made twice. The second time one renderer is + // disabled, and the selector re-uses the previous selection for the enabled renderer. So we + // expect two track selections, one of which will have been enabled twice. assertThat(createdTrackSelections).hasSize(2); - // Initially there are 2 track selections enabled. - // The second time one renderer is disabled, so only 1 track selection should be enabled. assertThat(numSelectionsEnabled).isEqualTo(3); } @@ -878,10 +868,13 @@ public final class ExoPlayerTest { new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1), null, Builder.VIDEO_FORMAT) { @Override protected FakeMediaPeriod createFakeMediaPeriod( - MediaPeriodId id, TrackGroupArray trackGroupArray, Allocator allocator) { + MediaPeriodId id, + TrackGroupArray trackGroupArray, + Allocator allocator, + EventDispatcher eventDispatcher) { // Defer completing preparation of the period until playback parameters have been set. fakeMediaPeriodHolder[0] = - new FakeMediaPeriod(trackGroupArray, /* deferOnPrepared= */ true); + new FakeMediaPeriod(trackGroupArray, eventDispatcher, /* deferOnPrepared= */ true); createPeriodCalledCountDownLatch.countDown(); return fakeMediaPeriodHolder[0]; } @@ -902,7 +895,7 @@ public final class ExoPlayerTest { } }) // Set playback parameters (while the fake media period is not yet prepared). - .setPlaybackParameters(new PlaybackParameters(2f, 2f)) + .setPlaybackParameters(new PlaybackParameters(/* speed= */ 2f, /* pitch= */ 2f)) // Complete preparation of the fake media period. .executeRunnable( new Runnable() { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/FormatTest.java b/library/core/src/test/java/com/google/android/exoplayer2/FormatTest.java index eb51485a36..7ca2181ebf 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/FormatTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/FormatTest.java @@ -20,20 +20,14 @@ import static com.google.android.exoplayer2.util.MimeTypes.VIDEO_MP4; import static com.google.android.exoplayer2.util.MimeTypes.VIDEO_WEBM; import static com.google.common.truth.Truth.assertThat; -import android.annotation.SuppressLint; -import android.annotation.TargetApi; -import android.media.MediaFormat; import android.os.Parcel; import com.google.android.exoplayer2.drm.DrmInitData; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.id3.TextInformationFrame; import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.util.MimeTypes; -import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.video.ColorInfo; -import java.nio.ByteBuffer; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.List; import org.junit.Test; @@ -85,73 +79,4 @@ public final class FormatTest { parcel.recycle(); } - @Test - public void testConversionToFrameworkMediaFormat() { - if (Util.SDK_INT < 16) { - // Test doesn't apply. - return; - } - - testConversionToFrameworkMediaFormatV16(Format.createVideoSampleFormat(null, "video/xyz", null, - 5000, 102400, 1280, 720, 30, INIT_DATA, null)); - testConversionToFrameworkMediaFormatV16(Format.createVideoSampleFormat(null, "video/xyz", null, - 5000, Format.NO_VALUE, 1280, 720, 30, null, null)); - testConversionToFrameworkMediaFormatV16(Format.createAudioSampleFormat(null, "audio/xyz", null, - 500, 128, 5, 44100, INIT_DATA, null, 0, null)); - testConversionToFrameworkMediaFormatV16(Format.createAudioSampleFormat(null, "audio/xyz", null, - 500, Format.NO_VALUE, 5, 44100, null, null, 0, null)); - testConversionToFrameworkMediaFormatV16(Format.createTextSampleFormat(null, "text/xyz", 0, - "eng")); - testConversionToFrameworkMediaFormatV16(Format.createTextSampleFormat(null, "text/xyz", 0, - null)); - } - - @SuppressLint("InlinedApi") - @TargetApi(16) - private static void testConversionToFrameworkMediaFormatV16(Format in) { - MediaFormat out = in.getFrameworkMediaFormatV16(); - assertThat(out.getString(MediaFormat.KEY_MIME)).isEqualTo(in.sampleMimeType); - assertOptionalV16(out, MediaFormat.KEY_LANGUAGE, in.language); - assertOptionalV16(out, MediaFormat.KEY_MAX_INPUT_SIZE, in.maxInputSize); - assertOptionalV16(out, MediaFormat.KEY_WIDTH, in.width); - assertOptionalV16(out, MediaFormat.KEY_HEIGHT, in.height); - assertOptionalV16(out, MediaFormat.KEY_CHANNEL_COUNT, in.channelCount); - assertOptionalV16(out, MediaFormat.KEY_SAMPLE_RATE, in.sampleRate); - assertOptionalV16(out, MediaFormat.KEY_FRAME_RATE, in.frameRate); - - for (int i = 0; i < in.initializationData.size(); i++) { - byte[] originalData = in.initializationData.get(i); - ByteBuffer frameworkBuffer = out.getByteBuffer("csd-" + i); - byte[] frameworkData = Arrays.copyOf(frameworkBuffer.array(), frameworkBuffer.limit()); - assertThat(frameworkData).isEqualTo(originalData); - } - } - - @TargetApi(16) - private static void assertOptionalV16(MediaFormat format, String key, String value) { - if (value == null) { - assertThat(format.containsKey(key)).isEqualTo(false); - } else { - assertThat(format.getString(key)).isEqualTo(value); - } - } - - @TargetApi(16) - private static void assertOptionalV16(MediaFormat format, String key, int value) { - if (value == Format.NO_VALUE) { - assertThat(format.containsKey(key)).isEqualTo(false); - } else { - assertThat(format.getInteger(key)).isEqualTo(value); - } - } - - @TargetApi(16) - private static void assertOptionalV16(MediaFormat format, String key, float value) { - if (value == Format.NO_VALUE) { - assertThat(format.containsKey(key)).isEqualTo(false); - } else { - assertThat(format.getFloat(key)).isEqualTo(value); - } - } - } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/TimelineTest.java b/library/core/src/test/java/com/google/android/exoplayer2/TimelineTest.java index b5457555ab..88617ce8ec 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/TimelineTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/TimelineTest.java @@ -34,7 +34,7 @@ public class TimelineTest { @Test public void testSinglePeriodTimeline() { Timeline timeline = new FakeTimeline(new TimelineWindowDefinition(1, 111)); - TimelineAsserts.assertWindowIds(timeline, 111); + TimelineAsserts.assertWindowTags(timeline, 111); TimelineAsserts.assertPeriodCounts(timeline, 1); TimelineAsserts.assertPreviousWindowIndices( timeline, Player.REPEAT_MODE_OFF, false, C.INDEX_UNSET); @@ -48,7 +48,7 @@ public class TimelineTest { @Test public void testMultiPeriodTimeline() { Timeline timeline = new FakeTimeline(new TimelineWindowDefinition(5, 111)); - TimelineAsserts.assertWindowIds(timeline, 111); + TimelineAsserts.assertWindowTags(timeline, 111); TimelineAsserts.assertPeriodCounts(timeline, 5); TimelineAsserts.assertPreviousWindowIndices( timeline, Player.REPEAT_MODE_OFF, false, C.INDEX_UNSET); 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 new file mode 100644 index 0000000000..829fa5a2b8 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java @@ -0,0 +1,1120 @@ +/* + * 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.analytics; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.os.Handler; +import android.os.SystemClock; +import android.support.annotation.Nullable; +import android.view.Surface; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.PlaybackParameters; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Renderer; +import com.google.android.exoplayer2.RenderersFactory; +import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.Timeline.Window; +import com.google.android.exoplayer2.audio.AudioRendererEventListener; +import com.google.android.exoplayer2.decoder.DecoderCounters; +import com.google.android.exoplayer2.drm.DrmSessionManager; +import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; +import com.google.android.exoplayer2.metadata.Metadata; +import com.google.android.exoplayer2.metadata.MetadataOutput; +import com.google.android.exoplayer2.source.ConcatenatingMediaSource; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; +import com.google.android.exoplayer2.source.MediaSourceEventListener.LoadEventInfo; +import com.google.android.exoplayer2.source.MediaSourceEventListener.MediaLoadData; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.testutil.ActionSchedule; +import com.google.android.exoplayer2.testutil.ActionSchedule.PlayerRunnable; +import com.google.android.exoplayer2.testutil.ExoPlayerTestRunner; +import com.google.android.exoplayer2.testutil.ExoPlayerTestRunner.Builder; +import com.google.android.exoplayer2.testutil.FakeMediaSource; +import com.google.android.exoplayer2.testutil.FakeRenderer; +import com.google.android.exoplayer2.testutil.FakeTimeline; +import com.google.android.exoplayer2.testutil.RobolectricUtil; +import com.google.android.exoplayer2.text.TextOutput; +import com.google.android.exoplayer2.trackselection.TrackSelectionArray; +import com.google.android.exoplayer2.util.Util; +import com.google.android.exoplayer2.video.VideoRendererEventListener; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; + +/** Integration test for {@link AnalyticsCollector}. */ +@RunWith(RobolectricTestRunner.class) +@Config(shadows = {RobolectricUtil.CustomLooper.class, RobolectricUtil.CustomMessageQueue.class}) +public final class AnalyticsCollectorTest { + + private static final int EVENT_PLAYER_STATE_CHANGED = 0; + private static final int EVENT_TIMELINE_CHANGED = 1; + private static final int EVENT_POSITION_DISCONTINUITY = 2; + private static final int EVENT_SEEK_STARTED = 3; + private static final int EVENT_SEEK_PROCESSED = 4; + private static final int EVENT_PLAYBACK_PARAMETERS_CHANGED = 5; + private static final int EVENT_REPEAT_MODE_CHANGED = 6; + private static final int EVENT_SHUFFLE_MODE_CHANGED = 7; + private static final int EVENT_LOADING_CHANGED = 8; + private static final int EVENT_PLAYER_ERROR = 9; + private static final int EVENT_TRACKS_CHANGED = 10; + private static final int EVENT_LOAD_STARTED = 11; + private static final int EVENT_LOAD_COMPLETED = 12; + private static final int EVENT_LOAD_CANCELED = 13; + private static final int EVENT_LOAD_ERROR = 14; + private static final int EVENT_DOWNSTREAM_FORMAT_CHANGED = 15; + private static final int EVENT_UPSTREAM_DISCARDED = 16; + private static final int EVENT_MEDIA_PERIOD_CREATED = 17; + private static final int EVENT_MEDIA_PERIOD_RELEASED = 18; + private static final int EVENT_READING_STARTED = 19; + private static final int EVENT_BANDWIDTH_ESTIMATE = 20; + private static final int EVENT_VIEWPORT_SIZE_CHANGED = 21; + private static final int EVENT_NETWORK_TYPE_CHANGED = 22; + private static final int EVENT_METADATA = 23; + private static final int EVENT_DECODER_ENABLED = 24; + private static final int EVENT_DECODER_INIT = 25; + private static final int EVENT_DECODER_FORMAT_CHANGED = 26; + private static final int EVENT_DECODER_DISABLED = 27; + private static final int EVENT_AUDIO_SESSION_ID = 28; + private static final int EVENT_AUDIO_UNDERRUN = 29; + 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_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); + private static final EventWindowAndPeriodId WINDOW_0 = + new EventWindowAndPeriodId(/* windowIndex= */ 0, /* mediaPeriodId= */ null); + private static final EventWindowAndPeriodId WINDOW_1 = + new EventWindowAndPeriodId(/* windowIndex= */ 1, /* mediaPeriodId= */ null); + private static final EventWindowAndPeriodId PERIOD_0 = + new EventWindowAndPeriodId( + /* windowIndex= */ 0, + new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 0)); + private static final EventWindowAndPeriodId PERIOD_1 = + new EventWindowAndPeriodId( + /* windowIndex= */ 1, + new MediaPeriodId(/* periodIndex= */ 1, /* windowSequenceNumber= */ 1)); + private static final EventWindowAndPeriodId PERIOD_0_SEQ_0 = PERIOD_0; + private static final EventWindowAndPeriodId PERIOD_1_SEQ_1 = PERIOD_1; + private static final EventWindowAndPeriodId PERIOD_0_SEQ_1 = + new EventWindowAndPeriodId( + /* windowIndex= */ 0, + new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 1)); + private static final EventWindowAndPeriodId PERIOD_1_SEQ_0 = + new EventWindowAndPeriodId( + /* windowIndex= */ 1, + new MediaPeriodId(/* periodIndex= */ 1, /* windowSequenceNumber= */ 0)); + private static final EventWindowAndPeriodId PERIOD_1_SEQ_2 = + new EventWindowAndPeriodId( + /* windowIndex= */ 1, + new MediaPeriodId(/* periodIndex= */ 1, /* windowSequenceNumber= */ 2)); + + @Test + public void testEmptyTimeline() throws Exception { + FakeMediaSource mediaSource = + new FakeMediaSource( + Timeline.EMPTY, /* manifest= */ null, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT); + TestAnalyticsListener listener = runAnalyticsTest(mediaSource); + + assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED)) + .containsExactly( + WINDOW_0 /* setPlayWhenReady */, WINDOW_0 /* BUFFERING */, WINDOW_0 /* ENDED */); + assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)).containsExactly(WINDOW_0); + listener.assertNoMoreEvents(); + } + + @Test + public void testSinglePeriod() throws Exception { + FakeMediaSource mediaSource = + new FakeMediaSource( + SINGLE_PERIOD_TIMELINE, + /* manifest= */ null, + Builder.VIDEO_FORMAT, + Builder.AUDIO_FORMAT); + TestAnalyticsListener listener = runAnalyticsTest(mediaSource); + + assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED)) + .containsExactly( + WINDOW_0 /* setPlayWhenReady */, + WINDOW_0 /* BUFFERING */, + PERIOD_0 /* READY */, + PERIOD_0 /* ENDED */); + assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)).containsExactly(WINDOW_0); + assertThat(listener.getEvents(EVENT_LOADING_CHANGED)) + .containsExactly(PERIOD_0 /* started */, PERIOD_0 /* stopped */); + assertThat(listener.getEvents(EVENT_TRACKS_CHANGED)).containsExactly(PERIOD_0); + assertThat(listener.getEvents(EVENT_LOAD_STARTED)) + .containsExactly(WINDOW_0 /* manifest */, PERIOD_0 /* media */); + assertThat(listener.getEvents(EVENT_LOAD_COMPLETED)) + .containsExactly(WINDOW_0 /* manifest */, PERIOD_0 /* media */); + assertThat(listener.getEvents(EVENT_DOWNSTREAM_FORMAT_CHANGED)) + .containsExactly(PERIOD_0 /* audio */, PERIOD_0 /* video */); + assertThat(listener.getEvents(EVENT_MEDIA_PERIOD_CREATED)).containsExactly(PERIOD_0); + assertThat(listener.getEvents(EVENT_READING_STARTED)).containsExactly(PERIOD_0); + assertThat(listener.getEvents(EVENT_DECODER_ENABLED)) + .containsExactly(PERIOD_0 /* audio */, PERIOD_0 /* video */); + assertThat(listener.getEvents(EVENT_DECODER_INIT)) + .containsExactly(PERIOD_0 /* audio */, PERIOD_0 /* video */); + assertThat(listener.getEvents(EVENT_DECODER_FORMAT_CHANGED)) + .containsExactly(PERIOD_0 /* audio */, PERIOD_0 /* video */); + assertThat(listener.getEvents(EVENT_AUDIO_SESSION_ID)).containsExactly(PERIOD_0); + assertThat(listener.getEvents(EVENT_DROPPED_VIDEO_FRAMES)).containsExactly(PERIOD_0); + assertThat(listener.getEvents(EVENT_VIDEO_SIZE_CHANGED)).containsExactly(PERIOD_0); + assertThat(listener.getEvents(EVENT_RENDERED_FIRST_FRAME)).containsExactly(PERIOD_0); + listener.assertNoMoreEvents(); + } + + @Test + public void testAutomaticPeriodTransition() throws Exception { + MediaSource mediaSource = + new ConcatenatingMediaSource( + new FakeMediaSource( + SINGLE_PERIOD_TIMELINE, + /* manifest= */ null, + Builder.VIDEO_FORMAT, + Builder.AUDIO_FORMAT), + new FakeMediaSource( + SINGLE_PERIOD_TIMELINE, + /* manifest= */ null, + Builder.VIDEO_FORMAT, + Builder.AUDIO_FORMAT)); + TestAnalyticsListener listener = runAnalyticsTest(mediaSource); + + assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED)) + .containsExactly( + WINDOW_0 /* setPlayWhenReady */, + WINDOW_0 /* BUFFERING */, + PERIOD_0 /* READY */, + PERIOD_1 /* ENDED */); + assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)).containsExactly(WINDOW_0); + assertThat(listener.getEvents(EVENT_POSITION_DISCONTINUITY)).containsExactly(PERIOD_1); + assertThat(listener.getEvents(EVENT_LOADING_CHANGED)) + .containsExactly(PERIOD_0, PERIOD_0, PERIOD_0, PERIOD_0); + assertThat(listener.getEvents(EVENT_TRACKS_CHANGED)).containsExactly(PERIOD_0, PERIOD_1); + assertThat(listener.getEvents(EVENT_LOAD_STARTED)) + .containsExactly( + WINDOW_0 /* manifest */, + PERIOD_0 /* media */, + WINDOW_1 /* manifest */, + PERIOD_1 /* media */); + assertThat(listener.getEvents(EVENT_LOAD_COMPLETED)) + .containsExactly( + WINDOW_0 /* manifest */, + PERIOD_0 /* media */, + WINDOW_1 /* manifest */, + PERIOD_1 /* media */); + assertThat(listener.getEvents(EVENT_DOWNSTREAM_FORMAT_CHANGED)) + .containsExactly( + PERIOD_0 /* audio */, PERIOD_0 /* video */, PERIOD_1 /* audio */, PERIOD_1 /* video */); + assertThat(listener.getEvents(EVENT_MEDIA_PERIOD_CREATED)).containsExactly(PERIOD_0, PERIOD_1); + assertThat(listener.getEvents(EVENT_MEDIA_PERIOD_RELEASED)).containsExactly(PERIOD_0); + assertThat(listener.getEvents(EVENT_READING_STARTED)).containsExactly(PERIOD_0, PERIOD_1); + assertThat(listener.getEvents(EVENT_DECODER_ENABLED)) + .containsExactly(PERIOD_0 /* audio */, PERIOD_0 /* video */); + assertThat(listener.getEvents(EVENT_DECODER_INIT)) + .containsExactly( + PERIOD_0 /* audio */, PERIOD_0 /* video */, PERIOD_1 /* audio */, PERIOD_1 /* video */); + assertThat(listener.getEvents(EVENT_DECODER_FORMAT_CHANGED)) + .containsExactly( + PERIOD_0 /* audio */, PERIOD_0 /* video */, PERIOD_1 /* audio */, PERIOD_1 /* video */); + assertThat(listener.getEvents(EVENT_AUDIO_SESSION_ID)).containsExactly(PERIOD_0); + assertThat(listener.getEvents(EVENT_DROPPED_VIDEO_FRAMES)).containsExactly(PERIOD_1); + assertThat(listener.getEvents(EVENT_VIDEO_SIZE_CHANGED)).containsExactly(PERIOD_0); + assertThat(listener.getEvents(EVENT_RENDERED_FIRST_FRAME)).containsExactly(PERIOD_0); + listener.assertNoMoreEvents(); + } + + @Test + public void testPeriodTransitionWithRendererChange() throws Exception { + MediaSource mediaSource = + new ConcatenatingMediaSource( + new FakeMediaSource(SINGLE_PERIOD_TIMELINE, /* manifest= */ null, Builder.VIDEO_FORMAT), + new FakeMediaSource( + SINGLE_PERIOD_TIMELINE, /* manifest= */ null, Builder.AUDIO_FORMAT)); + TestAnalyticsListener listener = runAnalyticsTest(mediaSource); + + assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED)) + .containsExactly( + WINDOW_0 /* setPlayWhenReady */, + WINDOW_0 /* BUFFERING */, + PERIOD_0 /* READY */, + PERIOD_1 /* BUFFERING */, + PERIOD_1 /* READY */, + PERIOD_1 /* ENDED */); + assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)).containsExactly(WINDOW_0); + assertThat(listener.getEvents(EVENT_POSITION_DISCONTINUITY)).containsExactly(PERIOD_1); + assertThat(listener.getEvents(EVENT_LOADING_CHANGED)) + .containsExactly(PERIOD_0, PERIOD_0, PERIOD_0, PERIOD_0); + assertThat(listener.getEvents(EVENT_TRACKS_CHANGED)).containsExactly(PERIOD_0, PERIOD_1); + assertThat(listener.getEvents(EVENT_LOAD_STARTED)) + .containsExactly( + WINDOW_0 /* manifest */, + PERIOD_0 /* media */, + WINDOW_1 /* manifest */, + PERIOD_1 /* media */); + assertThat(listener.getEvents(EVENT_LOAD_COMPLETED)) + .containsExactly( + WINDOW_0 /* manifest */, + PERIOD_0 /* media */, + WINDOW_1 /* manifest */, + PERIOD_1 /* media */); + assertThat(listener.getEvents(EVENT_DOWNSTREAM_FORMAT_CHANGED)) + .containsExactly(PERIOD_0 /* video */, PERIOD_1 /* audio */); + assertThat(listener.getEvents(EVENT_MEDIA_PERIOD_CREATED)).containsExactly(PERIOD_0, PERIOD_1); + assertThat(listener.getEvents(EVENT_MEDIA_PERIOD_RELEASED)).containsExactly(PERIOD_0); + assertThat(listener.getEvents(EVENT_READING_STARTED)).containsExactly(PERIOD_0, PERIOD_1); + assertThat(listener.getEvents(EVENT_DECODER_ENABLED)) + .containsExactly(PERIOD_0 /* video */, PERIOD_1 /* audio */); + assertThat(listener.getEvents(EVENT_DECODER_INIT)) + .containsExactly(PERIOD_0 /* video */, PERIOD_1 /* audio */); + assertThat(listener.getEvents(EVENT_DECODER_FORMAT_CHANGED)) + .containsExactly(PERIOD_0 /* video */, PERIOD_1 /* audio */); + assertThat(listener.getEvents(EVENT_DECODER_DISABLED)).containsExactly(PERIOD_0); + assertThat(listener.getEvents(EVENT_AUDIO_SESSION_ID)).containsExactly(PERIOD_1); + assertThat(listener.getEvents(EVENT_DROPPED_VIDEO_FRAMES)).containsExactly(PERIOD_0); + assertThat(listener.getEvents(EVENT_VIDEO_SIZE_CHANGED)).containsExactly(PERIOD_0); + assertThat(listener.getEvents(EVENT_RENDERED_FIRST_FRAME)).containsExactly(PERIOD_0); + listener.assertNoMoreEvents(); + } + + @Test + public void testSeekToOtherPeriod() throws Exception { + MediaSource mediaSource = + new ConcatenatingMediaSource( + new FakeMediaSource(SINGLE_PERIOD_TIMELINE, /* manifest= */ null, Builder.VIDEO_FORMAT), + new FakeMediaSource( + SINGLE_PERIOD_TIMELINE, /* manifest= */ null, Builder.AUDIO_FORMAT)); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("AnalyticsCollectorTest") + .pause() + .waitForPlaybackState(Player.STATE_READY) + .seek(/* windowIndex= */ 1, /* positionMs= */ 0) + .play() + .build(); + TestAnalyticsListener listener = runAnalyticsTest(mediaSource, actionSchedule); + + assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED)) + .containsExactly( + WINDOW_0 /* setPlayWhenReady=true */, + WINDOW_0 /* BUFFERING */, + WINDOW_0 /* setPlayWhenReady=false */, + PERIOD_0 /* READY */, + PERIOD_1 /* BUFFERING */, + PERIOD_1 /* READY */, + PERIOD_1 /* setPlayWhenReady=true */, + PERIOD_1 /* ENDED */); + assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)).containsExactly(WINDOW_0); + assertThat(listener.getEvents(EVENT_POSITION_DISCONTINUITY)).containsExactly(PERIOD_1); + assertThat(listener.getEvents(EVENT_SEEK_STARTED)).containsExactly(PERIOD_0); + assertThat(listener.getEvents(EVENT_SEEK_PROCESSED)).containsExactly(PERIOD_1); + List loadingEvents = listener.getEvents(EVENT_LOADING_CHANGED); + assertThat(loadingEvents).hasSize(4); + assertThat(loadingEvents).containsAllOf(PERIOD_0, PERIOD_0); + assertThat(listener.getEvents(EVENT_TRACKS_CHANGED)).containsExactly(PERIOD_0, PERIOD_1); + assertThat(listener.getEvents(EVENT_LOAD_STARTED)) + .containsExactly( + WINDOW_0 /* manifest */, + PERIOD_0 /* media */, + WINDOW_1 /* manifest */, + PERIOD_1 /* media */); + assertThat(listener.getEvents(EVENT_LOAD_COMPLETED)) + .containsExactly( + WINDOW_0 /* manifest */, + PERIOD_0 /* media */, + WINDOW_1 /* manifest */, + PERIOD_1 /* media */); + assertThat(listener.getEvents(EVENT_DOWNSTREAM_FORMAT_CHANGED)) + .containsExactly(PERIOD_0 /* video */, PERIOD_1 /* audio */); + assertThat(listener.getEvents(EVENT_MEDIA_PERIOD_CREATED)).containsExactly(PERIOD_0, PERIOD_1); + assertThat(listener.getEvents(EVENT_MEDIA_PERIOD_RELEASED)).containsExactly(PERIOD_0); + assertThat(listener.getEvents(EVENT_READING_STARTED)).containsExactly(PERIOD_0, PERIOD_1); + assertThat(listener.getEvents(EVENT_DECODER_ENABLED)) + .containsExactly(PERIOD_0 /* video */, PERIOD_1 /* audio */); + assertThat(listener.getEvents(EVENT_DECODER_INIT)) + .containsExactly(PERIOD_0 /* video */, PERIOD_1 /* audio */); + assertThat(listener.getEvents(EVENT_DECODER_FORMAT_CHANGED)) + .containsExactly(PERIOD_0 /* video */, PERIOD_1 /* audio */); + assertThat(listener.getEvents(EVENT_DECODER_DISABLED)).containsExactly(PERIOD_0); + assertThat(listener.getEvents(EVENT_AUDIO_SESSION_ID)).containsExactly(PERIOD_1); + assertThat(listener.getEvents(EVENT_VIDEO_SIZE_CHANGED)).containsExactly(PERIOD_0); + assertThat(listener.getEvents(EVENT_RENDERED_FIRST_FRAME)).containsExactly(PERIOD_0); + listener.assertNoMoreEvents(); + } + + @Test + public void testSeekBackAfterReadingAhead() throws Exception { + MediaSource mediaSource = + new ConcatenatingMediaSource( + new FakeMediaSource(SINGLE_PERIOD_TIMELINE, /* manifest= */ null, Builder.VIDEO_FORMAT), + new FakeMediaSource( + SINGLE_PERIOD_TIMELINE, + /* manifest= */ null, + Builder.VIDEO_FORMAT, + Builder.AUDIO_FORMAT)); + long periodDurationMs = + SINGLE_PERIOD_TIMELINE.getWindow(/* windowIndex= */ 0, new Window()).getDurationMs(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("AnalyticsCollectorTest") + .pause() + .waitForPlaybackState(Player.STATE_READY) + .playUntilPosition(/* windowIndex= */ 0, periodDurationMs) + .seek(/* positionMs= */ 0) + .waitForPlaybackState(Player.STATE_READY) + .play() + .build(); + TestAnalyticsListener listener = runAnalyticsTest(mediaSource, actionSchedule); + + assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED)) + .containsExactly( + WINDOW_0 /* setPlayWhenReady=true */, + WINDOW_0 /* BUFFERING */, + WINDOW_0 /* setPlayWhenReady=false */, + PERIOD_0 /* READY */, + PERIOD_0 /* setPlayWhenReady=true */, + PERIOD_0 /* setPlayWhenReady=false */, + PERIOD_0 /* BUFFERING */, + PERIOD_0 /* READY */, + PERIOD_0 /* setPlayWhenReady=true */, + PERIOD_1_SEQ_2 /* BUFFERING */, + PERIOD_1_SEQ_2 /* READY */, + PERIOD_1_SEQ_2 /* ENDED */); + assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)).containsExactly(WINDOW_0); + assertThat(listener.getEvents(EVENT_POSITION_DISCONTINUITY)) + .containsExactly(PERIOD_0, PERIOD_1_SEQ_2); + assertThat(listener.getEvents(EVENT_SEEK_STARTED)).containsExactly(PERIOD_0); + assertThat(listener.getEvents(EVENT_SEEK_PROCESSED)).containsExactly(PERIOD_0); + assertThat(listener.getEvents(EVENT_LOADING_CHANGED)) + .containsExactly(PERIOD_0, PERIOD_0, PERIOD_0, PERIOD_0, PERIOD_0, PERIOD_0); + assertThat(listener.getEvents(EVENT_TRACKS_CHANGED)).containsExactly(PERIOD_0, PERIOD_1_SEQ_2); + assertThat(listener.getEvents(EVENT_LOAD_STARTED)) + .containsExactly( + WINDOW_0 /* manifest */, + PERIOD_0 /* media */, + WINDOW_1 /* manifest */, + PERIOD_1_SEQ_1 /* media */, + PERIOD_1_SEQ_2 /* media */); + assertThat(listener.getEvents(EVENT_LOAD_COMPLETED)) + .containsExactly( + WINDOW_0 /* manifest */, + PERIOD_0 /* media */, + WINDOW_1 /* manifest */, + PERIOD_1_SEQ_1 /* media */, + PERIOD_1_SEQ_2 /* media */); + assertThat(listener.getEvents(EVENT_DOWNSTREAM_FORMAT_CHANGED)) + .containsExactly(PERIOD_0, PERIOD_1_SEQ_1, PERIOD_1_SEQ_2, PERIOD_1_SEQ_2); + assertThat(listener.getEvents(EVENT_MEDIA_PERIOD_CREATED)) + .containsExactly(PERIOD_0, PERIOD_1_SEQ_1, PERIOD_1_SEQ_2); + assertThat(listener.getEvents(EVENT_MEDIA_PERIOD_RELEASED)) + .containsExactly(PERIOD_0, PERIOD_1_SEQ_1); + assertThat(listener.getEvents(EVENT_READING_STARTED)) + .containsExactly(PERIOD_0, PERIOD_1_SEQ_1, PERIOD_1_SEQ_2); + assertThat(listener.getEvents(EVENT_DECODER_ENABLED)) + .containsExactly(PERIOD_0, PERIOD_0, PERIOD_1_SEQ_2); + assertThat(listener.getEvents(EVENT_DECODER_INIT)) + .containsExactly(PERIOD_0, PERIOD_1_SEQ_1, PERIOD_1_SEQ_2, PERIOD_1_SEQ_2); + assertThat(listener.getEvents(EVENT_DECODER_FORMAT_CHANGED)) + .containsExactly(PERIOD_0, PERIOD_1_SEQ_1, PERIOD_1_SEQ_2, PERIOD_1_SEQ_2); + assertThat(listener.getEvents(EVENT_DECODER_DISABLED)).containsExactly(PERIOD_0); + assertThat(listener.getEvents(EVENT_AUDIO_SESSION_ID)).containsExactly(PERIOD_1_SEQ_2); + assertThat(listener.getEvents(EVENT_DROPPED_VIDEO_FRAMES)) + .containsExactly(PERIOD_0, PERIOD_0, PERIOD_1_SEQ_2); + assertThat(listener.getEvents(EVENT_VIDEO_SIZE_CHANGED)) + .containsExactly(PERIOD_0, PERIOD_1_SEQ_2); + assertThat(listener.getEvents(EVENT_RENDERED_FIRST_FRAME)) + .containsExactly(PERIOD_0, PERIOD_1_SEQ_2); + listener.assertNoMoreEvents(); + } + + @Test + public void testPrepareNewSource() throws Exception { + MediaSource mediaSource1 = + new FakeMediaSource(SINGLE_PERIOD_TIMELINE, /* manifest= */ null, Builder.VIDEO_FORMAT); + MediaSource mediaSource2 = + new FakeMediaSource(SINGLE_PERIOD_TIMELINE, /* manifest= */ null, Builder.VIDEO_FORMAT); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("AnalyticsCollectorTest") + .pause() + .waitForPlaybackState(Player.STATE_READY) + .prepareSource(mediaSource2) + .play() + .build(); + TestAnalyticsListener listener = runAnalyticsTest(mediaSource1, actionSchedule); + + assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED)) + .containsExactly( + WINDOW_0 /* setPlayWhenReady=true */, + WINDOW_0 /* BUFFERING */, + WINDOW_0 /* setPlayWhenReady=false */, + PERIOD_0_SEQ_0 /* READY */, + WINDOW_0 /* BUFFERING */, + WINDOW_0 /* setPlayWhenReady=true */, + PERIOD_0_SEQ_1 /* READY */, + PERIOD_0_SEQ_1 /* ENDED */); + assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)) + .containsExactly(WINDOW_0 /* prepared */, WINDOW_0 /* reset */, WINDOW_0 /* prepared */); + assertThat(listener.getEvents(EVENT_LOADING_CHANGED)) + .containsExactly(PERIOD_0_SEQ_0, PERIOD_0_SEQ_0, PERIOD_0_SEQ_1, PERIOD_0_SEQ_1); + assertThat(listener.getEvents(EVENT_TRACKS_CHANGED)) + .containsExactly( + PERIOD_0_SEQ_0 /* prepared */, WINDOW_0 /* reset */, PERIOD_0_SEQ_1 /* prepared */); + assertThat(listener.getEvents(EVENT_LOAD_STARTED)) + .containsExactly( + WINDOW_0 /* manifest */, + PERIOD_0_SEQ_0 /* media */, + WINDOW_0 /* manifest */, + PERIOD_0_SEQ_1 /* media */); + assertThat(listener.getEvents(EVENT_LOAD_COMPLETED)) + .containsExactly( + WINDOW_0 /* manifest */, + PERIOD_0_SEQ_0 /* media */, + WINDOW_0 /* manifest */, + PERIOD_0_SEQ_1 /* media */); + assertThat(listener.getEvents(EVENT_DOWNSTREAM_FORMAT_CHANGED)) + .containsExactly(PERIOD_0_SEQ_0, PERIOD_0_SEQ_1); + assertThat(listener.getEvents(EVENT_MEDIA_PERIOD_CREATED)) + .containsExactly(PERIOD_0_SEQ_0, PERIOD_0_SEQ_1); + assertThat(listener.getEvents(EVENT_MEDIA_PERIOD_RELEASED)).containsExactly(PERIOD_0_SEQ_0); + assertThat(listener.getEvents(EVENT_READING_STARTED)) + .containsExactly(PERIOD_0_SEQ_0, PERIOD_0_SEQ_1); + assertThat(listener.getEvents(EVENT_DECODER_ENABLED)) + .containsExactly(PERIOD_0_SEQ_0, PERIOD_0_SEQ_1); + assertThat(listener.getEvents(EVENT_DECODER_INIT)) + .containsExactly(PERIOD_0_SEQ_0, PERIOD_0_SEQ_1); + assertThat(listener.getEvents(EVENT_DECODER_FORMAT_CHANGED)) + .containsExactly(PERIOD_0_SEQ_0, PERIOD_0_SEQ_1); + assertThat(listener.getEvents(EVENT_DECODER_DISABLED)).containsExactly(PERIOD_0_SEQ_0); + assertThat(listener.getEvents(EVENT_DROPPED_VIDEO_FRAMES)).containsExactly(PERIOD_0_SEQ_1); + assertThat(listener.getEvents(EVENT_VIDEO_SIZE_CHANGED)) + .containsExactly(PERIOD_0_SEQ_0, PERIOD_0_SEQ_1); + assertThat(listener.getEvents(EVENT_RENDERED_FIRST_FRAME)) + .containsExactly(PERIOD_0_SEQ_0, PERIOD_0_SEQ_1); + listener.assertNoMoreEvents(); + } + + @Test + public void testReprepareAfterError() throws Exception { + MediaSource mediaSource = + new FakeMediaSource(SINGLE_PERIOD_TIMELINE, /* manifest= */ null, Builder.VIDEO_FORMAT); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("AnalyticsCollectorTest") + .waitForPlaybackState(Player.STATE_READY) + .throwPlaybackException(ExoPlaybackException.createForSource(new IOException())) + .waitForPlaybackState(Player.STATE_IDLE) + .prepareSource(mediaSource, /* resetPosition= */ false, /* resetState= */ false) + .waitForPlaybackState(Player.STATE_ENDED) + .build(); + TestAnalyticsListener listener = runAnalyticsTest(mediaSource, actionSchedule); + + assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED)) + .containsExactly( + WINDOW_0 /* setPlayWhenReady=true */, + WINDOW_0 /* BUFFERING */, + PERIOD_0_SEQ_0 /* READY */, + WINDOW_0 /* IDLE */, + WINDOW_0 /* BUFFERING */, + PERIOD_0_SEQ_0 /* READY */, + PERIOD_0_SEQ_0 /* ENDED */); + // assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED)).doesNotContain(PERIOD_0_SEQ_1); + assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)) + .containsExactly(WINDOW_0 /* prepared */, WINDOW_0 /* prepared */); + assertThat(listener.getEvents(EVENT_LOADING_CHANGED)) + .containsExactly(PERIOD_0_SEQ_0, PERIOD_0_SEQ_0, PERIOD_0_SEQ_0, PERIOD_0_SEQ_0); + assertThat(listener.getEvents(EVENT_PLAYER_ERROR)).containsExactly(WINDOW_0); + assertThat(listener.getEvents(EVENT_TRACKS_CHANGED)) + .containsExactly(PERIOD_0_SEQ_0, PERIOD_0_SEQ_0); + assertThat(listener.getEvents(EVENT_LOAD_STARTED)) + .containsExactly( + WINDOW_0 /* manifest */, + PERIOD_0_SEQ_0 /* media */, + WINDOW_0 /* manifest */, + PERIOD_0_SEQ_0 /* media */); + assertThat(listener.getEvents(EVENT_LOAD_COMPLETED)) + .containsExactly( + WINDOW_0 /* manifest */, + PERIOD_0_SEQ_0 /* media */, + WINDOW_0 /* manifest */, + PERIOD_0_SEQ_0 /* media */); + assertThat(listener.getEvents(EVENT_DOWNSTREAM_FORMAT_CHANGED)) + .containsExactly(PERIOD_0_SEQ_0, PERIOD_0_SEQ_0); + assertThat(listener.getEvents(EVENT_MEDIA_PERIOD_CREATED)) + .containsExactly(PERIOD_0_SEQ_0, PERIOD_0_SEQ_0); + assertThat(listener.getEvents(EVENT_MEDIA_PERIOD_RELEASED)).containsExactly(PERIOD_0_SEQ_0); + assertThat(listener.getEvents(EVENT_READING_STARTED)) + .containsExactly(PERIOD_0_SEQ_0, PERIOD_0_SEQ_0); + assertThat(listener.getEvents(EVENT_DECODER_ENABLED)) + .containsExactly(PERIOD_0_SEQ_0, PERIOD_0_SEQ_0); + assertThat(listener.getEvents(EVENT_DECODER_INIT)) + .containsExactly(PERIOD_0_SEQ_0, PERIOD_0_SEQ_0); + assertThat(listener.getEvents(EVENT_DECODER_FORMAT_CHANGED)) + .containsExactly(PERIOD_0_SEQ_0, PERIOD_0_SEQ_0); + assertThat(listener.getEvents(EVENT_DECODER_DISABLED)).containsExactly(PERIOD_0_SEQ_0); + assertThat(listener.getEvents(EVENT_DROPPED_VIDEO_FRAMES)) + .containsExactly(PERIOD_0_SEQ_0, PERIOD_0_SEQ_0); + assertThat(listener.getEvents(EVENT_VIDEO_SIZE_CHANGED)) + .containsExactly(PERIOD_0_SEQ_0, PERIOD_0_SEQ_0); + assertThat(listener.getEvents(EVENT_RENDERED_FIRST_FRAME)) + .containsExactly(PERIOD_0_SEQ_0, PERIOD_0_SEQ_0); + listener.assertNoMoreEvents(); + } + + @Test + public void testDynamicTimelineChange() throws Exception { + MediaSource childMediaSource = + new FakeMediaSource(SINGLE_PERIOD_TIMELINE, /* manifest= */ null, Builder.VIDEO_FORMAT); + final ConcatenatingMediaSource concatenatedMediaSource = + new ConcatenatingMediaSource(childMediaSource, childMediaSource); + long periodDurationMs = + SINGLE_PERIOD_TIMELINE.getWindow(/* windowIndex= */ 0, new Window()).getDurationMs(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("AnalyticsCollectorTest") + .pause() + .waitForPlaybackState(Player.STATE_READY) + // Ensure second period is already being read from. + .playUntilPosition(/* windowIndex= */ 0, /* positionMs= */ periodDurationMs) + .executeRunnable( + new Runnable() { + @Override + public void run() { + concatenatedMediaSource.moveMediaSource( + /* currentIndex= */ 0, /* newIndex= */ 1); + } + }) + .waitForTimelineChanged(/* expectedTimeline= */ null) + .play() + .build(); + TestAnalyticsListener listener = runAnalyticsTest(concatenatedMediaSource, actionSchedule); + + assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED)) + .containsExactly( + WINDOW_0 /* setPlayWhenReady=true */, + WINDOW_0 /* BUFFERING */, + WINDOW_0 /* setPlayWhenReady=false */, + PERIOD_0_SEQ_0 /* READY */, + PERIOD_0_SEQ_0 /* setPlayWhenReady=true */, + PERIOD_0_SEQ_0 /* setPlayWhenReady=false */, + PERIOD_1_SEQ_0 /* setPlayWhenReady=true */, + PERIOD_1_SEQ_0 /* BUFFERING */, + PERIOD_1_SEQ_0 /* ENDED */); + assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)) + .containsExactly(WINDOW_0, PERIOD_1_SEQ_0); + assertThat(listener.getEvents(EVENT_LOADING_CHANGED)) + .containsExactly(PERIOD_0_SEQ_0, PERIOD_0_SEQ_0, PERIOD_0_SEQ_0, PERIOD_0_SEQ_0); + assertThat(listener.getEvents(EVENT_TRACKS_CHANGED)).containsExactly(PERIOD_0_SEQ_0); + assertThat(listener.getEvents(EVENT_LOAD_STARTED)) + .containsExactly( + WINDOW_0 /* manifest */, PERIOD_0_SEQ_0 /* media */, PERIOD_1_SEQ_1 /* media */); + assertThat(listener.getEvents(EVENT_LOAD_COMPLETED)) + .containsExactly( + WINDOW_0 /* manifest */, PERIOD_0_SEQ_0 /* media */, PERIOD_1_SEQ_1 /* media */); + assertThat(listener.getEvents(EVENT_DOWNSTREAM_FORMAT_CHANGED)) + .containsExactly(PERIOD_0_SEQ_0, PERIOD_1_SEQ_1); + assertThat(listener.getEvents(EVENT_MEDIA_PERIOD_CREATED)) + .containsExactly(PERIOD_0_SEQ_0, PERIOD_1_SEQ_1); + assertThat(listener.getEvents(EVENT_MEDIA_PERIOD_RELEASED)).containsExactly(PERIOD_0_SEQ_1); + assertThat(listener.getEvents(EVENT_READING_STARTED)) + .containsExactly(PERIOD_0_SEQ_0, PERIOD_1_SEQ_1); + assertThat(listener.getEvents(EVENT_DECODER_ENABLED)) + .containsExactly(PERIOD_0_SEQ_0, PERIOD_0_SEQ_0); + assertThat(listener.getEvents(EVENT_DECODER_INIT)) + .containsExactly(PERIOD_0_SEQ_0, PERIOD_1_SEQ_1); + assertThat(listener.getEvents(EVENT_DECODER_FORMAT_CHANGED)) + .containsExactly(PERIOD_0_SEQ_0, PERIOD_1_SEQ_1); + assertThat(listener.getEvents(EVENT_DECODER_DISABLED)).containsExactly(PERIOD_0_SEQ_0); + assertThat(listener.getEvents(EVENT_DROPPED_VIDEO_FRAMES)).containsExactly(PERIOD_0_SEQ_0); + assertThat(listener.getEvents(EVENT_VIDEO_SIZE_CHANGED)).containsExactly(PERIOD_0_SEQ_0); + assertThat(listener.getEvents(EVENT_RENDERED_FIRST_FRAME)).containsExactly(PERIOD_0_SEQ_0); + listener.assertNoMoreEvents(); + } + + @Test + public void testNotifyExternalEvents() throws Exception { + MediaSource mediaSource = new FakeMediaSource(SINGLE_PERIOD_TIMELINE, /* manifest= */ null); + final NetworkInfo networkInfo = + ((ConnectivityManager) + RuntimeEnvironment.application.getSystemService(Context.CONNECTIVITY_SERVICE)) + .getActiveNetworkInfo(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("AnalyticsCollectorTest") + .pause() + .waitForPlaybackState(Player.STATE_READY) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + player.getAnalyticsCollector().notifyNetworkTypeChanged(networkInfo); + player + .getAnalyticsCollector() + .notifyViewportSizeChanged(/* width= */ 320, /* height= */ 240); + player.getAnalyticsCollector().notifySeekStarted(); + } + }) + .seek(/* positionMs= */ 0) + .play() + .build(); + TestAnalyticsListener listener = runAnalyticsTest(mediaSource, actionSchedule); + + assertThat(listener.getEvents(EVENT_SEEK_STARTED)).containsExactly(PERIOD_0); + assertThat(listener.getEvents(EVENT_SEEK_PROCESSED)).containsExactly(PERIOD_0); + assertThat(listener.getEvents(EVENT_VIEWPORT_SIZE_CHANGED)).containsExactly(PERIOD_0); + assertThat(listener.getEvents(EVENT_NETWORK_TYPE_CHANGED)).containsExactly(PERIOD_0); + } + + private static TestAnalyticsListener runAnalyticsTest(MediaSource mediaSource) throws Exception { + return runAnalyticsTest(mediaSource, /* actionSchedule= */ null); + } + + private static TestAnalyticsListener runAnalyticsTest( + MediaSource mediaSource, @Nullable ActionSchedule actionSchedule) throws Exception { + RenderersFactory renderersFactory = + new RenderersFactory() { + @Override + public Renderer[] createRenderers( + Handler eventHandler, + VideoRendererEventListener videoRendererEventListener, + AudioRendererEventListener audioRendererEventListener, + TextOutput textRendererOutput, + MetadataOutput metadataRendererOutput, + @Nullable DrmSessionManager drmSessionManager) { + return new Renderer[] { + new FakeVideoRenderer(eventHandler, videoRendererEventListener), + new FakeAudioRenderer(eventHandler, audioRendererEventListener) + }; + } + }; + TestAnalyticsListener listener = new TestAnalyticsListener(); + try { + new ExoPlayerTestRunner.Builder() + .setMediaSource(mediaSource) + .setRenderersFactory(renderersFactory) + .setAnalyticsListener(listener) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + } catch (ExoPlaybackException e) { + // Ignore ExoPlaybackException as these may be expected. + } + return listener; + } + + private static final class FakeVideoRenderer extends FakeRenderer { + + private final VideoRendererEventListener.EventDispatcher eventDispatcher; + private final DecoderCounters decoderCounters; + private Format format; + private boolean renderedFirstFrame; + + public FakeVideoRenderer(Handler handler, VideoRendererEventListener eventListener) { + super(Builder.VIDEO_FORMAT); + eventDispatcher = new VideoRendererEventListener.EventDispatcher(handler, eventListener); + decoderCounters = new DecoderCounters(); + } + + @Override + protected void onEnabled(boolean joining) throws ExoPlaybackException { + super.onEnabled(joining); + eventDispatcher.enabled(decoderCounters); + renderedFirstFrame = false; + } + + @Override + protected void onStopped() throws ExoPlaybackException { + super.onStopped(); + eventDispatcher.droppedFrames(/* droppedFrameCount= */ 0, /* elapsedMs= */ 0); + } + + @Override + protected void onDisabled() { + super.onDisabled(); + eventDispatcher.disabled(decoderCounters); + } + + @Override + protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException { + super.onPositionReset(positionUs, joining); + renderedFirstFrame = false; + } + + @Override + protected void onFormatChanged(Format format) { + eventDispatcher.inputFormatChanged(format); + eventDispatcher.decoderInitialized( + /* decoderName= */ "fake.video.decoder", + /* initializedTimestampMs= */ SystemClock.elapsedRealtime(), + /* initializationDurationMs= */ 0); + this.format = format; + } + + @Override + protected void onBufferRead() { + if (!renderedFirstFrame) { + eventDispatcher.videoSizeChanged( + format.width, format.height, format.rotationDegrees, format.pixelWidthHeightRatio); + eventDispatcher.renderedFirstFrame(/* surface= */ null); + renderedFirstFrame = true; + } + } + } + + private static final class FakeAudioRenderer extends FakeRenderer { + + private final AudioRendererEventListener.EventDispatcher eventDispatcher; + private final DecoderCounters decoderCounters; + private boolean notifiedAudioSessionId; + + public FakeAudioRenderer(Handler handler, AudioRendererEventListener eventListener) { + super(Builder.AUDIO_FORMAT); + eventDispatcher = new AudioRendererEventListener.EventDispatcher(handler, eventListener); + decoderCounters = new DecoderCounters(); + } + + @Override + protected void onEnabled(boolean joining) throws ExoPlaybackException { + super.onEnabled(joining); + eventDispatcher.enabled(decoderCounters); + notifiedAudioSessionId = false; + } + + @Override + protected void onDisabled() { + super.onDisabled(); + eventDispatcher.disabled(decoderCounters); + } + + @Override + protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException { + super.onPositionReset(positionUs, joining); + } + + @Override + protected void onFormatChanged(Format format) { + eventDispatcher.inputFormatChanged(format); + eventDispatcher.decoderInitialized( + /* decoderName= */ "fake.audio.decoder", + /* initializedTimestampMs= */ SystemClock.elapsedRealtime(), + /* initializationDurationMs= */ 0); + } + + @Override + protected void onBufferRead() { + if (!notifiedAudioSessionId) { + eventDispatcher.audioSessionId(/* audioSessionId= */ 0); + notifiedAudioSessionId = true; + } + } + } + + private static final class EventWindowAndPeriodId { + + private final int windowIndex; + private final @Nullable MediaPeriodId mediaPeriodId; + + public EventWindowAndPeriodId(int windowIndex, @Nullable MediaPeriodId mediaPeriodId) { + this.windowIndex = windowIndex; + this.mediaPeriodId = mediaPeriodId; + } + + @Override + public boolean equals(Object other) { + if (!(other instanceof EventWindowAndPeriodId)) { + return false; + } + EventWindowAndPeriodId event = (EventWindowAndPeriodId) other; + return windowIndex == event.windowIndex && Util.areEqual(mediaPeriodId, event.mediaPeriodId); + } + + @Override + public String toString() { + return mediaPeriodId != null + ? "Event{" + + "window=" + + windowIndex + + ", period=" + + mediaPeriodId.periodIndex + + ", sequence=" + + mediaPeriodId.windowSequenceNumber + + '}' + : "Event{" + "window=" + windowIndex + ", period = null}"; + } + + @Override + public int hashCode() { + return 31 * windowIndex + (mediaPeriodId == null ? 0 : mediaPeriodId.hashCode()); + } + } + + private static final class TestAnalyticsListener implements AnalyticsListener { + + private final ArrayList reportedEvents; + + public TestAnalyticsListener() { + reportedEvents = new ArrayList<>(); + } + + public List getEvents(int eventType) { + ArrayList eventTimes = new ArrayList<>(); + Iterator eventIterator = reportedEvents.iterator(); + while (eventIterator.hasNext()) { + ReportedEvent event = eventIterator.next(); + if (event.eventType == eventType) { + eventTimes.add(event.eventWindowAndPeriodId); + eventIterator.remove(); + } + } + return eventTimes; + } + + public void assertNoMoreEvents() { + assertThat(reportedEvents).isEmpty(); + } + + @Override + public void onPlayerStateChanged( + EventTime eventTime, boolean playWhenReady, int playbackState) { + reportedEvents.add(new ReportedEvent(EVENT_PLAYER_STATE_CHANGED, eventTime)); + } + + @Override + public void onTimelineChanged(EventTime eventTime, int reason) { + reportedEvents.add(new ReportedEvent(EVENT_TIMELINE_CHANGED, eventTime)); + } + + @Override + public void onPositionDiscontinuity(EventTime eventTime, int reason) { + reportedEvents.add(new ReportedEvent(EVENT_POSITION_DISCONTINUITY, eventTime)); + } + + @Override + public void onSeekStarted(EventTime eventTime) { + reportedEvents.add(new ReportedEvent(EVENT_SEEK_STARTED, eventTime)); + } + + @Override + public void onSeekProcessed(EventTime eventTime) { + reportedEvents.add(new ReportedEvent(EVENT_SEEK_PROCESSED, eventTime)); + } + + @Override + public void onPlaybackParametersChanged( + EventTime eventTime, PlaybackParameters playbackParameters) { + reportedEvents.add(new ReportedEvent(EVENT_PLAYBACK_PARAMETERS_CHANGED, eventTime)); + } + + @Override + public void onRepeatModeChanged(EventTime eventTime, int repeatMode) { + reportedEvents.add(new ReportedEvent(EVENT_REPEAT_MODE_CHANGED, eventTime)); + } + + @Override + public void onShuffleModeChanged(EventTime eventTime, boolean shuffleModeEnabled) { + reportedEvents.add(new ReportedEvent(EVENT_SHUFFLE_MODE_CHANGED, eventTime)); + } + + @Override + public void onLoadingChanged(EventTime eventTime, boolean isLoading) { + reportedEvents.add(new ReportedEvent(EVENT_LOADING_CHANGED, eventTime)); + } + + @Override + public void onPlayerError(EventTime eventTime, ExoPlaybackException error) { + reportedEvents.add(new ReportedEvent(EVENT_PLAYER_ERROR, eventTime)); + } + + @Override + public void onTracksChanged( + EventTime eventTime, TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { + reportedEvents.add(new ReportedEvent(EVENT_TRACKS_CHANGED, eventTime)); + } + + @Override + public void onLoadStarted( + EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) { + reportedEvents.add(new ReportedEvent(EVENT_LOAD_STARTED, eventTime)); + } + + @Override + public void onLoadCompleted( + EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) { + reportedEvents.add(new ReportedEvent(EVENT_LOAD_COMPLETED, eventTime)); + } + + @Override + public void onLoadCanceled( + EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) { + reportedEvents.add(new ReportedEvent(EVENT_LOAD_CANCELED, eventTime)); + } + + @Override + public void onLoadError( + EventTime eventTime, + LoadEventInfo loadEventInfo, + MediaLoadData mediaLoadData, + IOException error, + boolean wasCanceled) { + reportedEvents.add(new ReportedEvent(EVENT_LOAD_ERROR, eventTime)); + } + + @Override + public void onDownstreamFormatChanged(EventTime eventTime, MediaLoadData mediaLoadData) { + reportedEvents.add(new ReportedEvent(EVENT_DOWNSTREAM_FORMAT_CHANGED, eventTime)); + } + + @Override + public void onUpstreamDiscarded(EventTime eventTime, MediaLoadData mediaLoadData) { + reportedEvents.add(new ReportedEvent(EVENT_UPSTREAM_DISCARDED, eventTime)); + } + + @Override + public void onMediaPeriodCreated(EventTime eventTime) { + reportedEvents.add(new ReportedEvent(EVENT_MEDIA_PERIOD_CREATED, eventTime)); + } + + @Override + public void onMediaPeriodReleased(EventTime eventTime) { + reportedEvents.add(new ReportedEvent(EVENT_MEDIA_PERIOD_RELEASED, eventTime)); + } + + @Override + public void onReadingStarted(EventTime eventTime) { + reportedEvents.add(new ReportedEvent(EVENT_READING_STARTED, eventTime)); + } + + @Override + public void onBandwidthEstimate( + EventTime eventTime, int totalLoadTimeMs, long totalBytesLoaded, long bitrateEstimate) { + reportedEvents.add(new ReportedEvent(EVENT_BANDWIDTH_ESTIMATE, eventTime)); + } + + @Override + public void onViewportSizeChange(EventTime eventTime, int width, int height) { + reportedEvents.add(new ReportedEvent(EVENT_VIEWPORT_SIZE_CHANGED, eventTime)); + } + + @Override + public void onNetworkTypeChanged(EventTime eventTime, @Nullable NetworkInfo networkInfo) { + reportedEvents.add(new ReportedEvent(EVENT_NETWORK_TYPE_CHANGED, eventTime)); + } + + @Override + public void onMetadata(EventTime eventTime, Metadata metadata) { + reportedEvents.add(new ReportedEvent(EVENT_METADATA, eventTime)); + } + + @Override + public void onDecoderEnabled( + EventTime eventTime, int trackType, DecoderCounters decoderCounters) { + reportedEvents.add(new ReportedEvent(EVENT_DECODER_ENABLED, eventTime)); + } + + @Override + public void onDecoderInitialized( + EventTime eventTime, int trackType, String decoderName, long initializationDurationMs) { + reportedEvents.add(new ReportedEvent(EVENT_DECODER_INIT, eventTime)); + } + + @Override + public void onDecoderInputFormatChanged(EventTime eventTime, int trackType, Format format) { + reportedEvents.add(new ReportedEvent(EVENT_DECODER_FORMAT_CHANGED, eventTime)); + } + + @Override + public void onDecoderDisabled( + EventTime eventTime, int trackType, DecoderCounters decoderCounters) { + reportedEvents.add(new ReportedEvent(EVENT_DECODER_DISABLED, eventTime)); + } + + @Override + public void onAudioSessionId(EventTime eventTime, int audioSessionId) { + reportedEvents.add(new ReportedEvent(EVENT_AUDIO_SESSION_ID, eventTime)); + } + + @Override + public void onAudioUnderrun( + EventTime eventTime, int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) { + reportedEvents.add(new ReportedEvent(EVENT_AUDIO_UNDERRUN, eventTime)); + } + + @Override + public void onDroppedVideoFrames(EventTime eventTime, int droppedFrames, long elapsedMs) { + reportedEvents.add(new ReportedEvent(EVENT_DROPPED_VIDEO_FRAMES, eventTime)); + } + + @Override + public void onVideoSizeChanged( + EventTime eventTime, + int width, + int height, + int unappliedRotationDegrees, + float pixelWidthHeightRatio) { + reportedEvents.add(new ReportedEvent(EVENT_VIDEO_SIZE_CHANGED, eventTime)); + } + + @Override + public void onRenderedFirstFrame(EventTime eventTime, Surface surface) { + reportedEvents.add(new ReportedEvent(EVENT_RENDERED_FIRST_FRAME, eventTime)); + } + + @Override + public void onDrmKeysLoaded(EventTime eventTime) { + reportedEvents.add(new ReportedEvent(EVENT_DRM_KEYS_LOADED, eventTime)); + } + + @Override + public void onDrmSessionManagerError(EventTime eventTime, Exception error) { + reportedEvents.add(new ReportedEvent(EVENT_DRM_ERROR, eventTime)); + } + + @Override + public void onDrmKeysRestored(EventTime eventTime) { + reportedEvents.add(new ReportedEvent(EVENT_DRM_KEYS_RESTORED, eventTime)); + } + + @Override + public void onDrmKeysRemoved(EventTime eventTime) { + reportedEvents.add(new ReportedEvent(EVENT_DRM_KEYS_REMOVED, eventTime)); + } + + private static final class ReportedEvent { + + public final int eventType; + public final EventWindowAndPeriodId eventWindowAndPeriodId; + + public ReportedEvent(int eventType, EventTime eventTime) { + this.eventType = eventType; + this.eventWindowAndPeriodId = + new EventWindowAndPeriodId(eventTime.windowIndex, eventTime.mediaPeriodId); + } + } + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/audio/SilenceSkippingAudioProcessorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/audio/SilenceSkippingAudioProcessorTest.java new file mode 100644 index 0000000000..115862074d --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/audio/SilenceSkippingAudioProcessorTest.java @@ -0,0 +1,424 @@ +/* + * 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.audio; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.audio.AudioProcessor.UnhandledFormatException; +import com.google.android.exoplayer2.util.Assertions; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.ShortBuffer; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +/** Unit tests for {@link SilenceSkippingAudioProcessor}. */ +@RunWith(RobolectricTestRunner.class) +public final class SilenceSkippingAudioProcessorTest { + + private static final int TEST_SIGNAL_SAMPLE_RATE_HZ = 1000; + private static final int TEST_SIGNAL_CHANNEL_COUNT = 2; + private static final int TEST_SIGNAL_SILENCE_DURATION_MS = 1000; + private static final int TEST_SIGNAL_NOISE_DURATION_MS = 1000; + private static final int TEST_SIGNAL_FRAME_COUNT = 100000; + + private static final int INPUT_BUFFER_SIZE = 100; + + private SilenceSkippingAudioProcessor silenceSkippingAudioProcessor; + + @Before + public void setUp() { + silenceSkippingAudioProcessor = new SilenceSkippingAudioProcessor(); + } + + @Test + public void testEnabledProcessor_isActive() throws Exception { + // Given an enabled processor. + silenceSkippingAudioProcessor.setEnabled(true); + + // When configuring it. + boolean reconfigured = + silenceSkippingAudioProcessor.configure( + TEST_SIGNAL_SAMPLE_RATE_HZ, TEST_SIGNAL_CHANNEL_COUNT, C.ENCODING_PCM_16BIT); + silenceSkippingAudioProcessor.flush(); + + // It's active. + assertThat(reconfigured).isTrue(); + assertThat(silenceSkippingAudioProcessor.isActive()).isTrue(); + } + + @Test + public void testDisabledProcessor_isNotActive() throws Exception { + // Given a disabled processor. + silenceSkippingAudioProcessor.setEnabled(false); + + // When configuring it. + silenceSkippingAudioProcessor.configure( + TEST_SIGNAL_SAMPLE_RATE_HZ, TEST_SIGNAL_CHANNEL_COUNT, C.ENCODING_PCM_16BIT); + + // It's not active. + assertThat(silenceSkippingAudioProcessor.isActive()).isFalse(); + } + + @Test + public void testDefaultProcessor_isNotEnabled() throws Exception { + // Given a processor in its default state. + // When reconfigured. + silenceSkippingAudioProcessor.configure( + TEST_SIGNAL_SAMPLE_RATE_HZ, TEST_SIGNAL_CHANNEL_COUNT, C.ENCODING_PCM_16BIT); + + // It's not active. + assertThat(silenceSkippingAudioProcessor.isActive()).isFalse(); + } + + @Test + public void testChangingSampleRate_requiresReconfiguration() throws Exception { + // Given an enabled processor and configured processor. + silenceSkippingAudioProcessor.setEnabled(true); + boolean reconfigured = + silenceSkippingAudioProcessor.configure( + TEST_SIGNAL_SAMPLE_RATE_HZ, TEST_SIGNAL_CHANNEL_COUNT, C.ENCODING_PCM_16BIT); + if (reconfigured) { + silenceSkippingAudioProcessor.flush(); + } + + // When reconfiguring it with a different sample rate. + reconfigured = + silenceSkippingAudioProcessor.configure( + TEST_SIGNAL_SAMPLE_RATE_HZ * 2, TEST_SIGNAL_CHANNEL_COUNT, C.ENCODING_PCM_16BIT); + + // It's reconfigured. + assertThat(reconfigured).isTrue(); + assertThat(silenceSkippingAudioProcessor.isActive()).isTrue(); + } + + @Test + public void testReconfiguringWithSameSampleRate_doesNotRequireReconfiguration() throws Exception { + // Given an enabled processor and configured processor. + silenceSkippingAudioProcessor.setEnabled(true); + boolean reconfigured = + silenceSkippingAudioProcessor.configure( + TEST_SIGNAL_SAMPLE_RATE_HZ, TEST_SIGNAL_CHANNEL_COUNT, C.ENCODING_PCM_16BIT); + assertThat(reconfigured).isTrue(); + silenceSkippingAudioProcessor.flush(); + + // When reconfiguring it with the same sample rate. + reconfigured = + silenceSkippingAudioProcessor.configure( + TEST_SIGNAL_SAMPLE_RATE_HZ, TEST_SIGNAL_CHANNEL_COUNT, C.ENCODING_PCM_16BIT); + + // It's not reconfigured but it is active. + assertThat(reconfigured).isFalse(); + assertThat(silenceSkippingAudioProcessor.isActive()).isTrue(); + } + + @Test + public void testSkipInSilentSignal_skipsEverything() throws Exception { + // Given a signal with only noise. + InputBufferProvider inputBufferProvider = + getInputBufferProviderForAlternatingSilenceAndNoise( + TEST_SIGNAL_SAMPLE_RATE_HZ, + TEST_SIGNAL_CHANNEL_COUNT, + TEST_SIGNAL_SILENCE_DURATION_MS, + /* noiseDurationMs= */ 0, + TEST_SIGNAL_FRAME_COUNT); + + // When processing the entire signal. + silenceSkippingAudioProcessor.setEnabled(true); + boolean reconfigured = + silenceSkippingAudioProcessor.configure( + TEST_SIGNAL_SAMPLE_RATE_HZ, TEST_SIGNAL_CHANNEL_COUNT, C.ENCODING_PCM_16BIT); + silenceSkippingAudioProcessor.flush(); + assertThat(reconfigured).isTrue(); + assertThat(silenceSkippingAudioProcessor.isActive()).isTrue(); + long totalOutputFrames = + process(silenceSkippingAudioProcessor, inputBufferProvider, INPUT_BUFFER_SIZE); + + // The entire signal is skipped. + assertThat(totalOutputFrames).isEqualTo(0); + assertThat(silenceSkippingAudioProcessor.getSkippedFrames()).isEqualTo(TEST_SIGNAL_FRAME_COUNT); + } + + @Test + public void testSkipInNoisySignal_skipsNothing() throws Exception { + // Given a signal with only silence. + InputBufferProvider inputBufferProvider = + getInputBufferProviderForAlternatingSilenceAndNoise( + TEST_SIGNAL_SAMPLE_RATE_HZ, + TEST_SIGNAL_CHANNEL_COUNT, + /* silenceDurationMs= */ 0, + TEST_SIGNAL_NOISE_DURATION_MS, + TEST_SIGNAL_FRAME_COUNT); + + // When processing the entire signal. + SilenceSkippingAudioProcessor silenceSkippingAudioProcessor = + new SilenceSkippingAudioProcessor(); + silenceSkippingAudioProcessor.setEnabled(true); + boolean reconfigured = + silenceSkippingAudioProcessor.configure( + TEST_SIGNAL_SAMPLE_RATE_HZ, TEST_SIGNAL_CHANNEL_COUNT, C.ENCODING_PCM_16BIT); + silenceSkippingAudioProcessor.flush(); + assertThat(reconfigured).isTrue(); + assertThat(silenceSkippingAudioProcessor.isActive()).isTrue(); + long totalOutputFrames = + process(silenceSkippingAudioProcessor, inputBufferProvider, INPUT_BUFFER_SIZE); + + // None of the signal is skipped. + assertThat(totalOutputFrames).isEqualTo(TEST_SIGNAL_FRAME_COUNT); + assertThat(silenceSkippingAudioProcessor.getSkippedFrames()).isEqualTo(0); + } + + @Test + public void testSkipInAlternatingTestSignal_hasCorrectOutputAndSkippedFrameCounts() + throws Exception { + // Given a signal that alternates between silence and noise. + InputBufferProvider inputBufferProvider = + getInputBufferProviderForAlternatingSilenceAndNoise( + TEST_SIGNAL_SAMPLE_RATE_HZ, + TEST_SIGNAL_CHANNEL_COUNT, + TEST_SIGNAL_SILENCE_DURATION_MS, + TEST_SIGNAL_NOISE_DURATION_MS, + TEST_SIGNAL_FRAME_COUNT); + + // When processing the entire signal. + SilenceSkippingAudioProcessor silenceSkippingAudioProcessor = + new SilenceSkippingAudioProcessor(); + silenceSkippingAudioProcessor.setEnabled(true); + boolean reconfigured = + silenceSkippingAudioProcessor.configure( + TEST_SIGNAL_SAMPLE_RATE_HZ, TEST_SIGNAL_CHANNEL_COUNT, C.ENCODING_PCM_16BIT); + silenceSkippingAudioProcessor.flush(); + assertThat(reconfigured).isTrue(); + assertThat(silenceSkippingAudioProcessor.isActive()).isTrue(); + long totalOutputFrames = + process(silenceSkippingAudioProcessor, inputBufferProvider, INPUT_BUFFER_SIZE); + + // The right number of frames are skipped/output. + assertThat(totalOutputFrames).isEqualTo(53990); + assertThat(silenceSkippingAudioProcessor.getSkippedFrames()).isEqualTo(46010); + } + + @Test + public void testSkipWithSmallerInputBufferSize_hasCorrectOutputAndSkippedFrameCounts() + throws Exception { + // Given a signal that alternates between silence and noise. + InputBufferProvider inputBufferProvider = + getInputBufferProviderForAlternatingSilenceAndNoise( + TEST_SIGNAL_SAMPLE_RATE_HZ, + TEST_SIGNAL_CHANNEL_COUNT, + TEST_SIGNAL_SILENCE_DURATION_MS, + TEST_SIGNAL_NOISE_DURATION_MS, + TEST_SIGNAL_FRAME_COUNT); + + // When processing the entire signal with a smaller input buffer size. + SilenceSkippingAudioProcessor silenceSkippingAudioProcessor = + new SilenceSkippingAudioProcessor(); + silenceSkippingAudioProcessor.setEnabled(true); + boolean reconfigured = + silenceSkippingAudioProcessor.configure( + TEST_SIGNAL_SAMPLE_RATE_HZ, TEST_SIGNAL_CHANNEL_COUNT, C.ENCODING_PCM_16BIT); + silenceSkippingAudioProcessor.flush(); + assertThat(reconfigured).isTrue(); + assertThat(silenceSkippingAudioProcessor.isActive()).isTrue(); + long totalOutputFrames = + process(silenceSkippingAudioProcessor, inputBufferProvider, /* inputBufferSize= */ 80); + + // The right number of frames are skipped/output. + assertThat(totalOutputFrames).isEqualTo(53990); + assertThat(silenceSkippingAudioProcessor.getSkippedFrames()).isEqualTo(46010); + } + + @Test + public void testSkipWithLargerInputBufferSize_hasCorrectOutputAndSkippedFrameCounts() + throws Exception { + // Given a signal that alternates between silence and noise. + InputBufferProvider inputBufferProvider = + getInputBufferProviderForAlternatingSilenceAndNoise( + TEST_SIGNAL_SAMPLE_RATE_HZ, + TEST_SIGNAL_CHANNEL_COUNT, + TEST_SIGNAL_SILENCE_DURATION_MS, + TEST_SIGNAL_NOISE_DURATION_MS, + TEST_SIGNAL_FRAME_COUNT); + + // When processing the entire signal with a larger input buffer size. + SilenceSkippingAudioProcessor silenceSkippingAudioProcessor = + new SilenceSkippingAudioProcessor(); + silenceSkippingAudioProcessor.setEnabled(true); + boolean reconfigured = + silenceSkippingAudioProcessor.configure( + TEST_SIGNAL_SAMPLE_RATE_HZ, TEST_SIGNAL_CHANNEL_COUNT, C.ENCODING_PCM_16BIT); + silenceSkippingAudioProcessor.flush(); + assertThat(reconfigured).isTrue(); + assertThat(silenceSkippingAudioProcessor.isActive()).isTrue(); + long totalOutputFrames = + process(silenceSkippingAudioProcessor, inputBufferProvider, /* inputBufferSize= */ 120); + + // The right number of frames are skipped/output. + assertThat(totalOutputFrames).isEqualTo(53990); + assertThat(silenceSkippingAudioProcessor.getSkippedFrames()).isEqualTo(46010); + } + + @Test + public void testSkipThenFlush_resetsSkippedFrameCount() throws Exception { + // Given a signal that alternates between silence and noise. + InputBufferProvider inputBufferProvider = + getInputBufferProviderForAlternatingSilenceAndNoise( + TEST_SIGNAL_SAMPLE_RATE_HZ, + TEST_SIGNAL_CHANNEL_COUNT, + TEST_SIGNAL_SILENCE_DURATION_MS, + TEST_SIGNAL_NOISE_DURATION_MS, + TEST_SIGNAL_FRAME_COUNT); + + // When processing the entire signal then flushing. + SilenceSkippingAudioProcessor silenceSkippingAudioProcessor = + new SilenceSkippingAudioProcessor(); + silenceSkippingAudioProcessor.setEnabled(true); + boolean reconfigured = + silenceSkippingAudioProcessor.configure( + TEST_SIGNAL_SAMPLE_RATE_HZ, TEST_SIGNAL_CHANNEL_COUNT, C.ENCODING_PCM_16BIT); + silenceSkippingAudioProcessor.flush(); + assertThat(reconfigured).isTrue(); + assertThat(silenceSkippingAudioProcessor.isActive()).isTrue(); + process(silenceSkippingAudioProcessor, inputBufferProvider, INPUT_BUFFER_SIZE); + silenceSkippingAudioProcessor.flush(); + + // The skipped frame count is zero. + assertThat(silenceSkippingAudioProcessor.getSkippedFrames()).isEqualTo(0); + } + + /** + * Processes the entire stream provided by {@code inputBufferProvider} in chunks of {@code + * inputBufferSize} and returns the total number of output frames. + */ + private static long process( + SilenceSkippingAudioProcessor processor, + InputBufferProvider inputBufferProvider, + int inputBufferSize) + throws UnhandledFormatException { + processor.flush(); + long totalOutputFrames = 0; + while (inputBufferProvider.hasRemaining()) { + ByteBuffer inputBuffer = inputBufferProvider.getNextInputBuffer(inputBufferSize); + while (inputBuffer.hasRemaining()) { + processor.queueInput(inputBuffer); + ByteBuffer outputBuffer = processor.getOutput(); + totalOutputFrames += outputBuffer.remaining() / (2 * processor.getOutputChannelCount()); + outputBuffer.clear(); + } + } + processor.queueEndOfStream(); + while (!processor.isEnded()) { + ByteBuffer outputBuffer = processor.getOutput(); + totalOutputFrames += outputBuffer.remaining() / (2 * processor.getOutputChannelCount()); + outputBuffer.clear(); + } + return totalOutputFrames; + } + + /** + * Returns an {@link InputBufferProvider} that provides input buffers for a stream that alternates + * between silence/noise of the specified durations to fill {@code totalFrameCount}. + */ + private static InputBufferProvider getInputBufferProviderForAlternatingSilenceAndNoise( + int sampleRateHz, + int channelCount, + int silenceDurationMs, + int noiseDurationMs, + int totalFrameCount) { + Pcm16BitAudioBuilder audioBuilder = new Pcm16BitAudioBuilder(channelCount, totalFrameCount); + while (!audioBuilder.isFull()) { + int silenceDurationFrames = (silenceDurationMs * sampleRateHz) / 1000; + audioBuilder.appendFrames(/* count= */ silenceDurationFrames, /* channelLevels= */ (short) 0); + int noiseDurationFrames = (noiseDurationMs * sampleRateHz) / 1000; + audioBuilder.appendFrames( + /* count= */ noiseDurationFrames, /* channelLevels= */ Short.MAX_VALUE); + } + return new InputBufferProvider(audioBuilder.build()); + } + + /** + * Wraps a {@link ShortBuffer} and provides a sequence of {@link ByteBuffer}s of specified sizes + * that contain copies of its data. + */ + private static final class InputBufferProvider { + + private final ShortBuffer buffer; + + public InputBufferProvider(ShortBuffer buffer) { + this.buffer = buffer; + } + + /** Returns the next buffer with size up to {@code sizeBytes}. */ + public ByteBuffer getNextInputBuffer(int sizeBytes) { + ByteBuffer inputBuffer = ByteBuffer.allocate(sizeBytes).order(ByteOrder.nativeOrder()); + ShortBuffer inputBufferAsShortBuffer = inputBuffer.asShortBuffer(); + int limit = buffer.limit(); + buffer.limit(Math.min(buffer.position() + sizeBytes / 2, limit)); + inputBufferAsShortBuffer.put(buffer); + buffer.limit(limit); + inputBuffer.limit(inputBufferAsShortBuffer.position() * 2); + return inputBuffer; + } + + /** Returns whether any more input can be provided via {@link #getNextInputBuffer(int)}. */ + public boolean hasRemaining() { + return buffer.hasRemaining(); + } + } + + /** Builder for {@link ShortBuffer}s that contain 16-bit PCM audio samples. */ + private static final class Pcm16BitAudioBuilder { + + private final int channelCount; + private final ShortBuffer buffer; + + private boolean built; + + public Pcm16BitAudioBuilder(int channelCount, int frameCount) { + this.channelCount = channelCount; + buffer = ByteBuffer.allocate(frameCount * channelCount * 2).asShortBuffer(); + } + + /** + * Appends {@code count} audio frames, using the specified {@code channelLevels} in each frame. + */ + public void appendFrames(int count, short... channelLevels) { + Assertions.checkState(!built); + for (int i = 0; i < count; i += channelCount) { + for (int j = 0; j < channelLevels.length; j++) { + buffer.put(channelLevels[j]); + } + } + } + + /** Returns whether the buffer is full. */ + public boolean isFull() { + Assertions.checkState(!built); + return !buffer.hasRemaining(); + } + + /** Returns the built buffer. After calling this method the builder should not be reused. */ + public ShortBuffer build() { + Assertions.checkState(!built); + built = true; + buffer.flip(); + return buffer; + } + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRendererTest.java b/library/core/src/test/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRendererTest.java index 9d5533e8ab..8dc60a15a4 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRendererTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRendererTest.java @@ -92,7 +92,7 @@ public class SimpleDecoderAudioRendererTest { audioRenderer.enable( RendererConfiguration.DEFAULT, new Format[] {FORMAT}, - new FakeSampleStream(FORMAT, false), + new FakeSampleStream(FORMAT, /* eventDispatcher= */ null, /* shouldOutputSample= */ false), 0, false, 0); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/audio/SonicAudioProcessorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/audio/SonicAudioProcessorTest.java index d060ba3f16..1ba462d4af 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/audio/SonicAudioProcessorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/audio/SonicAudioProcessorTest.java @@ -94,6 +94,7 @@ public final class SonicAudioProcessorTest { public void testIsActiveWithSpeedChange() throws Exception { sonicAudioProcessor.setSpeed(1.5f); sonicAudioProcessor.configure(44100, 2, C.ENCODING_PCM_16BIT); + sonicAudioProcessor.flush(); assertThat(sonicAudioProcessor.isActive()).isTrue(); } @@ -101,6 +102,7 @@ public final class SonicAudioProcessorTest { public void testIsActiveWithPitchChange() throws Exception { sonicAudioProcessor.setPitch(1.5f); sonicAudioProcessor.configure(44100, 2, C.ENCODING_PCM_16BIT); + sonicAudioProcessor.flush(); assertThat(sonicAudioProcessor.isActive()).isTrue(); } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/drm/ClearKeyUtilTest.java b/library/core/src/test/java/com/google/android/exoplayer2/drm/ClearKeyUtilTest.java index c84ca6182c..460a237698 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/drm/ClearKeyUtilTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/drm/ClearKeyUtilTest.java @@ -17,8 +17,7 @@ package com.google.android.exoplayer2.drm; import static com.google.common.truth.Truth.assertThat; -import com.google.android.exoplayer2.C; -import java.nio.charset.Charset; +import com.google.android.exoplayer2.util.Util; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; @@ -27,37 +26,115 @@ import org.robolectric.annotation.Config; /** * Unit test for {@link ClearKeyUtil}. */ -// TODO: When API level 27 is supported, add tests that check the adjust methods are no-ops. @RunWith(RobolectricTestRunner.class) public final class ClearKeyUtilTest { + private static final byte[] SINGLE_KEY_RESPONSE = + Util.getUtf8Bytes( + "{" + + "\"keys\":[" + + "{" + + "\"k\":\"abc_def-\"," + + "\"kid\":\"ab_cde-f\"," + + "\"kty\":\"o_c-t\"," + + "\"ignored\":\"ignored\"" + + "}" + + "]," + + "\"ignored\":\"ignored\"" + + "}"); + private static final byte[] MULTI_KEY_RESPONSE = + Util.getUtf8Bytes( + "{" + + "\"keys\":[" + + "{" + + "\"k\":\"abc_def-\"," + + "\"kid\":\"ab_cde-f\"," + + "\"kty\":\"oct\"," + + "\"ignored\":\"ignored\"" + + "},{" + + "\"k\":\"ghi_jkl-\"," + + "\"kid\":\"gh_ijk-l\"," + + "\"kty\":\"oct\"" + + "}" + + "]," + + "\"ignored\":\"ignored\"" + + "}"); + private static final byte[] KEY_REQUEST = + Util.getUtf8Bytes( + "{" + + "\"kids\":[" + + "\"abc+def/\"," + + "\"ab+cde/f\"" + + "]," + + "\"type\":\"temporary\"" + + "}"); + @Config(sdk = 26) @Test - public void testAdjustResponseDataV26() { - byte[] data = ("{\"keys\":[{" - + "\"k\":\"abc_def-\"," - + "\"kid\":\"ab_cde-f\"}]," - + "\"type\":\"abc_def-" - + "\"}").getBytes(Charset.forName(C.UTF8_NAME)); - // We expect "-" and "_" to be replaced with "+" and "\/" (forward slashes need to be escaped in - // JSON respectively, for "k" and "kid" only. - byte[] expected = ("{\"keys\":[{" - + "\"k\":\"abc\\/def+\"," - + "\"kid\":\"ab\\/cde+f\"}]," - + "\"type\":\"abc_def-" - + "\"}").getBytes(Charset.forName(C.UTF8_NAME)); - assertThat(ClearKeyUtil.adjustResponseData(data)).isEqualTo(expected); + public void testAdjustSingleKeyResponseDataV26() { + // Everything but the keys should be removed. Within each key only the k, kid and kty parameters + // should remain. Any "-" and "_" characters in the k and kid values should be replaced with "+" + // and "/". + byte[] expected = + Util.getUtf8Bytes( + "{" + + "\"keys\":[" + + "{" + + "\"k\":\"abc/def+\",\"kid\":\"ab/cde+f\",\"kty\":\"o_c-t\"" + + "}" + + "]" + + "}"); + assertThat(ClearKeyUtil.adjustResponseData(SINGLE_KEY_RESPONSE)).isEqualTo(expected); + } + + @Config(sdk = 26) + @Test + public void testAdjustMultiKeyResponseDataV26() { + // Everything but the keys should be removed. Within each key only the k, kid and kty parameters + // should remain. Any "-" and "_" characters in the k and kid values should be replaced with "+" + // and "/". + byte[] expected = + Util.getUtf8Bytes( + "{" + + "\"keys\":[" + + "{" + + "\"k\":\"abc/def+\",\"kid\":\"ab/cde+f\",\"kty\":\"oct\"" + + "},{" + + "\"k\":\"ghi/jkl+\",\"kid\":\"gh/ijk+l\",\"kty\":\"oct\"" + + "}" + + "]" + + "}"); + assertThat(ClearKeyUtil.adjustResponseData(MULTI_KEY_RESPONSE)).isEqualTo(expected); + } + + @Config(sdk = 27) + @Test + public void testAdjustResponseDataV27() { + // Response should be unchanged. + assertThat(ClearKeyUtil.adjustResponseData(SINGLE_KEY_RESPONSE)).isEqualTo(SINGLE_KEY_RESPONSE); } @Config(sdk = 26) @Test public void testAdjustRequestDataV26() { - byte[] data = "{\"kids\":[\"abc+def/\",\"ab+cde/f\"],\"type\":\"abc+def/\"}" - .getBytes(Charset.forName(C.UTF8_NAME)); // We expect "+" and "/" to be replaced with "-" and "_" respectively, for "kids". - byte[] expected = "{\"kids\":[\"abc-def_\",\"ab-cde_f\"],\"type\":\"abc+def/\"}" - .getBytes(Charset.forName(C.UTF8_NAME)); - assertThat(ClearKeyUtil.adjustRequestData(data)).isEqualTo(expected); + byte[] expected = + Util.getUtf8Bytes( + "{" + + "\"kids\":[" + + "\"abc-def_\"," + + "\"ab-cde_f\"" + + "]," + + "\"type\":\"temporary\"" + + "}"); + assertThat(ClearKeyUtil.adjustRequestData(KEY_REQUEST)).isEqualTo(expected); + } + + @Config(sdk = 27) + @Test + public void testAdjustRequestDataV27() { + // Request should be unchanged. + assertThat(ClearKeyUtil.adjustRequestData(KEY_REQUEST)).isEqualTo(KEY_REQUEST); } } 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/Id3PeekerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/Id3PeekerTest.java new file mode 100644 index 0000000000..a397f70886 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/Id3PeekerTest.java @@ -0,0 +1,111 @@ +/* + * 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; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.android.exoplayer2.metadata.Metadata; +import com.google.android.exoplayer2.metadata.id3.ApicFrame; +import com.google.android.exoplayer2.metadata.id3.CommentFrame; +import com.google.android.exoplayer2.metadata.id3.Id3Decoder; +import com.google.android.exoplayer2.metadata.id3.Id3DecoderTest; +import com.google.android.exoplayer2.testutil.FakeExtractorInput; +import java.io.IOException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +/** Unit test for {@link Id3Peeker}. */ +@RunWith(RobolectricTestRunner.class) +public final class Id3PeekerTest { + + @Test + public void testPeekId3Data_returnNull_ifId3TagNotPresentAtBeginningOfInput() + throws IOException, InterruptedException { + Id3Peeker id3Peeker = new Id3Peeker(); + FakeExtractorInput input = + new FakeExtractorInput.Builder() + .setData(new byte[] {1, 'I', 'D', '3', 2, 3, 4, 5, 6, 7, 8, 9, 10}) + .build(); + + Metadata metadata = id3Peeker.peekId3Data(input, /* id3FramePredicate= */ null); + assertThat(metadata).isNull(); + } + + @Test + public void testPeekId3Data_returnId3Tag_ifId3TagPresent() + throws IOException, InterruptedException { + Id3Peeker id3Peeker = new Id3Peeker(); + + byte[] rawId3 = + Id3DecoderTest.buildSingleFrameTag( + "APIC", + new byte[] { + 3, 105, 109, 97, 103, 101, 47, 106, 112, 101, 103, 0, 16, 72, 101, 108, 108, 111, 32, + 87, 111, 114, 108, 100, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 + }); + FakeExtractorInput input = new FakeExtractorInput.Builder().setData(rawId3).build(); + + Metadata metadata = id3Peeker.peekId3Data(input, /* id3FramePredicate= */ null); + assertThat(metadata.length()).isEqualTo(1); + + ApicFrame apicFrame = (ApicFrame) metadata.get(0); + assertThat(apicFrame.mimeType).isEqualTo("image/jpeg"); + assertThat(apicFrame.pictureType).isEqualTo(16); + assertThat(apicFrame.description).isEqualTo("Hello World"); + assertThat(apicFrame.pictureData).hasLength(10); + assertThat(apicFrame.pictureData).isEqualTo(new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 0}); + } + + @Test + public void testPeekId3Data_returnId3TagAccordingToGivenPredicate_ifId3TagPresent() + throws IOException, InterruptedException { + Id3Peeker id3Peeker = new Id3Peeker(); + + byte[] rawId3 = + Id3DecoderTest.buildMultiFramesTag( + new Id3DecoderTest.FrameSpec( + "COMM", + new byte[] { + 3, 101, 110, 103, 100, 101, 115, 99, 114, 105, 112, 116, 105, 111, 110, 0, 116, + 101, 120, 116, 0 + }), + new Id3DecoderTest.FrameSpec( + "APIC", + new byte[] { + 3, 105, 109, 97, 103, 101, 47, 106, 112, 101, 103, 0, 16, 72, 101, 108, 108, 111, + 32, 87, 111, 114, 108, 100, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 + })); + FakeExtractorInput input = new FakeExtractorInput.Builder().setData(rawId3).build(); + + Metadata metadata = + id3Peeker.peekId3Data( + input, + new Id3Decoder.FramePredicate() { + @Override + public boolean evaluate(int majorVersion, int id0, int id1, int id2, int id3) { + return id0 == 'C' && id1 == 'O' && id2 == 'M' && id3 == 'M'; + } + }); + assertThat(metadata.length()).isEqualTo(1); + + CommentFrame commentFrame = (CommentFrame) metadata.get(0); + assertThat(commentFrame.language).isEqualTo("eng"); + assertThat(commentFrame.description).isEqualTo("description"); + assertThat(commentFrame.text).isEqualTo("text"); + } +} 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/extractor/mkv/DefaultEbmlReaderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReaderTest.java index e44da0404b..7383f8a538 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReaderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReaderTest.java @@ -182,22 +182,22 @@ public class DefaultEbmlReaderTest { private final List events = new ArrayList<>(); @Override - public int getElementType(int id) { + public @ElementType int getElementType(int id) { switch (id) { case ID_EBML: case ID_SEGMENT: - return EbmlReader.TYPE_MASTER; + return TYPE_MASTER; case ID_EBML_READ_VERSION: case ID_DOC_TYPE_READ_VERSION: - return EbmlReader.TYPE_UNSIGNED_INT; + return TYPE_UNSIGNED_INT; case ID_DOC_TYPE: - return EbmlReader.TYPE_STRING; + return TYPE_STRING; case ID_SIMPLE_BLOCK: - return EbmlReader.TYPE_BINARY; + return TYPE_BINARY; case ID_DURATION: - return EbmlReader.TYPE_FLOAT; + return TYPE_FLOAT; default: - return EbmlReader.TYPE_UNKNOWN; + return TYPE_UNKNOWN; } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java index f5b0f48592..176211acb8 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java @@ -36,6 +36,12 @@ public final class FragmentedMp4ExtractorTest { getExtractorFactory(Collections.emptyList()), "mp4/sample_fragmented.mp4"); } + @Test + public void testSampleSeekable() throws Exception { + ExtractorAsserts.assertBehavior( + getExtractorFactory(Collections.emptyList()), "mp4/sample_fragmented_seekable.mp4"); + } + @Test public void testSampleWithSeiPayloadParsing() throws Exception { // Enabling the CEA-608 track enables SEI payload parsing. diff --git a/library/core/src/test/java/com/google/android/exoplayer2/metadata/id3/Id3DecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/metadata/id3/Id3DecoderTest.java index 4e7ae0eec0..0b992f0981 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/metadata/id3/Id3DecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/metadata/id3/Id3DecoderTest.java @@ -22,6 +22,7 @@ import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.MetadataDecoderException; import com.google.android.exoplayer2.util.Assertions; import java.nio.charset.Charset; +import java.util.Arrays; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; @@ -32,7 +33,7 @@ import org.robolectric.RobolectricTestRunner; @RunWith(RobolectricTestRunner.class) public final class Id3DecoderTest { - private static final byte[] TAG_HEADER = new byte[] {73, 68, 51, 4, 0, 0, 0, 0, 0, 0}; + private static final byte[] TAG_HEADER = new byte[] {'I', 'D', '3', 4, 0, 0, 0, 0, 0, 0}; private static final int FRAME_HEADER_LENGTH = 10; private static final int ID3_TEXT_ENCODING_UTF_8 = 3; @@ -202,33 +203,90 @@ public final class Id3DecoderTest { assertThat(commentFrame.text).isEmpty(); } - private static byte[] buildSingleFrameTag(String frameId, byte[] frameData) { - byte[] frameIdBytes = frameId.getBytes(Charset.forName(C.UTF8_NAME)); - Assertions.checkState(frameIdBytes.length == 4); + @Test + public void testDecodeMultiFrames() throws MetadataDecoderException { + byte[] rawId3 = + buildMultiFramesTag( + new FrameSpec( + "COMM", + new byte[] { + 3, 101, 110, 103, 100, 101, 115, 99, 114, 105, 112, 116, 105, 111, 110, 0, 116, + 101, 120, 116, 0 + }), + new FrameSpec( + "APIC", + new byte[] { + 3, 105, 109, 97, 103, 101, 47, 106, 112, 101, 103, 0, 16, 72, 101, 108, 108, 111, + 32, 87, 111, 114, 108, 100, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 + })); + Id3Decoder decoder = new Id3Decoder(); + Metadata metadata = decoder.decode(rawId3, rawId3.length); + assertThat(metadata.length()).isEqualTo(2); + CommentFrame commentFrame = (CommentFrame) metadata.get(0); + ApicFrame apicFrame = (ApicFrame) metadata.get(1); - byte[] tagData = new byte[TAG_HEADER.length + FRAME_HEADER_LENGTH + frameData.length]; - System.arraycopy(TAG_HEADER, 0, tagData, 0, TAG_HEADER.length); + assertThat(commentFrame.language).isEqualTo("eng"); + assertThat(commentFrame.description).isEqualTo("description"); + assertThat(commentFrame.text).isEqualTo("text"); + + assertThat(apicFrame.mimeType).isEqualTo("image/jpeg"); + assertThat(apicFrame.pictureType).isEqualTo(16); + assertThat(apicFrame.description).isEqualTo("Hello World"); + assertThat(apicFrame.pictureData).hasLength(10); + assertThat(apicFrame.pictureData).isEqualTo(new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 0}); + } + + public static byte[] buildSingleFrameTag(String frameId, byte[] frameData) { + return buildMultiFramesTag(new FrameSpec(frameId, frameData)); + } + + public static byte[] buildMultiFramesTag(FrameSpec... frames) { + int totalLength = TAG_HEADER.length; + for (FrameSpec frame : frames) { + byte[] frameData = frame.frameData; + totalLength += FRAME_HEADER_LENGTH + frameData.length; + } + byte[] tagData = Arrays.copyOf(TAG_HEADER, totalLength); // Fill in the size part of the tag header. int offset = TAG_HEADER.length - 4; - int tagSize = frameData.length + FRAME_HEADER_LENGTH; + int tagSize = totalLength - TAG_HEADER.length; tagData[offset++] = (byte) ((tagSize >> 21) & 0x7F); tagData[offset++] = (byte) ((tagSize >> 14) & 0x7F); tagData[offset++] = (byte) ((tagSize >> 7) & 0x7F); tagData[offset++] = (byte) (tagSize & 0x7F); - // Fill in the frame header. - tagData[offset++] = frameIdBytes[0]; - tagData[offset++] = frameIdBytes[1]; - tagData[offset++] = frameIdBytes[2]; - tagData[offset++] = frameIdBytes[3]; - tagData[offset++] = (byte) ((frameData.length >> 24) & 0xFF); - tagData[offset++] = (byte) ((frameData.length >> 16) & 0xFF); - tagData[offset++] = (byte) ((frameData.length >> 8) & 0xFF); - tagData[offset++] = (byte) (frameData.length & 0xFF); - offset += 2; // Frame flags set to 0 - // Fill in the frame data. - System.arraycopy(frameData, 0, tagData, offset, frameData.length); + for (FrameSpec frame : frames) { + byte[] frameData = frame.frameData; + String frameId = frame.frameId; + byte[] frameIdBytes = frameId.getBytes(Charset.forName(C.UTF8_NAME)); + Assertions.checkState(frameIdBytes.length == 4); + + // Fill in the frame header. + tagData[offset++] = frameIdBytes[0]; + tagData[offset++] = frameIdBytes[1]; + tagData[offset++] = frameIdBytes[2]; + tagData[offset++] = frameIdBytes[3]; + tagData[offset++] = (byte) ((frameData.length >> 24) & 0xFF); + tagData[offset++] = (byte) ((frameData.length >> 16) & 0xFF); + tagData[offset++] = (byte) ((frameData.length >> 8) & 0xFF); + tagData[offset++] = (byte) (frameData.length & 0xFF); + offset += 2; // Frame flags set to 0 + + // Fill in the frame data. + System.arraycopy(frameData, 0, tagData, offset, frameData.length); + offset += frameData.length; + } return tagData; } + /** Specify an ID3 frame. */ + public static final class FrameSpec { + public final String frameId; + public final byte[] frameData; + + public FrameSpec(String frameId, byte[] frameData) { + this.frameId = frameId; + this.frameData = frameData; + } + } } 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 new file mode 100644 index 0000000000..e821bc34a0 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/offline/ActionFileTest.java @@ -0,0 +1,267 @@ +/* + * 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.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; +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +/** + * Unit tests for {@link ProgressiveDownloadAction}. + */ +@RunWith(RobolectricTestRunner.class) +public class ActionFileTest { + + private File tempFile; + + @Before + public void setUp() throws Exception { + tempFile = Util.createTempFile(RuntimeEnvironment.application, "ExoPlayerTest"); + } + + @After + public void tearDown() throws Exception { + tempFile.delete(); + } + + @Test + public void testLoadNoDataThrowsIOException() throws Exception { + try { + loadActions(new Object[] {}); + Assert.fail(); + } catch (IOException e) { + // Expected exception. + } + } + + @Test + public void testLoadIncompleteHeaderThrowsIOException() throws Exception { + try { + loadActions(new Object[] {ActionFile.VERSION}); + Assert.fail(); + } catch (IOException e) { + // Expected exception. + } + } + + @Test + public void testLoadCompleteHeaderZeroAction() throws Exception { + DownloadAction[] actions = loadActions(new Object[] {ActionFile.VERSION, 0}); + assertThat(actions).isNotNull(); + assertThat(actions).hasLength(0); + } + + @Test + public void testLoadAction() throws Exception { + 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", FakeDownloadAction.VERSION, data); + } + + @Test + public void testLoadActions() throws Exception { + 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", FakeDownloadAction.VERSION, data1); + assertAction(actions[1], "type2", FakeDownloadAction.VERSION, data2); + } + + @Test + public void testLoadNotSupportedVersion() throws Exception { + try { + 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. + } + } + + @Test + public void testLoadNotSupportedType() throws Exception { + try { + 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. + } + } + + @Test + public void testStoreAndLoadNoActions() throws Exception { + doTestSerializationRoundTrip(new DownloadAction[0]); + } + + @Test + public void testStoreAndLoadActions() throws Exception { + 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, + Deserializer... deserializers) throws IOException { + ActionFile actionFile = new ActionFile(tempFile); + actionFile.store(actions); + assertThat(actionFile.load(deserializers)).isEqualTo(actions); + } + + private DownloadAction[] loadActions(Object[] values, Deserializer... deserializers) + throws IOException { + FileOutputStream fileOutputStream = new FileOutputStream(tempFile); + DataOutputStream dataOutputStream = new DataOutputStream(fileOutputStream); + try { + for (Object value : values) { + if (value instanceof Integer) { + dataOutputStream.writeInt((Integer) value); + } else if (value instanceof String) { + dataOutputStream.writeUTF((String) value); + } else if (value instanceof byte[]) { + byte[] data = (byte[]) value; + dataOutputStream.writeInt(data.length); + dataOutputStream.write(data); + } else { + throw new IllegalArgumentException(); + } + } + } finally { + dataOutputStream.close(); + } + return new ActionFile(tempFile).load(deserializers); + } + + private static void assertAction(DownloadAction action, String type, int version, byte[] data) { + assertThat(action).isInstanceOf(FakeDownloadAction.class); + assertThat(action.type).isEqualTo(type); + assertThat(((FakeDownloadAction) action).version).isEqualTo(version); + assertThat(((FakeDownloadAction) action).data).isEqualTo(data); + } + + private static class FakeDeserializer extends Deserializer { + + FakeDeserializer(String type) { + super(type, FakeDownloadAction.VERSION); + } + + @Override + public DownloadAction readFromStream(int version, DataInputStream input) throws IOException { + int dataLength = input.readInt(); + byte[] data = new byte[dataLength]; + input.readFully(data); + return new FakeDownloadAction(type, data); + } + } + + private static class FakeDownloadAction extends DownloadAction { + + public static final int VERSION = 0; + + 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.length); + output.write(data); + } + + @Override + protected Downloader createDownloader(DownloaderConstructorHelper downloaderConstructorHelper) { + return null; + } + + } + +} 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 new file mode 100644 index 0000000000..0d0bf73d04 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java @@ -0,0 +1,660 @@ +/* + * 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.offline; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; + +import android.net.Uri; +import com.google.android.exoplayer2.C; +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.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; + +/** Tests {@link DownloadManager}. */ +@RunWith(RobolectricTestRunner.class) +@Config(shadows = {RobolectricUtil.CustomLooper.class, RobolectricUtil.CustomMessageQueue.class}) +public class DownloadManagerTest { + + /* Used to check if condition becomes true in this time interval. */ + private static final int ASSERT_TRUE_TIMEOUT = 10000; + /* Used to check if condition stays false for this time interval. */ + private static final int ASSERT_FALSE_TIME = 1000; + /* Maximum retry delay in DownloadManager. */ + private static final int MAX_RETRY_DELAY = 5000; + + private static final int MIN_RETRY_COUNT = 3; + + 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"); + setUpDownloadManager(100); + } + + @After + public void tearDown() throws Exception { + releaseDownloadManager(); + actionFile.delete(); + dummyMainThread.release(); + } + + @Test + public void testDownloadActionRuns() throws Throwable { + doTestActionRuns(createDownloadAction(uri1)); + } + + @Test + public void testRemoveActionRuns() throws Throwable { + doTestActionRuns(createRemoveAction(uri1)); + } + + @Test + public void testDownloadRetriesThenFails() throws Throwable { + 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.assertFailed(); + downloadManagerListener.clearDownloadError(); + + downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); + } + + @Test + public void testDownloadNoRetryWhenCanceled() throws Throwable { + FakeDownloadAction downloadAction = createDownloadAction(uri1).ignoreInterrupts(); + downloadAction.getFakeDownloader().enableDownloadIOException = true; + downloadAction.post().assertStarted(); + + FakeDownloadAction removeAction = createRemoveAction(uri1).post(); + + downloadAction.unblock().assertCanceled(); + removeAction.unblock(); + + downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); + } + + @Test + public void testDownloadRetriesThenContinues() throws Throwable { + 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); + if (i == MIN_RETRY_COUNT) { + fakeDownloader.enableDownloadIOException = false; + } + fakeDownloader.unblock(); + } + downloadAction.assertCompleted(); + + downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); + } + + @Test + @SuppressWarnings({"NonAtomicVolatileUpdate", "NonAtomicOperationOnVolatileField"}) + public void testDownloadRetryCountResetsOnProgress() throws Throwable { + FakeDownloadAction downloadAction = createDownloadAction(uri1); + downloadAction.post(); + FakeDownloader fakeDownloader = downloadAction.getFakeDownloader(); + fakeDownloader.enableDownloadIOException = true; + fakeDownloader.downloadedBytes = 0; + for (int i = 0; i <= MIN_RETRY_COUNT + 10; i++) { + fakeDownloader.assertStarted(MAX_RETRY_DELAY); + fakeDownloader.downloadedBytes++; + if (i == MIN_RETRY_COUNT + 10) { + fakeDownloader.enableDownloadIOException = false; + } + fakeDownloader.unblock(); + } + downloadAction.assertCompleted(); + + downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); + } + + @Test + public void testDifferentMediaDownloadActionsStartInParallel() throws Throwable { + doTestActionsRunInParallel(createDownloadAction(uri1), createDownloadAction(uri2)); + } + + @Test + public void testDifferentMediaDifferentActionsStartInParallel() throws Throwable { + doTestActionsRunInParallel(createDownloadAction(uri1), createRemoveAction(uri2)); + } + + @Test + public void testSameMediaDownloadActionsStartInParallel() throws Throwable { + doTestActionsRunInParallel(createDownloadAction(uri1), createDownloadAction(uri1)); + } + + @Test + public void testSameMediaRemoveActionWaitsDownloadAction() throws Throwable { + doTestActionsRunSequentially(createDownloadAction(uri1), createRemoveAction(uri1)); + } + + @Test + public void testSameMediaDownloadActionWaitsRemoveAction() throws Throwable { + doTestActionsRunSequentially(createRemoveAction(uri1), createDownloadAction(uri1)); + } + + @Test + public void testSameMediaRemoveActionWaitsRemoveAction() throws Throwable { + doTestActionsRunSequentially(createRemoveAction(uri1), createRemoveAction(uri1)); + } + + @Test + public void testSameMediaMultipleActions() throws Throwable { + 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(); + downloadAction2.post().assertStarted(); + // removeAction1 is added. It interrupts the two download actions' threads but they are + // configured to ignore it so removeAction1 doesn't start. + removeAction1.post().assertDoesNotStart(); + + // downloadAction2 finishes but it isn't enough to start removeAction1. + 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().assertCanceled(); + removeAction1.assertStarted(); + // downloadAction3 still waits removeAction1 + downloadAction3.assertDoesNotStart(); + + // removeAction2 is posted. removeAction1 and downloadAction3 is canceled so removeAction2 + // starts immediately. + removeAction2.post(); + removeAction1.assertCanceled(); + downloadAction3.assertCanceled(); + removeAction2.assertStarted().unblock().assertCompleted(); + downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); + } + + @Test + public void testMultipleRemoveActionWaitsLastCancelsAllOther() throws Throwable { + FakeDownloadAction removeAction1 = createRemoveAction(uri1).ignoreInterrupts(); + FakeDownloadAction removeAction2 = createRemoveAction(uri1); + FakeDownloadAction removeAction3 = createRemoveAction(uri1); + + removeAction1.post().assertStarted(); + removeAction2.post().assertDoesNotStart(); + removeAction3.post().assertDoesNotStart(); + + removeAction2.assertCanceled(); + + removeAction1.unblock().assertCanceled(); + removeAction3.assertStarted().unblock().assertCompleted(); + + downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); + } + + @Test + public void testGetTasks() throws Throwable { + FakeDownloadAction removeAction = createRemoveAction(uri1); + FakeDownloadAction downloadAction1 = createDownloadAction(uri1); + FakeDownloadAction downloadAction2 = createDownloadAction(uri1); + + removeAction.post().assertStarted(); + downloadAction1.post().assertDoesNotStart(); + downloadAction2.post().assertDoesNotStart(); + + TaskState[] states = downloadManager.getAllTaskStates(); + assertThat(states).hasLength(3); + 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(uri1); + FakeDownloadAction downloadAction1 = createDownloadAction(uri1); + FakeDownloadAction downloadAction2 = createDownloadAction(uri1); + + removeAction.post().assertStarted(); + downloadAction1.post().assertDoesNotStart(); + downloadAction2.post().assertDoesNotStart(); + + removeAction.unblock().assertCompleted(); + downloadAction1.assertStarted(); + downloadAction2.assertStarted(); + downloadAction1.unblock().assertCompleted(); + downloadAction2.unblock().assertCompleted(); + + downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); + } + + @Test + public void testDifferentMediaDownloadActionsPreserveOrder() throws Throwable { + FakeDownloadAction removeAction = createRemoveAction(uri1).ignoreInterrupts(); + FakeDownloadAction downloadAction1 = createDownloadAction(uri1); + FakeDownloadAction downloadAction2 = createDownloadAction(uri2); + + removeAction.post().assertStarted(); + downloadAction1.post().assertDoesNotStart(); + downloadAction2.post().assertDoesNotStart(); + + removeAction.unblock().assertCompleted(); + downloadAction1.assertStarted(); + downloadAction2.assertStarted(); + downloadAction1.unblock().assertCompleted(); + downloadAction2.unblock().assertCompleted(); + + downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); + } + + @Test + public void testDifferentMediaRemoveActionsDoNotPreserveOrder() throws Throwable { + FakeDownloadAction downloadAction = createDownloadAction(uri1).ignoreInterrupts(); + FakeDownloadAction removeAction1 = createRemoveAction(uri1); + FakeDownloadAction removeAction2 = createRemoveAction(uri2); + + downloadAction.post().assertStarted(); + removeAction1.post().assertDoesNotStart(); + removeAction2.post().assertStarted(); + + downloadAction.unblock().assertCanceled(); + removeAction2.unblock().assertCompleted(); + + removeAction1.assertStarted(); + removeAction1.unblock().assertCompleted(); + + downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); + } + + @Test + public void testStopAndResume() throws Throwable { + 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(); + download2Action.post().assertDoesNotStart(); + + runOnMainThread( + new Runnable() { + @Override + public void run() { + downloadManager.stopDownloads(); + } + }); + + download1Action.assertStopped(); + + // remove actions aren't stopped. + 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.assertCanceled(); + remove1Action.assertStarted().unblock().assertCompleted(); + + // New download actions can be added but they don't start. + download3Action.post().assertDoesNotStart(); + + runOnMainThread( + new Runnable() { + @Override + public void run() { + downloadManager.startDownloads(); + } + }); + + download2Action.assertStarted().unblock().assertCompleted(); + download3Action.assertStarted().unblock().assertCompleted(); + + downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); + } + + @Test + public void testResumeBeforeTotallyStopped() throws Throwable { + setUpDownloadManager(2); + FakeDownloadAction download1Action = createDownloadAction(uri1).ignoreInterrupts(); + FakeDownloadAction download2Action = createDownloadAction(uri2); + FakeDownloadAction download3Action = createDownloadAction(uri3); + + download1Action.post().assertStarted(); + download2Action.post().assertStarted(); + // download3Action doesn't start as DM was configured to run two downloads in parallel. + download3Action.post().assertDoesNotStart(); + + runOnMainThread( + new Runnable() { + @Override + public void run() { + downloadManager.stopDownloads(); + } + }); + + // download1Action doesn't stop yet as it ignores interrupts. + download2Action.assertStopped(); + + runOnMainThread( + new Runnable() { + @Override + public void run() { + downloadManager.startDownloads(); + } + }); + + // download2Action starts immediately. + download2Action.assertStarted(); + + // download3Action doesn't start as download1Action still holds its slot. + download3Action.assertDoesNotStart(); + + // when unblocked download1Action stops and starts immediately. + download1Action.unblock().assertStopped().assertStarted(); + + download1Action.unblock(); + download2Action.unblock(); + download3Action.unblock(); + + downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); + } + + private void setUpDownloadManager(final int maxActiveDownloadTasks) throws Exception { + if (downloadManager != null) { + releaseDownloadManager(); + } + try { + runOnMainThread( + new Runnable() { + @Override + public void run() { + downloadManager = + new DownloadManager( + new DownloaderConstructorHelper( + Mockito.mock(Cache.class), DummyDataSource.FACTORY), + maxActiveDownloadTasks, + MIN_RETRY_COUNT, + actionFile, + ProgressiveDownloadAction.DESERIALIZER); + downloadManagerListener = + new TestDownloadManagerListener(downloadManager, dummyMainThread); + downloadManager.addListener(downloadManagerListener); + downloadManager.startDownloads(); + } + }); + } catch (Throwable throwable) { + throw new Exception(throwable); + } + } + + private void releaseDownloadManager() throws Exception { + try { + runOnMainThread( + new Runnable() { + @Override + public void run() { + downloadManager.release(); + } + }); + } catch (Throwable throwable) { + throw new Exception(throwable); + } + } + + private void doTestActionRuns(FakeDownloadAction action) throws Throwable { + action.post().assertStarted().unblock().assertCompleted(); + downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); + } + + private void doTestActionsRunSequentially(FakeDownloadAction action1, FakeDownloadAction action2) + throws Throwable { + action1.ignoreInterrupts().post().assertStarted(); + action2.post().assertDoesNotStart(); + + action1.unblock(); + action2.assertStarted(); + + action2.unblock().assertCompleted(); + downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); + } + + private void doTestActionsRunInParallel(FakeDownloadAction action1, FakeDownloadAction action2) + throws Throwable { + action1.post().assertStarted(); + action2.post().assertStarted(); + action1.unblock().assertCompleted(); + action2.unblock().assertCompleted(); + downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); + } + + private FakeDownloadAction createDownloadAction(Uri uri) { + return new FakeDownloadAction(uri, /* isRemoveAction= */ false); + } + + private FakeDownloadAction createRemoveAction(Uri uri) { + return new FakeDownloadAction(uri, /* isRemoveAction= */ true); + } + + private void runOnMainThread(final Runnable r) { + dummyMainThread.runOnMainThread(r); + } + + private class FakeDownloadAction extends DownloadAction { + + private final FakeDownloader downloader; + + private FakeDownloadAction(Uri uri, boolean isRemoveAction) { + super("Fake", /* version= */ 0, uri, isRemoveAction, /* data= */ null); + this.downloader = new FakeDownloader(isRemoveAction); + } + + @Override + protected void writeToStream(DataOutputStream output) { + // do nothing. + } + + @Override + protected Downloader createDownloader(DownloaderConstructorHelper downloaderConstructorHelper) { + return downloader; + } + + private FakeDownloader getFakeDownloader() { + return downloader; + } + + private FakeDownloadAction post() { + runOnMainThread( + new Runnable() { + @Override + public void run() { + downloadManager.handleAction(FakeDownloadAction.this); + } + }); + return this; + } + + private FakeDownloadAction assertDoesNotStart() throws InterruptedException { + Thread.sleep(ASSERT_FALSE_TIME); + assertThat(downloader.started.getCount()).isEqualTo(1); + return this; + } + + private FakeDownloadAction assertStarted() throws InterruptedException { + downloader.assertStarted(ASSERT_TRUE_TIMEOUT); + return assertState(TaskState.STATE_STARTED); + } + + private FakeDownloadAction assertCompleted() { + return assertState(TaskState.STATE_COMPLETED); + } + + private FakeDownloadAction assertFailed() { + return assertState(TaskState.STATE_FAILED); + } + + private FakeDownloadAction assertCanceled() { + return assertState(TaskState.STATE_CANCELED); + } + + private FakeDownloadAction assertStopped() { + return assertState(TaskState.STATE_QUEUED); + } + + private FakeDownloadAction assertState(@State int expectedState) { + while (true) { + Integer state = null; + try { + state = downloadManagerListener.pollStateChange(this, ASSERT_TRUE_TIMEOUT); + } catch (InterruptedException e) { + fail(e.getMessage()); + } + if (expectedState == state) { + return this; + } + } + } + + private FakeDownloadAction unblock() { + downloader.unblock(); + return this; + } + + private FakeDownloadAction ignoreInterrupts() { + downloader.ignoreInterrupts = true; + return this; + } + } + + private static class FakeDownloader implements Downloader { + + private final com.google.android.exoplayer2.util.ConditionVariable blocker; + private final boolean isRemoveAction; + + private CountDownLatch started; + private boolean ignoreInterrupts; + private volatile boolean enableDownloadIOException; + private volatile int downloadedBytes = C.LENGTH_UNSET; + + private FakeDownloader(boolean isRemoveAction) { + this.isRemoveAction = isRemoveAction; + this.started = new CountDownLatch(1); + this.blocker = new com.google.android.exoplayer2.util.ConditionVariable(); + } + + @Override + public void download() throws InterruptedException, IOException { + assertThat(isRemoveAction).isFalse(); + started.countDown(); + block(); + if (enableDownloadIOException) { + throw new IOException(); + } + } + + @Override + public void cancel() { + // Do nothing. + } + + @Override + public void remove() throws InterruptedException { + assertThat(isRemoveAction).isTrue(); + started.countDown(); + block(); + } + + private void block() throws InterruptedException { + try { + while (true) { + try { + blocker.block(); + break; + } catch (InterruptedException e) { + if (!ignoreInterrupts) { + throw e; + } + } + } + } finally { + blocker.close(); + } + } + + private FakeDownloader assertStarted(int timeout) throws InterruptedException { + assertThat(started.await(timeout, TimeUnit.MILLISECONDS)).isTrue(); + started = new CountDownLatch(1); + return this; + } + + private FakeDownloader unblock() { + blocker.open(); + return this; + } + + @Override + public long getDownloadedBytes() { + return downloadedBytes; + } + + @Override + public float getDownloadPercentage() { + return Float.NaN; + } + } +} 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 new file mode 100644 index 0000000000..bc3732e3d3 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/offline/ProgressiveDownloadActionTest.java @@ -0,0 +1,171 @@ +/* + * 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.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; +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; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; + +/** + * Unit tests for {@link ProgressiveDownloadAction}. + */ +@RunWith(RobolectricTestRunner.class) +public class ProgressiveDownloadActionTest { + + 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 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(uri1, false, null, null); + DownloaderConstructorHelper constructorHelper = new DownloaderConstructorHelper( + Mockito.mock(Cache.class), DummyDataSource.FACTORY); + assertThat(action.createDownloader(constructorHelper)).isNotNull(); + } + + @Test + public void testSameUriCacheKeyDifferentAction_IsSameMedia() throws Exception { + 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, 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, 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(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(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(uri1, true, null, null); + assertThat(action1.equals(action1)).isTrue(); + + 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(uri1, true, null, null); + ProgressiveDownloadAction action5 = new ProgressiveDownloadAction(uri1, false, null, null); + assertThat(action4.equals(action5)).isFalse(); + + 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(uri1, true, null, "key2"); + ProgressiveDownloadAction action9 = new ProgressiveDownloadAction(uri1, true, null, "key"); + assertThat(action8.equals(action9)).isFalse(); + + 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(uri1, false, null, null); + assertThat(action.type).isNotNull(); + } + + @Test + public void testSerializerWriteRead() throws Exception { + doTestSerializationRoundTrip(new ProgressiveDownloadAction(uri1, false, null, null)); + doTestSerializationRoundTrip(new ProgressiveDownloadAction(uri2, true, null, "key")); + } + + private void assertSameMedia( + ProgressiveDownloadAction action1, ProgressiveDownloadAction action2) { + assertThat(action1.isSameMedia(action2)).isTrue(); + assertThat(action2.isSameMedia(action1)).isTrue(); + } + + private void assertNotSameMedia( + ProgressiveDownloadAction action1, ProgressiveDownloadAction action2) { + assertThat(action1.isSameMedia(action2)).isFalse(); + assertThat(action2.isSameMedia(action1)).isFalse(); + } + + private static void doTestSerializationRoundTrip(ProgressiveDownloadAction action) + throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + DataOutputStream output = new DataOutputStream(out); + DownloadAction.serializeToStream(action, output); + + ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); + DataInputStream input = new DataInputStream(in); + DownloadAction action2 = + DownloadAction.deserializeFromStream( + new DownloadAction.Deserializer[] {ProgressiveDownloadAction.DESERIALIZER}, input); + + assertThat(action2).isEqualTo(action); + } + +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java index a4aa3eb938..e853529ae6 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java @@ -18,18 +18,25 @@ package com.google.android.exoplayer2.source; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; +import android.os.Handler; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline.Period; import com.google.android.exoplayer2.Timeline.Window; import com.google.android.exoplayer2.source.ClippingMediaSource.IllegalClippingException; +import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; +import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; +import com.google.android.exoplayer2.source.MediaSourceEventListener.MediaLoadData; +import com.google.android.exoplayer2.testutil.FakeMediaPeriod; import com.google.android.exoplayer2.testutil.FakeMediaSource; import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; import com.google.android.exoplayer2.testutil.MediaSourceTestRunner; import com.google.android.exoplayer2.testutil.RobolectricUtil; import com.google.android.exoplayer2.testutil.TimelineAsserts; +import com.google.android.exoplayer2.upstream.Allocator; import java.io.IOException; import org.junit.Before; import org.junit.Test; @@ -56,7 +63,7 @@ public final class ClippingMediaSourceTest { @Test public void testNoClipping() throws IOException { - Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true, false); + Timeline timeline = new SinglePeriodTimeline(TEST_PERIOD_DURATION_US, true, false); Timeline clippedTimeline = getClippedTimeline(timeline, 0, TEST_PERIOD_DURATION_US); @@ -70,7 +77,7 @@ public final class ClippingMediaSourceTest { @Test public void testClippingUnseekableWindowThrows() throws IOException { - Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), false, false); + Timeline timeline = new SinglePeriodTimeline(TEST_PERIOD_DURATION_US, false, false); // If the unseekable window isn't clipped, clipping succeeds. getClippedTimeline(timeline, 0, TEST_PERIOD_DURATION_US); @@ -85,19 +92,19 @@ public final class ClippingMediaSourceTest { @Test public void testClippingStart() throws IOException { - Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true, false); + Timeline timeline = new SinglePeriodTimeline(TEST_PERIOD_DURATION_US, true, false); Timeline clippedTimeline = getClippedTimeline(timeline, TEST_CLIP_AMOUNT_US, TEST_PERIOD_DURATION_US); assertThat(clippedTimeline.getWindow(0, window).getDurationUs()) .isEqualTo(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US); assertThat(clippedTimeline.getPeriod(0, period).getDurationUs()) - .isEqualTo(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US); + .isEqualTo(TEST_PERIOD_DURATION_US); } @Test public void testClippingEnd() throws IOException { - Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true, false); + Timeline timeline = new SinglePeriodTimeline(TEST_PERIOD_DURATION_US, true, false); Timeline clippedTimeline = getClippedTimeline(timeline, 0, TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US); @@ -121,12 +128,42 @@ public final class ClippingMediaSourceTest { assertThat(clippedTimeline.getWindow(0, window).getDurationUs()) .isEqualTo(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US * 3); assertThat(clippedTimeline.getPeriod(0, period).getDurationUs()) - .isEqualTo(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US * 3); + .isEqualTo(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US * 2); + } + + @Test + public void testClippingToEndOfSourceWithDurationSetsDuration() throws IOException { + // Create a child timeline that has a known duration. + Timeline timeline = + new SinglePeriodTimeline( + /* durationUs= */ TEST_PERIOD_DURATION_US, + /* isSeekable= */ true, + /* isDynamic= */ false); + + // When clipping to the end, the clipped timeline should also have a duration. + Timeline clippedTimeline = + getClippedTimeline(timeline, TEST_CLIP_AMOUNT_US, C.TIME_END_OF_SOURCE); + assertThat(clippedTimeline.getWindow(/* windowIndex= */ 0, window).getDurationUs()) + .isEqualTo(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US); + } + + @Test + public void testClippingToEndOfSourceWithUnsetDurationDoesNotSetDuration() throws IOException { + // Create a child timeline that has an unknown duration. + Timeline timeline = + new SinglePeriodTimeline( + /* durationUs= */ C.TIME_UNSET, /* isSeekable= */ true, /* isDynamic= */ false); + + // When clipping to the end, the clipped timeline should also have an unset duration. + Timeline clippedTimeline = + getClippedTimeline(timeline, TEST_CLIP_AMOUNT_US, C.TIME_END_OF_SOURCE); + assertThat(clippedTimeline.getWindow(/* windowIndex= */ 0, window).getDurationUs()) + .isEqualTo(C.TIME_UNSET); } @Test public void testClippingStartAndEnd() throws IOException { - Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true, false); + Timeline timeline = new SinglePeriodTimeline(TEST_PERIOD_DURATION_US, true, false); Timeline clippedTimeline = getClippedTimeline( @@ -134,7 +171,216 @@ public final class ClippingMediaSourceTest { assertThat(clippedTimeline.getWindow(0, window).getDurationUs()) .isEqualTo(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US * 3); assertThat(clippedTimeline.getPeriod(0, period).getDurationUs()) - .isEqualTo(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US * 3); + .isEqualTo(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US * 2); + } + + @Test + public void testClippingFromDefaultPosition() throws IOException { + Timeline timeline = + new SinglePeriodTimeline( + /* periodDurationUs= */ 3 * TEST_PERIOD_DURATION_US, + /* windowDurationUs= */ TEST_PERIOD_DURATION_US, + /* windowPositionInPeriodUs= */ TEST_PERIOD_DURATION_US, + /* windowDefaultStartPositionUs= */ TEST_CLIP_AMOUNT_US, + /* isSeekable= */ true, + /* isDynamic= */ true, + /* tag= */ null); + + Timeline clippedTimeline = getClippedTimeline(timeline, /* durationUs= */ TEST_CLIP_AMOUNT_US); + assertThat(clippedTimeline.getWindow(0, window).getDurationUs()).isEqualTo(TEST_CLIP_AMOUNT_US); + assertThat(clippedTimeline.getWindow(0, window).getDefaultPositionUs()).isEqualTo(0); + assertThat(clippedTimeline.getWindow(0, window).getPositionInFirstPeriodUs()) + .isEqualTo(TEST_PERIOD_DURATION_US + TEST_CLIP_AMOUNT_US); + assertThat(clippedTimeline.getPeriod(0, period).getDurationUs()) + .isEqualTo(TEST_PERIOD_DURATION_US + 2 * TEST_CLIP_AMOUNT_US); + } + + @Test + public void testAllowDynamicUpdatesWithOverlappingLiveWindow() throws IOException { + Timeline timeline1 = + new SinglePeriodTimeline( + /* periodDurationUs= */ 2 * TEST_PERIOD_DURATION_US, + /* windowDurationUs= */ TEST_PERIOD_DURATION_US, + /* windowPositionInPeriodUs= */ TEST_PERIOD_DURATION_US, + /* windowDefaultStartPositionUs= */ TEST_CLIP_AMOUNT_US, + /* isSeekable= */ true, + /* isDynamic= */ true, + /* tag= */ null); + Timeline timeline2 = + new SinglePeriodTimeline( + /* periodDurationUs= */ 3 * TEST_PERIOD_DURATION_US, + /* windowDurationUs= */ TEST_PERIOD_DURATION_US, + /* windowPositionInPeriodUs= */ 2 * TEST_PERIOD_DURATION_US, + /* windowDefaultStartPositionUs= */ TEST_CLIP_AMOUNT_US, + /* isSeekable= */ true, + /* isDynamic= */ true, + /* tag= */ null); + + Timeline[] clippedTimelines = + getClippedTimelines( + /* startUs= */ 0, + /* endUs= */ TEST_PERIOD_DURATION_US, + /* allowDynamicUpdates= */ true, + /* fromDefaultPosition= */ true, + timeline1, + timeline2); + assertThat(clippedTimelines[0].getWindow(0, window).getDurationUs()) + .isEqualTo(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US); + assertThat(clippedTimelines[0].getWindow(0, window).getDefaultPositionUs()).isEqualTo(0); + assertThat(clippedTimelines[0].getWindow(0, window).isDynamic).isTrue(); + assertThat(clippedTimelines[0].getWindow(0, window).getPositionInFirstPeriodUs()) + .isEqualTo(TEST_PERIOD_DURATION_US + TEST_CLIP_AMOUNT_US); + assertThat(clippedTimelines[0].getPeriod(0, period).getDurationUs()) + .isEqualTo(2 * TEST_PERIOD_DURATION_US); + assertThat(clippedTimelines[1].getWindow(0, window).getDurationUs()) + .isEqualTo(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US); + assertThat(clippedTimelines[1].getWindow(0, window).getDefaultPositionUs()).isEqualTo(0); + assertThat(clippedTimelines[1].getWindow(0, window).isDynamic).isTrue(); + assertThat(clippedTimelines[1].getWindow(0, window).getPositionInFirstPeriodUs()) + .isEqualTo(2 * TEST_PERIOD_DURATION_US + TEST_CLIP_AMOUNT_US); + assertThat(clippedTimelines[1].getPeriod(0, period).getDurationUs()) + .isEqualTo(3 * TEST_PERIOD_DURATION_US); + } + + @Test + public void testAllowDynamicUpdatesWithNonOverlappingLiveWindow() throws IOException { + Timeline timeline1 = + new SinglePeriodTimeline( + /* periodDurationUs= */ 2 * TEST_PERIOD_DURATION_US, + /* windowDurationUs= */ TEST_PERIOD_DURATION_US, + /* windowPositionInPeriodUs= */ TEST_PERIOD_DURATION_US, + /* windowDefaultStartPositionUs= */ TEST_CLIP_AMOUNT_US, + /* isSeekable= */ true, + /* isDynamic= */ true, + /* tag= */ null); + Timeline timeline2 = + new SinglePeriodTimeline( + /* periodDurationUs= */ 4 * TEST_PERIOD_DURATION_US, + /* windowDurationUs= */ TEST_PERIOD_DURATION_US, + /* windowPositionInPeriodUs= */ 3 * TEST_PERIOD_DURATION_US, + /* windowDefaultStartPositionUs= */ TEST_CLIP_AMOUNT_US, + /* isSeekable= */ true, + /* isDynamic= */ true, + /* tag= */ null); + + Timeline[] clippedTimelines = + getClippedTimelines( + /* startUs= */ 0, + /* endUs= */ TEST_PERIOD_DURATION_US, + /* allowDynamicUpdates= */ true, + /* fromDefaultPosition= */ true, + timeline1, + timeline2); + assertThat(clippedTimelines[0].getWindow(0, window).getDurationUs()) + .isEqualTo(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US); + assertThat(clippedTimelines[0].getWindow(0, window).getDefaultPositionUs()).isEqualTo(0); + assertThat(clippedTimelines[0].getWindow(0, window).isDynamic).isTrue(); + assertThat(clippedTimelines[0].getWindow(0, window).getPositionInFirstPeriodUs()) + .isEqualTo(TEST_PERIOD_DURATION_US + TEST_CLIP_AMOUNT_US); + assertThat(clippedTimelines[0].getPeriod(0, period).getDurationUs()) + .isEqualTo(2 * TEST_PERIOD_DURATION_US); + assertThat(clippedTimelines[1].getWindow(0, window).getDurationUs()) + .isEqualTo(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US); + assertThat(clippedTimelines[1].getWindow(0, window).getDefaultPositionUs()).isEqualTo(0); + assertThat(clippedTimelines[1].getWindow(0, window).isDynamic).isTrue(); + assertThat(clippedTimelines[1].getWindow(0, window).getPositionInFirstPeriodUs()) + .isEqualTo(3 * TEST_PERIOD_DURATION_US + TEST_CLIP_AMOUNT_US); + assertThat(clippedTimelines[1].getPeriod(0, period).getDurationUs()) + .isEqualTo(4 * TEST_PERIOD_DURATION_US); + } + + @Test + public void testDisallowDynamicUpdatesWithOverlappingLiveWindow() throws IOException { + Timeline timeline1 = + new SinglePeriodTimeline( + /* periodDurationUs= */ 2 * TEST_PERIOD_DURATION_US, + /* windowDurationUs= */ TEST_PERIOD_DURATION_US, + /* windowPositionInPeriodUs= */ TEST_PERIOD_DURATION_US, + /* windowDefaultStartPositionUs= */ TEST_CLIP_AMOUNT_US, + /* isSeekable= */ true, + /* isDynamic= */ true, + /* tag= */ null); + Timeline timeline2 = + new SinglePeriodTimeline( + /* periodDurationUs= */ 3 * TEST_PERIOD_DURATION_US, + /* windowDurationUs= */ TEST_PERIOD_DURATION_US, + /* windowPositionInPeriodUs= */ 2 * TEST_PERIOD_DURATION_US, + /* windowDefaultStartPositionUs= */ TEST_CLIP_AMOUNT_US, + /* isSeekable= */ true, + /* isDynamic= */ true, + /* tag= */ null); + + Timeline[] clippedTimelines = + getClippedTimelines( + /* startUs= */ 0, + /* endUs= */ TEST_PERIOD_DURATION_US, + /* allowDynamicUpdates= */ false, + /* fromDefaultPosition= */ true, + timeline1, + timeline2); + assertThat(clippedTimelines[0].getWindow(0, window).getDurationUs()) + .isEqualTo(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US); + assertThat(clippedTimelines[0].getWindow(0, window).getDefaultPositionUs()).isEqualTo(0); + assertThat(clippedTimelines[0].getWindow(0, window).isDynamic).isTrue(); + assertThat(clippedTimelines[0].getWindow(0, window).getPositionInFirstPeriodUs()) + .isEqualTo(TEST_PERIOD_DURATION_US + TEST_CLIP_AMOUNT_US); + assertThat(clippedTimelines[0].getPeriod(0, period).getDurationUs()) + .isEqualTo(2 * TEST_PERIOD_DURATION_US); + assertThat(clippedTimelines[1].getWindow(0, window).getDurationUs()) + .isEqualTo(TEST_CLIP_AMOUNT_US); + assertThat(clippedTimelines[1].getWindow(0, window).getDefaultPositionUs()) + .isEqualTo(TEST_CLIP_AMOUNT_US); + assertThat(clippedTimelines[1].getWindow(0, window).isDynamic).isFalse(); + assertThat(clippedTimelines[1].getWindow(0, window).getPositionInFirstPeriodUs()) + .isEqualTo(2 * TEST_PERIOD_DURATION_US); + assertThat(clippedTimelines[1].getPeriod(0, period).getDurationUs()) + .isEqualTo(2 * TEST_PERIOD_DURATION_US + TEST_CLIP_AMOUNT_US); + } + + @Test + public void testDisallowDynamicUpdatesWithNonOverlappingLiveWindow() throws IOException { + Timeline timeline1 = + new SinglePeriodTimeline( + /* periodDurationUs= */ 2 * TEST_PERIOD_DURATION_US, + /* windowDurationUs= */ TEST_PERIOD_DURATION_US, + /* windowPositionInPeriodUs= */ TEST_PERIOD_DURATION_US, + /* windowDefaultStartPositionUs= */ TEST_CLIP_AMOUNT_US, + /* isSeekable= */ true, + /* isDynamic= */ true, + /* tag= */ null); + Timeline timeline2 = + new SinglePeriodTimeline( + /* periodDurationUs= */ 4 * TEST_PERIOD_DURATION_US, + /* windowDurationUs= */ TEST_PERIOD_DURATION_US, + /* windowPositionInPeriodUs= */ 3 * TEST_PERIOD_DURATION_US, + /* windowDefaultStartPositionUs= */ TEST_CLIP_AMOUNT_US, + /* isSeekable= */ true, + /* isDynamic= */ true, + /* tag= */ null); + + Timeline[] clippedTimelines = + getClippedTimelines( + /* startUs= */ 0, + /* endUs= */ TEST_PERIOD_DURATION_US, + /* allowDynamicUpdates= */ false, + /* fromDefaultPosition= */ true, + timeline1, + timeline2); + assertThat(clippedTimelines[0].getWindow(0, window).getDurationUs()) + .isEqualTo(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US); + assertThat(clippedTimelines[0].getWindow(0, window).getDefaultPositionUs()).isEqualTo(0); + assertThat(clippedTimelines[0].getWindow(0, window).isDynamic).isTrue(); + assertThat(clippedTimelines[0].getWindow(0, window).getPositionInFirstPeriodUs()) + .isEqualTo(TEST_PERIOD_DURATION_US + TEST_CLIP_AMOUNT_US); + assertThat(clippedTimelines[0].getPeriod(0, period).getDurationUs()) + .isEqualTo(2 * TEST_PERIOD_DURATION_US); + assertThat(clippedTimelines[1].getWindow(0, window).getDurationUs()).isEqualTo(0); + assertThat(clippedTimelines[1].getWindow(0, window).getDefaultPositionUs()).isEqualTo(0); + assertThat(clippedTimelines[1].getWindow(0, window).isDynamic).isFalse(); + assertThat(clippedTimelines[1].getWindow(0, window).getPositionInFirstPeriodUs()) + .isEqualTo(3 * TEST_PERIOD_DURATION_US); + assertThat(clippedTimelines[1].getPeriod(0, period).getDurationUs()) + .isEqualTo(3 * TEST_PERIOD_DURATION_US); } @Test @@ -145,7 +391,7 @@ public final class ClippingMediaSourceTest { Timeline clippedTimeline = getClippedTimeline( timeline, TEST_CLIP_AMOUNT_US, TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US); - TimelineAsserts.assertWindowIds(clippedTimeline, 111); + TimelineAsserts.assertWindowTags(clippedTimeline, 111); TimelineAsserts.assertPeriodCounts(clippedTimeline, 1); TimelineAsserts.assertPreviousWindowIndices( clippedTimeline, Player.REPEAT_MODE_OFF, false, C.INDEX_UNSET); @@ -157,19 +403,193 @@ public final class ClippingMediaSourceTest { TimelineAsserts.assertNextWindowIndices(clippedTimeline, Player.REPEAT_MODE_ALL, false, 0); } + @Test + public void testEventTimeWithinClippedRange() throws IOException { + MediaLoadData mediaLoadData = + getClippingMediaSourceMediaLoadData( + /* clippingStartUs= */ TEST_CLIP_AMOUNT_US, + /* clippingEndUs= */ TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US, + /* eventStartUs= */ TEST_CLIP_AMOUNT_US + 1000, + /* eventEndUs= */ TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US - 1000); + assertThat(C.msToUs(mediaLoadData.mediaStartTimeMs)).isEqualTo(1000); + assertThat(C.msToUs(mediaLoadData.mediaEndTimeMs)) + .isEqualTo(TEST_PERIOD_DURATION_US - 2 * TEST_CLIP_AMOUNT_US - 1000); + } + + @Test + public void testEventTimeOutsideClippedRange() throws IOException { + MediaLoadData mediaLoadData = + getClippingMediaSourceMediaLoadData( + /* clippingStartUs= */ TEST_CLIP_AMOUNT_US, + /* clippingEndUs= */ TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US, + /* eventStartUs= */ TEST_CLIP_AMOUNT_US - 1000, + /* eventEndUs= */ TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US + 1000); + assertThat(C.msToUs(mediaLoadData.mediaStartTimeMs)).isEqualTo(0); + assertThat(C.msToUs(mediaLoadData.mediaEndTimeMs)) + .isEqualTo(TEST_PERIOD_DURATION_US - 2 * TEST_CLIP_AMOUNT_US); + } + + @Test + public void testUnsetEventTime() throws IOException { + MediaLoadData mediaLoadData = + getClippingMediaSourceMediaLoadData( + /* clippingStartUs= */ TEST_CLIP_AMOUNT_US, + /* clippingEndUs= */ TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US, + /* eventStartUs= */ C.TIME_UNSET, + /* eventEndUs= */ C.TIME_UNSET); + assertThat(C.msToUs(mediaLoadData.mediaStartTimeMs)).isEqualTo(C.TIME_UNSET); + assertThat(C.msToUs(mediaLoadData.mediaEndTimeMs)).isEqualTo(C.TIME_UNSET); + } + + @Test + public void testEventTimeWithUnsetDuration() throws IOException { + MediaLoadData mediaLoadData = + getClippingMediaSourceMediaLoadData( + /* clippingStartUs= */ TEST_CLIP_AMOUNT_US, + /* clippingEndUs= */ C.TIME_END_OF_SOURCE, + /* eventStartUs= */ TEST_CLIP_AMOUNT_US, + /* eventEndUs= */ TEST_CLIP_AMOUNT_US + 1_000_000); + assertThat(C.msToUs(mediaLoadData.mediaStartTimeMs)).isEqualTo(0); + assertThat(C.msToUs(mediaLoadData.mediaEndTimeMs)).isEqualTo(1_000_000); + } + + /** + * Wraps a timeline of duration {@link #TEST_PERIOD_DURATION_US} in a {@link ClippingMediaSource}, + * sends a media source event from the child source and returns the reported {@link MediaLoadData} + * for the clipping media source. + * + * @param clippingStartUs The start time of the media source clipping, in microseconds. + * @param clippingEndUs The end time of the media source clipping, in microseconds. + * @param eventStartUs The start time of the media source event (before clipping), in + * microseconds. + * @param eventEndUs The end time of the media source event (before clipping), in microseconds. + * @return The reported {@link MediaLoadData} for that event. + */ + private static MediaLoadData getClippingMediaSourceMediaLoadData( + long clippingStartUs, long clippingEndUs, final long eventStartUs, final long eventEndUs) + throws IOException { + FakeMediaSource fakeMediaSource = + new FakeMediaSource( + new SinglePeriodTimeline( + TEST_PERIOD_DURATION_US, /* isSeekable= */ true, /* isDynamic= */ false), + /* manifest= */ null) { + @Override + protected FakeMediaPeriod createFakeMediaPeriod( + MediaPeriodId id, + TrackGroupArray trackGroupArray, + Allocator allocator, + EventDispatcher eventDispatcher) { + eventDispatcher.downstreamFormatChanged( + new MediaLoadData( + C.DATA_TYPE_MEDIA, + C.TRACK_TYPE_UNKNOWN, + /* trackFormat= */ null, + C.SELECTION_REASON_UNKNOWN, + /* trackSelectionData= */ null, + C.usToMs(eventStartUs), + C.usToMs(eventEndUs))); + return super.createFakeMediaPeriod(id, trackGroupArray, allocator, eventDispatcher); + } + }; + final ClippingMediaSource clippingMediaSource = + new ClippingMediaSource(fakeMediaSource, clippingStartUs, clippingEndUs); + MediaSourceTestRunner testRunner = + new MediaSourceTestRunner(clippingMediaSource, /* allocator= */ null); + final MediaLoadData[] reportedMediaLoadData = new MediaLoadData[1]; + try { + testRunner.runOnPlaybackThread( + new Runnable() { + @Override + public void run() { + clippingMediaSource.addEventListener( + new Handler(), + new DefaultMediaSourceEventListener() { + @Override + public void onDownstreamFormatChanged( + int windowIndex, + @Nullable MediaPeriodId mediaPeriodId, + MediaLoadData mediaLoadData) { + reportedMediaLoadData[0] = mediaLoadData; + } + }); + } + }); + testRunner.prepareSource(); + // Create period to send the test event configured above. + testRunner.createPeriod( + new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 0)); + assertThat(reportedMediaLoadData[0]).isNotNull(); + } finally { + testRunner.release(); + } + return reportedMediaLoadData[0]; + } + /** * Wraps the specified timeline in a {@link ClippingMediaSource} and returns the clipped timeline. */ - private static Timeline getClippedTimeline(Timeline timeline, long startMs, long endMs) + private static Timeline getClippedTimeline(Timeline timeline, long startUs, long endUs) throws IOException { - FakeMediaSource fakeMediaSource = new FakeMediaSource(timeline, null); - ClippingMediaSource mediaSource = new ClippingMediaSource(fakeMediaSource, startMs, endMs); - MediaSourceTestRunner testRunner = new MediaSourceTestRunner(mediaSource, null); + FakeMediaSource fakeMediaSource = new FakeMediaSource(timeline, /* manifest= */ null); + ClippingMediaSource mediaSource = new ClippingMediaSource(fakeMediaSource, startUs, endUs); + return getClippedTimelines(fakeMediaSource, mediaSource)[0]; + } + + /** + * Wraps the specified timeline in a {@link ClippingMediaSource} and returns the clipped timeline. + */ + private static Timeline getClippedTimeline(Timeline timeline, long durationUs) + throws IOException { + FakeMediaSource fakeMediaSource = new FakeMediaSource(timeline, /* manifest= */ null); + ClippingMediaSource mediaSource = new ClippingMediaSource(fakeMediaSource, durationUs); + return getClippedTimelines(fakeMediaSource, mediaSource)[0]; + } + + /** + * Wraps the specified timelines in a {@link ClippingMediaSource} and returns the clipped timeline + * for each timeline update. + */ + private static Timeline[] getClippedTimelines( + long startUs, + long endUs, + boolean allowDynamicUpdates, + boolean fromDefaultPosition, + Timeline firstTimeline, + Timeline... additionalTimelines) + throws IOException { + FakeMediaSource fakeMediaSource = new FakeMediaSource(firstTimeline, /* manifest= */ null); + ClippingMediaSource mediaSource = + new ClippingMediaSource( + fakeMediaSource, + startUs, + endUs, + /* enableInitialDiscontinuity= */ true, + allowDynamicUpdates, + fromDefaultPosition); + return getClippedTimelines(fakeMediaSource, mediaSource, additionalTimelines); + } + + private static Timeline[] getClippedTimelines( + FakeMediaSource fakeMediaSource, + ClippingMediaSource clippingMediaSource, + Timeline... additionalTimelines) + throws IOException { + MediaSourceTestRunner testRunner = + new MediaSourceTestRunner(clippingMediaSource, /* allocator= */ null); + Timeline[] clippedTimelines = new Timeline[additionalTimelines.length + 1]; try { - Timeline clippedTimeline = testRunner.prepareSource(); + clippedTimelines[0] = testRunner.prepareSource(); + MediaPeriod mediaPeriod = + testRunner.createPeriod( + new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 0)); + for (int i = 0; i < additionalTimelines.length; i++) { + fakeMediaSource.setNewSourceInfo(additionalTimelines[i], /* newManifest= */ null); + clippedTimelines[i + 1] = testRunner.assertTimelineChangeBlocking(); + } + testRunner.releasePeriod(mediaPeriod); testRunner.releaseSource(); fakeMediaSource.assertReleased(); - return clippedTimeline; + return clippedTimelines; } finally { testRunner.release(); } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java index 465e08b5d2..5231fc22ed 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016 The Android Open Source Project + * 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. @@ -16,11 +16,16 @@ package com.google.android.exoplayer2.source; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.verify; +import android.os.ConditionVariable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.Timeline.Period; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; +import com.google.android.exoplayer2.testutil.DummyMainThread; import com.google.android.exoplayer2.testutil.FakeMediaSource; import com.google.android.exoplayer2.testutil.FakeShuffleOrder; import com.google.android.exoplayer2.testutil.FakeTimeline; @@ -29,8 +34,14 @@ import com.google.android.exoplayer2.testutil.MediaSourceTestRunner; import com.google.android.exoplayer2.testutil.RobolectricUtil; import com.google.android.exoplayer2.testutil.TimelineAsserts; import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.concurrent.CountDownLatch; +import org.junit.After; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Mockito; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; @@ -39,143 +50,303 @@ import org.robolectric.annotation.Config; @Config(shadows = {RobolectricUtil.CustomLooper.class, RobolectricUtil.CustomMessageQueue.class}) public final class ConcatenatingMediaSourceTest { - @Test - public void testEmptyConcatenation() throws IOException { - for (boolean atomic : new boolean[] {false, true}) { - Timeline timeline = getConcatenatedTimeline(atomic); - TimelineAsserts.assertEmpty(timeline); + private ConcatenatingMediaSource mediaSource; + private MediaSourceTestRunner testRunner; - timeline = getConcatenatedTimeline(atomic, Timeline.EMPTY); - TimelineAsserts.assertEmpty(timeline); + @Before + public void setUp() throws Exception { + mediaSource = new ConcatenatingMediaSource(/* isAtomic= */ false, new FakeShuffleOrder(0)); + testRunner = new MediaSourceTestRunner(mediaSource, null); + } - timeline = getConcatenatedTimeline(atomic, Timeline.EMPTY, Timeline.EMPTY, Timeline.EMPTY); - TimelineAsserts.assertEmpty(timeline); - } + @After + public void tearDown() throws Exception { + testRunner.release(); } @Test - public void testSingleMediaSource() throws IOException { - Timeline timeline = getConcatenatedTimeline(false, createFakeTimeline(3, 111)); - TimelineAsserts.assertWindowIds(timeline, 111); - TimelineAsserts.assertPeriodCounts(timeline, 3); - for (boolean shuffled : new boolean[] {false, true}) { - TimelineAsserts.assertPreviousWindowIndices( - timeline, Player.REPEAT_MODE_OFF, shuffled, C.INDEX_UNSET); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, 0); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, shuffled, 0); - TimelineAsserts.assertNextWindowIndices( - timeline, Player.REPEAT_MODE_OFF, shuffled, C.INDEX_UNSET); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, 0); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, shuffled, 0); + public void testPlaylistChangesAfterPreparation() throws IOException, InterruptedException { + Timeline timeline = testRunner.prepareSource(); + TimelineAsserts.assertEmpty(timeline); + + FakeMediaSource[] childSources = createMediaSources(7); + + // Add first source. + mediaSource.addMediaSource(childSources[0]); + timeline = testRunner.assertTimelineChangeBlocking(); + TimelineAsserts.assertPeriodCounts(timeline, 1); + TimelineAsserts.assertWindowTags(timeline, 111); + + // Add at front of queue. + mediaSource.addMediaSource(0, childSources[1]); + timeline = testRunner.assertTimelineChangeBlocking(); + TimelineAsserts.assertPeriodCounts(timeline, 2, 1); + TimelineAsserts.assertWindowTags(timeline, 222, 111); + + // Add at back of queue. + mediaSource.addMediaSource(childSources[2]); + timeline = testRunner.assertTimelineChangeBlocking(); + TimelineAsserts.assertPeriodCounts(timeline, 2, 1, 3); + TimelineAsserts.assertWindowTags(timeline, 222, 111, 333); + + // Add in the middle. + mediaSource.addMediaSource(1, childSources[3]); + timeline = testRunner.assertTimelineChangeBlocking(); + TimelineAsserts.assertPeriodCounts(timeline, 2, 4, 1, 3); + TimelineAsserts.assertWindowTags(timeline, 222, 444, 111, 333); + + // Add bulk. + mediaSource.addMediaSources( + 3, Arrays.asList(childSources[4], childSources[5], childSources[6])); + timeline = testRunner.assertTimelineChangeBlocking(); + TimelineAsserts.assertPeriodCounts(timeline, 2, 4, 1, 5, 6, 7, 3); + TimelineAsserts.assertWindowTags(timeline, 222, 444, 111, 555, 666, 777, 333); + + // Move sources. + mediaSource.moveMediaSource(2, 3); + timeline = testRunner.assertTimelineChangeBlocking(); + TimelineAsserts.assertPeriodCounts(timeline, 2, 4, 5, 1, 6, 7, 3); + TimelineAsserts.assertWindowTags(timeline, 222, 444, 555, 111, 666, 777, 333); + mediaSource.moveMediaSource(3, 2); + timeline = testRunner.assertTimelineChangeBlocking(); + TimelineAsserts.assertPeriodCounts(timeline, 2, 4, 1, 5, 6, 7, 3); + TimelineAsserts.assertWindowTags(timeline, 222, 444, 111, 555, 666, 777, 333); + mediaSource.moveMediaSource(0, 6); + timeline = testRunner.assertTimelineChangeBlocking(); + TimelineAsserts.assertPeriodCounts(timeline, 4, 1, 5, 6, 7, 3, 2); + TimelineAsserts.assertWindowTags(timeline, 444, 111, 555, 666, 777, 333, 222); + mediaSource.moveMediaSource(6, 0); + timeline = testRunner.assertTimelineChangeBlocking(); + TimelineAsserts.assertPeriodCounts(timeline, 2, 4, 1, 5, 6, 7, 3); + TimelineAsserts.assertWindowTags(timeline, 222, 444, 111, 555, 666, 777, 333); + + // Remove in the middle. + mediaSource.removeMediaSource(3); + testRunner.assertTimelineChangeBlocking(); + mediaSource.removeMediaSource(3); + testRunner.assertTimelineChangeBlocking(); + mediaSource.removeMediaSource(3); + testRunner.assertTimelineChangeBlocking(); + mediaSource.removeMediaSource(1); + timeline = testRunner.assertTimelineChangeBlocking(); + TimelineAsserts.assertPeriodCounts(timeline, 2, 1, 3); + TimelineAsserts.assertWindowTags(timeline, 222, 111, 333); + for (int i = 3; i <= 6; i++) { + childSources[i].assertReleased(); } - timeline = getConcatenatedTimeline(true, createFakeTimeline(3, 111)); - TimelineAsserts.assertWindowIds(timeline, 111); - TimelineAsserts.assertPeriodCounts(timeline, 3); - for (boolean shuffled : new boolean[] {false, true}) { - TimelineAsserts.assertPreviousWindowIndices( - timeline, Player.REPEAT_MODE_OFF, shuffled, C.INDEX_UNSET); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, 0); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, shuffled, 0); - TimelineAsserts.assertNextWindowIndices( - timeline, Player.REPEAT_MODE_OFF, shuffled, C.INDEX_UNSET); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, 0); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, shuffled, 0); - } - } + // Assert the correct child source preparation load events have been returned (with the + // respective window index at the time of preparation). + testRunner.assertCompletedManifestLoads(0, 0, 2, 1, 3, 4, 5); - @Test - public void testMultipleMediaSources() throws IOException { - Timeline[] timelines = { - createFakeTimeline(3, 111), createFakeTimeline(1, 222), createFakeTimeline(3, 333) - }; - Timeline timeline = getConcatenatedTimeline(false, timelines); - TimelineAsserts.assertWindowIds(timeline, 111, 222, 333); - TimelineAsserts.assertPeriodCounts(timeline, 3, 1, 3); - TimelineAsserts.assertPreviousWindowIndices( - timeline, Player.REPEAT_MODE_OFF, false, C.INDEX_UNSET, 0, 1); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 2); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 2, 0, 1); + // Assert correct next and previous indices behavior after some insertions and removals. TimelineAsserts.assertNextWindowIndices( timeline, Player.REPEAT_MODE_OFF, false, 1, 2, C.INDEX_UNSET); TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 2); TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 1, 2, 0); TimelineAsserts.assertPreviousWindowIndices( - timeline, Player.REPEAT_MODE_OFF, true, 1, 2, C.INDEX_UNSET); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, true, 0, 1, 2); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, true, 1, 2, 0); + timeline, Player.REPEAT_MODE_OFF, false, C.INDEX_UNSET, 0, 1); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 2); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 2, 0, 1); + assertThat(timeline.getFirstWindowIndex(false)).isEqualTo(0); + assertThat(timeline.getLastWindowIndex(false)).isEqualTo(timeline.getWindowCount() - 1); TimelineAsserts.assertNextWindowIndices( timeline, Player.REPEAT_MODE_OFF, true, C.INDEX_UNSET, 0, 1); TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, true, 0, 1, 2); TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, true, 2, 0, 1); - assertThat(timeline.getFirstWindowIndex(false)).isEqualTo(0); - assertThat(timeline.getLastWindowIndex(false)).isEqualTo(2); - assertThat(timeline.getFirstWindowIndex(true)).isEqualTo(2); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_OFF, true, 1, 2, C.INDEX_UNSET); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, true, 0, 1, 2); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, true, 1, 2, 0); + assertThat(timeline.getFirstWindowIndex(true)).isEqualTo(timeline.getWindowCount() - 1); assertThat(timeline.getLastWindowIndex(true)).isEqualTo(0); - timeline = getConcatenatedTimeline(true, timelines); - TimelineAsserts.assertWindowIds(timeline, 111, 222, 333); - TimelineAsserts.assertPeriodCounts(timeline, 3, 1, 3); - for (boolean shuffled : new boolean[] {false, true}) { - TimelineAsserts.assertPreviousWindowIndices( - timeline, Player.REPEAT_MODE_OFF, shuffled, C.INDEX_UNSET, 0, 1); - TimelineAsserts.assertPreviousWindowIndices( - timeline, Player.REPEAT_MODE_ONE, shuffled, 2, 0, 1); - TimelineAsserts.assertPreviousWindowIndices( - timeline, Player.REPEAT_MODE_ALL, shuffled, 2, 0, 1); - TimelineAsserts.assertNextWindowIndices( - timeline, Player.REPEAT_MODE_OFF, shuffled, 1, 2, C.INDEX_UNSET); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, 1, 2, 0); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, shuffled, 1, 2, 0); - assertThat(timeline.getFirstWindowIndex(shuffled)).isEqualTo(0); - assertThat(timeline.getLastWindowIndex(shuffled)).isEqualTo(2); + // Assert all periods can be prepared and the respective load events are returned. + testRunner.assertPrepareAndReleaseAllPeriods(); + assertCompletedAllMediaPeriodLoads(timeline); + + // Remove at front of queue. + mediaSource.removeMediaSource(0); + timeline = testRunner.assertTimelineChangeBlocking(); + TimelineAsserts.assertPeriodCounts(timeline, 1, 3); + TimelineAsserts.assertWindowTags(timeline, 111, 333); + childSources[1].assertReleased(); + + // Remove at back of queue. + mediaSource.removeMediaSource(1); + timeline = testRunner.assertTimelineChangeBlocking(); + TimelineAsserts.assertPeriodCounts(timeline, 1); + TimelineAsserts.assertWindowTags(timeline, 111); + childSources[2].assertReleased(); + + // Remove last source. + mediaSource.removeMediaSource(0); + timeline = testRunner.assertTimelineChangeBlocking(); + TimelineAsserts.assertEmpty(timeline); + childSources[3].assertReleased(); + } + + @Test + public void testPlaylistChangesBeforePreparation() throws IOException, InterruptedException { + FakeMediaSource[] childSources = createMediaSources(4); + mediaSource.addMediaSource(childSources[0]); + mediaSource.addMediaSource(childSources[1]); + mediaSource.addMediaSource(0, childSources[2]); + mediaSource.moveMediaSource(0, 2); + mediaSource.removeMediaSource(0); + mediaSource.moveMediaSource(1, 0); + mediaSource.addMediaSource(1, childSources[3]); + testRunner.assertNoTimelineChange(); + + Timeline timeline = testRunner.prepareSource(); + TimelineAsserts.assertPeriodCounts(timeline, 3, 4, 2); + TimelineAsserts.assertWindowTags(timeline, 333, 444, 222); + TimelineAsserts.assertNextWindowIndices( + timeline, Player.REPEAT_MODE_OFF, false, 1, 2, C.INDEX_UNSET); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_OFF, false, C.INDEX_UNSET, 0, 1); + TimelineAsserts.assertNextWindowIndices( + timeline, Player.REPEAT_MODE_OFF, true, C.INDEX_UNSET, 0, 1); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_OFF, true, 1, 2, C.INDEX_UNSET); + + testRunner.assertPrepareAndReleaseAllPeriods(); + testRunner.assertCompletedManifestLoads(0, 1, 2); + assertCompletedAllMediaPeriodLoads(timeline); + testRunner.releaseSource(); + for (int i = 1; i < 4; i++) { + childSources[i].assertReleased(); } } @Test - public void testNestedMediaSources() throws IOException { - Timeline timeline = - getConcatenatedTimeline( - false, - getConcatenatedTimeline(false, createFakeTimeline(1, 111), createFakeTimeline(1, 222)), - getConcatenatedTimeline(true, createFakeTimeline(1, 333), createFakeTimeline(1, 444))); - TimelineAsserts.assertWindowIds(timeline, 111, 222, 333, 444); - TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1, 1); - TimelineAsserts.assertPreviousWindowIndices( - timeline, Player.REPEAT_MODE_OFF, false, C.INDEX_UNSET, 0, 1, 2); - TimelineAsserts.assertPreviousWindowIndices( - timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 3, 2); - TimelineAsserts.assertPreviousWindowIndices( - timeline, Player.REPEAT_MODE_ALL, false, 3, 0, 1, 2); - TimelineAsserts.assertNextWindowIndices( - timeline, Player.REPEAT_MODE_OFF, false, 1, 2, 3, C.INDEX_UNSET); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 3, 2); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 1, 2, 3, 0); - TimelineAsserts.assertPreviousWindowIndices( - timeline, Player.REPEAT_MODE_OFF, true, 1, 3, C.INDEX_UNSET, 2); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, true, 0, 1, 3, 2); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, true, 1, 3, 0, 2); - TimelineAsserts.assertNextWindowIndices( - timeline, Player.REPEAT_MODE_OFF, true, C.INDEX_UNSET, 0, 3, 1); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, true, 0, 1, 3, 2); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, true, 2, 0, 3, 1); + public void testPlaylistWithLazyMediaSource() throws IOException, InterruptedException { + // Create some normal (immediately preparing) sources and some lazy sources whose timeline + // updates need to be triggered. + FakeMediaSource[] fastSources = createMediaSources(2); + final FakeMediaSource[] lazySources = new FakeMediaSource[4]; + for (int i = 0; i < 4; i++) { + lazySources[i] = new FakeMediaSource(null, null); + } + + // Add lazy sources and normal sources before preparation. Also remove one lazy source again + // before preparation to check it doesn't throw or change the result. + mediaSource.addMediaSource(lazySources[0]); + mediaSource.addMediaSource(0, fastSources[0]); + mediaSource.removeMediaSource(1); + mediaSource.addMediaSource(1, lazySources[1]); + testRunner.assertNoTimelineChange(); + + // Prepare and assert that the timeline contains all information for normal sources while having + // placeholder information for lazy sources. + Timeline timeline = testRunner.prepareSource(); + TimelineAsserts.assertPeriodCounts(timeline, 1, 1); + TimelineAsserts.assertWindowTags(timeline, 111, null); + TimelineAsserts.assertWindowIsDynamic(timeline, false, true); + + // Trigger source info refresh for lazy source and check that the timeline now contains all + // information for all windows. + testRunner.runOnPlaybackThread( + new Runnable() { + @Override + public void run() { + lazySources[1].setNewSourceInfo(createFakeTimeline(8), null); + } + }); + timeline = testRunner.assertTimelineChangeBlocking(); + TimelineAsserts.assertPeriodCounts(timeline, 1, 9); + TimelineAsserts.assertWindowTags(timeline, 111, 999); + TimelineAsserts.assertWindowIsDynamic(timeline, false, false); + testRunner.assertPrepareAndReleaseAllPeriods(); + testRunner.assertCompletedManifestLoads(0, 1); + assertCompletedAllMediaPeriodLoads(timeline); + + // Add further lazy and normal sources after preparation. Also remove one lazy source again to + // check it doesn't throw or change the result. + mediaSource.addMediaSource(1, lazySources[2]); + testRunner.assertTimelineChangeBlocking(); + mediaSource.addMediaSource(2, fastSources[1]); + testRunner.assertTimelineChangeBlocking(); + mediaSource.addMediaSource(0, lazySources[3]); + testRunner.assertTimelineChangeBlocking(); + mediaSource.removeMediaSource(2); + timeline = testRunner.assertTimelineChangeBlocking(); + TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 2, 9); + TimelineAsserts.assertWindowTags(timeline, null, 111, 222, 999); + TimelineAsserts.assertWindowIsDynamic(timeline, true, false, false, false); + + // Create a period from an unprepared lazy media source and assert Callback.onPrepared is not + // called yet. + MediaPeriod lazyPeriod = + testRunner.createPeriod( + new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 0)); + CountDownLatch preparedCondition = testRunner.preparePeriod(lazyPeriod, 0); + assertThat(preparedCondition.getCount()).isEqualTo(1); + + // Assert that a second period can also be created and released without problems. + MediaPeriod secondLazyPeriod = + testRunner.createPeriod( + new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 0)); + testRunner.releasePeriod(secondLazyPeriod); + + // Trigger source info refresh for lazy media source. Assert that now all information is + // available again and the previously created period now also finished preparing. + testRunner.runOnPlaybackThread( + new Runnable() { + @Override + public void run() { + lazySources[3].setNewSourceInfo(createFakeTimeline(7), null); + } + }); + timeline = testRunner.assertTimelineChangeBlocking(); + TimelineAsserts.assertPeriodCounts(timeline, 8, 1, 2, 9); + TimelineAsserts.assertWindowTags(timeline, 888, 111, 222, 999); + TimelineAsserts.assertWindowIsDynamic(timeline, false, false, false, false); + assertThat(preparedCondition.getCount()).isEqualTo(0); + + // Release the period and source. + testRunner.releasePeriod(lazyPeriod); + testRunner.releaseSource(); + + // Assert all sources were fully released. + for (FakeMediaSource fastSource : fastSources) { + fastSource.assertReleased(); + } + for (FakeMediaSource lazySource : lazySources) { + lazySource.assertReleased(); + } } @Test - public void testEmptyTimelineMediaSources() throws IOException { - // Empty timelines in the front, back, and the middle (single and multiple in a row). - Timeline[] timelines = { - Timeline.EMPTY, - createFakeTimeline(1, 111), - Timeline.EMPTY, - Timeline.EMPTY, - createFakeTimeline(2, 222), - Timeline.EMPTY, - createFakeTimeline(3, 333), - Timeline.EMPTY - }; - Timeline timeline = getConcatenatedTimeline(false, timelines); - TimelineAsserts.assertWindowIds(timeline, 111, 222, 333); + public void testEmptyTimelineMediaSource() throws IOException, InterruptedException { + Timeline timeline = testRunner.prepareSource(); + TimelineAsserts.assertEmpty(timeline); + + mediaSource.addMediaSource(new FakeMediaSource(Timeline.EMPTY, null)); + timeline = testRunner.assertTimelineChangeBlocking(); + TimelineAsserts.assertEmpty(timeline); + + mediaSource.addMediaSources( + Arrays.asList( + new MediaSource[] { + new FakeMediaSource(Timeline.EMPTY, null), new FakeMediaSource(Timeline.EMPTY, null), + new FakeMediaSource(Timeline.EMPTY, null), new FakeMediaSource(Timeline.EMPTY, null), + new FakeMediaSource(Timeline.EMPTY, null), new FakeMediaSource(Timeline.EMPTY, null) + })); + timeline = testRunner.assertTimelineChangeBlocking(); + TimelineAsserts.assertEmpty(timeline); + testRunner.assertCompletedManifestLoads(/* empty */ ); + + // Insert non-empty media source to leave empty sources at the start, the end, and the middle + // (with single and multiple empty sources in a row). + MediaSource[] mediaSources = createMediaSources(3); + mediaSource.addMediaSource(1, mediaSources[0]); + testRunner.assertTimelineChangeBlocking(); + mediaSource.addMediaSource(4, mediaSources[1]); + testRunner.assertTimelineChangeBlocking(); + mediaSource.addMediaSource(6, mediaSources[2]); + timeline = testRunner.assertTimelineChangeBlocking(); + TimelineAsserts.assertWindowTags(timeline, 111, 222, 333); TimelineAsserts.assertPeriodCounts(timeline, 1, 2, 3); TimelineAsserts.assertPreviousWindowIndices( timeline, Player.REPEAT_MODE_OFF, false, C.INDEX_UNSET, 0, 1); @@ -197,29 +368,265 @@ public final class ConcatenatingMediaSourceTest { assertThat(timeline.getLastWindowIndex(false)).isEqualTo(2); assertThat(timeline.getFirstWindowIndex(true)).isEqualTo(2); assertThat(timeline.getLastWindowIndex(true)).isEqualTo(0); + testRunner.assertPrepareAndReleaseAllPeriods(); + testRunner.assertCompletedManifestLoads(0, 1, 2); + assertCompletedAllMediaPeriodLoads(timeline); + } - timeline = getConcatenatedTimeline(true, timelines); - TimelineAsserts.assertWindowIds(timeline, 111, 222, 333); - TimelineAsserts.assertPeriodCounts(timeline, 1, 2, 3); - for (boolean shuffled : new boolean[] {false, true}) { - TimelineAsserts.assertPreviousWindowIndices( - timeline, Player.REPEAT_MODE_OFF, shuffled, C.INDEX_UNSET, 0, 1); - TimelineAsserts.assertPreviousWindowIndices( - timeline, Player.REPEAT_MODE_ONE, shuffled, 2, 0, 1); - TimelineAsserts.assertPreviousWindowIndices( - timeline, Player.REPEAT_MODE_ALL, shuffled, 2, 0, 1); - TimelineAsserts.assertNextWindowIndices( - timeline, Player.REPEAT_MODE_OFF, shuffled, 1, 2, C.INDEX_UNSET); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, 1, 2, 0); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, shuffled, 1, 2, 0); - assertThat(timeline.getFirstWindowIndex(shuffled)).isEqualTo(0); - assertThat(timeline.getLastWindowIndex(shuffled)).isEqualTo(2); + @Test + public void testDynamicChangeOfEmptyTimelines() throws IOException { + FakeMediaSource[] childSources = + new FakeMediaSource[] { + new FakeMediaSource(Timeline.EMPTY, /* manifest= */ null), + new FakeMediaSource(Timeline.EMPTY, /* manifest= */ null), + new FakeMediaSource(Timeline.EMPTY, /* manifest= */ null), + }; + Timeline nonEmptyTimeline = new FakeTimeline(/* windowCount = */ 1); + + mediaSource.addMediaSources(Arrays.asList(childSources)); + Timeline timeline = testRunner.prepareSource(); + TimelineAsserts.assertEmpty(timeline); + + childSources[0].setNewSourceInfo(nonEmptyTimeline, /* newManifest== */ null); + timeline = testRunner.assertTimelineChangeBlocking(); + TimelineAsserts.assertPeriodCounts(timeline, 1); + + childSources[2].setNewSourceInfo(nonEmptyTimeline, /* newManifest== */ null); + timeline = testRunner.assertTimelineChangeBlocking(); + TimelineAsserts.assertPeriodCounts(timeline, 1, 1); + + childSources[1].setNewSourceInfo(nonEmptyTimeline, /* newManifest== */ null); + timeline = testRunner.assertTimelineChangeBlocking(); + TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1); + } + + @Test + public void testIllegalArguments() { + MediaSource validSource = new FakeMediaSource(createFakeTimeline(1), null); + + // Null sources. + try { + mediaSource.addMediaSource(null); + fail("Null mediaSource not allowed."); + } catch (NullPointerException e) { + // Expected. + } + + MediaSource[] mediaSources = {validSource, null}; + try { + mediaSource.addMediaSources(Arrays.asList(mediaSources)); + fail("Null mediaSource not allowed."); + } catch (NullPointerException e) { + // Expected. + } + } + + @Test + public void testCustomCallbackBeforePreparationAddSingle() { + Runnable runnable = Mockito.mock(Runnable.class); + + mediaSource.addMediaSource(createFakeMediaSource(), runnable); + verify(runnable).run(); + } + + @Test + public void testCustomCallbackBeforePreparationAddMultiple() { + Runnable runnable = Mockito.mock(Runnable.class); + + mediaSource.addMediaSources( + Arrays.asList(new MediaSource[] {createFakeMediaSource(), createFakeMediaSource()}), + runnable); + verify(runnable).run(); + } + + @Test + public void testCustomCallbackBeforePreparationAddSingleWithIndex() { + Runnable runnable = Mockito.mock(Runnable.class); + + mediaSource.addMediaSource(/* index */ 0, createFakeMediaSource(), runnable); + verify(runnable).run(); + } + + @Test + public void testCustomCallbackBeforePreparationAddMultipleWithIndex() { + Runnable runnable = Mockito.mock(Runnable.class); + + mediaSource.addMediaSources( + /* index */ 0, + Arrays.asList(new MediaSource[] {createFakeMediaSource(), createFakeMediaSource()}), + runnable); + verify(runnable).run(); + } + + @Test + public void testCustomCallbackBeforePreparationRemove() { + Runnable runnable = Mockito.mock(Runnable.class); + + mediaSource.addMediaSource(createFakeMediaSource()); + mediaSource.removeMediaSource(/* index */ 0, runnable); + verify(runnable).run(); + } + + @Test + public void testCustomCallbackBeforePreparationMove() { + Runnable runnable = Mockito.mock(Runnable.class); + + mediaSource.addMediaSources( + Arrays.asList(new MediaSource[] {createFakeMediaSource(), createFakeMediaSource()})); + mediaSource.moveMediaSource(/* fromIndex */ 1, /* toIndex */ 0, runnable); + verify(runnable).run(); + } + + @Test + public void testCustomCallbackAfterPreparationAddSingle() throws IOException { + DummyMainThread dummyMainThread = new DummyMainThread(); + try { + testRunner.prepareSource(); + final TimelineGrabber timelineGrabber = new TimelineGrabber(testRunner); + dummyMainThread.runOnMainThread( + new Runnable() { + @Override + public void run() { + mediaSource.addMediaSource(createFakeMediaSource(), timelineGrabber); + } + }); + Timeline timeline = timelineGrabber.assertTimelineChangeBlocking(); + assertThat(timeline.getWindowCount()).isEqualTo(1); + } finally { + dummyMainThread.release(); + } + } + + @Test + public void testCustomCallbackAfterPreparationAddMultiple() throws IOException { + DummyMainThread dummyMainThread = new DummyMainThread(); + try { + testRunner.prepareSource(); + final TimelineGrabber timelineGrabber = new TimelineGrabber(testRunner); + dummyMainThread.runOnMainThread( + new Runnable() { + @Override + public void run() { + mediaSource.addMediaSources( + Arrays.asList( + new MediaSource[] {createFakeMediaSource(), createFakeMediaSource()}), + timelineGrabber); + } + }); + Timeline timeline = timelineGrabber.assertTimelineChangeBlocking(); + assertThat(timeline.getWindowCount()).isEqualTo(2); + } finally { + dummyMainThread.release(); + } + } + + @Test + public void testCustomCallbackAfterPreparationAddSingleWithIndex() throws IOException { + DummyMainThread dummyMainThread = new DummyMainThread(); + try { + testRunner.prepareSource(); + final TimelineGrabber timelineGrabber = new TimelineGrabber(testRunner); + dummyMainThread.runOnMainThread( + new Runnable() { + @Override + public void run() { + mediaSource.addMediaSource(/* index */ 0, createFakeMediaSource(), timelineGrabber); + } + }); + Timeline timeline = timelineGrabber.assertTimelineChangeBlocking(); + assertThat(timeline.getWindowCount()).isEqualTo(1); + } finally { + dummyMainThread.release(); + } + } + + @Test + public void testCustomCallbackAfterPreparationAddMultipleWithIndex() throws IOException { + DummyMainThread dummyMainThread = new DummyMainThread(); + try { + testRunner.prepareSource(); + final TimelineGrabber timelineGrabber = new TimelineGrabber(testRunner); + dummyMainThread.runOnMainThread( + new Runnable() { + @Override + public void run() { + mediaSource.addMediaSources( + /* index */ 0, + Arrays.asList( + new MediaSource[] {createFakeMediaSource(), createFakeMediaSource()}), + timelineGrabber); + } + }); + Timeline timeline = timelineGrabber.assertTimelineChangeBlocking(); + assertThat(timeline.getWindowCount()).isEqualTo(2); + } finally { + dummyMainThread.release(); + } + } + + @Test + public void testCustomCallbackAfterPreparationRemove() throws IOException { + DummyMainThread dummyMainThread = new DummyMainThread(); + try { + testRunner.prepareSource(); + dummyMainThread.runOnMainThread( + new Runnable() { + @Override + public void run() { + mediaSource.addMediaSource(createFakeMediaSource()); + } + }); + testRunner.assertTimelineChangeBlocking(); + + final TimelineGrabber timelineGrabber = new TimelineGrabber(testRunner); + dummyMainThread.runOnMainThread( + new Runnable() { + @Override + public void run() { + mediaSource.removeMediaSource(/* index */ 0, timelineGrabber); + } + }); + Timeline timeline = timelineGrabber.assertTimelineChangeBlocking(); + assertThat(timeline.getWindowCount()).isEqualTo(0); + } finally { + dummyMainThread.release(); + } + } + + @Test + public void testCustomCallbackAfterPreparationMove() throws IOException { + DummyMainThread dummyMainThread = new DummyMainThread(); + try { + testRunner.prepareSource(); + dummyMainThread.runOnMainThread( + new Runnable() { + @Override + public void run() { + mediaSource.addMediaSources( + Arrays.asList( + new MediaSource[] {createFakeMediaSource(), createFakeMediaSource()})); + } + }); + testRunner.assertTimelineChangeBlocking(); + + final TimelineGrabber timelineGrabber = new TimelineGrabber(testRunner); + dummyMainThread.runOnMainThread( + new Runnable() { + @Override + public void run() { + mediaSource.moveMediaSource(/* fromIndex */ 1, /* toIndex */ 0, timelineGrabber); + } + }); + Timeline timeline = timelineGrabber.assertTimelineChangeBlocking(); + assertThat(timeline.getWindowCount()).isEqualTo(2); + } finally { + dummyMainThread.release(); } } @Test public void testPeriodCreationWithAds() throws IOException, InterruptedException { - // Create media source with ad child source. + // Create concatenated media source with ad child source. Timeline timelineContentOnly = new FakeTimeline( new TimelineWindowDefinition(2, 111, true, false, 10 * C.MICROS_PER_SECOND)); @@ -235,68 +642,345 @@ public final class ConcatenatingMediaSourceTest { /* adsPerAdGroup= */ 1, /* adGroupTimesUs= */ 0))); FakeMediaSource mediaSourceContentOnly = new FakeMediaSource(timelineContentOnly, null); FakeMediaSource mediaSourceWithAds = new FakeMediaSource(timelineWithAds, null); - ConcatenatingMediaSource mediaSource = - new ConcatenatingMediaSource(mediaSourceContentOnly, mediaSourceWithAds); + mediaSource.addMediaSource(mediaSourceContentOnly); + mediaSource.addMediaSource(mediaSourceWithAds); - MediaSourceTestRunner testRunner = new MediaSourceTestRunner(mediaSource, null); - try { - Timeline timeline = testRunner.prepareSource(); - TimelineAsserts.assertAdGroupCounts(timeline, 0, 0, 1, 1); + Timeline timeline = testRunner.prepareSource(); - // Create all periods and assert period creation of child media sources has been called. - testRunner.assertPrepareAndReleaseAllPeriods(); - mediaSourceContentOnly.assertMediaPeriodCreated( - new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 0)); - mediaSourceContentOnly.assertMediaPeriodCreated( - new MediaPeriodId(/* periodIndex= */ 1, /* windowSequenceNumber= */ 0)); - mediaSourceWithAds.assertMediaPeriodCreated( - new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 1)); - mediaSourceWithAds.assertMediaPeriodCreated( - new MediaPeriodId(/* periodIndex= */ 1, /* windowSequenceNumber= */ 1)); - mediaSourceWithAds.assertMediaPeriodCreated( - new MediaPeriodId( - /* periodIndex= */ 0, - /* adGroupIndex= */ 0, - /* adIndexInAdGroup= */ 0, - /* windowSequenceNumber= */ 1)); - mediaSourceWithAds.assertMediaPeriodCreated( - new MediaPeriodId( - /* periodIndex= */ 1, - /* adGroupIndex= */ 0, - /* adIndexInAdGroup= */ 0, - /* windowSequenceNumber= */ 1)); - } finally { - testRunner.release(); - } + // Assert the timeline contains ad groups. + TimelineAsserts.assertAdGroupCounts(timeline, 0, 0, 1, 1); + + // Create all periods and assert period creation of child media sources has been called. + testRunner.assertPrepareAndReleaseAllPeriods(); + mediaSourceContentOnly.assertMediaPeriodCreated( + new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 0)); + mediaSourceContentOnly.assertMediaPeriodCreated( + new MediaPeriodId(/* periodIndex= */ 1, /* windowSequenceNumber= */ 0)); + mediaSourceWithAds.assertMediaPeriodCreated( + new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 1)); + mediaSourceWithAds.assertMediaPeriodCreated( + new MediaPeriodId(/* periodIndex= */ 1, /* windowSequenceNumber= */ 1)); + mediaSourceWithAds.assertMediaPeriodCreated( + new MediaPeriodId( + /* periodIndex= */ 0, + /* adGroupIndex= */ 0, + /* adIndexInAdGroup= */ 0, + /* windowSequenceNumber= */ 1)); + mediaSourceWithAds.assertMediaPeriodCreated( + new MediaPeriodId( + /* periodIndex= */ 1, + /* adGroupIndex= */ 0, + /* adIndexInAdGroup= */ 0, + /* windowSequenceNumber= */ 1)); + testRunner.assertCompletedManifestLoads(0, 1); + assertCompletedAllMediaPeriodLoads(timeline); } - /** - * Wraps the specified timelines in a {@link ConcatenatingMediaSource} and returns the - * concatenated timeline. - */ - private static Timeline getConcatenatedTimeline(boolean isRepeatOneAtomic, Timeline... timelines) - throws IOException { - FakeMediaSource[] mediaSources = new FakeMediaSource[timelines.length]; - for (int i = 0; i < timelines.length; i++) { - mediaSources[i] = new FakeMediaSource(timelines[i], null); - } + @Test + public void testAtomicTimelineWindowOrder() throws IOException { + // Release default test runner with non-atomic media source and replace with new test runner. + testRunner.release(); ConcatenatingMediaSource mediaSource = - new ConcatenatingMediaSource( - isRepeatOneAtomic, new FakeShuffleOrder(mediaSources.length), mediaSources); - MediaSourceTestRunner testRunner = new MediaSourceTestRunner(mediaSource, null); - try { - Timeline timeline = testRunner.prepareSource(); - testRunner.releaseSource(); - for (int i = 0; i < mediaSources.length; i++) { - mediaSources[i].assertReleased(); + new ConcatenatingMediaSource(/* isAtomic= */ true, new FakeShuffleOrder(0)); + testRunner = new MediaSourceTestRunner(mediaSource, null); + mediaSource.addMediaSources(Arrays.asList(createMediaSources(3))); + Timeline timeline = testRunner.prepareSource(); + TimelineAsserts.assertWindowTags(timeline, 111, 222, 333); + TimelineAsserts.assertPeriodCounts(timeline, 1, 2, 3); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ false, C.INDEX_UNSET, 0, 1); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ true, C.INDEX_UNSET, 0, 1); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ false, 2, 0, 1); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ true, 2, 0, 1); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ false, 2, 0, 1); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ true, 2, 0, 1); + TimelineAsserts.assertNextWindowIndices( + timeline, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ false, 1, 2, C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices( + timeline, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ true, 1, 2, C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices( + timeline, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ false, 1, 2, 0); + TimelineAsserts.assertNextWindowIndices( + timeline, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ true, 1, 2, 0); + TimelineAsserts.assertNextWindowIndices( + timeline, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ false, 1, 2, 0); + TimelineAsserts.assertNextWindowIndices( + timeline, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ true, 1, 2, 0); + assertThat(timeline.getFirstWindowIndex(/* shuffleModeEnabled= */ false)).isEqualTo(0); + assertThat(timeline.getFirstWindowIndex(/* shuffleModeEnabled= */ true)).isEqualTo(0); + assertThat(timeline.getLastWindowIndex(/* shuffleModeEnabled= */ false)).isEqualTo(2); + assertThat(timeline.getLastWindowIndex(/* shuffleModeEnabled= */ true)).isEqualTo(2); + } + + @Test + public void testNestedTimeline() throws IOException { + ConcatenatingMediaSource nestedSource1 = + new ConcatenatingMediaSource(/* isAtomic= */ false, new FakeShuffleOrder(0)); + ConcatenatingMediaSource nestedSource2 = + new ConcatenatingMediaSource(/* isAtomic= */ true, new FakeShuffleOrder(0)); + mediaSource.addMediaSource(nestedSource1); + mediaSource.addMediaSource(nestedSource2); + testRunner.prepareSource(); + FakeMediaSource[] childSources = createMediaSources(4); + nestedSource1.addMediaSource(childSources[0]); + testRunner.assertTimelineChangeBlocking(); + nestedSource1.addMediaSource(childSources[1]); + testRunner.assertTimelineChangeBlocking(); + nestedSource2.addMediaSource(childSources[2]); + testRunner.assertTimelineChangeBlocking(); + nestedSource2.addMediaSource(childSources[3]); + Timeline timeline = testRunner.assertTimelineChangeBlocking(); + + TimelineAsserts.assertWindowTags(timeline, 111, 222, 333, 444); + TimelineAsserts.assertPeriodCounts(timeline, 1, 2, 3, 4); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ false, C.INDEX_UNSET, 0, 1, 2); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ false, 0, 1, 3, 2); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ false, 3, 0, 1, 2); + TimelineAsserts.assertNextWindowIndices( + timeline, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ false, 1, 2, 3, C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices( + timeline, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ false, 0, 1, 3, 2); + TimelineAsserts.assertNextWindowIndices( + timeline, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ false, 1, 2, 3, 0); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ true, 1, 3, C.INDEX_UNSET, 2); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ true, 0, 1, 3, 2); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ true, 1, 3, 0, 2); + TimelineAsserts.assertNextWindowIndices( + timeline, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ true, C.INDEX_UNSET, 0, 3, 1); + TimelineAsserts.assertNextWindowIndices( + timeline, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ true, 0, 1, 3, 2); + TimelineAsserts.assertNextWindowIndices( + timeline, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ true, 2, 0, 3, 1); + } + + @Test + public void testRemoveChildSourceWithActiveMediaPeriod() throws IOException { + FakeMediaSource childSource = createFakeMediaSource(); + mediaSource.addMediaSource(childSource); + testRunner.prepareSource(); + MediaPeriod mediaPeriod = + testRunner.createPeriod( + new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 0)); + mediaSource.removeMediaSource(/* index= */ 0); + testRunner.assertTimelineChangeBlocking(); + testRunner.releasePeriod(mediaPeriod); + childSource.assertReleased(); + testRunner.releaseSource(); + } + + @Test + public void testDuplicateMediaSources() throws IOException, InterruptedException { + FakeMediaSource childSource = + new FakeMediaSource(new FakeTimeline(/* windowCount= */ 2), /* manifest= */ null); + + mediaSource.addMediaSource(childSource); + mediaSource.addMediaSource(childSource); + testRunner.prepareSource(); + mediaSource.addMediaSources(Arrays.asList(childSource, childSource)); + Timeline timeline = testRunner.assertTimelineChangeBlocking(); + + TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1, 1, 1, 1, 1, 1); + testRunner.assertPrepareAndReleaseAllPeriods(); + assertThat(childSource.getCreatedMediaPeriods()) + .containsAllOf( + new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 0), + new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 2), + new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 4), + new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 6), + new MediaPeriodId(/* periodIndex= */ 1, /* windowSequenceNumber= */ 1), + new MediaPeriodId(/* periodIndex= */ 1, /* windowSequenceNumber= */ 3), + new MediaPeriodId(/* periodIndex= */ 1, /* windowSequenceNumber= */ 5), + new MediaPeriodId(/* periodIndex= */ 1, /* windowSequenceNumber= */ 7)); + // Assert that only one manifest load is reported because the source is reused. + testRunner.assertCompletedManifestLoads(/* windowIndices= */ 0); + assertCompletedAllMediaPeriodLoads(timeline); + + testRunner.releaseSource(); + childSource.assertReleased(); + } + + @Test + public void testDuplicateNestedMediaSources() throws IOException, InterruptedException { + FakeMediaSource childSource = + new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1), /* manifest= */ null); + ConcatenatingMediaSource nestedConcatenation = new ConcatenatingMediaSource(); + + testRunner.prepareSource(); + mediaSource.addMediaSources( + Arrays.asList(childSource, nestedConcatenation, nestedConcatenation)); + testRunner.assertTimelineChangeBlocking(); + nestedConcatenation.addMediaSource(childSource); + testRunner.assertTimelineChangeBlocking(); + nestedConcatenation.addMediaSource(childSource); + Timeline timeline = testRunner.assertTimelineChangeBlocking(); + + TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1, 1, 1); + testRunner.assertPrepareAndReleaseAllPeriods(); + assertThat(childSource.getCreatedMediaPeriods()) + .containsAllOf( + new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 0), + new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 1), + new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 2), + new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 3), + new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 4)); + // Assert that only one manifest load is needed because the source is reused. + testRunner.assertCompletedManifestLoads(/* windowIndices= */ 0); + assertCompletedAllMediaPeriodLoads(timeline); + + testRunner.releaseSource(); + childSource.assertReleased(); + } + + @Test + public void testClear() throws IOException { + DummyMainThread dummyMainThread = new DummyMainThread(); + final FakeMediaSource preparedChildSource = createFakeMediaSource(); + final FakeMediaSource unpreparedChildSource = + new FakeMediaSource(/* timeline= */ null, /* manifest= */ null); + dummyMainThread.runOnMainThread( + new Runnable() { + @Override + public void run() { + mediaSource.addMediaSource(preparedChildSource); + mediaSource.addMediaSource(unpreparedChildSource); + } + }); + testRunner.prepareSource(); + final TimelineGrabber timelineGrabber = new TimelineGrabber(testRunner); + + dummyMainThread.runOnMainThread( + new Runnable() { + @Override + public void run() { + mediaSource.clear(timelineGrabber); + } + }); + + Timeline timeline = timelineGrabber.assertTimelineChangeBlocking(); + assertThat(timeline.isEmpty()).isTrue(); + preparedChildSource.assertReleased(); + unpreparedChildSource.assertReleased(); + } + + @Test + public void testReleaseAndReprepareSource() throws IOException { + Period period = new Period(); + FakeMediaSource[] fakeMediaSources = createMediaSources(/* count= */ 2); + mediaSource.addMediaSource(fakeMediaSources[0]); // Child source with 1 period. + mediaSource.addMediaSource(fakeMediaSources[1]); // Child source with 2 periods. + Timeline timeline = testRunner.prepareSource(); + Object periodId0 = timeline.getPeriod(/* periodIndex= */ 0, period, /* setIds= */ true).uid; + Object periodId1 = timeline.getPeriod(/* periodIndex= */ 1, period, /* setIds= */ true).uid; + Object periodId2 = timeline.getPeriod(/* periodIndex= */ 2, period, /* setIds= */ true).uid; + testRunner.releaseSource(); + + mediaSource.moveMediaSource(/* currentIndex= */ 1, /* newIndex= */ 0); + timeline = testRunner.prepareSource(); + Object newPeriodId0 = timeline.getPeriod(/* periodIndex= */ 0, period, /* setIds= */ true).uid; + Object newPeriodId1 = timeline.getPeriod(/* periodIndex= */ 1, period, /* setIds= */ true).uid; + Object newPeriodId2 = timeline.getPeriod(/* periodIndex= */ 2, period, /* setIds= */ true).uid; + assertThat(newPeriodId0).isEqualTo(periodId1); + assertThat(newPeriodId1).isEqualTo(periodId2); + assertThat(newPeriodId2).isEqualTo(periodId0); + } + + @Test + public void testChildTimelineChangeWithActiveMediaPeriod() throws IOException { + FakeMediaSource[] nestedChildSources = createMediaSources(/* count= */ 2); + ConcatenatingMediaSource childSource = new ConcatenatingMediaSource(nestedChildSources); + mediaSource.addMediaSource(childSource); + + testRunner.prepareSource(); + MediaPeriod mediaPeriod = + testRunner.createPeriod( + new MediaPeriodId(/* periodIndex= */ 1, /* windowSequenceNumber= */ 0)); + childSource.moveMediaSource(/* currentIndex= */ 0, /* newIndex= */ 1); + testRunner.assertTimelineChangeBlocking(); + testRunner.preparePeriod(mediaPeriod, /* positionUs= */ 0); + + testRunner.assertCompletedMediaPeriodLoads( + new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 0)); + } + + private void assertCompletedAllMediaPeriodLoads(Timeline timeline) { + Timeline.Period period = new Timeline.Period(); + Timeline.Window window = new Timeline.Window(); + ArrayList expectedMediaPeriodIds = new ArrayList<>(); + for (int windowIndex = 0; windowIndex < timeline.getWindowCount(); windowIndex++) { + timeline.getWindow(windowIndex, window); + for (int periodIndex = window.firstPeriodIndex; + periodIndex <= window.lastPeriodIndex; + periodIndex++) { + timeline.getPeriod(periodIndex, period); + expectedMediaPeriodIds.add(new MediaPeriodId(periodIndex, windowIndex)); + for (int adGroupIndex = 0; adGroupIndex < period.getAdGroupCount(); adGroupIndex++) { + for (int adIndex = 0; adIndex < period.getAdCountInAdGroup(adGroupIndex); adIndex++) { + expectedMediaPeriodIds.add( + new MediaPeriodId(periodIndex, adGroupIndex, adIndex, windowIndex)); + } + } + } + } + testRunner.assertCompletedMediaPeriodLoads( + expectedMediaPeriodIds.toArray(new MediaPeriodId[0])); + } + + private static FakeMediaSource[] createMediaSources(int count) { + FakeMediaSource[] sources = new FakeMediaSource[count]; + for (int i = 0; i < count; i++) { + sources[i] = new FakeMediaSource(createFakeTimeline(i), null); + } + return sources; + } + + private static FakeMediaSource createFakeMediaSource() { + return new FakeMediaSource(createFakeTimeline(/* index */ 0), null); + } + + private static FakeTimeline createFakeTimeline(int index) { + return new FakeTimeline(new TimelineWindowDefinition(index + 1, (index + 1) * 111)); + } + + private static final class TimelineGrabber implements Runnable { + + private final MediaSourceTestRunner testRunner; + private final ConditionVariable finishedCondition; + + private Timeline timeline; + private AssertionError error; + + public TimelineGrabber(MediaSourceTestRunner testRunner) { + this.testRunner = testRunner; + finishedCondition = new ConditionVariable(); + } + + @Override + public void run() { + try { + timeline = testRunner.assertTimelineChange(); + } catch (AssertionError e) { + error = e; + } + finishedCondition.open(); + } + + public Timeline assertTimelineChangeBlocking() { + assertThat(finishedCondition.block(MediaSourceTestRunner.TIMEOUT_MS)).isTrue(); + if (error != null) { + throw error; } return timeline; - } finally { - testRunner.release(); } } - - private static FakeTimeline createFakeTimeline(int periodCount, int windowId) { - return new FakeTimeline(new TimelineWindowDefinition(periodCount, windowId)); - } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java deleted file mode 100644 index 24f1ddd5ed..0000000000 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java +++ /dev/null @@ -1,867 +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; - -import static com.google.common.truth.Truth.assertThat; -import static org.junit.Assert.fail; -import static org.mockito.Mockito.verify; - -import android.os.ConditionVariable; -import android.os.Handler; -import android.os.HandlerThread; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; -import com.google.android.exoplayer2.testutil.FakeMediaSource; -import com.google.android.exoplayer2.testutil.FakeShuffleOrder; -import com.google.android.exoplayer2.testutil.FakeTimeline; -import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; -import com.google.android.exoplayer2.testutil.MediaSourceTestRunner; -import com.google.android.exoplayer2.testutil.RobolectricUtil; -import com.google.android.exoplayer2.testutil.TimelineAsserts; -import java.io.IOException; -import java.util.Arrays; -import java.util.concurrent.CountDownLatch; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mockito; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; - -/** Unit tests for {@link DynamicConcatenatingMediaSource} */ -@RunWith(RobolectricTestRunner.class) -@Config(shadows = {RobolectricUtil.CustomLooper.class, RobolectricUtil.CustomMessageQueue.class}) -public final class DynamicConcatenatingMediaSourceTest { - - private DynamicConcatenatingMediaSource mediaSource; - private MediaSourceTestRunner testRunner; - - @Before - public void setUp() throws Exception { - mediaSource = - new DynamicConcatenatingMediaSource(/* isAtomic= */ false, new FakeShuffleOrder(0)); - testRunner = new MediaSourceTestRunner(mediaSource, null); - } - - @After - public void tearDown() throws Exception { - testRunner.release(); - } - - @Test - public void testPlaylistChangesAfterPreparation() throws IOException, InterruptedException { - Timeline timeline = testRunner.prepareSource(); - TimelineAsserts.assertEmpty(timeline); - - FakeMediaSource[] childSources = createMediaSources(7); - - // Add first source. - mediaSource.addMediaSource(childSources[0]); - timeline = testRunner.assertTimelineChangeBlocking(); - TimelineAsserts.assertPeriodCounts(timeline, 1); - TimelineAsserts.assertWindowIds(timeline, 111); - - // Add at front of queue. - mediaSource.addMediaSource(0, childSources[1]); - timeline = testRunner.assertTimelineChangeBlocking(); - TimelineAsserts.assertPeriodCounts(timeline, 2, 1); - TimelineAsserts.assertWindowIds(timeline, 222, 111); - - // Add at back of queue. - mediaSource.addMediaSource(childSources[2]); - timeline = testRunner.assertTimelineChangeBlocking(); - TimelineAsserts.assertPeriodCounts(timeline, 2, 1, 3); - TimelineAsserts.assertWindowIds(timeline, 222, 111, 333); - - // Add in the middle. - mediaSource.addMediaSource(1, childSources[3]); - timeline = testRunner.assertTimelineChangeBlocking(); - TimelineAsserts.assertPeriodCounts(timeline, 2, 4, 1, 3); - TimelineAsserts.assertWindowIds(timeline, 222, 444, 111, 333); - - // Add bulk. - mediaSource.addMediaSources( - 3, Arrays.asList(childSources[4], childSources[5], childSources[6])); - timeline = testRunner.assertTimelineChangeBlocking(); - TimelineAsserts.assertPeriodCounts(timeline, 2, 4, 1, 5, 6, 7, 3); - TimelineAsserts.assertWindowIds(timeline, 222, 444, 111, 555, 666, 777, 333); - - // Move sources. - mediaSource.moveMediaSource(2, 3); - timeline = testRunner.assertTimelineChangeBlocking(); - TimelineAsserts.assertPeriodCounts(timeline, 2, 4, 5, 1, 6, 7, 3); - TimelineAsserts.assertWindowIds(timeline, 222, 444, 555, 111, 666, 777, 333); - mediaSource.moveMediaSource(3, 2); - timeline = testRunner.assertTimelineChangeBlocking(); - TimelineAsserts.assertPeriodCounts(timeline, 2, 4, 1, 5, 6, 7, 3); - TimelineAsserts.assertWindowIds(timeline, 222, 444, 111, 555, 666, 777, 333); - mediaSource.moveMediaSource(0, 6); - timeline = testRunner.assertTimelineChangeBlocking(); - TimelineAsserts.assertPeriodCounts(timeline, 4, 1, 5, 6, 7, 3, 2); - TimelineAsserts.assertWindowIds(timeline, 444, 111, 555, 666, 777, 333, 222); - mediaSource.moveMediaSource(6, 0); - timeline = testRunner.assertTimelineChangeBlocking(); - TimelineAsserts.assertPeriodCounts(timeline, 2, 4, 1, 5, 6, 7, 3); - TimelineAsserts.assertWindowIds(timeline, 222, 444, 111, 555, 666, 777, 333); - - // Remove in the middle. - mediaSource.removeMediaSource(3); - testRunner.assertTimelineChangeBlocking(); - mediaSource.removeMediaSource(3); - testRunner.assertTimelineChangeBlocking(); - mediaSource.removeMediaSource(3); - testRunner.assertTimelineChangeBlocking(); - mediaSource.removeMediaSource(1); - timeline = testRunner.assertTimelineChangeBlocking(); - TimelineAsserts.assertPeriodCounts(timeline, 2, 1, 3); - TimelineAsserts.assertWindowIds(timeline, 222, 111, 333); - for (int i = 3; i <= 6; i++) { - childSources[i].assertReleased(); - } - - // Assert correct next and previous indices behavior after some insertions and removals. - TimelineAsserts.assertNextWindowIndices( - timeline, Player.REPEAT_MODE_OFF, false, 1, 2, C.INDEX_UNSET); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 2); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 1, 2, 0); - TimelineAsserts.assertPreviousWindowIndices( - timeline, Player.REPEAT_MODE_OFF, false, C.INDEX_UNSET, 0, 1); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 2); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 2, 0, 1); - assertThat(timeline.getFirstWindowIndex(false)).isEqualTo(0); - assertThat(timeline.getLastWindowIndex(false)).isEqualTo(timeline.getWindowCount() - 1); - TimelineAsserts.assertNextWindowIndices( - timeline, Player.REPEAT_MODE_OFF, true, C.INDEX_UNSET, 0, 1); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, true, 0, 1, 2); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, true, 2, 0, 1); - TimelineAsserts.assertPreviousWindowIndices( - timeline, Player.REPEAT_MODE_OFF, true, 1, 2, C.INDEX_UNSET); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, true, 0, 1, 2); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, true, 1, 2, 0); - assertThat(timeline.getFirstWindowIndex(true)).isEqualTo(timeline.getWindowCount() - 1); - assertThat(timeline.getLastWindowIndex(true)).isEqualTo(0); - - // Assert all periods can be prepared. - testRunner.assertPrepareAndReleaseAllPeriods(); - - // Remove at front of queue. - mediaSource.removeMediaSource(0); - timeline = testRunner.assertTimelineChangeBlocking(); - TimelineAsserts.assertPeriodCounts(timeline, 1, 3); - TimelineAsserts.assertWindowIds(timeline, 111, 333); - childSources[1].assertReleased(); - - // Remove at back of queue. - mediaSource.removeMediaSource(1); - timeline = testRunner.assertTimelineChangeBlocking(); - TimelineAsserts.assertPeriodCounts(timeline, 1); - TimelineAsserts.assertWindowIds(timeline, 111); - childSources[2].assertReleased(); - - // Remove last source. - mediaSource.removeMediaSource(0); - timeline = testRunner.assertTimelineChangeBlocking(); - TimelineAsserts.assertEmpty(timeline); - childSources[3].assertReleased(); - } - - @Test - public void testPlaylistChangesBeforePreparation() throws IOException, InterruptedException { - FakeMediaSource[] childSources = createMediaSources(4); - mediaSource.addMediaSource(childSources[0]); - mediaSource.addMediaSource(childSources[1]); - mediaSource.addMediaSource(0, childSources[2]); - mediaSource.moveMediaSource(0, 2); - mediaSource.removeMediaSource(0); - mediaSource.moveMediaSource(1, 0); - mediaSource.addMediaSource(1, childSources[3]); - testRunner.assertNoTimelineChange(); - - Timeline timeline = testRunner.prepareSource(); - TimelineAsserts.assertPeriodCounts(timeline, 3, 4, 2); - TimelineAsserts.assertWindowIds(timeline, 333, 444, 222); - TimelineAsserts.assertNextWindowIndices( - timeline, Player.REPEAT_MODE_OFF, false, 1, 2, C.INDEX_UNSET); - TimelineAsserts.assertPreviousWindowIndices( - timeline, Player.REPEAT_MODE_OFF, false, C.INDEX_UNSET, 0, 1); - TimelineAsserts.assertNextWindowIndices( - timeline, Player.REPEAT_MODE_OFF, true, C.INDEX_UNSET, 0, 1); - TimelineAsserts.assertPreviousWindowIndices( - timeline, Player.REPEAT_MODE_OFF, true, 1, 2, C.INDEX_UNSET); - - testRunner.assertPrepareAndReleaseAllPeriods(); - mediaSource.releaseSource(); - for (int i = 1; i < 4; i++) { - childSources[i].assertReleased(); - } - } - - @Test - public void testPlaylistWithLazyMediaSource() throws IOException, InterruptedException { - // Create some normal (immediately preparing) sources and some lazy sources whose timeline - // updates need to be triggered. - FakeMediaSource[] fastSources = createMediaSources(2); - final FakeMediaSource[] lazySources = new FakeMediaSource[4]; - for (int i = 0; i < 4; i++) { - lazySources[i] = new FakeMediaSource(null, null); - } - - // Add lazy sources and normal sources before preparation. Also remove one lazy source again - // before preparation to check it doesn't throw or change the result. - mediaSource.addMediaSource(lazySources[0]); - mediaSource.addMediaSource(0, fastSources[0]); - mediaSource.removeMediaSource(1); - mediaSource.addMediaSource(1, lazySources[1]); - testRunner.assertNoTimelineChange(); - - // Prepare and assert that the timeline contains all information for normal sources while having - // placeholder information for lazy sources. - Timeline timeline = testRunner.prepareSource(); - TimelineAsserts.assertPeriodCounts(timeline, 1, 1); - TimelineAsserts.assertWindowIds(timeline, 111, null); - TimelineAsserts.assertWindowIsDynamic(timeline, false, true); - - // Trigger source info refresh for lazy source and check that the timeline now contains all - // information for all windows. - testRunner.runOnPlaybackThread( - new Runnable() { - @Override - public void run() { - lazySources[1].setNewSourceInfo(createFakeTimeline(8), null); - } - }); - timeline = testRunner.assertTimelineChangeBlocking(); - TimelineAsserts.assertPeriodCounts(timeline, 1, 9); - TimelineAsserts.assertWindowIds(timeline, 111, 999); - TimelineAsserts.assertWindowIsDynamic(timeline, false, false); - testRunner.assertPrepareAndReleaseAllPeriods(); - - // Add further lazy and normal sources after preparation. Also remove one lazy source again to - // check it doesn't throw or change the result. - mediaSource.addMediaSource(1, lazySources[2]); - testRunner.assertTimelineChangeBlocking(); - mediaSource.addMediaSource(2, fastSources[1]); - testRunner.assertTimelineChangeBlocking(); - mediaSource.addMediaSource(0, lazySources[3]); - testRunner.assertTimelineChangeBlocking(); - mediaSource.removeMediaSource(2); - timeline = testRunner.assertTimelineChangeBlocking(); - TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 2, 9); - TimelineAsserts.assertWindowIds(timeline, null, 111, 222, 999); - TimelineAsserts.assertWindowIsDynamic(timeline, true, false, false, false); - - // Create a period from an unprepared lazy media source and assert Callback.onPrepared is not - // called yet. - MediaPeriod lazyPeriod = - testRunner.createPeriod( - new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 0)); - CountDownLatch preparedCondition = testRunner.preparePeriod(lazyPeriod, 0); - assertThat(preparedCondition.getCount()).isEqualTo(1); - - // Assert that a second period can also be created and released without problems. - MediaPeriod secondLazyPeriod = - testRunner.createPeriod( - new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 0)); - testRunner.releasePeriod(secondLazyPeriod); - - // Trigger source info refresh for lazy media source. Assert that now all information is - // available again and the previously created period now also finished preparing. - testRunner.runOnPlaybackThread( - new Runnable() { - @Override - public void run() { - lazySources[3].setNewSourceInfo(createFakeTimeline(7), null); - } - }); - timeline = testRunner.assertTimelineChangeBlocking(); - TimelineAsserts.assertPeriodCounts(timeline, 8, 1, 2, 9); - TimelineAsserts.assertWindowIds(timeline, 888, 111, 222, 999); - TimelineAsserts.assertWindowIsDynamic(timeline, false, false, false, false); - assertThat(preparedCondition.getCount()).isEqualTo(0); - - // Release the period and source. - testRunner.releasePeriod(lazyPeriod); - testRunner.releaseSource(); - - // Assert all sources were fully released. - for (FakeMediaSource fastSource : fastSources) { - fastSource.assertReleased(); - } - for (FakeMediaSource lazySource : lazySources) { - lazySource.assertReleased(); - } - } - - @Test - public void testEmptyTimelineMediaSource() throws IOException, InterruptedException { - Timeline timeline = testRunner.prepareSource(); - TimelineAsserts.assertEmpty(timeline); - - mediaSource.addMediaSource(new FakeMediaSource(Timeline.EMPTY, null)); - timeline = testRunner.assertTimelineChangeBlocking(); - TimelineAsserts.assertEmpty(timeline); - - mediaSource.addMediaSources( - Arrays.asList( - new MediaSource[] { - new FakeMediaSource(Timeline.EMPTY, null), new FakeMediaSource(Timeline.EMPTY, null), - new FakeMediaSource(Timeline.EMPTY, null), new FakeMediaSource(Timeline.EMPTY, null), - new FakeMediaSource(Timeline.EMPTY, null), new FakeMediaSource(Timeline.EMPTY, null) - })); - timeline = testRunner.assertTimelineChangeBlocking(); - TimelineAsserts.assertEmpty(timeline); - - // Insert non-empty media source to leave empty sources at the start, the end, and the middle - // (with single and multiple empty sources in a row). - MediaSource[] mediaSources = createMediaSources(3); - mediaSource.addMediaSource(1, mediaSources[0]); - testRunner.assertTimelineChangeBlocking(); - mediaSource.addMediaSource(4, mediaSources[1]); - testRunner.assertTimelineChangeBlocking(); - mediaSource.addMediaSource(6, mediaSources[2]); - timeline = testRunner.assertTimelineChangeBlocking(); - TimelineAsserts.assertWindowIds(timeline, 111, 222, 333); - TimelineAsserts.assertPeriodCounts(timeline, 1, 2, 3); - TimelineAsserts.assertPreviousWindowIndices( - timeline, Player.REPEAT_MODE_OFF, false, C.INDEX_UNSET, 0, 1); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 2); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 2, 0, 1); - TimelineAsserts.assertNextWindowIndices( - timeline, Player.REPEAT_MODE_OFF, false, 1, 2, C.INDEX_UNSET); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 2); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 1, 2, 0); - TimelineAsserts.assertPreviousWindowIndices( - timeline, Player.REPEAT_MODE_OFF, true, 1, 2, C.INDEX_UNSET); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, true, 0, 1, 2); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, true, 1, 2, 0); - TimelineAsserts.assertNextWindowIndices( - timeline, Player.REPEAT_MODE_OFF, true, C.INDEX_UNSET, 0, 1); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, true, 0, 1, 2); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, true, 2, 0, 1); - assertThat(timeline.getFirstWindowIndex(false)).isEqualTo(0); - assertThat(timeline.getLastWindowIndex(false)).isEqualTo(2); - assertThat(timeline.getFirstWindowIndex(true)).isEqualTo(2); - assertThat(timeline.getLastWindowIndex(true)).isEqualTo(0); - testRunner.assertPrepareAndReleaseAllPeriods(); - } - - @Test - public void testDynamicChangeOfEmptyTimelines() throws IOException { - FakeMediaSource[] childSources = - new FakeMediaSource[] { - new FakeMediaSource(Timeline.EMPTY, /* manifest= */ null), - new FakeMediaSource(Timeline.EMPTY, /* manifest= */ null), - new FakeMediaSource(Timeline.EMPTY, /* manifest= */ null), - }; - Timeline nonEmptyTimeline = new FakeTimeline(/* windowCount = */ 1); - - mediaSource.addMediaSources(Arrays.asList(childSources)); - Timeline timeline = testRunner.prepareSource(); - TimelineAsserts.assertEmpty(timeline); - - childSources[0].setNewSourceInfo(nonEmptyTimeline, /* newManifest== */ null); - timeline = testRunner.assertTimelineChangeBlocking(); - TimelineAsserts.assertPeriodCounts(timeline, 1); - - childSources[2].setNewSourceInfo(nonEmptyTimeline, /* newManifest== */ null); - timeline = testRunner.assertTimelineChangeBlocking(); - TimelineAsserts.assertPeriodCounts(timeline, 1, 1); - - childSources[1].setNewSourceInfo(nonEmptyTimeline, /* newManifest== */ null); - timeline = testRunner.assertTimelineChangeBlocking(); - TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1); - } - - @Test - public void testIllegalArguments() { - MediaSource validSource = new FakeMediaSource(createFakeTimeline(1), null); - - // Null sources. - try { - mediaSource.addMediaSource(null); - fail("Null mediaSource not allowed."); - } catch (NullPointerException e) { - // Expected. - } - - MediaSource[] mediaSources = {validSource, null}; - try { - mediaSource.addMediaSources(Arrays.asList(mediaSources)); - fail("Null mediaSource not allowed."); - } catch (NullPointerException e) { - // Expected. - } - - // Duplicate sources. - mediaSource.addMediaSource(validSource); - try { - mediaSource.addMediaSource(validSource); - fail("Duplicate mediaSource not allowed."); - } catch (IllegalArgumentException e) { - // Expected. - } - - mediaSources = - new MediaSource[] {new FakeMediaSource(createFakeTimeline(2), null), validSource}; - try { - mediaSource.addMediaSources(Arrays.asList(mediaSources)); - fail("Duplicate mediaSource not allowed."); - } catch (IllegalArgumentException e) { - // Expected. - } - } - - @Test - public void testCustomCallbackBeforePreparationAddSingle() { - Runnable runnable = Mockito.mock(Runnable.class); - - mediaSource.addMediaSource(createFakeMediaSource(), runnable); - verify(runnable).run(); - } - - @Test - public void testCustomCallbackBeforePreparationAddMultiple() { - Runnable runnable = Mockito.mock(Runnable.class); - - mediaSource.addMediaSources( - Arrays.asList(new MediaSource[] {createFakeMediaSource(), createFakeMediaSource()}), - runnable); - verify(runnable).run(); - } - - @Test - public void testCustomCallbackBeforePreparationAddSingleWithIndex() { - Runnable runnable = Mockito.mock(Runnable.class); - - mediaSource.addMediaSource(/* index */ 0, createFakeMediaSource(), runnable); - verify(runnable).run(); - } - - @Test - public void testCustomCallbackBeforePreparationAddMultipleWithIndex() { - Runnable runnable = Mockito.mock(Runnable.class); - - mediaSource.addMediaSources( - /* index */ 0, - Arrays.asList(new MediaSource[] {createFakeMediaSource(), createFakeMediaSource()}), - runnable); - verify(runnable).run(); - } - - @Test - public void testCustomCallbackBeforePreparationRemove() { - Runnable runnable = Mockito.mock(Runnable.class); - - mediaSource.addMediaSource(createFakeMediaSource()); - mediaSource.removeMediaSource(/* index */ 0, runnable); - verify(runnable).run(); - } - - @Test - public void testCustomCallbackBeforePreparationMove() { - Runnable runnable = Mockito.mock(Runnable.class); - - mediaSource.addMediaSources( - Arrays.asList(new MediaSource[] {createFakeMediaSource(), createFakeMediaSource()})); - mediaSource.moveMediaSource(/* fromIndex */ 1, /* toIndex */ 0, runnable); - verify(runnable).run(); - } - - @Test - public void testCustomCallbackAfterPreparationAddSingle() throws IOException { - DummyMainThread dummyMainThread = new DummyMainThread(); - try { - testRunner.prepareSource(); - final TimelineGrabber timelineGrabber = new TimelineGrabber(testRunner); - dummyMainThread.runOnMainThread( - new Runnable() { - @Override - public void run() { - mediaSource.addMediaSource(createFakeMediaSource(), timelineGrabber); - } - }); - Timeline timeline = timelineGrabber.assertTimelineChangeBlocking(); - assertThat(timeline.getWindowCount()).isEqualTo(1); - } finally { - dummyMainThread.release(); - } - } - - @Test - public void testCustomCallbackAfterPreparationAddMultiple() throws IOException { - DummyMainThread dummyMainThread = new DummyMainThread(); - try { - testRunner.prepareSource(); - final TimelineGrabber timelineGrabber = new TimelineGrabber(testRunner); - dummyMainThread.runOnMainThread( - new Runnable() { - @Override - public void run() { - mediaSource.addMediaSources( - Arrays.asList( - new MediaSource[] {createFakeMediaSource(), createFakeMediaSource()}), - timelineGrabber); - } - }); - Timeline timeline = timelineGrabber.assertTimelineChangeBlocking(); - assertThat(timeline.getWindowCount()).isEqualTo(2); - } finally { - dummyMainThread.release(); - } - } - - @Test - public void testCustomCallbackAfterPreparationAddSingleWithIndex() throws IOException { - DummyMainThread dummyMainThread = new DummyMainThread(); - try { - testRunner.prepareSource(); - final TimelineGrabber timelineGrabber = new TimelineGrabber(testRunner); - dummyMainThread.runOnMainThread( - new Runnable() { - @Override - public void run() { - mediaSource.addMediaSource(/* index */ 0, createFakeMediaSource(), timelineGrabber); - } - }); - Timeline timeline = timelineGrabber.assertTimelineChangeBlocking(); - assertThat(timeline.getWindowCount()).isEqualTo(1); - } finally { - dummyMainThread.release(); - } - } - - @Test - public void testCustomCallbackAfterPreparationAddMultipleWithIndex() throws IOException { - DummyMainThread dummyMainThread = new DummyMainThread(); - try { - testRunner.prepareSource(); - final TimelineGrabber timelineGrabber = new TimelineGrabber(testRunner); - dummyMainThread.runOnMainThread( - new Runnable() { - @Override - public void run() { - mediaSource.addMediaSources( - /* index */ 0, - Arrays.asList( - new MediaSource[] {createFakeMediaSource(), createFakeMediaSource()}), - timelineGrabber); - } - }); - Timeline timeline = timelineGrabber.assertTimelineChangeBlocking(); - assertThat(timeline.getWindowCount()).isEqualTo(2); - } finally { - dummyMainThread.release(); - } - } - - @Test - public void testCustomCallbackAfterPreparationRemove() throws IOException { - DummyMainThread dummyMainThread = new DummyMainThread(); - try { - testRunner.prepareSource(); - dummyMainThread.runOnMainThread( - new Runnable() { - @Override - public void run() { - mediaSource.addMediaSource(createFakeMediaSource()); - } - }); - testRunner.assertTimelineChangeBlocking(); - - final TimelineGrabber timelineGrabber = new TimelineGrabber(testRunner); - dummyMainThread.runOnMainThread( - new Runnable() { - @Override - public void run() { - mediaSource.removeMediaSource(/* index */ 0, timelineGrabber); - } - }); - Timeline timeline = timelineGrabber.assertTimelineChangeBlocking(); - assertThat(timeline.getWindowCount()).isEqualTo(0); - } finally { - dummyMainThread.release(); - } - } - - @Test - public void testCustomCallbackAfterPreparationMove() throws IOException { - DummyMainThread dummyMainThread = new DummyMainThread(); - try { - testRunner.prepareSource(); - dummyMainThread.runOnMainThread( - new Runnable() { - @Override - public void run() { - mediaSource.addMediaSources( - Arrays.asList( - new MediaSource[] {createFakeMediaSource(), createFakeMediaSource()})); - } - }); - testRunner.assertTimelineChangeBlocking(); - - final TimelineGrabber timelineGrabber = new TimelineGrabber(testRunner); - dummyMainThread.runOnMainThread( - new Runnable() { - @Override - public void run() { - mediaSource.moveMediaSource(/* fromIndex */ 1, /* toIndex */ 0, timelineGrabber); - } - }); - Timeline timeline = timelineGrabber.assertTimelineChangeBlocking(); - assertThat(timeline.getWindowCount()).isEqualTo(2); - } finally { - dummyMainThread.release(); - } - } - - @Test - public void testPeriodCreationWithAds() throws IOException, InterruptedException { - // Create dynamic media source with ad child source. - Timeline timelineContentOnly = - new FakeTimeline( - new TimelineWindowDefinition(2, 111, true, false, 10 * C.MICROS_PER_SECOND)); - Timeline timelineWithAds = - new FakeTimeline( - new TimelineWindowDefinition( - 2, - 222, - true, - false, - 10 * C.MICROS_PER_SECOND, - FakeTimeline.createAdPlaybackState( - /* adsPerAdGroup= */ 1, /* adGroupTimesUs= */ 0))); - FakeMediaSource mediaSourceContentOnly = new FakeMediaSource(timelineContentOnly, null); - FakeMediaSource mediaSourceWithAds = new FakeMediaSource(timelineWithAds, null); - mediaSource.addMediaSource(mediaSourceContentOnly); - mediaSource.addMediaSource(mediaSourceWithAds); - - Timeline timeline = testRunner.prepareSource(); - - // Assert the timeline contains ad groups. - TimelineAsserts.assertAdGroupCounts(timeline, 0, 0, 1, 1); - - // Create all periods and assert period creation of child media sources has been called. - testRunner.assertPrepareAndReleaseAllPeriods(); - mediaSourceContentOnly.assertMediaPeriodCreated( - new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 0)); - mediaSourceContentOnly.assertMediaPeriodCreated( - new MediaPeriodId(/* periodIndex= */ 1, /* windowSequenceNumber= */ 0)); - mediaSourceWithAds.assertMediaPeriodCreated( - new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 1)); - mediaSourceWithAds.assertMediaPeriodCreated( - new MediaPeriodId(/* periodIndex= */ 1, /* windowSequenceNumber= */ 1)); - mediaSourceWithAds.assertMediaPeriodCreated( - new MediaPeriodId( - /* periodIndex= */ 0, - /* adGroupIndex= */ 0, - /* adIndexInAdGroup= */ 0, - /* windowSequenceNumber= */ 1)); - mediaSourceWithAds.assertMediaPeriodCreated( - new MediaPeriodId( - /* periodIndex= */ 1, - /* adGroupIndex= */ 0, - /* adIndexInAdGroup= */ 0, - /* windowSequenceNumber= */ 1)); - } - - @Test - public void testAtomicTimelineWindowOrder() throws IOException { - // Release default test runner with non-atomic media source and replace with new test runner. - testRunner.release(); - DynamicConcatenatingMediaSource mediaSource = - new DynamicConcatenatingMediaSource(/* isAtomic= */ true, new FakeShuffleOrder(0)); - testRunner = new MediaSourceTestRunner(mediaSource, null); - mediaSource.addMediaSources(Arrays.asList(createMediaSources(3))); - Timeline timeline = testRunner.prepareSource(); - TimelineAsserts.assertWindowIds(timeline, 111, 222, 333); - TimelineAsserts.assertPeriodCounts(timeline, 1, 2, 3); - TimelineAsserts.assertPreviousWindowIndices( - timeline, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ false, C.INDEX_UNSET, 0, 1); - TimelineAsserts.assertPreviousWindowIndices( - timeline, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ true, C.INDEX_UNSET, 0, 1); - TimelineAsserts.assertPreviousWindowIndices( - timeline, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ false, 2, 0, 1); - TimelineAsserts.assertPreviousWindowIndices( - timeline, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ true, 2, 0, 1); - TimelineAsserts.assertPreviousWindowIndices( - timeline, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ false, 2, 0, 1); - TimelineAsserts.assertPreviousWindowIndices( - timeline, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ true, 2, 0, 1); - TimelineAsserts.assertNextWindowIndices( - timeline, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ false, 1, 2, C.INDEX_UNSET); - TimelineAsserts.assertNextWindowIndices( - timeline, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ true, 1, 2, C.INDEX_UNSET); - TimelineAsserts.assertNextWindowIndices( - timeline, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ false, 1, 2, 0); - TimelineAsserts.assertNextWindowIndices( - timeline, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ true, 1, 2, 0); - TimelineAsserts.assertNextWindowIndices( - timeline, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ false, 1, 2, 0); - TimelineAsserts.assertNextWindowIndices( - timeline, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ true, 1, 2, 0); - assertThat(timeline.getFirstWindowIndex(/* shuffleModeEnabled= */ false)).isEqualTo(0); - assertThat(timeline.getFirstWindowIndex(/* shuffleModeEnabled= */ true)).isEqualTo(0); - assertThat(timeline.getLastWindowIndex(/* shuffleModeEnabled= */ false)).isEqualTo(2); - assertThat(timeline.getLastWindowIndex(/* shuffleModeEnabled= */ true)).isEqualTo(2); - } - - @Test - public void testNestedTimeline() throws IOException { - DynamicConcatenatingMediaSource nestedSource1 = - new DynamicConcatenatingMediaSource(/* isAtomic= */ false, new FakeShuffleOrder(0)); - DynamicConcatenatingMediaSource nestedSource2 = - new DynamicConcatenatingMediaSource(/* isAtomic= */ true, new FakeShuffleOrder(0)); - mediaSource.addMediaSource(nestedSource1); - mediaSource.addMediaSource(nestedSource2); - testRunner.prepareSource(); - FakeMediaSource[] childSources = createMediaSources(4); - nestedSource1.addMediaSource(childSources[0]); - testRunner.assertTimelineChangeBlocking(); - nestedSource1.addMediaSource(childSources[1]); - testRunner.assertTimelineChangeBlocking(); - nestedSource2.addMediaSource(childSources[2]); - testRunner.assertTimelineChangeBlocking(); - nestedSource2.addMediaSource(childSources[3]); - Timeline timeline = testRunner.assertTimelineChangeBlocking(); - - TimelineAsserts.assertWindowIds(timeline, 111, 222, 333, 444); - TimelineAsserts.assertPeriodCounts(timeline, 1, 2, 3, 4); - TimelineAsserts.assertPreviousWindowIndices( - timeline, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ false, C.INDEX_UNSET, 0, 1, 2); - TimelineAsserts.assertPreviousWindowIndices( - timeline, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ false, 0, 1, 3, 2); - TimelineAsserts.assertPreviousWindowIndices( - timeline, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ false, 3, 0, 1, 2); - TimelineAsserts.assertNextWindowIndices( - timeline, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ false, 1, 2, 3, C.INDEX_UNSET); - TimelineAsserts.assertNextWindowIndices( - timeline, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ false, 0, 1, 3, 2); - TimelineAsserts.assertNextWindowIndices( - timeline, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ false, 1, 2, 3, 0); - TimelineAsserts.assertPreviousWindowIndices( - timeline, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ true, 1, 3, C.INDEX_UNSET, 2); - TimelineAsserts.assertPreviousWindowIndices( - timeline, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ true, 0, 1, 3, 2); - TimelineAsserts.assertPreviousWindowIndices( - timeline, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ true, 1, 3, 0, 2); - TimelineAsserts.assertNextWindowIndices( - timeline, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ true, C.INDEX_UNSET, 0, 3, 1); - TimelineAsserts.assertNextWindowIndices( - timeline, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ true, 0, 1, 3, 2); - TimelineAsserts.assertNextWindowIndices( - timeline, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ true, 2, 0, 3, 1); - } - - @Test - public void testRemoveChildSourceWithActiveMediaPeriod() throws IOException { - FakeMediaSource childSource = createFakeMediaSource(); - mediaSource.addMediaSource(childSource); - testRunner.prepareSource(); - MediaPeriod mediaPeriod = - testRunner.createPeriod( - new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 0)); - mediaSource.removeMediaSource(/* index= */ 0); - testRunner.assertTimelineChangeBlocking(); - testRunner.releasePeriod(mediaPeriod); - childSource.assertReleased(); - testRunner.releaseSource(); - } - - private static FakeMediaSource[] createMediaSources(int count) { - FakeMediaSource[] sources = new FakeMediaSource[count]; - for (int i = 0; i < count; i++) { - sources[i] = new FakeMediaSource(createFakeTimeline(i), null); - } - return sources; - } - - private static FakeMediaSource createFakeMediaSource() { - return new FakeMediaSource(createFakeTimeline(/* index */ 0), null); - } - - private static FakeTimeline createFakeTimeline(int index) { - return new FakeTimeline(new TimelineWindowDefinition(index + 1, (index + 1) * 111)); - } - - private static final class DummyMainThread { - - private final HandlerThread thread; - private final Handler handler; - - private DummyMainThread() { - thread = new HandlerThread("DummyMainThread"); - thread.start(); - handler = new Handler(thread.getLooper()); - } - - /** - * Runs the provided {@link Runnable} on the main thread, blocking until execution completes. - * - * @param runnable The {@link Runnable} to run. - */ - public void runOnMainThread(final Runnable runnable) { - final ConditionVariable finishedCondition = new ConditionVariable(); - handler.post( - new Runnable() { - @Override - public void run() { - runnable.run(); - finishedCondition.open(); - } - }); - assertThat(finishedCondition.block(MediaSourceTestRunner.TIMEOUT_MS)).isTrue(); - } - - public void release() { - thread.quit(); - } - } - - private static final class TimelineGrabber implements Runnable { - - private final MediaSourceTestRunner testRunner; - private final ConditionVariable finishedCondition; - - private Timeline timeline; - private AssertionError error; - - public TimelineGrabber(MediaSourceTestRunner testRunner) { - this.testRunner = testRunner; - finishedCondition = new ConditionVariable(); - } - - @Override - public void run() { - try { - timeline = testRunner.assertTimelineChange(); - } catch (AssertionError e) { - error = e; - } - finishedCondition.open(); - } - - public Timeline assertTimelineChangeBlocking() { - assertThat(finishedCondition.block(MediaSourceTestRunner.TIMEOUT_MS)).isTrue(); - if (error != null) { - throw error; - } - return timeline; - } - } -} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java index 6aa710aff4..d639bc168a 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java @@ -50,7 +50,7 @@ public class LoopingMediaSourceTest { @Test public void testSingleLoop() throws IOException { Timeline timeline = getLoopingTimeline(multiWindowTimeline, 1); - TimelineAsserts.assertWindowIds(timeline, 111, 222, 333); + TimelineAsserts.assertWindowTags(timeline, 111, 222, 333); TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1); for (boolean shuffled : new boolean[] {false, true}) { TimelineAsserts.assertPreviousWindowIndices( @@ -69,7 +69,7 @@ public class LoopingMediaSourceTest { @Test public void testMultiLoop() throws IOException { Timeline timeline = getLoopingTimeline(multiWindowTimeline, 3); - TimelineAsserts.assertWindowIds(timeline, 111, 222, 333, 111, 222, 333, 111, 222, 333); + TimelineAsserts.assertWindowTags(timeline, 111, 222, 333, 111, 222, 333, 111, 222, 333); TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1, 1, 1, 1, 1, 1, 1); for (boolean shuffled : new boolean[] {false, true}) { TimelineAsserts.assertPreviousWindowIndices( @@ -90,7 +90,7 @@ public class LoopingMediaSourceTest { @Test public void testInfiniteLoop() throws IOException { Timeline timeline = getLoopingTimeline(multiWindowTimeline, Integer.MAX_VALUE); - TimelineAsserts.assertWindowIds(timeline, 111, 222, 333); + TimelineAsserts.assertWindowTags(timeline, 111, 222, 333); TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1); for (boolean shuffled : new boolean[] {false, true}) { TimelineAsserts.assertPreviousWindowIndices( diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/SinglePeriodTimelineTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/SinglePeriodTimelineTest.java index 2627052cc5..2587b78d99 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/SinglePeriodTimelineTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/SinglePeriodTimelineTest.java @@ -56,8 +56,15 @@ public final class SinglePeriodTimelineTest { @Test public void testGetPeriodPositionDynamicWindowKnownDuration() { long windowDurationUs = 1000; - SinglePeriodTimeline timeline = new SinglePeriodTimeline(windowDurationUs, windowDurationUs, 0, - 0, false, true); + SinglePeriodTimeline timeline = + new SinglePeriodTimeline( + windowDurationUs, + windowDurationUs, + /* windowPositionInPeriodUs= */ 0, + /* windowDefaultStartPositionUs= */ 0, + /* isSeekable= */ false, + /* isDynamic= */ true, + /* tag= */ null); // Should return null with a positive position projection beyond window duration. Pair position = timeline.getPeriodPosition(window, period, 0, C.TIME_UNSET, windowDurationUs + 1); @@ -72,4 +79,48 @@ public final class SinglePeriodTimelineTest { assertThat(position.second).isEqualTo(0); } + @Test + public void setNullTag_returnsNullTag_butUsesDefaultUid() { + SinglePeriodTimeline timeline = + new SinglePeriodTimeline( + /* durationUs= */ C.TIME_UNSET, + /* isSeekable= */ false, + /* isDynamic= */ false, + /* tag= */ null); + + assertThat(timeline.getWindow(/* windowIndex= */ 0, window, /* setTag= */ false).tag).isNull(); + assertThat(timeline.getWindow(/* windowIndex= */ 0, window, /* setTag= */ true).tag).isNull(); + assertThat(timeline.getPeriod(/* periodIndex= */ 0, period, /* setIds= */ false).id).isNull(); + assertThat(timeline.getPeriod(/* periodIndex= */ 0, period, /* setIds= */ true).id).isNull(); + assertThat(timeline.getPeriod(/* periodIndex= */ 0, period, /* setIds= */ false).uid).isNull(); + assertThat(timeline.getPeriod(/* periodIndex= */ 0, period, /* setIds= */ true).uid) + .isNotNull(); + } + + @Test + public void setTag_isUsedForWindowTag() { + Object tag = new Object(); + SinglePeriodTimeline timeline = + new SinglePeriodTimeline( + /* durationUs= */ C.TIME_UNSET, /* isSeekable= */ false, /* isDynamic= */ false, tag); + + assertThat(timeline.getWindow(/* windowIndex= */ 0, window, /* setTag= */ false).tag).isNull(); + assertThat(timeline.getWindow(/* windowIndex= */ 0, window, /* setTag= */ true).tag) + .isEqualTo(tag); + } + + @Test + public void getIndexOfPeriod_returnsPeriod() { + SinglePeriodTimeline timeline = + new SinglePeriodTimeline( + /* durationUs= */ C.TIME_UNSET, + /* isSeekable= */ false, + /* isDynamic= */ false, + /* tag= */ null); + Object uid = timeline.getPeriod(/* periodIndex= */ 0, period, /* setIds= */ true).uid; + + assertThat(timeline.getIndexOfPeriod(uid)).isEqualTo(0); + assertThat(timeline.getIndexOfPeriod(/* uid= */ null)).isEqualTo(C.INDEX_UNSET); + assertThat(timeline.getIndexOfPeriod(/* uid= */ new Object())).isEqualTo(C.INDEX_UNSET); + } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/TrackGroupArrayTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/TrackGroupArrayTest.java new file mode 100644 index 0000000000..86778a0fa0 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/TrackGroupArrayTest.java @@ -0,0 +1,51 @@ +/* + * 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; + +import static com.google.common.truth.Truth.assertThat; + +import android.os.Parcel; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.util.MimeTypes; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +/** Unit test for {@link TrackGroupArray}. */ +@RunWith(RobolectricTestRunner.class) +public final class TrackGroupArrayTest { + + @Test + public void testParcelable() { + Format format1 = Format.createSampleFormat("1", MimeTypes.VIDEO_H264, 0); + Format format2 = Format.createSampleFormat("2", MimeTypes.AUDIO_AAC, 0); + Format format3 = Format.createSampleFormat("3", MimeTypes.VIDEO_H264, 0); + + TrackGroup trackGroup1 = new TrackGroup(format1, format2); + TrackGroup trackGroup2 = new TrackGroup(format3); + + TrackGroupArray trackGroupArrayToParcel = new TrackGroupArray(trackGroup1, trackGroup2); + + Parcel parcel = Parcel.obtain(); + trackGroupArrayToParcel.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + + TrackGroupArray trackGroupArrayFromParcel = TrackGroupArray.CREATOR.createFromParcel(parcel); + assertThat(trackGroupArrayFromParcel).isEqualTo(trackGroupArrayToParcel); + + parcel.recycle(); + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/TrackGroupTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/TrackGroupTest.java new file mode 100644 index 0000000000..1900f3c586 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/TrackGroupTest.java @@ -0,0 +1,47 @@ +/* + * 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; + +import static com.google.common.truth.Truth.assertThat; + +import android.os.Parcel; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.util.MimeTypes; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +/** Unit test for {@link TrackGroup}. */ +@RunWith(RobolectricTestRunner.class) +public final class TrackGroupTest { + + @Test + public void testParcelable() { + Format format1 = Format.createSampleFormat("1", MimeTypes.VIDEO_H264, 0); + Format format2 = Format.createSampleFormat("2", MimeTypes.AUDIO_AAC, 0); + + TrackGroup trackGroupToParcel = new TrackGroup(format1, format2); + + Parcel parcel = Parcel.obtain(); + trackGroupToParcel.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + + TrackGroup trackGroupFromParcel = TrackGroup.CREATOR.createFromParcel(parcel); + assertThat(trackGroupFromParcel).isEqualTo(trackGroupToParcel); + + parcel.recycle(); + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelectionTest.java b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelectionTest.java index 956174f43b..4026bc0c37 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelectionTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelectionTest.java @@ -52,7 +52,6 @@ public final class AdaptiveTrackSelectionTest { public void setUp() { initMocks(this); fakeClock = new FakeClock(0); - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(BandwidthMeter.NO_ESTIMATE); } @Test @@ -62,7 +61,8 @@ public final class AdaptiveTrackSelectionTest { Format format3 = videoFormat(/* bitrate= */ 2000, /* width= */ 960, /* height= */ 720); TrackGroup trackGroup = new TrackGroup(format1, format2, format3); - adaptiveTrackSelection = adaptiveTrackSelection(trackGroup, /* initialBitrate= */ 1000); + when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(1000L); + adaptiveTrackSelection = adaptiveTrackSelection(trackGroup); assertThat(adaptiveTrackSelection.getSelectedFormat()).isEqualTo(format2); assertThat(adaptiveTrackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_INITIAL); @@ -76,8 +76,7 @@ public final class AdaptiveTrackSelectionTest { TrackGroup trackGroup = new TrackGroup(format1, format2, format3); when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(500L); - - adaptiveTrackSelection = adaptiveTrackSelection(trackGroup, /* initialBitrate= */ 1000); + adaptiveTrackSelection = adaptiveTrackSelection(trackGroup); assertThat(adaptiveTrackSelection.getSelectedFormat()).isEqualTo(format1); assertThat(adaptiveTrackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_INITIAL); @@ -90,13 +89,12 @@ public final class AdaptiveTrackSelectionTest { Format format3 = videoFormat(/* bitrate= */ 2000, /* width= */ 960, /* height= */ 720); TrackGroup trackGroup = new TrackGroup(format1, format2, format3); - // initially bandwidth meter does not have any estimation. The second measurement onward returns - // 2000L, which prompts the track selection to switch up if possible. - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(BandwidthMeter.NO_ESTIMATE, 2000L); - + // The second measurement onward returns 2000L, which prompts the track selection to switch up + // if possible. + when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(1000L, 2000L); adaptiveTrackSelection = adaptiveTrackSelectionWithMinDurationForQualityIncreaseMs( - trackGroup, /* initialBitrate= */ 1000, /* minDurationForQualityIncreaseMs= */ 10_000); + trackGroup, /* minDurationForQualityIncreaseMs= */ 10_000); adaptiveTrackSelection.updateSelectedTrack( /* playbackPositionUs= */ 0, @@ -117,13 +115,12 @@ public final class AdaptiveTrackSelectionTest { Format format3 = videoFormat(/* bitrate= */ 2000, /* width= */ 960, /* height= */ 720); TrackGroup trackGroup = new TrackGroup(format1, format2, format3); - // initially bandwidth meter does not have any estimation. The second measurement onward returns - // 2000L, which prompts the track selection to switch up if possible. - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(BandwidthMeter.NO_ESTIMATE, 2000L); - + // The second measurement onward returns 2000L, which prompts the track selection to switch up + // if possible. + when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(1000L, 2000L); adaptiveTrackSelection = adaptiveTrackSelectionWithMinDurationForQualityIncreaseMs( - trackGroup, /* initialBitrate= */ 1000, /* minDurationForQualityIncreaseMs= */ 10_000); + trackGroup, /* minDurationForQualityIncreaseMs= */ 10_000); adaptiveTrackSelection.updateSelectedTrack( /* playbackPositionUs= */ 0, @@ -144,13 +141,12 @@ public final class AdaptiveTrackSelectionTest { Format format3 = videoFormat(/* bitrate= */ 2000, /* width= */ 960, /* height= */ 720); TrackGroup trackGroup = new TrackGroup(format1, format2, format3); - // initially bandwidth meter does not have any estimation. The second measurement onward returns - // 500L, which prompts the track selection to switch down if necessary. - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(BandwidthMeter.NO_ESTIMATE, 500L); - + // The second measurement onward returns 500L, which prompts the track selection to switch down + // if necessary. + when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(1000L, 500L); adaptiveTrackSelection = adaptiveTrackSelectionWithMaxDurationForQualityDecreaseMs( - trackGroup, /* initialBitrate= */ 1000, /* maxDurationForQualityDecreaseMs= */ 25_000); + trackGroup, /* maxDurationForQualityDecreaseMs= */ 25_000); adaptiveTrackSelection.updateSelectedTrack( /* playbackPositionUs= */ 0, @@ -171,13 +167,12 @@ public final class AdaptiveTrackSelectionTest { Format format3 = videoFormat(/* bitrate= */ 2000, /* width= */ 960, /* height= */ 720); TrackGroup trackGroup = new TrackGroup(format1, format2, format3); - // initially bandwidth meter does not have any estimation. The second measurement onward returns - // 500L, which prompts the track selection to switch down if necessary. - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(BandwidthMeter.NO_ESTIMATE, 500L); - + // The second measurement onward returns 500L, which prompts the track selection to switch down + // if necessary. + when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(1000L, 500L); adaptiveTrackSelection = adaptiveTrackSelectionWithMaxDurationForQualityDecreaseMs( - trackGroup, /* initialBitrate= */ 1000, /* maxDurationForQualityDecreaseMs= */ 25_000); + trackGroup, /* maxDurationForQualityDecreaseMs= */ 25_000); adaptiveTrackSelection.updateSelectedTrack( /* playbackPositionUs= */ 0, @@ -210,7 +205,7 @@ public final class AdaptiveTrackSelectionTest { queue.add(chunk3); when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(500L); - adaptiveTrackSelection = adaptiveTrackSelection(trackGroup, /* initialBitrate= */ 1000); + adaptiveTrackSelection = adaptiveTrackSelection(trackGroup); int size = adaptiveTrackSelection.evaluateQueueSize(0, queue); assertThat(size).isEqualTo(3); @@ -238,7 +233,6 @@ public final class AdaptiveTrackSelectionTest { adaptiveTrackSelection = adaptiveTrackSelectionWithMinTimeBetweenBufferReevaluationMs( trackGroup, - /* initialBitrate= */ 1000, /* durationToRetainAfterDiscardMs= */ 15_000, /* minTimeBetweenBufferReevaluationMs= */ 2000); @@ -276,7 +270,6 @@ public final class AdaptiveTrackSelectionTest { adaptiveTrackSelection = adaptiveTrackSelectionWithMinTimeBetweenBufferReevaluationMs( trackGroup, - /* initialBitrate= */ 1000, /* durationToRetainAfterDiscardMs= */ 15_000, /* minTimeBetweenBufferReevaluationMs= */ 2000); @@ -294,12 +287,11 @@ public final class AdaptiveTrackSelectionTest { assertThat(newSize).isEqualTo(2); } - private AdaptiveTrackSelection adaptiveTrackSelection(TrackGroup trackGroup, int initialBitrate) { + private AdaptiveTrackSelection adaptiveTrackSelection(TrackGroup trackGroup) { return new AdaptiveTrackSelection( trackGroup, selectedAllTracksInGroup(trackGroup), mockBandwidthMeter, - initialBitrate, AdaptiveTrackSelection.DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS, AdaptiveTrackSelection.DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS, AdaptiveTrackSelection.DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS, @@ -310,12 +302,11 @@ public final class AdaptiveTrackSelectionTest { } private AdaptiveTrackSelection adaptiveTrackSelectionWithMinDurationForQualityIncreaseMs( - TrackGroup trackGroup, int initialBitrate, long minDurationForQualityIncreaseMs) { + TrackGroup trackGroup, long minDurationForQualityIncreaseMs) { return new AdaptiveTrackSelection( trackGroup, selectedAllTracksInGroup(trackGroup), mockBandwidthMeter, - initialBitrate, minDurationForQualityIncreaseMs, AdaptiveTrackSelection.DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS, AdaptiveTrackSelection.DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS, @@ -326,12 +317,11 @@ public final class AdaptiveTrackSelectionTest { } private AdaptiveTrackSelection adaptiveTrackSelectionWithMaxDurationForQualityDecreaseMs( - TrackGroup trackGroup, int initialBitrate, long maxDurationForQualityDecreaseMs) { + TrackGroup trackGroup, long maxDurationForQualityDecreaseMs) { return new AdaptiveTrackSelection( trackGroup, selectedAllTracksInGroup(trackGroup), mockBandwidthMeter, - initialBitrate, AdaptiveTrackSelection.DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS, maxDurationForQualityDecreaseMs, AdaptiveTrackSelection.DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS, @@ -343,18 +333,16 @@ public final class AdaptiveTrackSelectionTest { private AdaptiveTrackSelection adaptiveTrackSelectionWithMinTimeBetweenBufferReevaluationMs( TrackGroup trackGroup, - int initialBitrate, long durationToRetainAfterDiscardMs, long minTimeBetweenBufferReevaluationMs) { return new AdaptiveTrackSelection( trackGroup, selectedAllTracksInGroup(trackGroup), mockBandwidthMeter, - initialBitrate, AdaptiveTrackSelection.DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS, AdaptiveTrackSelection.DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS, durationToRetainAfterDiscardMs, - /* bandwidth fraction= */ 1.0f, + /* bandwidthFraction= */ 1.0f, AdaptiveTrackSelection.DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE, minTimeBetweenBufferReevaluationMs, fakeClock); 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 4b2a3a5ad6..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 @@ -1,21 +1,42 @@ +/* + * 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.trackselection; import static com.google.android.exoplayer2.RendererCapabilities.FORMAT_EXCEEDS_CAPABILITIES; import static com.google.android.exoplayer2.RendererCapabilities.FORMAT_HANDLED; +import static com.google.android.exoplayer2.RendererConfiguration.DEFAULT; import static com.google.common.truth.Truth.assertThat; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; 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; import com.google.android.exoplayer2.RendererCapabilities; +import com.google.android.exoplayer2.RendererConfiguration; 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; @@ -39,6 +60,54 @@ public final class DefaultTrackSelectorTest { private static final RendererCapabilities ALL_AUDIO_FORMAT_EXCEEDED_RENDERER_CAPABILITIES = new FakeRendererCapabilities(C.TRACK_TYPE_AUDIO, FORMAT_EXCEEDS_CAPABILITIES); + private static final RendererCapabilities VIDEO_CAPABILITIES = + new FakeRendererCapabilities(C.TRACK_TYPE_VIDEO); + private static final RendererCapabilities AUDIO_CAPABILITIES = + new FakeRendererCapabilities(C.TRACK_TYPE_AUDIO); + private static final RendererCapabilities NO_SAMPLE_CAPABILITIES = + new FakeRendererCapabilities(C.TRACK_TYPE_NONE); + private static final RendererCapabilities[] RENDERER_CAPABILITIES = + new RendererCapabilities[] {VIDEO_CAPABILITIES, AUDIO_CAPABILITIES}; + private static final RendererCapabilities[] RENDERER_CAPABILITIES_WITH_NO_SAMPLE_RENDERER = + new RendererCapabilities[] {VIDEO_CAPABILITIES, NO_SAMPLE_CAPABILITIES}; + + private static final TrackGroup VIDEO_TRACK_GROUP = + new TrackGroup( + Format.createVideoSampleFormat( + "video", + MimeTypes.VIDEO_H264, + null, + Format.NO_VALUE, + Format.NO_VALUE, + 1024, + 768, + Format.NO_VALUE, + null, + null)); + private static final TrackGroup AUDIO_TRACK_GROUP = + new TrackGroup( + Format.createAudioSampleFormat( + "audio", + MimeTypes.AUDIO_AAC, + null, + Format.NO_VALUE, + Format.NO_VALUE, + 2, + 44100, + null, + null, + 0, + null)); + private static final TrackGroupArray TRACK_GROUPS = + new TrackGroupArray(VIDEO_TRACK_GROUP, AUDIO_TRACK_GROUP); + + private static final TrackSelection[] TRACK_SELECTIONS = + new TrackSelection[] { + new FixedTrackSelection(VIDEO_TRACK_GROUP, 0), new FixedTrackSelection(AUDIO_TRACK_GROUP, 0) + }; + private static final TrackSelection[] TRACK_SELECTIONS_WITH_NO_SAMPLE_RENDERER = + new TrackSelection[] {new FixedTrackSelection(VIDEO_TRACK_GROUP, 0), null}; + @Mock private InvalidationListener invalidationListener; @@ -50,6 +119,161 @@ public final class DefaultTrackSelectorTest { trackSelector = new DefaultTrackSelector(); } + /** 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, + /* disabledTextTrackSelectionFlags= */ 0, + /* forceLowestBitrate= */ true, + /* allowMixedMimeAdaptiveness= */ false, + /* allowNonSeamlessAdaptiveness= */ true, + /* maxVideoWidth= */ 1, + /* maxVideoHeight= */ 2, + /* maxVideoBitrate= */ 3, + /* exceedVideoConstraintsIfNecessary= */ false, + /* exceedRendererCapabilitiesIfNecessary= */ true, + /* viewportWidth= */ 4, + /* viewportHeight= */ 5, + /* viewportOrientationMayChange= */ false, + /* tunnelingAudioSessionId= */ C.AUDIO_SESSION_ID_UNSET); + + Parcel parcel = Parcel.obtain(); + parametersToParcel.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + + Parameters parametersFromParcel = Parameters.CREATOR.createFromParcel(parcel); + assertThat(parametersFromParcel).isEqualTo(parametersToParcel); + + 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.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) + .isEqualTo(new RendererConfiguration[] {null, DEFAULT}); + } + + /** Tests that a null override can be cleared. */ + @Test + public void testSelectTracksWithClearedNullOverride() throws ExoPlaybackException { + DefaultTrackSelector trackSelector = new DefaultTrackSelector(); + 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) + .isEqualTo(new RendererConfiguration[] {DEFAULT, DEFAULT}); + } + + /** Tests that an override is not applied for a different set of available track groups. */ + @Test + public void testSelectTracksWithNullOverrideForDifferentTracks() throws ExoPlaybackException { + DefaultTrackSelector trackSelector = new DefaultTrackSelector(); + trackSelector.setParameters( + trackSelector + .buildUponParameters() + .setSelectionOverride(0, new TrackGroupArray(VIDEO_TRACK_GROUP), null)); + TrackSelectorResult result = + trackSelector.selectTracks( + RENDERER_CAPABILITIES, + new TrackGroupArray(VIDEO_TRACK_GROUP, AUDIO_TRACK_GROUP, VIDEO_TRACK_GROUP)); + assertTrackSelections(result, TRACK_SELECTIONS); + assertThat(result.rendererConfigurations) + .isEqualTo(new RendererConfiguration[] {DEFAULT, DEFAULT}); + } + + /** Tests disabling a renderer. */ + @Test + public void testSelectTracksWithDisabledRenderer() throws ExoPlaybackException { + DefaultTrackSelector trackSelector = new DefaultTrackSelector(); + 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}) + .isEqualTo(result.rendererConfigurations); + } + + /** Tests that a disabled renderer can be enabled again. */ + @Test + public void testSelectTracksWithClearedDisabledRenderer() throws ExoPlaybackException { + DefaultTrackSelector trackSelector = new DefaultTrackSelector(); + 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}) + .isEqualTo(result.rendererConfigurations); + } + + /** Tests a no-sample renderer is enabled without a track selection by default. */ + @Test + public void testSelectTracksWithNoSampleRenderer() throws ExoPlaybackException { + DefaultTrackSelector trackSelector = new DefaultTrackSelector(); + TrackSelectorResult result = + trackSelector.selectTracks(RENDERER_CAPABILITIES_WITH_NO_SAMPLE_RENDERER, TRACK_GROUPS); + assertTrackSelections(result, TRACK_SELECTIONS_WITH_NO_SAMPLE_RENDERER); + assertThat(new RendererConfiguration[] {DEFAULT, DEFAULT}) + .isEqualTo(result.rendererConfigurations); + } + + /** Tests disabling a no-sample renderer. */ + @Test + public void testSelectTracksWithDisabledNoSampleRenderer() throws ExoPlaybackException { + DefaultTrackSelector trackSelector = new DefaultTrackSelector(); + 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); + assertThat(new RendererConfiguration[] {DEFAULT, null}) + .isEqualTo(result.rendererConfigurations); + } + /** * Tests that track selector will not call * {@link InvalidationListener#onTrackSelectionsInvalidated()} when it's set with default @@ -732,6 +956,13 @@ public final class DefaultTrackSelectorTest { assertThat(result.selections.get(0).getSelectedFormat()).isEqualTo(lowerBitrateFormat); } + private static void assertTrackSelections(TrackSelectorResult result, TrackSelection[] expected) { + assertThat(result.length).isEqualTo(expected.length); + for (int i = 0; i < expected.length; i++) { + assertThat(result.selections.get(i)).isEqualTo(expected[i]); + } + } + private static TrackGroupArray singleTrackGroup(Format... formats) { return new TrackGroupArray(new TrackGroup(formats)); } 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 b80110365c..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 @@ -15,9 +15,9 @@ */ package com.google.android.exoplayer2.trackselection; -import static com.google.android.exoplayer2.RendererConfiguration.DEFAULT; import static com.google.common.truth.Truth.assertThat; +import android.util.Pair; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; @@ -26,7 +26,6 @@ import com.google.android.exoplayer2.RendererConfiguration; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.util.MimeTypes; -import java.util.Arrays; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; @@ -41,16 +40,9 @@ public final class MappingTrackSelectorTest { new FakeRendererCapabilities(C.TRACK_TYPE_VIDEO); private static final RendererCapabilities AUDIO_CAPABILITIES = new FakeRendererCapabilities(C.TRACK_TYPE_AUDIO); - private static final RendererCapabilities NO_SAMPLE_CAPABILITIES = - new FakeRendererCapabilities(C.TRACK_TYPE_NONE); private static final RendererCapabilities[] RENDERER_CAPABILITIES = new RendererCapabilities[] { VIDEO_CAPABILITIES, AUDIO_CAPABILITIES }; - private static final RendererCapabilities[] RENDERER_CAPABILITIES_WITH_NO_SAMPLE_RENDERER = - new RendererCapabilities[] { - VIDEO_CAPABILITIES, AUDIO_CAPABILITIES, NO_SAMPLE_CAPABILITIES - }; - private static final TrackGroup VIDEO_TRACK_GROUP = new TrackGroup( Format.createVideoSampleFormat("video", MimeTypes.VIDEO_H264, null, Format.NO_VALUE, Format.NO_VALUE, 1024, 768, Format.NO_VALUE, null, null)); @@ -60,18 +52,6 @@ public final class MappingTrackSelectorTest { private static final TrackGroupArray TRACK_GROUPS = new TrackGroupArray( VIDEO_TRACK_GROUP, AUDIO_TRACK_GROUP); - private static final TrackSelection[] TRACK_SELECTIONS = new TrackSelection[] { - new FixedTrackSelection(VIDEO_TRACK_GROUP, 0), - new FixedTrackSelection(AUDIO_TRACK_GROUP, 0) - }; - - private static final TrackSelection[] TRACK_SELECTIONS_WITH_NO_SAMPLE_RENDERER = - new TrackSelection[] { - new FixedTrackSelection(VIDEO_TRACK_GROUP, 0), - new FixedTrackSelection(AUDIO_TRACK_GROUP, 0), - null - }; - /** * Tests that the video and audio track groups are mapped onto the correct renderers. */ @@ -112,219 +92,30 @@ public final class MappingTrackSelectorTest { } /** - * Tests the result of {@link MappingTrackSelector#selectTracks(RendererCapabilities[], - * TrackGroupArray[], int[][][])} is propagated correctly to the result of - * {@link MappingTrackSelector#selectTracks(RendererCapabilities[], TrackGroupArray)}. - */ - @Test - public void testSelectTracks() throws ExoPlaybackException { - FakeMappingTrackSelector trackSelector = new FakeMappingTrackSelector( - TRACK_SELECTIONS); - TrackSelectorResult result = trackSelector.selectTracks(RENDERER_CAPABILITIES, TRACK_GROUPS); - assertThat(result.selections.get(0)).isEqualTo(TRACK_SELECTIONS[0]); - assertThat(result.selections.get(1)).isEqualTo(TRACK_SELECTIONS[1]); - assertThat(new boolean[] {true, true}).isEqualTo(result.renderersEnabled); - assertThat(new RendererConfiguration[] {DEFAULT, DEFAULT}) - .isEqualTo(result.rendererConfigurations); - } - - /** - * Tests that a null override clears a track selection. - */ - @Test - public void testSelectTracksWithNullOverride() throws ExoPlaybackException { - FakeMappingTrackSelector trackSelector = new FakeMappingTrackSelector( - TRACK_SELECTIONS); - trackSelector.setSelectionOverride(0, new TrackGroupArray(VIDEO_TRACK_GROUP), null); - TrackSelectorResult result = trackSelector.selectTracks(RENDERER_CAPABILITIES, TRACK_GROUPS); - assertThat(result.selections.get(0)).isNull(); - assertThat(result.selections.get(1)).isEqualTo(TRACK_SELECTIONS[1]); - assertThat(new boolean[] {false, true}).isEqualTo(result.renderersEnabled); - assertThat(new RendererConfiguration[] {null, DEFAULT}) - .isEqualTo(result.rendererConfigurations); - } - - /** - * Tests that a null override can be cleared. - */ - @Test - public void testSelectTracksWithClearedNullOverride() throws ExoPlaybackException { - FakeMappingTrackSelector trackSelector = new FakeMappingTrackSelector( - TRACK_SELECTIONS); - trackSelector.setSelectionOverride(0, new TrackGroupArray(VIDEO_TRACK_GROUP), null); - trackSelector.clearSelectionOverride(0, new TrackGroupArray(VIDEO_TRACK_GROUP)); - TrackSelectorResult result = trackSelector.selectTracks(RENDERER_CAPABILITIES, TRACK_GROUPS); - assertThat(result.selections.get(0)).isEqualTo(TRACK_SELECTIONS[0]); - assertThat(result.selections.get(1)).isEqualTo(TRACK_SELECTIONS[1]); - assertThat(new boolean[] {true, true}).isEqualTo(result.renderersEnabled); - assertThat(new RendererConfiguration[] {DEFAULT, DEFAULT}) - .isEqualTo(result.rendererConfigurations); - } - - /** - * Tests that an override is not applied for a different set of available track groups. - */ - @Test - public void testSelectTracksWithNullOverrideForDifferentTracks() throws ExoPlaybackException { - FakeMappingTrackSelector trackSelector = new FakeMappingTrackSelector( - TRACK_SELECTIONS); - trackSelector.setSelectionOverride(0, new TrackGroupArray(VIDEO_TRACK_GROUP), null); - TrackSelectorResult result = trackSelector.selectTracks(RENDERER_CAPABILITIES, - new TrackGroupArray(VIDEO_TRACK_GROUP, AUDIO_TRACK_GROUP, VIDEO_TRACK_GROUP)); - assertThat(result.selections.get(0)).isEqualTo(TRACK_SELECTIONS[0]); - assertThat(result.selections.get(1)).isEqualTo(TRACK_SELECTIONS[1]); - assertThat(new boolean[] {true, true}).isEqualTo(result.renderersEnabled); - assertThat(new RendererConfiguration[] {DEFAULT, DEFAULT}) - .isEqualTo(result.rendererConfigurations); - } - - /** - * Tests the result of {@link MappingTrackSelector#selectTracks(RendererCapabilities[], - * TrackGroupArray[], int[][][])} is propagated correctly to the result of - * {@link MappingTrackSelector#selectTracks(RendererCapabilities[], TrackGroupArray)} - * when there is no-sample renderer. - */ - @Test - public void testSelectTracksWithNoSampleRenderer() throws ExoPlaybackException { - TrackSelection[] expectedTrackSelection = TRACK_SELECTIONS_WITH_NO_SAMPLE_RENDERER; - FakeMappingTrackSelector trackSelector = new FakeMappingTrackSelector(expectedTrackSelection); - TrackSelectorResult result = trackSelector.selectTracks( - RENDERER_CAPABILITIES_WITH_NO_SAMPLE_RENDERER, TRACK_GROUPS); - assertThat(result.selections.get(0)).isEqualTo(expectedTrackSelection[0]); - assertThat(result.selections.get(1)).isEqualTo(expectedTrackSelection[1]); - assertThat(result.selections.get(2)).isNull(); - assertThat(new boolean[] {true, true, true}).isEqualTo(result.renderersEnabled); - assertThat(new RendererConfiguration[] {DEFAULT, DEFAULT, DEFAULT}) - .isEqualTo(result.rendererConfigurations); - } - - /** - * Tests that a null override clears a track selection when there is no-sample renderer. - */ - @Test - public void testSelectTracksWithNoSampleRendererWithNullOverride() throws ExoPlaybackException { - TrackSelection[] expectedTrackSelection = TRACK_SELECTIONS_WITH_NO_SAMPLE_RENDERER; - FakeMappingTrackSelector trackSelector = new FakeMappingTrackSelector(expectedTrackSelection); - trackSelector.setSelectionOverride(0, new TrackGroupArray(VIDEO_TRACK_GROUP), null); - TrackSelectorResult result = trackSelector.selectTracks( - RENDERER_CAPABILITIES_WITH_NO_SAMPLE_RENDERER, TRACK_GROUPS); - assertThat(result.selections.get(0)).isNull(); - assertThat(result.selections.get(1)).isEqualTo(expectedTrackSelection[1]); - assertThat(result.selections.get(2)).isNull(); - assertThat(new boolean[] {false, true, true}).isEqualTo(result.renderersEnabled); - assertThat(new RendererConfiguration[] {null, DEFAULT, DEFAULT}) - .isEqualTo(result.rendererConfigurations); - } - - /** - * Tests that a null override can be cleared when there is no-sample renderer. - */ - @Test - public void testSelectTracksWithNoSampleRendererWithClearedNullOverride() - throws ExoPlaybackException { - TrackSelection[] expectedTrackSelection = TRACK_SELECTIONS_WITH_NO_SAMPLE_RENDERER; - FakeMappingTrackSelector trackSelector = new FakeMappingTrackSelector(expectedTrackSelection); - trackSelector.setSelectionOverride(0, new TrackGroupArray(VIDEO_TRACK_GROUP), null); - trackSelector.clearSelectionOverride(0, new TrackGroupArray(VIDEO_TRACK_GROUP)); - TrackSelectorResult result = trackSelector.selectTracks( - RENDERER_CAPABILITIES_WITH_NO_SAMPLE_RENDERER, TRACK_GROUPS); - assertThat(result.selections.get(0)).isEqualTo(expectedTrackSelection[0]); - assertThat(result.selections.get(1)).isEqualTo(expectedTrackSelection[1]); - assertThat(result.selections.get(2)).isNull(); - assertThat(new boolean[] {true, true, true}).isEqualTo(result.renderersEnabled); - assertThat(new RendererConfiguration[] {DEFAULT, DEFAULT, DEFAULT}) - .isEqualTo(result.rendererConfigurations); - } - - /** - * Tests that an override is not applied for a different set of available track groups - * when there is no-sample renderer. - */ - @Test - public void testSelectTracksWithNoSampleRendererWithNullOverrideForDifferentTracks() - throws ExoPlaybackException { - TrackSelection[] expectedTrackSelection = TRACK_SELECTIONS_WITH_NO_SAMPLE_RENDERER; - FakeMappingTrackSelector trackSelector = new FakeMappingTrackSelector(expectedTrackSelection); - trackSelector.setSelectionOverride(0, new TrackGroupArray(VIDEO_TRACK_GROUP), null); - TrackSelectorResult result = trackSelector.selectTracks( - RENDERER_CAPABILITIES_WITH_NO_SAMPLE_RENDERER, - new TrackGroupArray(VIDEO_TRACK_GROUP, AUDIO_TRACK_GROUP, VIDEO_TRACK_GROUP)); - assertThat(result.selections.get(0)).isEqualTo(expectedTrackSelection[0]); - assertThat(result.selections.get(1)).isEqualTo(expectedTrackSelection[1]); - assertThat(result.selections.get(2)).isNull(); - assertThat(new boolean[] {true, true, true}).isEqualTo(result.renderersEnabled); - assertThat(new RendererConfiguration[] {DEFAULT, DEFAULT, DEFAULT}) - .isEqualTo(result.rendererConfigurations); - } - - /** - * Tests that disabling another renderer works when there is no-sample renderer. - */ - @Test - public void testSelectTracksDisablingNormalRendererWithNoSampleRenderer() - throws ExoPlaybackException { - TrackSelection[] expectedTrackSelection = TRACK_SELECTIONS_WITH_NO_SAMPLE_RENDERER; - FakeMappingTrackSelector trackSelector = new FakeMappingTrackSelector(expectedTrackSelection); - trackSelector.setRendererDisabled(0, true); - TrackSelectorResult result = trackSelector.selectTracks( - RENDERER_CAPABILITIES_WITH_NO_SAMPLE_RENDERER, TRACK_GROUPS); - assertThat(result.selections.get(0)).isNull(); - assertThat(result.selections.get(1)).isEqualTo(expectedTrackSelection[1]); - assertThat(result.selections.get(2)).isNull(); - assertThat(new boolean[] {false, true, true}).isEqualTo(result.renderersEnabled); - assertThat(new RendererConfiguration[] {null, DEFAULT, DEFAULT}) - .isEqualTo(result.rendererConfigurations); - } - - /** - * Tests that disabling no-sample renderer work. - */ - @Test - public void testSelectTracksDisablingNoSampleRenderer() - throws ExoPlaybackException { - TrackSelection[] expectedTrackSelection = TRACK_SELECTIONS_WITH_NO_SAMPLE_RENDERER; - FakeMappingTrackSelector trackSelector = new FakeMappingTrackSelector(expectedTrackSelection); - trackSelector.setRendererDisabled(2, true); - TrackSelectorResult result = trackSelector.selectTracks( - RENDERER_CAPABILITIES_WITH_NO_SAMPLE_RENDERER, TRACK_GROUPS); - assertThat(result.selections.get(0)).isEqualTo(expectedTrackSelection[0]); - assertThat(result.selections.get(1)).isEqualTo(expectedTrackSelection[1]); - assertThat(result.selections.get(2)).isNull(); - assertThat(new boolean[] {true, true, false}).isEqualTo(result.renderersEnabled); - assertThat(new RendererConfiguration[] {DEFAULT, DEFAULT, null}) - .isEqualTo(result.rendererConfigurations); - } - - /** - * A {@link MappingTrackSelector} that returns a fixed result from - * {@link #selectTracks(RendererCapabilities[], TrackGroupArray[], int[][][])}. + * A {@link MappingTrackSelector} that stashes the {@link MappedTrackInfo} passed to {@link + * #selectTracks(MappedTrackInfo, int[][][], int[])}. */ private static final class FakeMappingTrackSelector extends MappingTrackSelector { - private final TrackSelection[] result; - private TrackGroupArray[] lastRendererTrackGroupArrays; - - public FakeMappingTrackSelector(TrackSelection... result) { - this.result = result.length == 0 ? null : result; - } + private MappedTrackInfo lastMappedTrackInfo; @Override - protected TrackSelection[] selectTracks(RendererCapabilities[] rendererCapabilities, - TrackGroupArray[] rendererTrackGroupArrays, int[][][] rendererFormatSupports) + protected Pair selectTracks( + MappedTrackInfo mappedTrackInfo, + int[][][] rendererFormatSupports, + int[] rendererMixedMimeTypeAdaptationSupports) throws ExoPlaybackException { - lastRendererTrackGroupArrays = rendererTrackGroupArrays; - TrackSelection[] trackSelectionResult = new TrackSelection[rendererCapabilities.length]; - return result == null ? trackSelectionResult - // return a copy of the provided result, because MappingTrackSelector - // might modify the returned array here, and we don't want that to affect - // the original array. - : Arrays.asList(result).toArray(trackSelectionResult); + int rendererCount = mappedTrackInfo.getRendererCount(); + lastMappedTrackInfo = mappedTrackInfo; + return Pair.create( + new RendererConfiguration[rendererCount], new TrackSelection[rendererCount]); } public void assertMappedTrackGroups(int rendererIndex, TrackGroup... expected) { - assertThat(lastRendererTrackGroupArrays[rendererIndex].length).isEqualTo(expected.length); + TrackGroupArray rendererTrackGroupArray = lastMappedTrackInfo.getTrackGroups(rendererIndex); + assertThat(rendererTrackGroupArray.length).isEqualTo(expected.length); for (int i = 0; i < expected.length; i++) { - assertThat(lastRendererTrackGroupArrays[rendererIndex].get(i)).isEqualTo(expected[i]); + assertThat(rendererTrackGroupArray.get(i)).isEqualTo(expected[i]); } } 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/DefaultContentMetadataTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/DefaultContentMetadataTest.java new file mode 100644 index 0000000000..e1dc68eac6 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/DefaultContentMetadataTest.java @@ -0,0 +1,199 @@ +/* + * 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.upstream.cache; + +import static com.google.common.truth.Truth.assertThat; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +/** Tests {@link DefaultContentMetadata}. */ +@RunWith(RobolectricTestRunner.class) +public class DefaultContentMetadataTest { + + private DefaultContentMetadata contentMetadata; + + @Before + public void setUp() throws Exception { + contentMetadata = createContentMetadata(); + } + + @Test + public void testContainsReturnsFalseWhenEmpty() throws Exception { + assertThat(contentMetadata.contains("test metadata")).isFalse(); + } + + @Test + public void testContainsReturnsTrueForInitialValue() throws Exception { + contentMetadata = createContentMetadata("metadata name", "value"); + assertThat(contentMetadata.contains("metadata name")).isTrue(); + } + + @Test + public void testGetReturnsDefaultValueWhenValueIsNotAvailable() throws Exception { + assertThat(contentMetadata.get("metadata name", "default value")).isEqualTo("default value"); + } + + @Test + public void testGetReturnsInitialValue() throws Exception { + contentMetadata = createContentMetadata("metadata name", "value"); + assertThat(contentMetadata.get("metadata name", "default value")).isEqualTo("value"); + } + + @Test + public void testEmptyMutationDoesNotFail() throws Exception { + ContentMetadataMutations mutations = new ContentMetadataMutations(); + DefaultContentMetadata.EMPTY.copyWithMutationsApplied(mutations); + } + + @Test + public void testAddNewMetadata() throws Exception { + ContentMetadataMutations mutations = new ContentMetadataMutations(); + mutations.set("metadata name", "value"); + contentMetadata = contentMetadata.copyWithMutationsApplied(mutations); + assertThat(contentMetadata.get("metadata name", "default value")).isEqualTo("value"); + } + + @Test + public void testAddNewIntMetadata() throws Exception { + ContentMetadataMutations mutations = new ContentMetadataMutations(); + mutations.set("metadata name", 5); + contentMetadata = contentMetadata.copyWithMutationsApplied(mutations); + assertThat(contentMetadata.get("metadata name", 0)).isEqualTo(5); + } + + @Test + public void testAddNewByteArrayMetadata() throws Exception { + ContentMetadataMutations mutations = new ContentMetadataMutations(); + byte[] value = {1, 2, 3}; + mutations.set("metadata name", value); + contentMetadata = contentMetadata.copyWithMutationsApplied(mutations); + assertThat(contentMetadata.get("metadata name", new byte[] {})).isEqualTo(value); + } + + @Test + public void testNewMetadataNotWrittenBeforeCommitted() throws Exception { + ContentMetadataMutations mutations = new ContentMetadataMutations(); + mutations.set("metadata name", "value"); + assertThat(contentMetadata.get("metadata name", "default value")).isEqualTo("default value"); + } + + @Test + public void testEditMetadata() throws Exception { + contentMetadata = createContentMetadata("metadata name", "value"); + ContentMetadataMutations mutations = new ContentMetadataMutations(); + mutations.set("metadata name", "edited value"); + contentMetadata = contentMetadata.copyWithMutationsApplied(mutations); + assertThat(contentMetadata.get("metadata name", "default value")).isEqualTo("edited value"); + } + + @Test + public void testRemoveMetadata() throws Exception { + contentMetadata = createContentMetadata("metadata name", "value"); + ContentMetadataMutations mutations = new ContentMetadataMutations(); + mutations.remove("metadata name"); + contentMetadata = contentMetadata.copyWithMutationsApplied(mutations); + assertThat(contentMetadata.get("metadata name", "default value")).isEqualTo("default value"); + } + + @Test + public void testAddAndRemoveMetadata() throws Exception { + ContentMetadataMutations mutations = new ContentMetadataMutations(); + mutations.set("metadata name", "value"); + mutations.remove("metadata name"); + contentMetadata = contentMetadata.copyWithMutationsApplied(mutations); + assertThat(contentMetadata.get("metadata name", "default value")).isEqualTo("default value"); + } + + @Test + public void testRemoveAndAddMetadata() throws Exception { + ContentMetadataMutations mutations = new ContentMetadataMutations(); + mutations.remove("metadata name"); + mutations.set("metadata name", "value"); + contentMetadata = contentMetadata.copyWithMutationsApplied(mutations); + assertThat(contentMetadata.get("metadata name", "default value")).isEqualTo("value"); + } + + @Test + public void testSerializeDeserialize() throws Exception { + byte[] metadata3 = {1, 2, 3}; + contentMetadata = + createContentMetadata( + "metadata1 name", "value", "metadata2 name", 12345, "metadata3 name", metadata3); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + contentMetadata.writeToStream(new DataOutputStream(outputStream)); + ByteArrayInputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray()); + DefaultContentMetadata contentMetadata2 = + DefaultContentMetadata.readFromStream(new DataInputStream(inputStream)); + + assertThat(contentMetadata2.get("metadata1 name", "default value")).isEqualTo("value"); + assertThat(contentMetadata2.get("metadata2 name", 0)).isEqualTo(12345); + assertThat(contentMetadata2.get("metadata3 name", new byte[] {})).isEqualTo(metadata3); + } + + @Test + public void testEqualsStringValues() throws Exception { + DefaultContentMetadata metadata1 = createContentMetadata("metadata1", "value"); + DefaultContentMetadata metadata2 = createContentMetadata("metadata1", "value"); + assertThat(metadata1).isEqualTo(metadata2); + } + + @Test + public void testEquals() throws Exception { + DefaultContentMetadata metadata1 = + createContentMetadata( + "metadata1", "value", "metadata2", 12345, "metadata3", new byte[] {1, 2, 3}); + DefaultContentMetadata metadata2 = + createContentMetadata( + "metadata2", 12345, "metadata3", new byte[] {1, 2, 3}, "metadata1", "value"); + assertThat(metadata1).isEqualTo(metadata2); + assertThat(metadata1.hashCode()).isEqualTo(metadata2.hashCode()); + } + + @Test + public void testNotEquals() throws Exception { + DefaultContentMetadata metadata1 = createContentMetadata("metadata1", new byte[] {1, 2, 3}); + DefaultContentMetadata metadata2 = createContentMetadata("metadata1", new byte[] {3, 2, 1}); + assertThat(metadata1).isNotEqualTo(metadata2); + assertThat(metadata1.hashCode()).isNotEqualTo(metadata2.hashCode()); + } + + private DefaultContentMetadata createContentMetadata(Object... pairs) { + assertThat(pairs.length % 2).isEqualTo(0); + ContentMetadataMutations mutations = new ContentMetadataMutations(); + for (int i = 0; i < pairs.length; i += 2) { + String name = (String) pairs[i]; + Object value = pairs[i + 1]; + if (value instanceof String) { + mutations.set(name, (String) value); + } else if (value instanceof byte[]) { + mutations.set(name, (byte[]) value); + } else if (value instanceof Number) { + mutations.set(name, ((Number) value).longValue()); + } else { + throw new IllegalArgumentException(); + } + } + return DefaultContentMetadata.EMPTY.copyWithMutationsApplied(mutations); + } +} 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 89ace34edc..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 @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.upstream.cache; import static com.google.android.exoplayer2.C.LENGTH_UNSET; import static com.google.android.exoplayer2.util.Util.toByteArray; import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; import static org.mockito.Mockito.doAnswer; import com.google.android.exoplayer2.C; @@ -31,7 +32,6 @@ import java.util.NavigableSet; import java.util.Random; import java.util.Set; import org.junit.After; -import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -111,31 +111,27 @@ public class SimpleCacheTest { SimpleCache simpleCache = getSimpleCache(); assertThat(simpleCache.getContentLength(KEY_1)).isEqualTo(LENGTH_UNSET); + simpleCache.setContentLength(KEY_1, 15); assertThat(simpleCache.getContentLength(KEY_1)).isEqualTo(15); simpleCache.startReadWrite(KEY_1, 0); - addCache(simpleCache, KEY_1, 0, 15); - simpleCache.setContentLength(KEY_1, 150); assertThat(simpleCache.getContentLength(KEY_1)).isEqualTo(150); addCache(simpleCache, KEY_1, 140, 10); + simpleCache.release(); + // Check if values are kept after cache is reloaded. SimpleCache simpleCache2 = getSimpleCache(); - Set keys = simpleCache.getKeys(); - Set keys2 = simpleCache2.getKeys(); - assertThat(keys2).isEqualTo(keys); - for (String key : keys) { - assertThat(simpleCache2.getContentLength(key)).isEqualTo(simpleCache.getContentLength(key)); - assertThat(simpleCache2.getCachedSpans(key)).isEqualTo(simpleCache.getCachedSpans(key)); - } + assertThat(simpleCache2.getContentLength(KEY_1)).isEqualTo(150); // Removing the last span shouldn't cause the length be change next time cache loaded SimpleCacheSpan lastSpan = simpleCache2.startReadWrite(KEY_1, 145); simpleCache2.removeSpan(lastSpan); + simpleCache2.release(); simpleCache2 = getSimpleCache(); assertThat(simpleCache2.getContentLength(KEY_1)).isEqualTo(150); } @@ -148,6 +144,7 @@ public class SimpleCacheTest { CacheSpan cacheSpan1 = simpleCache.startReadWrite(KEY_1, 0); addCache(simpleCache, KEY_1, 0, 15); simpleCache.releaseHoleSpan(cacheSpan1); + simpleCache.release(); // Reload cache simpleCache = getSimpleCache(); @@ -166,6 +163,7 @@ public class SimpleCacheTest { CacheSpan cacheSpan1 = simpleCache.startReadWrite(KEY_1, 0); addCache(simpleCache, KEY_1, 0, 15); simpleCache.releaseHoleSpan(cacheSpan1); + simpleCache.release(); // Reload cache simpleCache = getEncryptedSimpleCache(key); @@ -184,6 +182,7 @@ public class SimpleCacheTest { CacheSpan cacheSpan1 = simpleCache.startReadWrite(KEY_1, 0); addCache(simpleCache, KEY_1, 0, 15); simpleCache.releaseHoleSpan(cacheSpan1); + simpleCache.release(); // Reload cache byte[] key2 = "Foo12345Foo12345".getBytes(C.UTF8_NAME); // 128 bit key @@ -203,6 +202,7 @@ public class SimpleCacheTest { CacheSpan cacheSpan1 = simpleCache.startReadWrite(KEY_1, 0); addCache(simpleCache, KEY_1, 0, 15); simpleCache.releaseHoleSpan(cacheSpan1); + simpleCache.release(); // Reload cache simpleCache = getSimpleCache(); @@ -269,7 +269,7 @@ public class SimpleCacheTest { // Adding more content will make LeastRecentlyUsedCacheEvictor evict previous content. try { addCache(simpleCache, KEY_1, 15, 15); - Assert.fail("Exception was expected"); + assertWithMessage("Exception was expected").fail(); } catch (CacheException e) { // do nothing. } @@ -283,6 +283,40 @@ public class SimpleCacheTest { assertThat(cachedSpans.pollFirst().position).isEqualTo(15); } + @Test + public void testUsingReleasedSimpleCacheThrowsException() throws Exception { + SimpleCache simpleCache = new SimpleCache(cacheDir, new NoOpCacheEvictor()); + simpleCache.release(); + + try { + simpleCache.startReadWriteNonBlocking(KEY_1, 0); + assertWithMessage("Exception was expected").fail(); + } catch (RuntimeException 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() + throws Exception { + SimpleCache simpleCache = new SimpleCache(cacheDir, new NoOpCacheEvictor()); + simpleCache.release(); + + new SimpleCache(cacheDir, new NoOpCacheEvictor()); + } + private SimpleCache getSimpleCache() { return new SimpleCache(cacheDir, new NoOpCacheEvictor()); } 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/DashMediaPeriod.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java index 00baf15228..d2982481e0 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java @@ -79,6 +79,7 @@ import java.util.List; private DashManifest manifest; private int periodIndex; private List eventStreams; + private boolean notifiedReadingStarted; public DashMediaPeriod( int id, @@ -114,6 +115,7 @@ import java.util.List; eventStreams); trackGroups = result.first; trackGroupInfos = result.second; + eventDispatcher.mediaPeriodCreated(); } /** @@ -148,6 +150,7 @@ import java.util.List; for (ChunkSampleStream sampleStream : sampleStreams) { sampleStream.release(this); } + eventDispatcher.mediaPeriodReleased(); } // ChunkSampleStream.ReleaseCallback implementation. @@ -325,6 +328,10 @@ import java.util.List; @Override public long readDiscontinuity() { + if (!notifiedReadingStarted) { + eventDispatcher.readingStarted(); + notifiedReadingStarted = true; + } return C.TIME_UNSET; } diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java index 98783ac93e..7b854e9d29 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java @@ -27,6 +27,7 @@ import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.source.BaseMediaSource; import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory; import com.google.android.exoplayer2.source.DefaultCompositeSequenceableLoaderFactory; import com.google.android.exoplayer2.source.MediaPeriod; @@ -59,7 +60,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; /** A DASH {@link MediaSource}. */ -public final class DashMediaSource implements MediaSource { +public final class DashMediaSource extends BaseMediaSource { static { ExoPlayerLibraryInfo.registerModule("goog.exo.dash"); @@ -76,6 +77,7 @@ public final class DashMediaSource implements MediaSource { private int minLoadableRetryCount; private long livePresentationDelayMs; private boolean isCreateCalled; + private @Nullable Object tag; /** * Creates a new factory for {@link DashMediaSource}s. @@ -96,6 +98,21 @@ public final class DashMediaSource implements MediaSource { compositeSequenceableLoaderFactory = new DefaultCompositeSequenceableLoaderFactory(); } + /** + * Sets a tag for the media source which will be published in the {@link + * com.google.android.exoplayer2.Timeline} of the source as {@link + * com.google.android.exoplayer2.Timeline.Window#tag}. + * + * @param tag A tag for the media source. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. + */ + public Factory setTag(Object tag) { + Assertions.checkState(!isCreateCalled); + this.tag = tag; + return this; + } + /** * Sets the minimum number of times to retry if a loading error occurs. The default value is * {@link #DEFAULT_MIN_LOADABLE_RETRY_COUNT}. @@ -166,60 +183,54 @@ public final class DashMediaSource implements MediaSource { * sideloaded manifest. * * @param manifest The manifest. {@link DashManifest#dynamic} must be false. - * @param eventHandler A handler for events. - * @param eventListener A listener of events. * @return The new {@link DashMediaSource}. * @throws IllegalArgumentException If {@link DashManifest#dynamic} is true. */ - public DashMediaSource createMediaSource( - DashManifest manifest, - @Nullable Handler eventHandler, - @Nullable MediaSourceEventListener eventListener) { + public DashMediaSource createMediaSource(DashManifest manifest) { Assertions.checkArgument(!manifest.dynamic); isCreateCalled = true; return new DashMediaSource( manifest, - null, - null, - null, + /* manifestUri= */ null, + /* manifestDataSourceFactory= */ null, + /* manifestParser= */ null, chunkSourceFactory, compositeSequenceableLoaderFactory, minLoadableRetryCount, livePresentationDelayMs, - eventHandler, - eventListener); + tag); } /** - * Returns a new {@link DashMediaSource} using the current parameters. Media source events will - * not be delivered. - * - * @param manifestUri The manifest {@link Uri}. - * @return The new {@link DashMediaSource}. + * @deprecated Use {@link #createMediaSource(DashManifest)} and {@link + * #addEventListener(Handler, MediaSourceEventListener)} instead. */ - public DashMediaSource createMediaSource(Uri manifestUri) { - return createMediaSource(manifestUri, null, null); + @Deprecated + public DashMediaSource createMediaSource( + DashManifest manifest, + @Nullable Handler eventHandler, + @Nullable MediaSourceEventListener eventListener) { + DashMediaSource mediaSource = createMediaSource(manifest); + if (eventHandler != null && eventListener != null) { + mediaSource.addEventListener(eventHandler, eventListener); + } + return mediaSource; } /** * Returns a new {@link DashMediaSource} using the current parameters. * * @param manifestUri The manifest {@link Uri}. - * @param eventHandler A handler for events. - * @param eventListener A listener of events. * @return The new {@link DashMediaSource}. */ @Override - public DashMediaSource createMediaSource( - Uri manifestUri, - @Nullable Handler eventHandler, - @Nullable MediaSourceEventListener eventListener) { + public DashMediaSource createMediaSource(Uri manifestUri) { isCreateCalled = true; if (manifestParser == null) { manifestParser = new DashManifestParser(); } return new DashMediaSource( - null, + /* manifest= */ null, Assertions.checkNotNull(manifestUri), manifestDataSourceFactory, manifestParser, @@ -227,8 +238,23 @@ public final class DashMediaSource implements MediaSource { compositeSequenceableLoaderFactory, minLoadableRetryCount, livePresentationDelayMs, - eventHandler, - eventListener); + tag); + } + + /** + * @deprecated Use {@link #createMediaSource(Uri)} and {@link #addEventListener(Handler, + * MediaSourceEventListener)} instead. + */ + @Deprecated + public DashMediaSource createMediaSource( + Uri manifestUri, + @Nullable Handler eventHandler, + @Nullable MediaSourceEventListener eventListener) { + DashMediaSource mediaSource = createMediaSource(manifestUri); + if (eventHandler != null && eventListener != null) { + mediaSource.addEventListener(eventHandler, eventListener); + } + return mediaSource; } @Override @@ -255,8 +281,8 @@ public final class DashMediaSource implements MediaSource { public static final long DEFAULT_LIVE_PRESENTATION_DELAY_FIXED_MS = 30000; /** - * The interval in milliseconds between invocations of - * {@link MediaSource.Listener#onSourceInfoRefreshed(MediaSource, Timeline, Object)} when the + * The interval in milliseconds between invocations of {@link + * SourceInfoRefreshListener#onSourceInfoRefreshed(MediaSource, Timeline, Object)} when the * source's {@link Timeline} is changing dynamically (for example, for incomplete live streams). */ private static final int NOTIFY_MANIFEST_INTERVAL_MS = 5000; @@ -273,7 +299,7 @@ public final class DashMediaSource implements MediaSource { private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; private final int minLoadableRetryCount; private final long livePresentationDelayMs; - private final EventDispatcher eventDispatcher; + private final EventDispatcher manifestEventDispatcher; private final ParsingLoadable.Parser manifestParser; private final ManifestCallback manifestCallback; private final Object manifestUriLock; @@ -282,8 +308,8 @@ public final class DashMediaSource implements MediaSource { private final Runnable simulateManifestRefreshRunnable; private final PlayerEmsgCallback playerEmsgCallback; private final LoaderErrorThrower manifestLoadErrorThrower; + private final @Nullable Object tag; - private Listener sourceListener; private DataSource dataSource; private Loader loader; @@ -340,9 +366,19 @@ public final class DashMediaSource implements MediaSource { int minLoadableRetryCount, Handler eventHandler, MediaSourceEventListener eventListener) { - this(manifest, null, null, null, chunkSourceFactory, - new DefaultCompositeSequenceableLoaderFactory(), minLoadableRetryCount, - DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS, eventHandler, eventListener); + this( + manifest, + /* manifestUri= */ null, + /* manifestDataSourceFactory= */ null, + /* manifestParser= */ null, + chunkSourceFactory, + new DefaultCompositeSequenceableLoaderFactory(), + minLoadableRetryCount, + DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS, + /* tag= */ null); + if (eventHandler != null && eventListener != null) { + addEventListener(eventHandler, eventListener); + } } /** @@ -427,9 +463,19 @@ public final class DashMediaSource implements MediaSource { long livePresentationDelayMs, Handler eventHandler, MediaSourceEventListener eventListener) { - this(null, manifestUri, manifestDataSourceFactory, manifestParser, chunkSourceFactory, - new DefaultCompositeSequenceableLoaderFactory(), minLoadableRetryCount, - livePresentationDelayMs, eventHandler, eventListener); + this( + /* manifest= */ null, + manifestUri, + manifestDataSourceFactory, + manifestParser, + chunkSourceFactory, + new DefaultCompositeSequenceableLoaderFactory(), + minLoadableRetryCount, + livePresentationDelayMs, + /* tag= */ null); + if (eventHandler != null && eventListener != null) { + addEventListener(eventHandler, eventListener); + } } private DashMediaSource( @@ -441,8 +487,7 @@ public final class DashMediaSource implements MediaSource { CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory, int minLoadableRetryCount, long livePresentationDelayMs, - Handler eventHandler, - MediaSourceEventListener eventListener) { + @Nullable Object tag) { this.initialManifestUri = manifestUri; this.manifest = manifest; this.manifestUri = manifestUri; @@ -452,8 +497,9 @@ public final class DashMediaSource implements MediaSource { this.minLoadableRetryCount = minLoadableRetryCount; this.livePresentationDelayMs = livePresentationDelayMs; this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory; + this.tag = tag; sideloadedManifest = manifest != null; - eventDispatcher = new EventDispatcher(eventHandler, eventListener); + manifestEventDispatcher = createEventDispatcher(/* mediaPeriodId= */ null); manifestUriLock = new Object(); periodsById = new SparseArray<>(); playerEmsgCallback = new DefaultPlayerEmsgCallback(); @@ -497,8 +543,7 @@ public final class DashMediaSource implements MediaSource { // MediaSource implementation. @Override - public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { - sourceListener = listener; + public void prepareSourceInternal(ExoPlayer player, boolean isTopLevelSource) { if (sideloadedManifest) { processManifest(false); } else { @@ -517,8 +562,8 @@ public final class DashMediaSource implements MediaSource { @Override public MediaPeriod createPeriod(MediaPeriodId periodId, Allocator allocator) { int periodIndex = periodId.periodIndex; - EventDispatcher periodEventDispatcher = eventDispatcher.copyWithMediaTimeOffsetMs( - manifest.getPeriod(periodIndex).startMs); + EventDispatcher periodEventDispatcher = + createEventDispatcher(periodId, manifest.getPeriod(periodIndex).startMs); DashMediaPeriod mediaPeriod = new DashMediaPeriod( firstPeriodId + periodIndex, @@ -544,7 +589,7 @@ public final class DashMediaSource implements MediaSource { } @Override - public void releaseSource() { + public void releaseSourceInternal() { manifestLoadPending = false; dataSource = null; if (loader != null) { @@ -590,8 +635,12 @@ public final class DashMediaSource implements MediaSource { /* package */ void onManifestLoadCompleted(ParsingLoadable loadable, long elapsedRealtimeMs, long loadDurationMs) { - eventDispatcher.loadCompleted(loadable.dataSpec, loadable.type, elapsedRealtimeMs, - loadDurationMs, loadable.bytesLoaded()); + manifestEventDispatcher.loadCompleted( + loadable.dataSpec, + loadable.type, + elapsedRealtimeMs, + loadDurationMs, + loadable.bytesLoaded()); DashManifest newManifest = loadable.getResult(); int periodCount = manifest == null ? 0 : manifest.getPeriodCount(); @@ -613,7 +662,8 @@ public final class DashMediaSource implements MediaSource { Log.w(TAG, "Loaded out of sync manifest"); isManifestStale = true; } else if (dynamicMediaPresentationEnded - || newManifest.publishTimeMs <= expiredManifestPublishTimeUs) { + || (expiredManifestPublishTimeUs != C.TIME_UNSET + && newManifest.publishTimeMs * 1000 <= expiredManifestPublishTimeUs)) { // If we receive a dynamic manifest that's older than expected (i.e. its publish time has // expired, or it's dynamic and we know the presentation has ended), then this manifest is // stale. @@ -666,33 +716,61 @@ public final class DashMediaSource implements MediaSource { } } - /* package */ int onManifestLoadError(ParsingLoadable loadable, - long elapsedRealtimeMs, long loadDurationMs, IOException error) { + /* package */ @Loader.RetryAction + int onManifestLoadError( + ParsingLoadable loadable, + long elapsedRealtimeMs, + long loadDurationMs, + IOException error) { boolean isFatal = error instanceof ParserException; - eventDispatcher.loadError(loadable.dataSpec, loadable.type, elapsedRealtimeMs, loadDurationMs, - loadable.bytesLoaded(), error, isFatal); + manifestEventDispatcher.loadError( + loadable.dataSpec, + loadable.type, + elapsedRealtimeMs, + loadDurationMs, + loadable.bytesLoaded(), + error, + isFatal); return isFatal ? Loader.DONT_RETRY_FATAL : Loader.RETRY; } /* package */ void onUtcTimestampLoadCompleted(ParsingLoadable loadable, long elapsedRealtimeMs, long loadDurationMs) { - eventDispatcher.loadCompleted(loadable.dataSpec, loadable.type, elapsedRealtimeMs, - loadDurationMs, loadable.bytesLoaded()); + manifestEventDispatcher.loadCompleted( + loadable.dataSpec, + loadable.type, + elapsedRealtimeMs, + loadDurationMs, + loadable.bytesLoaded()); onUtcTimestampResolved(loadable.getResult() - elapsedRealtimeMs); } - /* package */ int onUtcTimestampLoadError(ParsingLoadable loadable, long elapsedRealtimeMs, - long loadDurationMs, IOException error) { - eventDispatcher.loadError(loadable.dataSpec, loadable.type, elapsedRealtimeMs, loadDurationMs, - loadable.bytesLoaded(), error, true); + /* package */ @Loader.RetryAction + int onUtcTimestampLoadError( + ParsingLoadable loadable, + long elapsedRealtimeMs, + long loadDurationMs, + IOException error) { + manifestEventDispatcher.loadError( + loadable.dataSpec, + loadable.type, + elapsedRealtimeMs, + loadDurationMs, + loadable.bytesLoaded(), + error, + true); onUtcTimestampResolutionError(error); return Loader.DONT_RETRY; } /* package */ void onLoadCanceled(ParsingLoadable loadable, long elapsedRealtimeMs, long loadDurationMs) { - eventDispatcher.loadCanceled(loadable.dataSpec, loadable.type, elapsedRealtimeMs, - loadDurationMs, loadable.bytesLoaded()); + manifestEventDispatcher.loadCanceled( + loadable.dataSpec, + loadable.type, + elapsedRealtimeMs, + loadDurationMs, + loadable.bytesLoaded()); } // Internal methods. @@ -807,10 +885,17 @@ public final class DashMediaSource implements MediaSource { } long windowStartTimeMs = manifest.availabilityStartTimeMs + manifest.getPeriod(0).startMs + C.usToMs(currentStartTimeUs); - DashTimeline timeline = new DashTimeline(manifest.availabilityStartTimeMs, windowStartTimeMs, - firstPeriodId, currentStartTimeUs, windowDurationUs, windowDefaultStartPositionUs, - manifest); - sourceListener.onSourceInfoRefreshed(this, timeline, manifest); + DashTimeline timeline = + new DashTimeline( + manifest.availabilityStartTimeMs, + windowStartTimeMs, + firstPeriodId, + currentStartTimeUs, + windowDurationUs, + windowDefaultStartPositionUs, + manifest, + tag); + refreshSourceInfo(timeline, manifest); if (!sideloadedManifest) { // Remove any pending simulated refresh. @@ -821,7 +906,9 @@ public final class DashMediaSource implements MediaSource { } if (manifestLoadPending) { startLoadingManifest(); - } else if (scheduleRefresh && manifest.dynamic) { + } else if (scheduleRefresh + && manifest.dynamic + && manifest.minUpdatePeriodMs != C.TIME_UNSET) { // Schedule an explicit refresh if needed. long minUpdatePeriodMs = manifest.minUpdatePeriodMs; if (minUpdatePeriodMs == 0) { @@ -867,7 +954,7 @@ public final class DashMediaSource implements MediaSource { private void startLoading(ParsingLoadable loadable, Loader.Callback> callback, int minRetryCount) { long elapsedRealtimeMs = loader.startLoading(loadable, callback, minRetryCount); - eventDispatcher.loadStarted(loadable.dataSpec, loadable.type, elapsedRealtimeMs); + manifestEventDispatcher.loadStarted(loadable.dataSpec, loadable.type, elapsedRealtimeMs); } private long getNowUnixTimeUs() { @@ -936,10 +1023,17 @@ public final class DashMediaSource implements MediaSource { private final long windowDurationUs; private final long windowDefaultStartPositionUs; private final DashManifest manifest; + private final @Nullable Object windowTag; - public DashTimeline(long presentationStartTimeMs, long windowStartTimeMs, int firstPeriodId, - long offsetInFirstPeriodUs, long windowDurationUs, long windowDefaultStartPositionUs, - DashManifest manifest) { + public DashTimeline( + long presentationStartTimeMs, + long windowStartTimeMs, + int firstPeriodId, + long offsetInFirstPeriodUs, + long windowDurationUs, + long windowDefaultStartPositionUs, + DashManifest manifest, + @Nullable Object windowTag) { this.presentationStartTimeMs = presentationStartTimeMs; this.windowStartTimeMs = windowStartTimeMs; this.firstPeriodId = firstPeriodId; @@ -947,6 +1041,7 @@ public final class DashMediaSource implements MediaSource { this.windowDurationUs = windowDurationUs; this.windowDefaultStartPositionUs = windowDefaultStartPositionUs; this.manifest = manifest; + this.windowTag = windowTag; } @Override @@ -971,14 +1066,23 @@ public final class DashMediaSource implements MediaSource { } @Override - public Window getWindow(int windowIndex, Window window, boolean setIdentifier, - long defaultPositionProjectionUs) { + public Window getWindow( + int windowIndex, Window window, boolean setTag, long defaultPositionProjectionUs) { Assertions.checkIndex(windowIndex, 0, 1); long windowDefaultStartPositionUs = getAdjustedWindowDefaultStartPositionUs( defaultPositionProjectionUs); - return window.set(null, presentationStartTimeMs, windowStartTimeMs, true /* isSeekable */, - manifest.dynamic, windowDefaultStartPositionUs, windowDurationUs, 0, - manifest.getPeriodCount() - 1, offsetInFirstPeriodUs); + Object tag = setTag ? windowTag : null; + return window.set( + tag, + presentationStartTimeMs, + windowStartTimeMs, + /* isSeekable= */ true, + manifest.dynamic, + windowDefaultStartPositionUs, + windowDurationUs, + /* firstPeriodIndex= */ 0, + manifest.getPeriodCount() - 1, + offsetInFirstPeriodUs); } @Override @@ -1068,8 +1172,11 @@ public final class DashMediaSource implements MediaSource { } @Override - public int onLoadError(ParsingLoadable loadable, - long elapsedRealtimeMs, long loadDurationMs, IOException error) { + public @Loader.RetryAction int onLoadError( + ParsingLoadable loadable, + long elapsedRealtimeMs, + long loadDurationMs, + IOException error) { return onManifestLoadError(loadable, elapsedRealtimeMs, loadDurationMs, error); } @@ -1090,8 +1197,11 @@ public final class DashMediaSource implements MediaSource { } @Override - public int onLoadError(ParsingLoadable loadable, long elapsedRealtimeMs, - long loadDurationMs, IOException error) { + public @Loader.RetryAction int onLoadError( + ParsingLoadable loadable, + long elapsedRealtimeMs, + long loadDurationMs, + IOException error) { return onUtcTimestampLoadError(loadable, elapsedRealtimeMs, loadDurationMs, error); } 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/DefaultDashChunkSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java index a76349adac..4cb14d6614 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java @@ -328,9 +328,18 @@ public class DefaultDashChunkSource implements DashChunkSource { int maxSegmentCount = (int) Math.min(maxSegmentsPerLoad, lastAvailableSegmentNum - segmentNum + 1); - out.chunk = newMediaChunk(representationHolder, dataSource, trackType, - trackSelection.getSelectedFormat(), trackSelection.getSelectionReason(), - trackSelection.getSelectionData(), segmentNum, maxSegmentCount); + long seekTimeUs = previous == null ? loadPositionUs : C.TIME_UNSET; + out.chunk = + newMediaChunk( + representationHolder, + dataSource, + trackType, + trackSelection.getSelectedFormat(), + trackSelection.getSelectionReason(), + trackSelection.getSelectionData(), + segmentNum, + maxSegmentCount, + seekTimeUs); } @Override @@ -442,7 +451,8 @@ public class DefaultDashChunkSource implements DashChunkSource { int trackSelectionReason, Object trackSelectionData, long firstSegmentNum, - int maxSegmentCount) { + int maxSegmentCount, + long seekTimeUs) { Representation representation = representationHolder.representation; long startTimeUs = representationHolder.getSegmentStartTimeUs(firstSegmentNum); RangedUri segmentUri = representationHolder.getSegmentUrl(firstSegmentNum); @@ -469,9 +479,19 @@ public class DefaultDashChunkSource implements DashChunkSource { DataSpec dataSpec = new DataSpec(segmentUri.resolveUri(baseUrl), segmentUri.start, segmentUri.length, representation.getCacheKey()); long sampleOffsetUs = -representation.presentationTimeOffsetUs; - return new ContainerMediaChunk(dataSource, dataSpec, trackFormat, trackSelectionReason, - trackSelectionData, startTimeUs, endTimeUs, firstSegmentNum, segmentCount, - sampleOffsetUs, representationHolder.extractorWrapper); + return new ContainerMediaChunk( + dataSource, + dataSpec, + trackFormat, + trackSelectionReason, + trackSelectionData, + startTimeUs, + endTimeUs, + seekTimeUs, + firstSegmentNum, + segmentCount, + sampleOffsetUs, + representationHolder.extractorWrapper); } } 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/DashManifestParser.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java index aeae720517..0a4274e674 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java @@ -112,7 +112,7 @@ public class DashManifestParser extends DefaultHandler long durationMs = parseDuration(xpp, "mediaPresentationDuration", C.TIME_UNSET); long minBufferTimeMs = parseDuration(xpp, "minBufferTime", C.TIME_UNSET); String typeString = xpp.getAttributeValue(null, "type"); - boolean dynamic = typeString != null && typeString.equals("dynamic"); + boolean dynamic = typeString != null && "dynamic".equals(typeString); long minUpdateTimeMs = dynamic ? parseDuration(xpp, "minimumUpdatePeriod", C.TIME_UNSET) : C.TIME_UNSET; long timeShiftBufferDepthMs = dynamic @@ -732,19 +732,23 @@ public class DashManifestParser extends DefaultHandler /** * Parses a single Event node in the manifest. - *

    + * * @param xpp The current xml parser. * @param schemeIdUri The schemeIdUri of the parent EventStream. * @param value The schemeIdUri of the parent EventStream. * @param timescale The timescale of the parent EventStream. - * @param scratchOutputStream A {@link ByteArrayOutputStream} that's used when parsing event + * @param scratchOutputStream A {@link ByteArrayOutputStream} that is used when parsing event * objects. * @return The {@link EventMessage} parsed from this EventStream node. * @throws XmlPullParserException If there is any error parsing this node. * @throws IOException If there is any error reading from the underlying input stream. */ - protected EventMessage parseEvent(XmlPullParser xpp, String schemeIdUri, String value, - long timescale, ByteArrayOutputStream scratchOutputStream) + protected EventMessage parseEvent( + XmlPullParser xpp, + String schemeIdUri, + String value, + long timescale, + ByteArrayOutputStream scratchOutputStream) throws IOException, XmlPullParserException { long id = parseLong(xpp, "id", 0); long duration = parseLong(xpp, "duration", C.TIME_UNSET); 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 84c899f6c2..0000000000 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/FilteringDashManifestParser.java +++ /dev/null @@ -1,43 +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 com.google.android.exoplayer2.upstream.ParsingLoadable.Parser; -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; - -/** - * 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 ArrayList filter; - - /** @param filter The representation keys that should be retained in the parsed manifests. */ - public FilteringDashManifestParser(ArrayList filter) { - this.dashManifestParser = new DashManifestParser(); - this.filter = filter; - } - - @Override - public DashManifest parse(Uri uri, InputStream inputStream) throws IOException { - return dashManifestParser.parse(uri, inputStream).copy(filter); - } -} 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(TYPE, VERSION) { + + @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); + } + }; + + /** + * @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 uri, boolean isRemoveAction, @Nullable byte[] data, List keys) { + super(TYPE, VERSION, uri, isRemoveAction, data, keys); + } + + @Override + protected DashDownloader createDownloader(DownloaderConstructorHelper constructorHelper) { + return new DashDownloader(uri, keys, constructorHelper); + } + + @Override + protected void writeKey(DataOutputStream output, RepresentationKey key) throws IOException { + output.writeInt(key.periodIndex); + output.writeInt(key.adaptationSetIndex); + output.writeInt(key.representationIndex); + } + +} 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 4a90feb532..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,57 +38,42 @@ 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: * - *

    - * {@code
    + * 
    {@code
      * SimpleCache cache = new SimpleCache(downloadFolder, new NoOpCacheEvictor());
      * 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);}
    - * 
    + * new CacheDataSource(cache, factory.createDataSource(), CacheDataSource.FLAG_BLOCK_ON_CACHE); + * }
    */ 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 @@ -96,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/AndroidManifest.xml b/library/dash/src/test/AndroidManifest.xml index d18291d0ee..e20c1fbb9f 100644 --- a/library/dash/src/test/AndroidManifest.xml +++ b/library/dash/src/test/AndroidManifest.xml @@ -14,9 +14,4 @@ limitations under the License. --> - - - - - + 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 new file mode 100644 index 0000000000..43d9bd9965 --- /dev/null +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadActionTest.java @@ -0,0 +1,205 @@ +/* + * 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.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; +import com.google.android.exoplayer2.upstream.DummyDataSource; +import com.google.android.exoplayer2.upstream.cache.Cache; +import java.io.ByteArrayInputStream; +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; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; + +/** + * Unit tests for {@link DashDownloadAction}. + */ +@RunWith(RobolectricTestRunner.class) +public class DashDownloadActionTest { + + 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 testDownloadActionIsNotRemoveAction() { + DashDownloadAction action = newAction(uri1, /* isRemoveAction= */ false, /* data= */ null); + assertThat(action.isRemoveAction).isFalse(); + } + + @Test + public void testRemoveActionisRemoveAction() { + DashDownloadAction action2 = newAction(uri1, /* isRemoveAction= */ true, /* data= */ null); + assertThat(action2.isRemoveAction).isTrue(); + } + + @Test + public void testCreateDownloader() { + MockitoAnnotations.initMocks(this); + 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() { + 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() { + 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() { + DashDownloadAction action1 = newAction(uri1, /* isRemoveAction= */ true, /* data= */ null); + assertThat(action1.equals(action1)).isTrue(); + + DashDownloadAction action2 = newAction(uri1, /* isRemoveAction= */ true, /* data= */ null); + DashDownloadAction action3 = newAction(uri1, /* isRemoveAction= */ true, /* data= */ null); + assertEqual(action2, action3); + + DashDownloadAction action4 = newAction(uri1, /* isRemoveAction= */ true, /* data= */ null); + DashDownloadAction action5 = newAction(uri1, /* isRemoveAction= */ false, /* data= */ null); + assertNotEqual(action4, action5); + + DashDownloadAction action6 = newAction(uri1, /* isRemoveAction= */ false, /* data= */ null); + DashDownloadAction action7 = + newAction( + uri1, /* isRemoveAction= */ false, /* data= */ null, new RepresentationKey(0, 0, 0)); + assertNotEqual(action6, action7); + + DashDownloadAction action8 = + newAction( + uri1, /* isRemoveAction= */ false, /* data= */ null, new RepresentationKey(1, 1, 1)); + DashDownloadAction action9 = + newAction( + uri1, /* isRemoveAction= */ false, /* data= */ null, new RepresentationKey(0, 0, 0)); + assertNotEqual(action8, action9); + + DashDownloadAction action10 = newAction(uri1, /* isRemoveAction= */ true, /* data= */ null); + DashDownloadAction action11 = newAction(uri2, /* isRemoveAction= */ true, /* data= */ null); + assertNotEqual(action10, action11); + + 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 = + 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 = newAction(uri1, /* isRemoveAction= */ false, /* data= */ null); + DashDownloadAction action17 = newAction(uri1, /* isRemoveAction= */ false, /* data= */ null); + assertEqual(action16, action17); + } + + @Test + public void testSerializerGetType() { + DashDownloadAction action = newAction(uri1, /* isRemoveAction= */ false, /* data= */ null); + assertThat(action.type).isNotNull(); + } + + @Test + public void testSerializerWriteRead() throws Exception { + 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) { + assertThat(action1).isNotEqualTo(action2); + assertThat(action2).isNotEqualTo(action1); + } + + private static void assertEqual(DashDownloadAction action1, DashDownloadAction action2) { + assertThat(action1).isEqualTo(action2); + assertThat(action2).isEqualTo(action1); + } + + private static void doTestSerializationRoundTrip(DashDownloadAction action) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + DataOutputStream output = new DataOutputStream(out); + DownloadAction.serializeToStream(action, output); + + ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); + DataInputStream input = new DataInputStream(in); + DownloadAction action2 = + DownloadAction.deserializeFromStream( + new DownloadAction.Deserializer[] {DashDownloadAction.DESERIALIZER}, input); + + 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 d00b24e84f..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,25 +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(null); - dashDownloader.download(null); - assertCachedData(cache, fakeDataSet); - dashDownloader.remove(); - - dashDownloader.selectRepresentations(new RepresentationKey[0]); - dashDownloader.download(null); - assertCachedData(cache, fakeDataSet); - dashDownloader.remove(); } @Test @@ -223,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); @@ -257,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); @@ -289,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); } @@ -317,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) @@ -375,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); } @@ -391,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 new file mode 100644 index 0000000000..8ca2aa083b --- /dev/null +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadManagerDashTest.java @@ -0,0 +1,288 @@ +/* + * 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.dash.offline; + +import static com.google.android.exoplayer2.source.dash.offline.DashDownloadTestData.TEST_MPD; +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.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; +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; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; + +/** Tests {@link DownloadManager}. */ +@RunWith(RobolectricTestRunner.class) +@Config(shadows = {RobolectricUtil.CustomLooper.class, RobolectricUtil.CustomMessageQueue.class}) +public class DownloadManagerDashTest { + + private static final int ASSERT_TRUE_TIMEOUT = 1000; + + private SimpleCache cache; + private File tempFolder; + private FakeDataSet fakeDataSet; + private DownloadManager downloadManager; + private RepresentationKey fakeRepresentationKey1; + private RepresentationKey fakeRepresentationKey2; + private TestDownloadManagerListener downloadManagerListener; + private File actionFile; + private DummyMainThread dummyMainThread; + + @Before + public void setUp() throws Exception { + dummyMainThread = new DummyMainThread(); + Context context = RuntimeEnvironment.application; + tempFolder = Util.createTempDirectory(context, "ExoPlayerTest"); + File cacheFolder = new File(tempFolder, "cache"); + cacheFolder.mkdir(); + cache = new SimpleCache(cacheFolder, new NoOpCacheEvictor()); + MockitoAnnotations.initMocks(this); + 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) + .setRandomData("text_segment_1", 1) + .setRandomData("text_segment_2", 2) + .setRandomData("text_segment_3", 3); + + fakeRepresentationKey1 = new RepresentationKey(0, 0, 0); + fakeRepresentationKey2 = new RepresentationKey(0, 1, 0); + actionFile = new File(tempFolder, "actionFile"); + createDownloadManager(); + } + + @After + public void tearDown() throws Exception { + downloadManager.release(); + Util.recursiveDelete(tempFolder); + dummyMainThread.release(); + } + + // Disabled due to flakiness. + @Ignore + @Test + public void testSaveAndLoadActionFile() throws Throwable { + // Configure fakeDataSet to block until interrupted when TEST_MPD is read. + fakeDataSet + .newData(TEST_MPD_URI) + .appendReadAction( + new Runnable() { + @SuppressWarnings("InfiniteLoopStatement") + @Override + public void run() { + try { + // Wait until interrupted. + while (true) { + Thread.sleep(100000); + } + } catch (InterruptedException ignored) { + Thread.currentThread().interrupt(); + } + } + }) + .appendReadData(TEST_MPD) + .endData(); + + // Run DM accessing code on UI/main thread as it should be. Also not to block handling of loaded + // actions. + dummyMainThread.runOnMainThread( + new Runnable() { + @Override + public void run() { + // Setup an Action and immediately release the DM. + handleDownloadAction(fakeRepresentationKey1, fakeRepresentationKey2); + downloadManager.release(); + } + }); + + assertThat(actionFile.exists()).isTrue(); + assertThat(actionFile.length()).isGreaterThan(0L); + assertCacheEmpty(cache); + + // Revert fakeDataSet to normal. + fakeDataSet.setData(TEST_MPD_URI, TEST_MPD); + + dummyMainThread.runOnMainThread( + new Runnable() { + @Override + public void run() { + createDownloadManager(); + } + }); + + // Block on the test thread. + blockUntilTasksCompleteAndThrowAnyDownloadError(); + assertCachedData(cache, fakeDataSet); + } + + @Test + public void testHandleDownloadAction() throws Throwable { + handleDownloadAction(fakeRepresentationKey1, fakeRepresentationKey2); + blockUntilTasksCompleteAndThrowAnyDownloadError(); + assertCachedData(cache, fakeDataSet); + } + + @Test + public void testHandleMultipleDownloadAction() throws Throwable { + handleDownloadAction(fakeRepresentationKey1); + handleDownloadAction(fakeRepresentationKey2); + blockUntilTasksCompleteAndThrowAnyDownloadError(); + assertCachedData(cache, fakeDataSet); + } + + @Test + public void testHandleInterferingDownloadAction() throws Throwable { + fakeDataSet + .newData("audio_segment_2") + .appendReadAction( + new Runnable() { + @Override + public void run() { + handleDownloadAction(fakeRepresentationKey2); + } + }) + .appendReadData(TestUtil.buildTestData(5)) + .endData(); + + handleDownloadAction(fakeRepresentationKey1); + + blockUntilTasksCompleteAndThrowAnyDownloadError(); + assertCachedData(cache, fakeDataSet); + } + + @Test + public void testHandleRemoveAction() throws Throwable { + handleDownloadAction(fakeRepresentationKey1); + + blockUntilTasksCompleteAndThrowAnyDownloadError(); + + handleRemoveAction(); + + blockUntilTasksCompleteAndThrowAnyDownloadError(); + + assertCacheEmpty(cache); + } + + // Disabled due to flakiness. + @Ignore + @Test + public void testHandleRemoveActionBeforeDownloadFinish() throws Throwable { + handleDownloadAction(fakeRepresentationKey1); + handleRemoveAction(); + + blockUntilTasksCompleteAndThrowAnyDownloadError(); + + assertCacheEmpty(cache); + } + + @Test + public void testHandleInterferingRemoveAction() throws Throwable { + final ConditionVariable downloadInProgressCondition = new ConditionVariable(); + fakeDataSet + .newData("audio_segment_2") + .appendReadAction( + new Runnable() { + @Override + public void run() { + downloadInProgressCondition.open(); + } + }) + .appendReadData(TestUtil.buildTestData(5)) + .endData(); + + handleDownloadAction(fakeRepresentationKey1); + + assertThat(downloadInProgressCondition.block(ASSERT_TRUE_TIMEOUT)).isTrue(); + + handleRemoveAction(); + + blockUntilTasksCompleteAndThrowAnyDownloadError(); + + assertCacheEmpty(cache); + } + + private void blockUntilTasksCompleteAndThrowAnyDownloadError() throws Throwable { + downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); + } + + private void handleDownloadAction(RepresentationKey... keys) { + downloadManager.handleAction(newAction(TEST_MPD_URI, false, null, keys)); + } + + private void handleRemoveAction() { + downloadManager.handleAction(newAction(TEST_MPD_URI, true, null)); + } + + private void createDownloadManager() { + dummyMainThread.runOnMainThread( + new Runnable() { + @Override + public void run() { + Factory fakeDataSourceFactory = + new FakeDataSource.Factory(null).setFakeDataSet(fakeDataSet); + downloadManager = + new DownloadManager( + new DownloaderConstructorHelper(cache, fakeDataSourceFactory), + /* maxSimultaneousDownloads= */ 1, + /* minRetryCount= */ 3, + actionFile, + DashDownloadAction.DESERIALIZER); + + 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 new file mode 100644 index 0000000000..745acd9bbf --- /dev/null +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadServiceDashTest.java @@ -0,0 +1,244 @@ +/* + * 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.dash.offline; + +import static com.google.android.exoplayer2.source.dash.offline.DashDownloadTestData.TEST_MPD; +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 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; +import com.google.android.exoplayer2.scheduler.Scheduler; +import com.google.android.exoplayer2.source.dash.manifest.RepresentationKey; +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; +import com.google.android.exoplayer2.upstream.cache.SimpleCache; +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; + +/** Unit tests for {@link DownloadService}. */ +@RunWith(RobolectricTestRunner.class) +@Config(shadows = {RobolectricUtil.CustomLooper.class, RobolectricUtil.CustomMessageQueue.class}) +public class DownloadServiceDashTest { + + private SimpleCache cache; + private File tempFolder; + private FakeDataSet fakeDataSet; + private RepresentationKey fakeRepresentationKey1; + private RepresentationKey fakeRepresentationKey2; + private Context context; + private DownloadService dashDownloadService; + private ConditionVariable pauseDownloadCondition; + private TestDownloadManagerListener downloadManagerListener; + private DummyMainThread dummyMainThread; + + @Before + public void setUp() throws IOException { + dummyMainThread = new DummyMainThread(); + context = RuntimeEnvironment.application; + tempFolder = Util.createTempDirectory(context, "ExoPlayerTest"); + cache = new SimpleCache(tempFolder, new NoOpCacheEvictor()); + + Runnable pauseAction = + new Runnable() { + @Override + public void run() { + if (pauseDownloadCondition != null) { + try { + pauseDownloadCondition.block(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } + }; + fakeDataSet = + new FakeDataSet() + .setData(TEST_MPD_URI, TEST_MPD) + .newData("audio_init_data") + .appendReadAction(pauseAction) + .appendReadData(TestUtil.buildTestData(10)) + .endData() + .setRandomData("audio_segment_1", 4) + .setRandomData("audio_segment_2", 5) + .setRandomData("audio_segment_3", 6) + .setRandomData("text_segment_1", 1) + .setRandomData("text_segment_2", 2) + .setRandomData("text_segment_3", 3); + final DataSource.Factory fakeDataSourceFactory = + new FakeDataSource.Factory(null).setFakeDataSet(fakeDataSet); + fakeRepresentationKey1 = new RepresentationKey(0, 0, 0); + fakeRepresentationKey2 = new RepresentationKey(0, 1, 0); + + dummyMainThread.runOnMainThread( + new Runnable() { + @Override + public void run() { + File actionFile; + 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, + 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() { + 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); + + downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); + + assertCachedData(cache, fakeDataSet); + } + + @Ignore // b/78877092 + @Test + public void testRemoveAction() throws Throwable { + downloadKeys(fakeRepresentationKey1, fakeRepresentationKey2); + + downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); + + removeAll(); + + downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); + + assertCacheEmpty(cache); + } + + @Ignore // b/78877092 + @Test + public void testRemoveBeforeDownloadComplete() throws Throwable { + pauseDownloadCondition = new ConditionVariable(); + downloadKeys(fakeRepresentationKey1, fakeRepresentationKey2); + + removeAll(); + + downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); + + assertCacheEmpty(cache); + } + + private void removeAll() throws Throwable { + callDownloadServiceOnStart(newAction(TEST_MPD_URI, true, null)); + } + + private void downloadKeys(RepresentationKey... keys) { + callDownloadServiceOnStart(newAction(TEST_MPD_URI, false, null, keys)); + } + + private void callDownloadServiceOnStart(final DashDownloadAction action) { + dummyMainThread.runOnMainThread( + new Runnable() { + @Override + public void run() { + Intent startIntent = + 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 d8496a63d2..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 @@ -105,6 +105,7 @@ import java.util.List; // in TrackSelection to avoid unexpected behavior. private TrackSelection trackSelection; private long liveEdgeTimeUs; + private boolean seenExpectedPlaylistError; /** * @param extractorFactory An {@link HlsExtractorFactory} from which to obtain the extractors for @@ -150,7 +151,7 @@ import java.util.List; if (fatalError != null) { throw fatalError; } - if (expectedPlaylistUrl != null) { + if (expectedPlaylistUrl != null && seenExpectedPlaylistError) { playlistTracker.maybeThrowPlaylistRefreshError(expectedPlaylistUrl); } } @@ -217,8 +218,6 @@ import java.util.List; HlsChunkHolder out) { int oldVariantIndex = previous == null ? C.INDEX_UNSET : trackGroup.indexOf(previous.trackFormat); - expectedPlaylistUrl = null; - long bufferedDurationUs = loadPositionUs - playbackPositionUs; long timeToLiveEdgeUs = resolveTimeToLiveEdgeUs(playbackPositionUs); if (previous != null && !independentSegments) { @@ -243,6 +242,7 @@ import java.util.List; HlsUrl selectedUrl = variants[selectedVariantIndex]; if (!playlistTracker.isSnapshotValid(selectedUrl)) { out.playlist = selectedUrl; + seenExpectedPlaylistError &= expectedPlaylistUrl == selectedUrl; expectedPlaylistUrl = selectedUrl; // Retry when playlist is refreshed. return; @@ -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; @@ -291,10 +293,14 @@ import java.util.List; out.endOfStream = true; } else /* Live */ { out.playlist = selectedUrl; + seenExpectedPlaylistError &= expectedPlaylistUrl == selectedUrl; expectedPlaylistUrl = selectedUrl; } return; } + // We have a valid playlist snapshot, we can discard any playlist errors at this point. + seenExpectedPlaylistError = false; + expectedPlaylistUrl = null; // Handle encryption. HlsMediaPlaylist.Segment segment = mediaPlaylist.segments.get(chunkIndex); @@ -316,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, @@ -389,19 +395,25 @@ import java.util.List; } /** - * Called when a playlist is blacklisted. + * Called when a playlist load encounters an error. * - * @param url The url that references the blacklisted playlist. - * @param blacklistMs The amount of milliseconds for which the playlist was blacklisted. + * @param url The url of the playlist whose load encountered an error. + * @param shouldBlacklist Whether the playlist should be blacklisted. + * @return True if blacklisting did not encounter errors. False otherwise. */ - public void onPlaylistBlacklisted(HlsUrl url, long blacklistMs) { + public boolean onPlaylistError(HlsUrl url, boolean shouldBlacklist) { int trackGroupIndex = trackGroup.indexOf(url.format); - if (trackGroupIndex != C.INDEX_UNSET) { - int trackSelectionIndex = trackSelection.indexOf(trackGroupIndex); - if (trackSelectionIndex != C.INDEX_UNSET) { - trackSelection.blacklist(trackSelectionIndex, blacklistMs); - } + if (trackGroupIndex == C.INDEX_UNSET) { + return true; } + int trackSelectionIndex = trackSelection.indexOf(trackGroupIndex); + if (trackSelectionIndex == C.INDEX_UNSET) { + return true; + } + seenExpectedPlaylistError |= expectedPlaylistUrl == url; + return !shouldBlacklist + || trackSelection.blacklist( + trackSelectionIndex, ChunkedTrackBlacklistUtil.DEFAULT_TRACK_BLACKLIST_MS); } // Private methods. 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 11602c722f..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 @@ -15,7 +15,6 @@ */ package com.google.android.exoplayer2.source.hls; -import android.os.Handler; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.SeekParameters; @@ -55,7 +54,6 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper private final Allocator allocator; private final IdentityHashMap streamWrapperIndices; private final TimestampAdjusterProvider timestampAdjusterProvider; - private final Handler continueLoadingHandler; private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; private final boolean allowChunklessPreparation; @@ -65,6 +63,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper private HlsSampleStreamWrapper[] sampleStreamWrappers; private HlsSampleStreamWrapper[] enabledSampleStreamWrappers; private SequenceableLoader compositeSequenceableLoader; + private boolean notifiedReadingStarted; public HlsMediaPeriod( HlsExtractorFactory extractorFactory, @@ -83,19 +82,21 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper this.allocator = allocator; this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory; this.allowChunklessPreparation = allowChunklessPreparation; + compositeSequenceableLoader = + compositeSequenceableLoaderFactory.createCompositeSequenceableLoader(); streamWrapperIndices = new IdentityHashMap<>(); timestampAdjusterProvider = new TimestampAdjusterProvider(); - continueLoadingHandler = new Handler(); sampleStreamWrappers = new HlsSampleStreamWrapper[0]; enabledSampleStreamWrappers = new HlsSampleStreamWrapper[0]; + eventDispatcher.mediaPeriodCreated(); } public void release() { playlistTracker.removeListener(this); - continueLoadingHandler.removeCallbacksAndMessages(null); for (HlsSampleStreamWrapper sampleStreamWrapper : sampleStreamWrappers) { sampleStreamWrapper.release(); } + eventDispatcher.mediaPeriodReleased(); } @Override @@ -211,7 +212,15 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper @Override public boolean continueLoading(long positionUs) { - return compositeSequenceableLoader.continueLoading(positionUs); + if (trackGroups == null) { + // Preparation is still going on. + for (HlsSampleStreamWrapper wrapper : sampleStreamWrappers) { + wrapper.continuePreparing(); + } + return false; + } else { + return compositeSequenceableLoader.continueLoading(positionUs); + } } @Override @@ -221,6 +230,10 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper @Override public long readDiscontinuity() { + if (!notifiedReadingStarted) { + eventDispatcher.readingStarted(); + notifiedReadingStarted = true; + } return C.TIME_UNSET; } @@ -281,10 +294,6 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper @Override public void onContinueLoadingRequested(HlsSampleStreamWrapper sampleStreamWrapper) { - if (trackGroups == null) { - // Still preparing. - return; - } callback.onContinueLoadingRequested(this); } @@ -292,15 +301,17 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper @Override public void onPlaylistChanged() { - continuePreparingOrLoading(); + callback.onContinueLoadingRequested(this); } @Override - public void onPlaylistBlacklisted(HlsUrl url, long blacklistMs) { + public boolean onPlaylistError(HlsUrl url, boolean shouldBlacklist) { + boolean noBlacklistingFailure = true; for (HlsSampleStreamWrapper streamWrapper : sampleStreamWrappers) { - streamWrapper.onPlaylistBlacklisted(url, blacklistMs); + noBlacklistingFailure &= streamWrapper.onPlaylistError(url, shouldBlacklist); } - continuePreparingOrLoading(); + callback.onContinueLoadingRequested(this); + return noBlacklistingFailure; } // Internal methods. @@ -333,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(); } @@ -351,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. @@ -375,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. @@ -452,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(); @@ -468,17 +492,6 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper muxedAudioFormat, minLoadableRetryCount, eventDispatcher); } - private void continuePreparingOrLoading() { - if (trackGroups != null) { - callback.onContinueLoadingRequested(this); - } else { - // Some of the wrappers were waiting for their media playlist to prepare. - for (HlsSampleStreamWrapper wrapper : sampleStreamWrappers) { - wrapper.continuePreparing(); - } - } - } - private static Format deriveVideoFormat(Format variantFormat) { String codecs = Util.getCodecsOfType(variantFormat.codecs, C.TRACK_TYPE_VIDEO); String mimeType = MimeTypes.getMediaMimeType(codecs); diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java index 1fe0d72ea1..01bb36f6ce 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java @@ -22,6 +22,7 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.extractor.Extractor; +import com.google.android.exoplayer2.source.BaseMediaSource; import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory; import com.google.android.exoplayer2.source.DefaultCompositeSequenceableLoaderFactory; import com.google.android.exoplayer2.source.MediaPeriod; @@ -42,11 +43,9 @@ import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; import java.util.List; -/** - * An HLS {@link MediaSource}. - */ -public final class HlsMediaSource implements MediaSource, - HlsPlaylistTracker.PrimaryPlaylistListener { +/** An HLS {@link MediaSource}. */ +public final class HlsMediaSource extends BaseMediaSource + implements HlsPlaylistTracker.PrimaryPlaylistListener { static { ExoPlayerLibraryInfo.registerModule("goog.exo.hls"); @@ -63,6 +62,7 @@ public final class HlsMediaSource implements MediaSource, private int minLoadableRetryCount; private boolean allowChunklessPreparation; private boolean isCreateCalled; + private @Nullable Object tag; /** * Creates a new factory for {@link HlsMediaSource}s. @@ -88,6 +88,21 @@ public final class HlsMediaSource implements MediaSource, compositeSequenceableLoaderFactory = new DefaultCompositeSequenceableLoaderFactory(); } + /** + * Sets a tag for the media source which will be published in the {@link + * com.google.android.exoplayer2.Timeline} of the source as {@link + * com.google.android.exoplayer2.Timeline.Window#tag}. + * + * @param tag A tag for the media source. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. + */ + public Factory setTag(Object tag) { + Assertions.checkState(!isCreateCalled); + this.tag = tag; + return this; + } + /** * Sets the factory for {@link Extractor}s for the segments. The default value is {@link * HlsExtractorFactory#DEFAULT}. @@ -164,29 +179,13 @@ public final class HlsMediaSource implements MediaSource, return this; } - /** - * Returns a new {@link HlsMediaSource} using the current parameters. Media source events will - * not be delivered. - * - * @return The new {@link HlsMediaSource}. - */ - public HlsMediaSource createMediaSource(Uri playlistUri) { - return createMediaSource(playlistUri, null, null); - } - /** * Returns a new {@link HlsMediaSource} using the current parameters. * - * @param playlistUri The playlist {@link Uri}. - * @param eventHandler A handler for events. - * @param eventListener A listener of events. * @return The new {@link HlsMediaSource}. */ @Override - public HlsMediaSource createMediaSource( - Uri playlistUri, - @Nullable Handler eventHandler, - @Nullable MediaSourceEventListener eventListener) { + public HlsMediaSource createMediaSource(Uri playlistUri) { isCreateCalled = true; if (playlistParser == null) { playlistParser = new HlsPlaylistParser(); @@ -197,10 +196,25 @@ public final class HlsMediaSource implements MediaSource, extractorFactory, compositeSequenceableLoaderFactory, minLoadableRetryCount, - eventHandler, - eventListener, playlistParser, - allowChunklessPreparation); + allowChunklessPreparation, + tag); + } + + /** + * @deprecated Use {@link #createMediaSource(Uri)} and {@link #addEventListener(Handler, + * MediaSourceEventListener)} instead. + */ + @Deprecated + public HlsMediaSource createMediaSource( + Uri playlistUri, + @Nullable Handler eventHandler, + @Nullable MediaSourceEventListener eventListener) { + HlsMediaSource mediaSource = createMediaSource(playlistUri); + if (eventHandler != null && eventListener != null) { + mediaSource.addEventListener(eventHandler, eventListener); + } + return mediaSource; } @Override @@ -219,12 +233,11 @@ public final class HlsMediaSource implements MediaSource, private final HlsDataSourceFactory dataSourceFactory; private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; private final int minLoadableRetryCount; - private final EventDispatcher eventDispatcher; private final ParsingLoadable.Parser playlistParser; private final boolean allowChunklessPreparation; + private final @Nullable Object tag; private HlsPlaylistTracker playlistTracker; - private Listener sourceListener; /** * @param manifestUri The {@link Uri} of the HLS manifest. @@ -296,10 +309,12 @@ public final class HlsMediaSource implements MediaSource, extractorFactory, new DefaultCompositeSequenceableLoaderFactory(), minLoadableRetryCount, - eventHandler, - eventListener, playlistParser, - false); + /* allowChunklessPreparation= */ false, + /* tag= */ null); + if (eventHandler != null && eventListener != null) { + addEventListener(eventHandler, eventListener); + } } private HlsMediaSource( @@ -308,10 +323,9 @@ public final class HlsMediaSource implements MediaSource, HlsExtractorFactory extractorFactory, CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory, int minLoadableRetryCount, - Handler eventHandler, - MediaSourceEventListener eventListener, ParsingLoadable.Parser playlistParser, - boolean allowChunklessPreparation) { + boolean allowChunklessPreparation, + @Nullable Object tag) { this.manifestUri = manifestUri; this.dataSourceFactory = dataSourceFactory; this.extractorFactory = extractorFactory; @@ -319,12 +333,12 @@ public final class HlsMediaSource implements MediaSource, this.minLoadableRetryCount = minLoadableRetryCount; this.playlistParser = playlistParser; this.allowChunklessPreparation = allowChunklessPreparation; - eventDispatcher = new EventDispatcher(eventHandler, eventListener); + this.tag = tag; } @Override - public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { - sourceListener = listener; + public void prepareSourceInternal(ExoPlayer player, boolean isTopLevelSource) { + EventDispatcher eventDispatcher = createEventDispatcher(/* mediaPeriodId= */ null); playlistTracker = new HlsPlaylistTracker(manifestUri, dataSourceFactory, eventDispatcher, minLoadableRetryCount, this, playlistParser); playlistTracker.start(); @@ -338,6 +352,7 @@ public final class HlsMediaSource implements MediaSource, @Override public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { Assertions.checkArgument(id.periodIndex == 0); + EventDispatcher eventDispatcher = createEventDispatcher(id); return new HlsMediaPeriod( extractorFactory, playlistTracker, @@ -355,12 +370,11 @@ public final class HlsMediaSource implements MediaSource, } @Override - public void releaseSource() { + public void releaseSourceInternal() { if (playlistTracker != null) { playlistTracker.release(); playlistTracker = null; } - sourceListener = null; } @Override @@ -395,7 +409,8 @@ public final class HlsMediaSource implements MediaSource, /* windowPositionInPeriodUs= */ offsetFromInitialStartTimeUs, windowDefaultStartPositionUs, /* isSeekable= */ true, - /* isDynamic= */ !playlist.hasEndTag); + /* isDynamic= */ !playlist.hasEndTag, + tag); } else /* not live */ { if (windowDefaultStartPositionUs == C.TIME_UNSET) { windowDefaultStartPositionUs = 0; @@ -409,10 +424,10 @@ public final class HlsMediaSource implements MediaSource, /* windowPositionInPeriodUs= */ 0, windowDefaultStartPositionUs, /* isSeekable= */ true, - /* isDynamic= */ false); + /* isDynamic= */ false, + tag); } - sourceListener.onSourceInfoRefreshed(this, timeline, - new HlsManifest(playlistTracker.getMasterPlaylist(), playlist)); + refreshSourceInfo(timeline, new HlsManifest(playlistTracker.getMasterPlaylist(), playlist)); } } 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 f027ba5b05..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 @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.source.hls; import android.os.Handler; +import android.support.annotation.IntDef; import android.util.Log; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; @@ -43,6 +44,8 @@ import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Arrays; @@ -73,6 +76,14 @@ 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 {} + private static final int PRIMARY_TYPE_NONE = 0; private static final int PRIMARY_TYPE_TEXT = 1; private static final int PRIMARY_TYPE_AUDIO = 2; @@ -107,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; @@ -182,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(); } @@ -201,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; @@ -407,8 +422,8 @@ import java.util.Arrays; chunkSource.setIsTimestampMaster(isTimestampMaster); } - public void onPlaylistBlacklisted(HlsUrl url, long blacklistMs) { - chunkSource.onPlaylistBlacklisted(url, blacklistMs); + public boolean onPlaylistError(HlsUrl url, boolean shouldBlacklist) { + return chunkSource.onPlaylistError(url, shouldBlacklist); } // SampleStream implementation. @@ -583,8 +598,8 @@ import java.util.Arrays; } @Override - public int onLoadError(Chunk loadable, long elapsedRealtimeMs, long loadDurationMs, - IOException error) { + public @Loader.RetryAction int onLoadError( + Chunk loadable, long elapsedRealtimeMs, long loadDurationMs, IOException error) { long bytesLoaded = loadable.bytesLoaded(); boolean isMediaChunk = isMediaChunk(loadable); boolean cancelable = !isMediaChunk || bytesLoaded == 0; @@ -773,7 +788,7 @@ import java.util.Arrays; mapSampleQueuesToMatchTrackGroups(); } else { // Tracks are created using media segment information. - buildTracks(); + buildTracksFromSampleStreams(); prepared = true; callback.onPrepared(); } @@ -797,41 +812,42 @@ 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. - int primaryExtractorTrackType = PRIMARY_TYPE_NONE; + @PrimaryTrackType int primaryExtractorTrackType = PRIMARY_TYPE_NONE; int primaryExtractorTrackIndex = C.INDEX_UNSET; int extractorTrackCount = sampleQueues.length; for (int i = 0; i < extractorTrackCount; i++) { String sampleMimeType = sampleQueues[i].getUpstreamFormat().sampleMimeType; - int trackType; + @PrimaryTrackType int trackType; if (MimeTypes.isVideo(sampleMimeType)) { trackType = PRIMARY_TYPE_VIDEO; } else if (MimeTypes.isAudio(sampleMimeType)) { @@ -880,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 new file mode 100644 index 0000000000..e56bf66efd --- /dev/null +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadAction.java @@ -0,0 +1,75 @@ +/* + * 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.hls.offline; + +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.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 { + + private static final String TYPE = "hls"; + private static final int VERSION = 0; + + public static final Deserializer DESERIALIZER = + new SegmentDownloadActionDeserializer(TYPE, VERSION) { + + @Override + 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 uri, boolean isRemoveAction, byte[] data, List keys) { + return new HlsDownloadAction(uri, isRemoveAction, data, keys); + } + }; + + /** + * @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 uri, boolean isRemoveAction, @Nullable byte[] data, List keys) { + super(TYPE, VERSION, uri, isRemoveAction, data, keys); + } + + @Override + protected HlsDownloader createDownloader(DownloaderConstructorHelper constructorHelper) { + return new HlsDownloader(uri, keys, constructorHelper); + } + + @Override + 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 24fa0df7d1..0000000000 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/FilteringHlsPlaylistParser.java +++ /dev/null @@ -1,45 +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 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. */ - public FilteringHlsPlaylistParser(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) { - return ((HlsMasterPlaylist) hlsPlaylist).copy(filter); - } 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 loadable, long elapsedRealtimeMs, - long loadDurationMs, IOException error) { + public @Loader.RetryAction int onLoadError( + ParsingLoadable loadable, + long elapsedRealtimeMs, + long loadDurationMs, + IOException error) { boolean isFatal = error instanceof ParserException; eventDispatcher.loadError(loadable.dataSpec, C.DATA_TYPE_MANIFEST, elapsedRealtimeMs, loadDurationMs, loadable.bytesLoaded(), error, isFatal); @@ -388,11 +390,13 @@ public final class HlsPlaylistTracker implements Loader.Callback loadable, long elapsedRealtimeMs, - long loadDurationMs, IOException error) { + public @Loader.RetryAction int onLoadError( + ParsingLoadable loadable, + long elapsedRealtimeMs, + long loadDurationMs, + IOException error) { boolean isFatal = error instanceof ParserException; eventDispatcher.loadError(loadable.dataSpec, C.DATA_TYPE_MANIFEST, elapsedRealtimeMs, loadDurationMs, loadable.bytesLoaded(), error, isFatal); + boolean shouldBlacklist = ChunkedTrackBlacklistUtil.shouldBlacklist(error); + boolean shouldRetryIfNotFatal = notifyPlaylistError(playlistUrl, shouldBlacklist); if (isFatal) { return Loader.DONT_RETRY_FATAL; } - boolean shouldRetry = true; - if (ChunkedTrackBlacklistUtil.shouldBlacklist(error)) { - shouldRetry = blacklistPlaylist(); + if (shouldBlacklist) { + shouldRetryIfNotFatal |= blacklistPlaylist(); } - return shouldRetry ? Loader.RETRY : Loader.DONT_RETRY; + return shouldRetryIfNotFatal ? Loader.RETRY : Loader.DONT_RETRY; } // Runnable implementation. @@ -597,11 +605,13 @@ public final class HlsPlaylistTracker implements Loader.Callback C.usToMs(playlistSnapshot.targetDurationUs) * PLAYLIST_STUCK_TARGET_DURATION_COEFFICIENT) { // The playlist seems to be stuck. Blacklist it. playlistError = new PlaylistStuckException(playlistUrl.url); + notifyPlaylistError(playlistUrl, true); blacklistPlaylist(); } } @@ -625,7 +635,6 @@ public final class HlsPlaylistTracker implements Loader.Callback { + + /** 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/AndroidManifest.xml b/library/hls/src/test/AndroidManifest.xml index 89fab4d9a2..326ff48b16 100644 --- a/library/hls/src/test/AndroidManifest.xml +++ b/library/hls/src/test/AndroidManifest.xml @@ -14,9 +14,4 @@ limitations under the License. --> - - - - - + 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 1e6b98092b..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,41 +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(null); - hlsDownloader.download(null); assertCachedData(cache, fakeDataSet); - hlsDownloader.remove(); - - 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, @@ -221,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/DefaultSsChunkSource.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java index d0e5ed29af..de236c3514 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java @@ -203,6 +203,7 @@ public class DefaultSsChunkSource implements SsChunkSource { long chunkStartTimeUs = streamElement.getStartTimeUs(chunkIndex); long chunkEndTimeUs = chunkStartTimeUs + streamElement.getChunkDurationUs(chunkIndex); + long chunkSeekTimeUs = previous == null ? loadPositionUs : C.TIME_UNSET; int currentAbsoluteChunkIndex = chunkIndex + currentManifestChunkOffset; int trackSelectionIndex = trackSelection.getSelectedIndex(); @@ -211,9 +212,19 @@ public class DefaultSsChunkSource implements SsChunkSource { int manifestTrackIndex = trackSelection.getIndexInTrackGroup(trackSelectionIndex); Uri uri = streamElement.buildRequestUri(manifestTrackIndex, chunkIndex); - out.chunk = newMediaChunk(trackSelection.getSelectedFormat(), dataSource, uri, null, - currentAbsoluteChunkIndex, chunkStartTimeUs, chunkEndTimeUs, - trackSelection.getSelectionReason(), trackSelection.getSelectionData(), extractorWrapper); + out.chunk = + newMediaChunk( + trackSelection.getSelectedFormat(), + dataSource, + uri, + null, + currentAbsoluteChunkIndex, + chunkStartTimeUs, + chunkEndTimeUs, + chunkSeekTimeUs, + trackSelection.getSelectionReason(), + trackSelection.getSelectionData(), + extractorWrapper); } @Override @@ -229,15 +240,34 @@ public class DefaultSsChunkSource implements SsChunkSource { // Private methods. - private static MediaChunk newMediaChunk(Format format, DataSource dataSource, Uri uri, - String cacheKey, int chunkIndex, long chunkStartTimeUs, long chunkEndTimeUs, - int trackSelectionReason, Object trackSelectionData, ChunkExtractorWrapper extractorWrapper) { + private static MediaChunk newMediaChunk( + Format format, + DataSource dataSource, + Uri uri, + String cacheKey, + int chunkIndex, + long chunkStartTimeUs, + long chunkEndTimeUs, + long chunkSeekTimeUs, + int trackSelectionReason, + Object trackSelectionData, + ChunkExtractorWrapper extractorWrapper) { DataSpec dataSpec = new DataSpec(uri, 0, C.LENGTH_UNSET, cacheKey); // In SmoothStreaming each chunk contains sample timestamps relative to the start of the chunk. // To convert them the absolute timestamps, we need to set sampleOffsetUs to chunkStartTimeUs. long sampleOffsetUs = chunkStartTimeUs; - return new ContainerMediaChunk(dataSource, dataSpec, format, trackSelectionReason, - trackSelectionData, chunkStartTimeUs, chunkEndTimeUs, chunkIndex, 1, sampleOffsetUs, + return new ContainerMediaChunk( + dataSource, + dataSpec, + format, + trackSelectionReason, + trackSelectionData, + chunkStartTimeUs, + chunkEndTimeUs, + chunkSeekTimeUs, + chunkIndex, + /* chunkCount= */ 1, + sampleOffsetUs, extractorWrapper); } diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java index a600741362..9a0d57ff31 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java @@ -56,6 +56,7 @@ import java.util.ArrayList; private SsManifest manifest; private ChunkSampleStream[] sampleStreams; private SequenceableLoader compositeSequenceableLoader; + private boolean notifiedReadingStarted; public SsMediaPeriod(SsManifest manifest, SsChunkSource.Factory chunkSourceFactory, CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory, @@ -82,6 +83,7 @@ import java.util.ArrayList; sampleStreams = newSampleStreamArray(0); compositeSequenceableLoader = compositeSequenceableLoaderFactory.createCompositeSequenceableLoader(sampleStreams); + eventDispatcher.mediaPeriodCreated(); } public void updateManifest(SsManifest manifest) { @@ -96,6 +98,7 @@ import java.util.ArrayList; for (ChunkSampleStream sampleStream : sampleStreams) { sampleStream.release(); } + eventDispatcher.mediaPeriodReleased(); } @Override @@ -167,6 +170,10 @@ import java.util.ArrayList; @Override public long readDiscontinuity() { + if (!notifiedReadingStarted) { + eventDispatcher.readingStarted(); + notifiedReadingStarted = true; + } return C.TIME_UNSET; } diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java index da9024a5b5..72d1ba1efd 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java @@ -24,6 +24,7 @@ import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.source.BaseMediaSource; import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory; import com.google.android.exoplayer2.source.DefaultCompositeSequenceableLoaderFactory; import com.google.android.exoplayer2.source.MediaPeriod; @@ -46,11 +47,9 @@ import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; import java.util.ArrayList; -/** - * A SmoothStreaming {@link MediaSource}. - */ -public final class SsMediaSource implements MediaSource, - Loader.Callback> { +/** A SmoothStreaming {@link MediaSource}. */ +public final class SsMediaSource extends BaseMediaSource + implements Loader.Callback> { static { ExoPlayerLibraryInfo.registerModule("goog.exo.smoothstreaming"); @@ -67,6 +66,7 @@ public final class SsMediaSource implements MediaSource, private int minLoadableRetryCount; private long livePresentationDelayMs; private boolean isCreateCalled; + private @Nullable Object tag; /** * Creates a new factory for {@link SsMediaSource}s. @@ -87,6 +87,20 @@ public final class SsMediaSource implements MediaSource, compositeSequenceableLoaderFactory = new DefaultCompositeSequenceableLoaderFactory(); } + /** + * Sets a tag for the media source which will be published in the {@link Timeline} of the source + * as {@link Timeline.Window#tag}. + * + * @param tag A tag for the media source. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. + */ + public Factory setTag(Object tag) { + Assertions.checkState(!isCreateCalled); + this.tag = tag; + return this; + } + /** * Sets the minimum number of times to retry if a loading error occurs. The default value is * {@link #DEFAULT_MIN_LOADABLE_RETRY_COUNT}. @@ -154,60 +168,54 @@ public final class SsMediaSource implements MediaSource, * manifest. * * @param manifest The manifest. {@link SsManifest#isLive} must be false. - * @param eventHandler A handler for events. - * @param eventListener A listener of events. * @return The new {@link SsMediaSource}. * @throws IllegalArgumentException If {@link SsManifest#isLive} is true. */ - public SsMediaSource createMediaSource( - SsManifest manifest, - @Nullable Handler eventHandler, - @Nullable MediaSourceEventListener eventListener) { + public SsMediaSource createMediaSource(SsManifest manifest) { Assertions.checkArgument(!manifest.isLive); isCreateCalled = true; return new SsMediaSource( manifest, - null, - null, - null, + /* manifestUri= */ null, + /* manifestDataSourceFactory= */ null, + /* manifestParser= */ null, chunkSourceFactory, compositeSequenceableLoaderFactory, minLoadableRetryCount, livePresentationDelayMs, - eventHandler, - eventListener); + tag); } /** - * Returns a new {@link SsMediaSource} using the current parameters. Media source events will - * not be delivered. - * - * @param manifestUri The manifest {@link Uri}. - * @return The new {@link SsMediaSource}. + * @deprecated Use {@link #createMediaSource(SsManifest)} and {@link #addEventListener(Handler, + * MediaSourceEventListener)} instead. */ - public SsMediaSource createMediaSource(Uri manifestUri) { - return createMediaSource(manifestUri, null, null); + @Deprecated + public SsMediaSource createMediaSource( + SsManifest manifest, + @Nullable Handler eventHandler, + @Nullable MediaSourceEventListener eventListener) { + SsMediaSource mediaSource = createMediaSource(manifest); + if (eventHandler != null && eventListener != null) { + mediaSource.addEventListener(eventHandler, eventListener); + } + return mediaSource; } /** * Returns a new {@link SsMediaSource} using the current parameters. * * @param manifestUri The manifest {@link Uri}. - * @param eventHandler A handler for events. - * @param eventListener A listener of events. * @return The new {@link SsMediaSource}. */ @Override - public SsMediaSource createMediaSource( - Uri manifestUri, - @Nullable Handler eventHandler, - @Nullable MediaSourceEventListener eventListener) { + public SsMediaSource createMediaSource(Uri manifestUri) { isCreateCalled = true; if (manifestParser == null) { manifestParser = new SsManifestParser(); } return new SsMediaSource( - null, + /* manifest= */ null, Assertions.checkNotNull(manifestUri), manifestDataSourceFactory, manifestParser, @@ -215,8 +223,23 @@ public final class SsMediaSource implements MediaSource, compositeSequenceableLoaderFactory, minLoadableRetryCount, livePresentationDelayMs, - eventHandler, - eventListener); + tag); + } + + /** + * @deprecated Use {@link #createMediaSource(Uri)} and {@link #addEventListener(Handler, + * MediaSourceEventListener)} instead. + */ + @Deprecated + public SsMediaSource createMediaSource( + Uri manifestUri, + @Nullable Handler eventHandler, + @Nullable MediaSourceEventListener eventListener) { + SsMediaSource mediaSource = createMediaSource(manifestUri); + if (eventHandler != null && eventListener != null) { + mediaSource.addEventListener(eventHandler, eventListener); + } + return mediaSource; } @Override @@ -252,11 +275,11 @@ public final class SsMediaSource implements MediaSource, private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; private final int minLoadableRetryCount; private final long livePresentationDelayMs; - private final EventDispatcher eventDispatcher; + private final EventDispatcher manifestEventDispatcher; private final ParsingLoadable.Parser manifestParser; private final ArrayList mediaPeriods; + private final @Nullable Object tag; - private Listener sourceListener; private DataSource manifestDataSource; private Loader manifestLoader; private LoaderErrorThrower manifestLoaderErrorThrower; @@ -302,9 +325,19 @@ public final class SsMediaSource implements MediaSource, int minLoadableRetryCount, Handler eventHandler, MediaSourceEventListener eventListener) { - this(manifest, null, null, null, chunkSourceFactory, - new DefaultCompositeSequenceableLoaderFactory(), minLoadableRetryCount, - DEFAULT_LIVE_PRESENTATION_DELAY_MS, eventHandler, eventListener); + this( + manifest, + /* manifestUri= */ null, + /* manifestDataSourceFactory= */ null, + /* manifestParser= */ null, + chunkSourceFactory, + new DefaultCompositeSequenceableLoaderFactory(), + minLoadableRetryCount, + DEFAULT_LIVE_PRESENTATION_DELAY_MS, + /* tag= */ null); + if (eventHandler != null && eventListener != null) { + addEventListener(eventHandler, eventListener); + } } /** @@ -385,9 +418,19 @@ public final class SsMediaSource implements MediaSource, long livePresentationDelayMs, Handler eventHandler, MediaSourceEventListener eventListener) { - this(null, manifestUri, manifestDataSourceFactory, manifestParser, chunkSourceFactory, - new DefaultCompositeSequenceableLoaderFactory(), minLoadableRetryCount, - livePresentationDelayMs, eventHandler, eventListener); + this( + /* manifest= */ null, + manifestUri, + manifestDataSourceFactory, + manifestParser, + chunkSourceFactory, + new DefaultCompositeSequenceableLoaderFactory(), + minLoadableRetryCount, + livePresentationDelayMs, + /* tag= */ null); + if (eventHandler != null && eventListener != null) { + addEventListener(eventHandler, eventListener); + } } private SsMediaSource( @@ -399,8 +442,7 @@ public final class SsMediaSource implements MediaSource, CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory, int minLoadableRetryCount, long livePresentationDelayMs, - Handler eventHandler, - MediaSourceEventListener eventListener) { + @Nullable Object tag) { Assertions.checkState(manifest == null || !manifest.isLive); this.manifest = manifest; this.manifestUri = manifestUri == null ? null : SsUtil.fixManifestUri(manifestUri); @@ -410,7 +452,8 @@ public final class SsMediaSource implements MediaSource, this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory; this.minLoadableRetryCount = minLoadableRetryCount; this.livePresentationDelayMs = livePresentationDelayMs; - this.eventDispatcher = new EventDispatcher(eventHandler, eventListener); + this.manifestEventDispatcher = createEventDispatcher(/* mediaPeriodId= */ null); + this.tag = tag; sideloadedManifest = manifest != null; mediaPeriods = new ArrayList<>(); } @@ -418,8 +461,7 @@ public final class SsMediaSource implements MediaSource, // MediaSource implementation. @Override - public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { - sourceListener = listener; + public void prepareSourceInternal(ExoPlayer player, boolean isTopLevelSource) { if (sideloadedManifest) { manifestLoaderErrorThrower = new LoaderErrorThrower.Dummy(); processManifest(); @@ -440,6 +482,7 @@ public final class SsMediaSource implements MediaSource, @Override public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { Assertions.checkArgument(id.periodIndex == 0); + EventDispatcher eventDispatcher = createEventDispatcher(id); SsMediaPeriod period = new SsMediaPeriod(manifest, chunkSourceFactory, compositeSequenceableLoaderFactory, minLoadableRetryCount, eventDispatcher, manifestLoaderErrorThrower, allocator); @@ -454,8 +497,7 @@ public final class SsMediaSource implements MediaSource, } @Override - public void releaseSource() { - sourceListener = null; + public void releaseSourceInternal() { manifest = sideloadedManifest ? manifest : null; manifestDataSource = null; manifestLoadStartTimestamp = 0; @@ -474,8 +516,12 @@ public final class SsMediaSource implements MediaSource, @Override public void onLoadCompleted(ParsingLoadable loadable, long elapsedRealtimeMs, long loadDurationMs) { - eventDispatcher.loadCompleted(loadable.dataSpec, loadable.type, elapsedRealtimeMs, - loadDurationMs, loadable.bytesLoaded()); + manifestEventDispatcher.loadCompleted( + loadable.dataSpec, + loadable.type, + elapsedRealtimeMs, + loadDurationMs, + loadable.bytesLoaded()); manifest = loadable.getResult(); manifestLoadStartTimestamp = elapsedRealtimeMs - loadDurationMs; processManifest(); @@ -485,7 +531,7 @@ public final class SsMediaSource implements MediaSource, @Override public void onLoadCanceled(ParsingLoadable loadable, long elapsedRealtimeMs, long loadDurationMs, boolean released) { - eventDispatcher.loadCanceled( + manifestEventDispatcher.loadCanceled( loadable.dataSpec, loadable.type, elapsedRealtimeMs, @@ -494,11 +540,20 @@ public final class SsMediaSource implements MediaSource, } @Override - public int onLoadError(ParsingLoadable loadable, long elapsedRealtimeMs, - long loadDurationMs, IOException error) { + public @Loader.RetryAction int onLoadError( + ParsingLoadable loadable, + long elapsedRealtimeMs, + long loadDurationMs, + IOException error) { boolean isFatal = error instanceof ParserException; - eventDispatcher.loadError(loadable.dataSpec, loadable.type, elapsedRealtimeMs, loadDurationMs, - loadable.bytesLoaded(), error, isFatal); + manifestEventDispatcher.loadError( + loadable.dataSpec, + loadable.type, + elapsedRealtimeMs, + loadDurationMs, + loadable.bytesLoaded(), + error, + isFatal); return isFatal ? Loader.DONT_RETRY_FATAL : Loader.RETRY; } @@ -522,8 +577,15 @@ public final class SsMediaSource implements MediaSource, Timeline timeline; if (startTimeUs == Long.MAX_VALUE) { long periodDurationUs = manifest.isLive ? C.TIME_UNSET : 0; - timeline = new SinglePeriodTimeline(periodDurationUs, 0, 0, 0, true /* isSeekable */, - manifest.isLive /* isDynamic */); + timeline = + new SinglePeriodTimeline( + periodDurationUs, + /* windowDurationUs= */ 0, + /* windowPositionInPeriodUs= */ 0, + /* windowDefaultStartPositionUs= */ 0, + /* isSeekable= */ true, + manifest.isLive, + tag); } else if (manifest.isLive) { if (manifest.dvrWindowLengthUs != C.TIME_UNSET && manifest.dvrWindowLengthUs > 0) { startTimeUs = Math.max(startTimeUs, endTimeUs - manifest.dvrWindowLengthUs); @@ -536,15 +598,29 @@ public final class SsMediaSource implements MediaSource, // it to the middle of the window. defaultStartPositionUs = Math.min(MIN_LIVE_DEFAULT_START_POSITION_US, durationUs / 2); } - timeline = new SinglePeriodTimeline(C.TIME_UNSET, durationUs, startTimeUs, - defaultStartPositionUs, true /* isSeekable */, true /* isDynamic */); + timeline = + new SinglePeriodTimeline( + /* periodDurationUs= */ C.TIME_UNSET, + durationUs, + startTimeUs, + defaultStartPositionUs, + /* isSeekable= */ true, + /* isDynamic= */ true, + tag); } else { long durationUs = manifest.durationUs != C.TIME_UNSET ? manifest.durationUs : endTimeUs - startTimeUs; - timeline = new SinglePeriodTimeline(startTimeUs + durationUs, durationUs, startTimeUs, 0, - true /* isSeekable */, false /* isDynamic */); + timeline = + new SinglePeriodTimeline( + startTimeUs + durationUs, + durationUs, + startTimeUs, + /* windowDefaultStartPositionUs= */ 0, + /* isSeekable= */ true, + /* isDynamic= */ false, + tag); } - sourceListener.onSourceInfoRefreshed(this, timeline, manifest); + refreshSourceInfo(timeline, manifest); } private void scheduleManifestRefresh() { @@ -565,7 +641,7 @@ public final class SsMediaSource implements MediaSource, ParsingLoadable loadable = new ParsingLoadable<>(manifestDataSource, manifestUri, C.DATA_TYPE_MANIFEST, manifestParser); long elapsedRealtimeMs = manifestLoader.startLoading(loadable, this, minLoadableRetryCount); - eventDispatcher.loadStarted(loadable.dataSpec, loadable.type, elapsedRealtimeMs); + manifestEventDispatcher.loadStarted(loadable.dataSpec, loadable.type, elapsedRealtimeMs); } } 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 eed040df97..0000000000 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/FilteringSsManifestParser.java +++ /dev/null @@ -1,42 +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 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. */ - public FilteringSsManifestParser(List filter) { - this.ssManifestParser = new SsManifestParser(); - this.filter = filter; - } - - @Override - public SsManifest parse(Uri uri, InputStream inputStream) throws IOException { - return ssManifestParser.parse(uri, inputStream).copy(filter); - } -} 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 new file mode 100644 index 0000000000..d4b3ef6622 --- /dev/null +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadAction.java @@ -0,0 +1,73 @@ +/* + * 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.offline; + +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.offline.SegmentDownloadAction; +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 { + + private static final String TYPE = "ss"; + private static final int VERSION = 0; + + public static final Deserializer DESERIALIZER = + new SegmentDownloadActionDeserializer(TYPE, VERSION) { + + @Override + protected StreamKey readKey(DataInputStream input) throws IOException { + return new StreamKey(input.readInt(), input.readInt()); + } + + @Override + protected DownloadAction createDownloadAction( + Uri uri, boolean isRemoveAction, byte[] data, List keys) { + return new SsDownloadAction(uri, isRemoveAction, data, keys); + } + }; + + /** + * @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 uri, boolean isRemoveAction, @Nullable byte[] data, List keys) { + super(TYPE, VERSION, uri, isRemoveAction, data, keys); + } + + @Override + protected SsDownloader createDownloader(DownloaderConstructorHelper constructorHelper) { + return new SsDownloader(uri, keys, constructorHelper); + } + + @Override + 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/AndroidManifest.xml b/library/smoothstreaming/src/test/AndroidManifest.xml index 61eb3caddf..712169a7d0 100644 --- a/library/smoothstreaming/src/test/AndroidManifest.xml +++ b/library/smoothstreaming/src/test/AndroidManifest.xml @@ -14,9 +14,4 @@ limitations under the License. --> - - - - - + 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/build.gradle b/library/ui/build.gradle index 9689fcef97..017d7e3e14 100644 --- a/library/ui/build.gradle +++ b/library/ui/build.gradle @@ -34,6 +34,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') + implementation 'com.android.support:support-media-compat:' + supportLibraryVersion implementation 'com.android.support:support-annotations:' + supportLibraryVersion } diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java index d039581cf0..227eb52e79 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java @@ -28,6 +28,22 @@ import java.lang.annotation.RetentionPolicy; */ public final class AspectRatioFrameLayout extends FrameLayout { + /** Listener to be notified about changes of the aspect ratios of this view. */ + public interface AspectRatioListener { + + /** + * Called when either the target aspect ratio or the view aspect ratio is updated. + * + * @param targetAspectRatio The aspect ratio that has been set in {@link #setAspectRatio(float)} + * @param naturalAspectRatio The natural aspect ratio of this view (before its width and height + * are modified to satisfy the target aspect ratio). + * @param aspectRatioMismatch Whether the target and natural aspect ratios differ enough for + * changing the resize mode to have an effect. + */ + void onAspectRatioUpdated( + float targetAspectRatio, float naturalAspectRatio, boolean aspectRatioMismatch); + } + // LINT.IfChange /** Resize modes for {@link AspectRatioFrameLayout}. */ @Retention(RetentionPolicy.SOURCE) @@ -73,8 +89,12 @@ public final class AspectRatioFrameLayout extends FrameLayout { */ private static final float MAX_ASPECT_RATIO_DEFORMATION_FRACTION = 0.01f; + private final AspectRatioUpdateDispatcher aspectRatioUpdateDispatcher; + + private AspectRatioListener aspectRatioListener; + private float videoAspectRatio; - private int resizeMode; + private @ResizeMode int resizeMode; public AspectRatioFrameLayout(Context context) { this(context, null); @@ -92,6 +112,7 @@ public final class AspectRatioFrameLayout extends FrameLayout { a.recycle(); } } + aspectRatioUpdateDispatcher = new AspectRatioUpdateDispatcher(); } /** @@ -106,6 +127,15 @@ public final class AspectRatioFrameLayout extends FrameLayout { } } + /** + * Sets the {@link AspectRatioListener}. + * + * @param listener The listener to be notified about aspect ratios changes. + */ + public void setAspectRatioListener(AspectRatioListener listener) { + this.aspectRatioListener = listener; + } + /** * Returns the resize mode. */ @@ -128,7 +158,7 @@ public final class AspectRatioFrameLayout extends FrameLayout { @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); - if (resizeMode == RESIZE_MODE_FILL || videoAspectRatio <= 0) { + if (videoAspectRatio <= 0) { // Aspect ratio not set. return; } @@ -139,6 +169,7 @@ public final class AspectRatioFrameLayout extends FrameLayout { float aspectDeformation = videoAspectRatio / viewAspectRatio - 1; if (Math.abs(aspectDeformation) <= MAX_ASPECT_RATIO_DEFORMATION_FRACTION) { // We're within the allowed tolerance. + aspectRatioUpdateDispatcher.scheduleUpdate(videoAspectRatio, viewAspectRatio, false); return; } @@ -156,16 +187,51 @@ public final class AspectRatioFrameLayout extends FrameLayout { height = (int) (width / videoAspectRatio); } break; - default: + case RESIZE_MODE_FIT: if (aspectDeformation > 0) { height = (int) (width / videoAspectRatio); } else { width = (int) (height * videoAspectRatio); } break; + case RESIZE_MODE_FILL: + default: + // Ignore target aspect ratio + break; } + aspectRatioUpdateDispatcher.scheduleUpdate(videoAspectRatio, viewAspectRatio, true); super.onMeasure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)); } + /** Dispatches updates to {@link AspectRatioListener}. */ + private final class AspectRatioUpdateDispatcher implements Runnable { + + private float targetAspectRatio; + private float naturalAspectRatio; + private boolean aspectRatioMismatch; + private boolean isScheduled; + + public void scheduleUpdate( + float targetAspectRatio, float naturalAspectRatio, boolean aspectRatioMismatch) { + this.targetAspectRatio = targetAspectRatio; + this.naturalAspectRatio = naturalAspectRatio; + this.aspectRatioMismatch = aspectRatioMismatch; + + if (!isScheduled) { + isScheduled = true; + post(this); + } + } + + @Override + public void run() { + isScheduled = false; + if (aspectRatioListener == null) { + return; + } + aspectRatioListener.onAspectRatioUpdated( + targetAspectRatio, naturalAspectRatio, aspectRatioMismatch); + } + } } 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 new file mode 100644 index 0000000000..0a841fa38f --- /dev/null +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DownloadNotificationUtil.java @@ -0,0 +1,154 @@ +/* + * 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.Notification; +import android.app.PendingIntent; +import android.content.Context; +import android.support.annotation.DrawableRes; +import android.support.annotation.Nullable; +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 for creating download notifications. */ +public final class DownloadNotificationUtil { + + private static final @StringRes int NULL_STRING_ID = 0; + + private DownloadNotificationUtil() {} + + /** + * Returns a progress notification for the given task states. + * + * @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. + * @return The notification. + */ + public static Notification buildDownloadCompletedNotification( + Context context, + @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, + @StringRes int titleStringId) { + NotificationCompat.Builder notificationBuilder = + new NotificationCompat.Builder(context, channelId).setSmallIcon(smallIcon); + if (titleStringId != NULL_STRING_ID) { + notificationBuilder.setContentTitle(context.getResources().getString(titleStringId)); + } + if (contentIntent != null) { + notificationBuilder.setContentIntent(contentIntent); + } + if (message != null) { + notificationBuilder.setStyle(new NotificationCompat.BigTextStyle().bigText(message)); + } + 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 new file mode 100644 index 0000000000..4c258c748f --- /dev/null +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java @@ -0,0 +1,1021 @@ +/* + * 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 static java.lang.annotation.RetentionPolicy.SOURCE; + +import android.app.Notification; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +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 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; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * A notification manager to start, update and cancel a media style notification reflecting the + * player state. + * + *

    The notification is cancelled when {@code null} is passed to {@link #setPlayer(Player)} or + * when an intent with action {@link #ACTION_STOP} is received. + * + *

    If the player is released it must be removed from the manager by calling {@code + * setPlayer(null)} which will cancel the notification. + * + *

    Action customization

    + * + * Standard playback actions can be shown or omitted as follows: + * + *
      + *
    • {@code useNavigationActions} - Sets whether the navigation previous and next actions + * are displayed. + *
        + *
      • Corresponding setter: {@link #setUseNavigationActions(boolean)} + *
      + *
    • {@code stopAction} - Sets which stop action should be used. If set to null, the stop + * action is not displayed. + *
        + *
      • Corresponding setter: {@link #setStopAction(String)}} + *
      + *
    • {@code rewindIncrementMs} - Sets the rewind increment. If set to zero the rewind + * action is not displayed. + *
        + *
      • Corresponding setter: {@link #setRewindIncrementMs(long)} + *
      • Default: {@link #DEFAULT_REWIND_MS} (5000) + *
      + *
    • {@code fastForwardIncrementMs} - Sets the fast forward increment. If set to zero the + * fast forward action is not included in the notification. + *
        + *
      • Corresponding setter: {@link #setFastForwardIncrementMs(long)}} + *
      • Default: {@link #DEFAULT_FAST_FORWARD_MS} (5000) + *
      + *
    + */ +public class PlayerNotificationManager { + + /** An adapter to provide content assets of the media currently playing. */ + public interface MediaDescriptionAdapter { + + /** + * Gets the content title for the current media item. + * + *

    See {@link NotificationCompat.Builder#setContentTitle(CharSequence)}. + * + * @param player The {@link Player} for which a notification is being built. + */ + String getCurrentContentTitle(Player player); + + /** + * Creates a content intent for the current media item. + * + *

    See {@link NotificationCompat.Builder#setContentIntent(PendingIntent)}. + * + * @param player The {@link Player} for which a notification is being built. + */ + @Nullable + PendingIntent createCurrentContentIntent(Player player); + + /** + * Gets the content text for the current media item. + * + *

    See {@link NotificationCompat.Builder#setContentText(CharSequence)}. + * + * @param player The {@link Player} for which a notification is being built. + */ + @Nullable + String getCurrentContentText(Player player); + + /** + * Gets the large icon for the current media item. + * + *

    When a bitmap initially needs to be asynchronously loaded, a placeholder (or null) can be + * returned and the bitmap asynchronously passed to the {@link BitmapCallback} once it is + * loaded. Because the adapter may be called multiple times for the same media item, the bitmap + * should be cached by the app and whenever possible be returned synchronously at subsequent + * calls for the same media item. + * + *

    See {@link NotificationCompat.Builder#setLargeIcon(Bitmap)}. + * + * @param player The {@link Player} for which a notification is being built. + * @param callback A {@link BitmapCallback} to provide a {@link Bitmap} asynchronously. + */ + @Nullable + Bitmap getCurrentLargeIcon(Player player, BitmapCallback callback); + } + + /** Defines and handles custom actions. */ + public interface CustomActionReceiver { + + /** Gets the actions handled by this receiver. */ + Map createCustomActions(Context context); + + /** + * Gets the actions to be included in the notification given the current player state. + * + * @param player The {@link Player} for which a notification is being built. + * @return The actions to be included in the notification. + */ + List getCustomActions(Player player); + + /** + * Called when a custom action has been received. + * + * @param player The player. + * @param action The action from {@link Intent#getAction()}. + * @param intent The received {@link Intent}. + */ + void onCustomAction(Player player, String action, Intent intent); + } + + /** A listener for start and cancellation of the notification. */ + public interface NotificationListener { + + /** + * Called after the notification has been started. + * + * @param notificationId The id with which the notification has been posted. + * @param notification The {@link Notification}. + */ + void onNotificationStarted(int notificationId, Notification notification); + + /** + * Called after the notification has been cancelled. + * + * @param notificationId The id of the notification which has been cancelled. + */ + void onNotificationCancelled(int notificationId); + } + + /** Receives a {@link Bitmap}. */ + public final class BitmapCallback { + private final int notificationTag; + + /** Create the receiver. */ + private BitmapCallback(int notificationTag) { + this.notificationTag = notificationTag; + } + + /** + * Called when {@link Bitmap} is available. + * + * @param bitmap The bitmap to use as the large icon of the notification. + */ + public void onBitmap(final Bitmap bitmap) { + if (bitmap != null) { + mainHandler.post( + new Runnable() { + @Override + public void run() { + if (notificationTag == currentNotificationTag && isNotificationStarted) { + updateNotification(bitmap); + } + } + }); + } + } + } + + /** The action which starts playback. */ + public static final String ACTION_PLAY = "com.google.android.exoplayer.play"; + /** The action which pauses playback. */ + public static final String ACTION_PAUSE = "com.google.android.exoplayer.pause"; + /** The action which skips to the previous window. */ + public static final String ACTION_PREVIOUS = "com.google.android.exoplayer.prev"; + /** The action which skips to the next window. */ + public static final String ACTION_NEXT = "com.google.android.exoplayer.next"; + /** The action which fast forwards. */ + public static final String ACTION_FAST_FORWARD = "com.google.android.exoplayer.ffwd"; + /** The action which rewinds. */ + public static final String ACTION_REWIND = "com.google.android.exoplayer.rewind"; + /** The action which cancels the notification and stops playback. */ + public static final String ACTION_STOP = "com.google.android.exoplayer.stop"; + + /** Visibility of notification on the lock screen. */ + @Retention(SOURCE) + @IntDef({ + NotificationCompat.VISIBILITY_PRIVATE, + NotificationCompat.VISIBILITY_PUBLIC, + NotificationCompat.VISIBILITY_SECRET + }) + 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. */ + public static final int DEFAULT_REWIND_MS = 5000; + + 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 Map playbackActions; + private final Map customActions; + + private Player player; + private ControlDispatcher controlDispatcher; + private boolean isNotificationStarted; + private int currentNotificationTag; + private NotificationListener notificationListener; + private MediaSessionCompat.Token mediaSessionToken; + private boolean useNavigationActions; + 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 @DrawableRes int smallIconResourceId; + private int visibility; + private @Priority int priority; + private boolean ongoing; + private boolean useChronometer; + private boolean wasPlayWhenReady; + private int lastPlaybackState; + + /** + * Creates a notification manager and a low-priority notification channel with the specified + * {@code channelId} and {@code channelName}. + * + * @param context The {@link Context}. + * @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 static PlayerNotificationManager createWithNotificationChannel( + Context context, + String channelId, + @StringRes int channelName, + int notificationId, + MediaDescriptionAdapter mediaDescriptionAdapter) { + NotificationUtil.createNotificationChannel( + context, channelId, channelName, NotificationUtil.IMPORTANCE_LOW); + return new PlayerNotificationManager( + context, channelId, notificationId, mediaDescriptionAdapter); + } + + /** + * 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 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, + String channelId, + int notificationId, + MediaDescriptionAdapter mediaDescriptionAdapter, + @Nullable CustomActionReceiver customActionReceiver) { + this.context = context.getApplicationContext(); + this.channelId = channelId; + this.notificationId = notificationId; + this.mediaDescriptionAdapter = mediaDescriptionAdapter; + this.customActionReceiver = customActionReceiver; + this.controlDispatcher = new DefaultControlDispatcher(); + mainHandler = new Handler(Looper.getMainLooper()); + notificationManager = NotificationManagerCompat.from(context); + playerListener = new PlayerListener(); + notificationBroadcastReceiver = new NotificationBroadcastReceiver(); + intentFilter = new IntentFilter(); + + // initialize actions + playbackActions = createPlaybackActions(context); + for (String action : playbackActions.keySet()) { + intentFilter.addAction(action); + } + customActions = + customActionReceiver != null + ? customActionReceiver.createCustomActions(context) + : Collections.emptyMap(); + for (String action : customActions.keySet()) { + intentFilter.addAction(action); + } + + 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); + setVisibility(NotificationCompat.VISIBILITY_PUBLIC); + } + + /** + * Sets the {@link Player}. + * + *

    Setting the player starts a notification immediately unless the player is in {@link + * Player#STATE_IDLE}, in which case the notification is started as soon as the player transitions + * away from being idle. + * + *

    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(@Nullable Player player) { + if (this.player == player) { + return; + } + if (this.player != null) { + this.player.removeListener(playerListener); + if (player == null) { + stopNotification(); + } + } + this.player = player; + if (player != null) { + wasPlayWhenReady = player.getPlayWhenReady(); + lastPlaybackState = player.getPlaybackState(); + player.addListener(playerListener); + if (lastPlaybackState != Player.STATE_IDLE) { + startOrUpdateNotification(); + } + } + } + + /** + * Sets the {@link ControlDispatcher}. + * + * @param controlDispatcher The {@link ControlDispatcher}, or null to use {@link + * DefaultControlDispatcher}. + */ + public final void setControlDispatcher(ControlDispatcher controlDispatcher) { + this.controlDispatcher = + controlDispatcher != null ? controlDispatcher : new DefaultControlDispatcher(); + } + + /** + * Sets the {@link NotificationListener}. + * + * @param notificationListener The {@link NotificationListener}. + */ + public final void setNotificationListener(NotificationListener notificationListener) { + this.notificationListener = notificationListener; + } + + /** + * Sets the fast forward increment in milliseconds. + * + * @param fastForwardMs The fast forward increment in milliseconds. A value of zero will cause the + * fast forward action to be disabled. + */ + public final void setFastForwardIncrementMs(long fastForwardMs) { + if (this.fastForwardMs == fastForwardMs) { + return; + } + this.fastForwardMs = fastForwardMs; + maybeUpdateNotification(); + } + + /** + * Sets the rewind increment in milliseconds. + * + * @param rewindMs The rewind increment in milliseconds. A value of zero will cause the rewind + * action to be disabled. + */ + public final void setRewindIncrementMs(long rewindMs) { + if (this.rewindMs == rewindMs) { + return; + } + this.rewindMs = rewindMs; + maybeUpdateNotification(); + } + + /** + * Sets whether the navigation actions should be used. + * + * @param useNavigationActions Whether to use navigation actions or not. + */ + public final void setUseNavigationActions(boolean useNavigationActions) { + if (this.useNavigationActions != useNavigationActions) { + this.useNavigationActions = useNavigationActions; + maybeUpdateNotification(); + } + } + + /** + * 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. + * + * @param stopAction The name of the stop action which must be {@link #ACTION_STOP} or an action + * provided by the {@link CustomActionReceiver}. {@code null} to omit the stop action. + */ + public final void setStopAction(@Nullable String stopAction) { + if (Util.areEqual(stopAction, this.stopAction)) { + return; + } + 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 { + stopPendingIntent = null; + } + maybeUpdateNotification(); + } + + /** + * Sets the {@link MediaSessionCompat.Token}. + * + * @param token The {@link MediaSessionCompat.Token}. + */ + public final void setMediaSessionToken(MediaSessionCompat.Token token) { + if (!Util.areEqual(this.mediaSessionToken, token)) { + mediaSessionToken = token; + maybeUpdateNotification(); + } + } + + /** + * Sets the badge icon type of the notification. + * + *

    See {@link NotificationCompat.Builder#setBadgeIconType(int)}. + * + * @param badgeIconType The badge icon type. + */ + public final void setBadgeIconType(@NotificationCompat.BadgeIconType int badgeIconType) { + if (this.badgeIconType == badgeIconType) { + return; + } + switch (badgeIconType) { + case NotificationCompat.BADGE_ICON_NONE: + case NotificationCompat.BADGE_ICON_SMALL: + case NotificationCompat.BADGE_ICON_LARGE: + this.badgeIconType = badgeIconType; + break; + default: + throw new IllegalArgumentException(); + } + maybeUpdateNotification(); + } + + /** + * 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. + * + *

    See {@link NotificationCompat.Builder#setColorized(boolean)}. + * + * @param colorized Whether to colorize the notification. + */ + public final void setColorized(boolean colorized) { + if (this.colorized != colorized) { + this.colorized = colorized; + maybeUpdateNotification(); + } + } + + /** + * Sets the defaults. + * + *

    See {@link NotificationCompat.Builder#setDefaults(int)}. + * + * @param defaults The default notification options. + */ + public final void setDefaults(int defaults) { + if (this.defaults != defaults) { + this.defaults = defaults; + maybeUpdateNotification(); + } + } + + /** + * Sets the accent color of the notification. + * + *

    See {@link NotificationCompat.Builder#setColor(int)}. + * + * @param color The color, in ARGB integer form like the constants in {@link Color}. + */ + public final void setColor(int color) { + if (this.color != color) { + this.color = color; + maybeUpdateNotification(); + } + } + + /** + * Sets whether the notification should be ongoing. If {@code false} the user can dismiss the + * notification by swiping. If in addition the stop action is enabled dismissing the notification + * triggers the stop action. + * + *

    See {@link NotificationCompat.Builder#setOngoing(boolean)}. + * + * @param ongoing Whether {@code true} the notification is ongoing and not dismissible. + */ + public final void setOngoing(boolean ongoing) { + if (this.ongoing != ongoing) { + this.ongoing = ongoing; + maybeUpdateNotification(); + } + } + + /** + * 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. + * + *

    See {@link NotificationCompat.Builder#setSmallIcon(int)}. + * + * @param smallIconResourceId The resource id of the small icon. + */ + public final void setSmallIcon(@DrawableRes int smallIconResourceId) { + if (this.smallIconResourceId != smallIconResourceId) { + this.smallIconResourceId = smallIconResourceId; + maybeUpdateNotification(); + } + } + + /** + * Sets whether the elapsed time of the media playback should be displayed + * + *

    See {@link NotificationCompat.Builder#setUsesChronometer(boolean)}. + * + * @param useChronometer Whether to use chronometer. + */ + public final void setUseChronometer(boolean useChronometer) { + if (this.useChronometer != useChronometer) { + this.useChronometer = useChronometer; + maybeUpdateNotification(); + } + } + + /** + * Sets the visibility of the notification which determines whether and how the notification is + * shown when the device is in lock screen mode. + * + *

    See {@link NotificationCompat.Builder#setVisibility(int)}. + * + * @param visibility The visibility which must be one of {@link + * NotificationCompat#VISIBILITY_PUBLIC}, {@link NotificationCompat#VISIBILITY_PRIVATE} or + * {@link NotificationCompat#VISIBILITY_SECRET}. + */ + public final void setVisibility(@Visibility int visibility) { + if (this.visibility == visibility) { + return; + } + switch (visibility) { + case NotificationCompat.VISIBILITY_PRIVATE: + case NotificationCompat.VISIBILITY_PUBLIC: + case NotificationCompat.VISIBILITY_SECRET: + this.visibility = visibility; + break; + default: + throw new IllegalStateException(); + } + maybeUpdateNotification(); + } + + private Notification updateNotification(Bitmap bitmap) { + Notification notification = createNotification(player, bitmap); + notificationManager.notify(notificationId, notification); + return notification; + } + + private void startOrUpdateNotification() { + Notification notification = updateNotification(null); + if (!isNotificationStarted) { + isNotificationStarted = true; + context.registerReceiver(notificationBroadcastReceiver, intentFilter); + if (notificationListener != null) { + notificationListener.onNotificationStarted(notificationId, notification); + } + } + } + + private void maybeUpdateNotification() { + if (isNotificationStarted) { + updateNotification(null); + } + } + + private void stopNotification() { + if (isNotificationStarted) { + notificationManager.cancel(notificationId); + isNotificationStarted = false; + context.unregisterReceiver(notificationBroadcastReceiver); + if (notificationListener != null) { + notificationListener.onNotificationCancelled(notificationId); + } + } + } + + private Map createPlaybackActions(Context context) { + Map actions = new HashMap<>(); + Intent playIntent = new Intent(ACTION_PLAY).setPackage(context.getPackageName()); + actions.put( + ACTION_PLAY, + new NotificationCompat.Action( + R.drawable.exo_notification_play, + context.getString(R.string.exo_controls_play_description), + PendingIntent.getBroadcast(context, 0, playIntent, PendingIntent.FLAG_CANCEL_CURRENT))); + Intent pauseIntent = new Intent(ACTION_PAUSE).setPackage(context.getPackageName()); + actions.put( + ACTION_PAUSE, + new NotificationCompat.Action( + R.drawable.exo_notification_pause, + context.getString(R.string.exo_controls_pause_description), + PendingIntent.getBroadcast( + context, 0, pauseIntent, PendingIntent.FLAG_CANCEL_CURRENT))); + Intent stopIntent = new Intent(ACTION_STOP).setPackage(context.getPackageName()); + actions.put( + ACTION_STOP, + new NotificationCompat.Action( + R.drawable.exo_notification_stop, + context.getString(R.string.exo_controls_stop_description), + PendingIntent.getBroadcast(context, 0, stopIntent, PendingIntent.FLAG_CANCEL_CURRENT))); + Intent rewindIntent = new Intent(ACTION_REWIND).setPackage(context.getPackageName()); + actions.put( + ACTION_REWIND, + new NotificationCompat.Action( + R.drawable.exo_notification_rewind, + context.getString(R.string.exo_controls_rewind_description), + PendingIntent.getBroadcast( + context, 0, rewindIntent, PendingIntent.FLAG_CANCEL_CURRENT))); + Intent fastForwardIntent = new Intent(ACTION_FAST_FORWARD).setPackage(context.getPackageName()); + actions.put( + ACTION_FAST_FORWARD, + new NotificationCompat.Action( + R.drawable.exo_notification_fastforward, + context.getString(R.string.exo_controls_fastforward_description), + PendingIntent.getBroadcast( + context, 0, fastForwardIntent, PendingIntent.FLAG_CANCEL_CURRENT))); + Intent previousIntent = new Intent(ACTION_PREVIOUS).setPackage(context.getPackageName()); + actions.put( + ACTION_PREVIOUS, + new NotificationCompat.Action( + R.drawable.exo_notification_previous, + context.getString(R.string.exo_controls_previous_description), + PendingIntent.getBroadcast( + context, 0, previousIntent, PendingIntent.FLAG_CANCEL_CURRENT))); + Intent nextIntent = new Intent(ACTION_NEXT).setPackage(context.getPackageName()); + actions.put( + ACTION_NEXT, + new NotificationCompat.Action( + R.drawable.exo_notification_next, + context.getString(R.string.exo_controls_next_description), + PendingIntent.getBroadcast(context, 0, nextIntent, PendingIntent.FLAG_CANCEL_CURRENT))); + return actions; + } + + /** + * Creates the notification given the current player state. + * + * @param player The player for which state to build a notification. + * @param largeIcon The large icon to be used. + * @return The {@link Notification} which has been built. + */ + protected Notification createNotification(Player player, @Nullable Bitmap largeIcon) { + boolean isPlayingAd = player.isPlayingAd(); + NotificationCompat.Builder builder = new NotificationCompat.Builder(context, channelId); + List actionNames = getActions(player); + for (int i = 0; i < actionNames.size(); i++) { + String actionName = actionNames.get(i); + NotificationCompat.Action action = + playbackActions.containsKey(actionName) + ? playbackActions.get(actionName) + : customActions.get(actionName); + if (action != null) { + builder.addAction(action); + } + } + // Create a media style notification. + MediaStyle mediaStyle = new MediaStyle(); + builder.setStyle(mediaStyle); + if (mediaSessionToken != null) { + mediaStyle.setMediaSession(mediaSessionToken); + } + mediaStyle.setShowActionsInCompactView(getActionIndicesForCompactView(player)); + // Configure stop action (eg. when user dismisses the notification when !isOngoing). + boolean useStopAction = stopAction != null && !isPlayingAd; + mediaStyle.setShowCancelButton(useStopAction); + if (useStopAction) { + builder.setDeleteIntent(stopPendingIntent); + mediaStyle.setCancelButtonIntent(stopPendingIntent); + } + // Set notification properties from getters. + builder + .setBadgeIconType(badgeIconType) + .setOngoing(ongoing) + .setColor(color) + .setColorized(colorized) + .setSmallIcon(smallIconResourceId) + .setVisibility(visibility) + .setPriority(priority) + .setDefaults(defaults); + if (useChronometer + && !player.isCurrentWindowDynamic() + && player.getPlayWhenReady() + && player.getPlaybackState() == Player.STATE_READY) { + builder + .setWhen(System.currentTimeMillis() - player.getContentPosition()) + .setShowWhen(true) + .setUsesChronometer(true); + } else { + builder.setShowWhen(false).setUsesChronometer(false); + } + // Set media specific notification properties from MediaDescriptionAdapter. + builder.setContentTitle(mediaDescriptionAdapter.getCurrentContentTitle(player)); + builder.setContentText(mediaDescriptionAdapter.getCurrentContentText(player)); + if (largeIcon == null) { + largeIcon = + mediaDescriptionAdapter.getCurrentLargeIcon( + player, new BitmapCallback(++currentNotificationTag)); + } + if (largeIcon != null) { + builder.setLargeIcon(largeIcon); + } + PendingIntent contentIntent = mediaDescriptionAdapter.createCurrentContentIntent(player); + if (contentIntent != null) { + builder.setContentIntent(contentIntent); + } + return builder.build(); + } + + /** + * Gets the names and order of the actions to be included in the notification at the current + * player state. + * + *

    The playback and custom actions are combined and placed in the following order if not + * omitted: + * + *

    +   *   +------------------------------------------------------------------------+
    +   *   | prev | << | play/pause | >> | next | custom actions | stop |
    +   *   +------------------------------------------------------------------------+
    +   * 
    + * + *

    This method can be safely overridden. However, the names must be of the playback actions + * {@link #ACTION_PAUSE}, {@link #ACTION_PLAY}, {@link #ACTION_FAST_FORWARD}, {@link + * #ACTION_REWIND}, {@link #ACTION_NEXT} or {@link #ACTION_PREVIOUS}, or a key contained in the + * map returned by {@link CustomActionReceiver#createCustomActions(Context)}. Otherwise the action + * name is ignored. + */ + protected List getActions(Player player) { + List stringActions = new ArrayList<>(); + if (!player.isPlayingAd()) { + if (useNavigationActions) { + stringActions.add(ACTION_PREVIOUS); + } + if (rewindMs > 0) { + stringActions.add(ACTION_REWIND); + } + if (usePlayPauseActions) { + if (player.getPlayWhenReady()) { + stringActions.add(ACTION_PAUSE); + } else { + stringActions.add(ACTION_PLAY); + } + } + if (fastForwardMs > 0) { + stringActions.add(ACTION_FAST_FORWARD); + } + if (useNavigationActions && player.getNextWindowIndex() != C.INDEX_UNSET) { + stringActions.add(ACTION_NEXT); + } + if (!customActions.isEmpty()) { + stringActions.addAll(customActionReceiver.getCustomActions(player)); + } + if (ACTION_STOP.equals(stopAction)) { + stringActions.add(stopAction); + } + } + return stringActions; + } + + /** + * Gets an array with the indices of the buttons to be shown in compact mode. + * + *

    This method can be overridden. The indices must refer to the list of actions returned by + * {@link #getActions(Player)}. + * + * @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}; + } + + private class PlayerListener extends Player.DefaultEventListener { + + @Override + public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { + if ((wasPlayWhenReady != playWhenReady && playbackState != Player.STATE_IDLE) + || lastPlaybackState != playbackState) { + startOrUpdateNotification(); + } + wasPlayWhenReady = playWhenReady; + lastPlaybackState = playbackState; + } + + @Override + public void onTimelineChanged(Timeline timeline, Object manifest, int reason) { + if (player.getPlaybackState() == Player.STATE_IDLE) { + return; + } + startOrUpdateNotification(); + } + + @Override + public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { + if (player.getPlaybackState() == Player.STATE_IDLE) { + return; + } + startOrUpdateNotification(); + } + + @Override + public void onPositionDiscontinuity(int reason) { + startOrUpdateNotification(); + } + + @Override + public void onRepeatModeChanged(int repeatMode) { + if (player.getPlaybackState() == Player.STATE_IDLE) { + return; + } + startOrUpdateNotification(); + } + } + + private class NotificationBroadcastReceiver extends BroadcastReceiver { + + private final Timeline.Window window; + + /** Creates the broadcast receiver. */ + public NotificationBroadcastReceiver() { + window = new Timeline.Window(); + } + + @Override + public void onReceive(Context context, Intent intent) { + if (!isNotificationStarted) { + return; + } + String action = intent.getAction(); + if (ACTION_PLAY.equals(action) || ACTION_PAUSE.equals(action)) { + controlDispatcher.dispatchSetPlayWhenReady(player, ACTION_PLAY.equals(action)); + } else if (ACTION_FAST_FORWARD.equals(action) || ACTION_REWIND.equals(action)) { + long increment = ACTION_FAST_FORWARD.equals(action) ? fastForwardMs : -rewindMs; + controlDispatcher.dispatchSeekTo( + player, player.getCurrentWindowIndex(), player.getCurrentPosition() + increment); + } else if (ACTION_NEXT.equals(action)) { + int nextWindowIndex = player.getNextWindowIndex(); + if (nextWindowIndex != C.INDEX_UNSET) { + controlDispatcher.dispatchSeekTo(player, nextWindowIndex, C.TIME_UNSET); + } + } else if (ACTION_PREVIOUS.equals(action)) { + player.getCurrentTimeline().getWindow(player.getCurrentWindowIndex(), window); + int previousWindowIndex = player.getPreviousWindowIndex(); + if (previousWindowIndex != C.INDEX_UNSET + && (player.getCurrentPosition() <= MAX_POSITION_FOR_SEEK_TO_PREVIOUS + || (window.isDynamic && !window.isSeekable))) { + controlDispatcher.dispatchSeekTo(player, previousWindowIndex, C.TIME_UNSET); + } else { + controlDispatcher.dispatchSeekTo(player, player.getCurrentWindowIndex(), C.TIME_UNSET); + } + } else if (ACTION_STOP.equals(action)) { + controlDispatcher.dispatchStop(player, true); + stopNotification(); + } else if (customActions.containsKey(action)) { + customActionReceiver.onCustomAction(player, action, intent); + } + } + } +} 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 24f365236f..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}. *
      @@ -111,7 +120,9 @@ import java.util.List; *
    • {@code surface_type} - The type of surface view used for video playbacks. Valid * values are {@code surface_view}, {@code texture_view} and {@code none}. Using {@code none} * is recommended for audio only applications, since creating the surface can be expensive. - * Using {@code surface_view} is recommended for video applications. + * Using {@code surface_view} is recommended for video applications. Note, TextureView can + * only be used in a hardware accelerated window. When rendered in software, TextureView will + * draw nothing. *
        *
      • Corresponding method: None *
      • Default: {@code surface_view} @@ -162,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} @@ -170,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. *
            @@ -211,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; @@ -219,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; @@ -242,6 +267,8 @@ public class PlayerView extends FrameLayout { surfaceView = null; artworkView = null; subtitleView = null; + bufferingView = null; + errorMessageView = null; controller = null; componentListener = null; overlayFrameLayout = null; @@ -267,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 { @@ -284,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 { @@ -339,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); @@ -436,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) { @@ -478,6 +522,12 @@ public class PlayerView extends FrameLayout { contentFrame.setResizeMode(resizeMode); } + /** Returns the resize mode. */ + public @ResizeMode int getResizeMode() { + Assertions.checkState(contentFrame != null); + return contentFrame.getResizeMode(); + } + /** Returns whether artwork is displayed if present in the media. */ public boolean getUseArtwork() { return useArtwork; @@ -550,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()) { @@ -750,6 +838,33 @@ public class PlayerView extends FrameLayout { controller.setShowMultiWindowTimeBar(showMultiWindowTimeBar); } + /** + * Sets the millisecond positions of extra ad markers relative to the start of the window (or + * timeline, if in multi-window mode) and whether each extra ad has been played or not. The + * markers are shown in addition to any ad markers for ads in the player's timeline. + * + * @param extraAdGroupTimesMs The millisecond timestamps of the extra ad markers to show, or + * {@code null} to show no extra ad markers. + * @param extraPlayedAdGroups Whether each ad has been played, or {@code null} to show no extra ad + * markers. + */ + public void setExtraAdGroupMarkers( + @Nullable long[] extraAdGroupTimesMs, @Nullable boolean[] extraPlayedAdGroups) { + Assertions.checkState(controller != null); + controller.setExtraAdGroupMarkers(extraAdGroupTimesMs, extraPlayedAdGroups); + } + + /** + * Set the {@link AspectRatioFrameLayout.AspectRatioListener}. + * + * @param listener The listener to be notified about aspect ratios changes of the video content or + * the content frame. + */ + public void setAspectRatioListener(AspectRatioFrameLayout.AspectRatioListener listener) { + Assertions.checkState(contentFrame != null); + contentFrame.setAspectRatioListener(listener); + } + /** * Gets the view onto which video is rendered. This is a: * @@ -919,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)); @@ -1035,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/library/ui/src/main/res/drawable-anydpi-v21/exo_controls_fastforward.xml b/library/ui/src/main/res/drawable-anydpi-v21/exo_icon_fastforward.xml similarity index 100% rename from library/ui/src/main/res/drawable-anydpi-v21/exo_controls_fastforward.xml rename to library/ui/src/main/res/drawable-anydpi-v21/exo_icon_fastforward.xml diff --git a/library/ui/src/main/res/drawable-anydpi-v21/exo_controls_next.xml b/library/ui/src/main/res/drawable-anydpi-v21/exo_icon_next.xml similarity index 100% rename from library/ui/src/main/res/drawable-anydpi-v21/exo_controls_next.xml rename to library/ui/src/main/res/drawable-anydpi-v21/exo_icon_next.xml diff --git a/library/ui/src/main/res/drawable-anydpi-v21/exo_controls_pause.xml b/library/ui/src/main/res/drawable-anydpi-v21/exo_icon_pause.xml similarity index 100% rename from library/ui/src/main/res/drawable-anydpi-v21/exo_controls_pause.xml rename to library/ui/src/main/res/drawable-anydpi-v21/exo_icon_pause.xml diff --git a/library/ui/src/main/res/drawable-anydpi-v21/exo_controls_play.xml b/library/ui/src/main/res/drawable-anydpi-v21/exo_icon_play.xml similarity index 100% rename from library/ui/src/main/res/drawable-anydpi-v21/exo_controls_play.xml rename to library/ui/src/main/res/drawable-anydpi-v21/exo_icon_play.xml diff --git a/library/ui/src/main/res/drawable-anydpi-v21/exo_controls_previous.xml b/library/ui/src/main/res/drawable-anydpi-v21/exo_icon_previous.xml similarity index 100% rename from library/ui/src/main/res/drawable-anydpi-v21/exo_controls_previous.xml rename to library/ui/src/main/res/drawable-anydpi-v21/exo_icon_previous.xml diff --git a/library/ui/src/main/res/drawable-anydpi-v21/exo_controls_rewind.xml b/library/ui/src/main/res/drawable-anydpi-v21/exo_icon_rewind.xml similarity index 100% rename from library/ui/src/main/res/drawable-anydpi-v21/exo_controls_rewind.xml rename to library/ui/src/main/res/drawable-anydpi-v21/exo_icon_rewind.xml diff --git a/library/ui/src/main/res/drawable-anydpi-v21/exo_icon_stop.xml b/library/ui/src/main/res/drawable-anydpi-v21/exo_icon_stop.xml new file mode 100644 index 0000000000..2e1e40cbb5 --- /dev/null +++ b/library/ui/src/main/res/drawable-anydpi-v21/exo_icon_stop.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/library/ui/src/main/res/drawable-hdpi/exo_controls_fastforward.png b/library/ui/src/main/res/drawable-hdpi/exo_controls_fastforward.png deleted file mode 100644 index 843df84091..0000000000 Binary files a/library/ui/src/main/res/drawable-hdpi/exo_controls_fastforward.png and /dev/null differ diff --git a/library/ui/src/main/res/drawable-hdpi/exo_controls_next.png b/library/ui/src/main/res/drawable-hdpi/exo_controls_next.png deleted file mode 100644 index c37541472e..0000000000 Binary files a/library/ui/src/main/res/drawable-hdpi/exo_controls_next.png and /dev/null differ diff --git a/library/ui/src/main/res/drawable-hdpi/exo_controls_pause.png b/library/ui/src/main/res/drawable-hdpi/exo_controls_pause.png deleted file mode 100644 index 0a23452746..0000000000 Binary files a/library/ui/src/main/res/drawable-hdpi/exo_controls_pause.png and /dev/null differ diff --git a/library/ui/src/main/res/drawable-hdpi/exo_controls_play.png b/library/ui/src/main/res/drawable-hdpi/exo_controls_play.png deleted file mode 100644 index e98e2b9cbe..0000000000 Binary files a/library/ui/src/main/res/drawable-hdpi/exo_controls_play.png and /dev/null differ diff --git a/library/ui/src/main/res/drawable-hdpi/exo_controls_previous.png b/library/ui/src/main/res/drawable-hdpi/exo_controls_previous.png deleted file mode 100644 index 3eae5c883b..0000000000 Binary files a/library/ui/src/main/res/drawable-hdpi/exo_controls_previous.png and /dev/null differ diff --git a/library/ui/src/main/res/drawable-hdpi/exo_controls_rewind.png b/library/ui/src/main/res/drawable-hdpi/exo_controls_rewind.png deleted file mode 100644 index 36537d3b73..0000000000 Binary files a/library/ui/src/main/res/drawable-hdpi/exo_controls_rewind.png and /dev/null differ diff --git a/library/ui/src/main/res/drawable-hdpi/exo_icon_fastforward.png b/library/ui/src/main/res/drawable-hdpi/exo_icon_fastforward.png new file mode 100644 index 0000000000..5699614c6b Binary files /dev/null and b/library/ui/src/main/res/drawable-hdpi/exo_icon_fastforward.png differ diff --git a/library/ui/src/main/res/drawable-hdpi/exo_icon_next.png b/library/ui/src/main/res/drawable-hdpi/exo_icon_next.png new file mode 100644 index 0000000000..303e896187 Binary files /dev/null and b/library/ui/src/main/res/drawable-hdpi/exo_icon_next.png differ diff --git a/library/ui/src/main/res/drawable-hdpi/exo_icon_pause.png b/library/ui/src/main/res/drawable-hdpi/exo_icon_pause.png new file mode 100644 index 0000000000..f49aed7571 Binary files /dev/null and b/library/ui/src/main/res/drawable-hdpi/exo_icon_pause.png differ diff --git a/library/ui/src/main/res/drawable-hdpi/exo_icon_play.png b/library/ui/src/main/res/drawable-hdpi/exo_icon_play.png new file mode 100644 index 0000000000..5a3e037ae9 Binary files /dev/null and b/library/ui/src/main/res/drawable-hdpi/exo_icon_play.png differ diff --git a/library/ui/src/main/res/drawable-hdpi/exo_icon_previous.png b/library/ui/src/main/res/drawable-hdpi/exo_icon_previous.png new file mode 100644 index 0000000000..2c3b3af982 Binary files /dev/null and b/library/ui/src/main/res/drawable-hdpi/exo_icon_previous.png differ diff --git a/library/ui/src/main/res/drawable-hdpi/exo_icon_rewind.png b/library/ui/src/main/res/drawable-hdpi/exo_icon_rewind.png new file mode 100644 index 0000000000..d9e279231a Binary files /dev/null and b/library/ui/src/main/res/drawable-hdpi/exo_icon_rewind.png differ diff --git a/library/ui/src/main/res/drawable-hdpi/exo_icon_stop.png b/library/ui/src/main/res/drawable-hdpi/exo_icon_stop.png new file mode 100644 index 0000000000..3ad2c9c4e3 Binary files /dev/null and b/library/ui/src/main/res/drawable-hdpi/exo_icon_stop.png differ diff --git a/library/ui/src/main/res/drawable-hdpi/exo_notification_small_icon.png b/library/ui/src/main/res/drawable-hdpi/exo_notification_small_icon.png new file mode 100644 index 0000000000..ecf3df3cb1 Binary files /dev/null and b/library/ui/src/main/res/drawable-hdpi/exo_notification_small_icon.png differ diff --git a/library/ui/src/main/res/drawable-ldpi/exo_controls_fastforward.png b/library/ui/src/main/res/drawable-ldpi/exo_controls_fastforward.png deleted file mode 100644 index 19b9e6015c..0000000000 Binary files a/library/ui/src/main/res/drawable-ldpi/exo_controls_fastforward.png and /dev/null differ diff --git a/library/ui/src/main/res/drawable-ldpi/exo_controls_next.png b/library/ui/src/main/res/drawable-ldpi/exo_controls_next.png deleted file mode 100644 index d4872037aa..0000000000 Binary files a/library/ui/src/main/res/drawable-ldpi/exo_controls_next.png and /dev/null differ diff --git a/library/ui/src/main/res/drawable-ldpi/exo_controls_pause.png b/library/ui/src/main/res/drawable-ldpi/exo_controls_pause.png deleted file mode 100644 index 616ec42f39..0000000000 Binary files a/library/ui/src/main/res/drawable-ldpi/exo_controls_pause.png and /dev/null differ diff --git a/library/ui/src/main/res/drawable-ldpi/exo_controls_play.png b/library/ui/src/main/res/drawable-ldpi/exo_controls_play.png deleted file mode 100644 index 5d1c702892..0000000000 Binary files a/library/ui/src/main/res/drawable-ldpi/exo_controls_play.png and /dev/null differ diff --git a/library/ui/src/main/res/drawable-ldpi/exo_controls_previous.png b/library/ui/src/main/res/drawable-ldpi/exo_controls_previous.png deleted file mode 100644 index 930534d312..0000000000 Binary files a/library/ui/src/main/res/drawable-ldpi/exo_controls_previous.png and /dev/null differ diff --git a/library/ui/src/main/res/drawable-ldpi/exo_controls_rewind.png b/library/ui/src/main/res/drawable-ldpi/exo_controls_rewind.png deleted file mode 100644 index 83d71782f6..0000000000 Binary files a/library/ui/src/main/res/drawable-ldpi/exo_controls_rewind.png and /dev/null differ diff --git a/library/ui/src/main/res/drawable-ldpi/exo_icon_fastforward.png b/library/ui/src/main/res/drawable-ldpi/exo_icon_fastforward.png new file mode 100644 index 0000000000..e63921abe6 Binary files /dev/null and b/library/ui/src/main/res/drawable-ldpi/exo_icon_fastforward.png differ diff --git a/library/ui/src/main/res/drawable-ldpi/exo_icon_next.png b/library/ui/src/main/res/drawable-ldpi/exo_icon_next.png new file mode 100644 index 0000000000..78f9bed762 Binary files /dev/null and b/library/ui/src/main/res/drawable-ldpi/exo_icon_next.png differ diff --git a/library/ui/src/main/res/drawable-ldpi/exo_icon_pause.png b/library/ui/src/main/res/drawable-ldpi/exo_icon_pause.png new file mode 100644 index 0000000000..1818039e51 Binary files /dev/null and b/library/ui/src/main/res/drawable-ldpi/exo_icon_pause.png differ diff --git a/library/ui/src/main/res/drawable-ldpi/exo_icon_play.png b/library/ui/src/main/res/drawable-ldpi/exo_icon_play.png new file mode 100644 index 0000000000..f0b0570d0b Binary files /dev/null and b/library/ui/src/main/res/drawable-ldpi/exo_icon_play.png differ diff --git a/library/ui/src/main/res/drawable-ldpi/exo_icon_previous.png b/library/ui/src/main/res/drawable-ldpi/exo_icon_previous.png new file mode 100644 index 0000000000..4d2eccfe9a Binary files /dev/null and b/library/ui/src/main/res/drawable-ldpi/exo_icon_previous.png differ diff --git a/library/ui/src/main/res/drawable-ldpi/exo_icon_rewind.png b/library/ui/src/main/res/drawable-ldpi/exo_icon_rewind.png new file mode 100644 index 0000000000..8cd1daa810 Binary files /dev/null and b/library/ui/src/main/res/drawable-ldpi/exo_icon_rewind.png differ diff --git a/library/ui/src/main/res/drawable-ldpi/exo_icon_stop.png b/library/ui/src/main/res/drawable-ldpi/exo_icon_stop.png new file mode 100644 index 0000000000..836f4dbb55 Binary files /dev/null and b/library/ui/src/main/res/drawable-ldpi/exo_icon_stop.png differ diff --git a/library/ui/src/main/res/drawable-ldpi/exo_notification_small_icon.png b/library/ui/src/main/res/drawable-ldpi/exo_notification_small_icon.png new file mode 100644 index 0000000000..e5104fcd62 Binary files /dev/null and b/library/ui/src/main/res/drawable-ldpi/exo_notification_small_icon.png differ diff --git a/library/ui/src/main/res/drawable-mdpi/exo_controls_fastforward.png b/library/ui/src/main/res/drawable-mdpi/exo_controls_fastforward.png deleted file mode 100644 index ee3efe1d69..0000000000 Binary files a/library/ui/src/main/res/drawable-mdpi/exo_controls_fastforward.png and /dev/null differ diff --git a/library/ui/src/main/res/drawable-mdpi/exo_controls_next.png b/library/ui/src/main/res/drawable-mdpi/exo_controls_next.png deleted file mode 100644 index 9d4d7469ed..0000000000 Binary files a/library/ui/src/main/res/drawable-mdpi/exo_controls_next.png and /dev/null differ diff --git a/library/ui/src/main/res/drawable-mdpi/exo_controls_pause.png b/library/ui/src/main/res/drawable-mdpi/exo_controls_pause.png deleted file mode 100644 index f54c942201..0000000000 Binary files a/library/ui/src/main/res/drawable-mdpi/exo_controls_pause.png and /dev/null differ diff --git a/library/ui/src/main/res/drawable-mdpi/exo_controls_play.png b/library/ui/src/main/res/drawable-mdpi/exo_controls_play.png deleted file mode 100644 index dd0c142859..0000000000 Binary files a/library/ui/src/main/res/drawable-mdpi/exo_controls_play.png and /dev/null differ diff --git a/library/ui/src/main/res/drawable-mdpi/exo_controls_previous.png b/library/ui/src/main/res/drawable-mdpi/exo_controls_previous.png deleted file mode 100644 index 950e213d2f..0000000000 Binary files a/library/ui/src/main/res/drawable-mdpi/exo_controls_previous.png and /dev/null differ diff --git a/library/ui/src/main/res/drawable-mdpi/exo_controls_rewind.png b/library/ui/src/main/res/drawable-mdpi/exo_controls_rewind.png deleted file mode 100644 index e75efae189..0000000000 Binary files a/library/ui/src/main/res/drawable-mdpi/exo_controls_rewind.png and /dev/null differ diff --git a/library/ui/src/main/res/drawable-mdpi/exo_icon_fastforward.png b/library/ui/src/main/res/drawable-mdpi/exo_icon_fastforward.png new file mode 100644 index 0000000000..1b42a5315f Binary files /dev/null and b/library/ui/src/main/res/drawable-mdpi/exo_icon_fastforward.png differ diff --git a/library/ui/src/main/res/drawable-mdpi/exo_icon_next.png b/library/ui/src/main/res/drawable-mdpi/exo_icon_next.png new file mode 100644 index 0000000000..a93aae0f34 Binary files /dev/null and b/library/ui/src/main/res/drawable-mdpi/exo_icon_next.png differ diff --git a/library/ui/src/main/res/drawable-mdpi/exo_icon_pause.png b/library/ui/src/main/res/drawable-mdpi/exo_icon_pause.png new file mode 100644 index 0000000000..3e150b5a45 Binary files /dev/null and b/library/ui/src/main/res/drawable-mdpi/exo_icon_pause.png differ diff --git a/library/ui/src/main/res/drawable-mdpi/exo_icon_play.png b/library/ui/src/main/res/drawable-mdpi/exo_icon_play.png new file mode 100644 index 0000000000..692d8c2ad9 Binary files /dev/null and b/library/ui/src/main/res/drawable-mdpi/exo_icon_play.png differ diff --git a/library/ui/src/main/res/drawable-mdpi/exo_icon_previous.png b/library/ui/src/main/res/drawable-mdpi/exo_icon_previous.png new file mode 100644 index 0000000000..ea83907d8e Binary files /dev/null and b/library/ui/src/main/res/drawable-mdpi/exo_icon_previous.png differ diff --git a/library/ui/src/main/res/drawable-mdpi/exo_icon_rewind.png b/library/ui/src/main/res/drawable-mdpi/exo_icon_rewind.png new file mode 100644 index 0000000000..231bcee4ca Binary files /dev/null and b/library/ui/src/main/res/drawable-mdpi/exo_icon_rewind.png differ diff --git a/library/ui/src/main/res/drawable-mdpi/exo_icon_stop.png b/library/ui/src/main/res/drawable-mdpi/exo_icon_stop.png new file mode 100644 index 0000000000..2aeffbb6cf Binary files /dev/null and b/library/ui/src/main/res/drawable-mdpi/exo_icon_stop.png differ diff --git a/library/ui/src/main/res/drawable-mdpi/exo_notification_small_icon.png b/library/ui/src/main/res/drawable-mdpi/exo_notification_small_icon.png new file mode 100644 index 0000000000..7242e1f5c8 Binary files /dev/null and b/library/ui/src/main/res/drawable-mdpi/exo_notification_small_icon.png differ diff --git a/library/ui/src/main/res/drawable-xhdpi/exo_controls_fastforward.png b/library/ui/src/main/res/drawable-xhdpi/exo_controls_fastforward.png deleted file mode 100644 index ead712cfe9..0000000000 Binary files a/library/ui/src/main/res/drawable-xhdpi/exo_controls_fastforward.png and /dev/null differ diff --git a/library/ui/src/main/res/drawable-xhdpi/exo_controls_next.png b/library/ui/src/main/res/drawable-xhdpi/exo_controls_next.png deleted file mode 100644 index bc1ebf83c5..0000000000 Binary files a/library/ui/src/main/res/drawable-xhdpi/exo_controls_next.png and /dev/null differ diff --git a/library/ui/src/main/res/drawable-xhdpi/exo_controls_play.png b/library/ui/src/main/res/drawable-xhdpi/exo_controls_play.png deleted file mode 100644 index f2f934413e..0000000000 Binary files a/library/ui/src/main/res/drawable-xhdpi/exo_controls_play.png and /dev/null differ diff --git a/library/ui/src/main/res/drawable-xhdpi/exo_controls_previous.png b/library/ui/src/main/res/drawable-xhdpi/exo_controls_previous.png deleted file mode 100644 index d197eff873..0000000000 Binary files a/library/ui/src/main/res/drawable-xhdpi/exo_controls_previous.png and /dev/null differ diff --git a/library/ui/src/main/res/drawable-xhdpi/exo_controls_rewind.png b/library/ui/src/main/res/drawable-xhdpi/exo_controls_rewind.png deleted file mode 100644 index 3340ef9bd2..0000000000 Binary files a/library/ui/src/main/res/drawable-xhdpi/exo_controls_rewind.png and /dev/null differ diff --git a/library/ui/src/main/res/drawable-xhdpi/exo_icon_fastforward.png b/library/ui/src/main/res/drawable-xhdpi/exo_icon_fastforward.png new file mode 100644 index 0000000000..ab7e1fd334 Binary files /dev/null and b/library/ui/src/main/res/drawable-xhdpi/exo_icon_fastforward.png differ diff --git a/library/ui/src/main/res/drawable-xhdpi/exo_icon_next.png b/library/ui/src/main/res/drawable-xhdpi/exo_icon_next.png new file mode 100644 index 0000000000..f3552d7216 Binary files /dev/null and b/library/ui/src/main/res/drawable-xhdpi/exo_icon_next.png differ diff --git a/library/ui/src/main/res/drawable-xhdpi/exo_controls_pause.png b/library/ui/src/main/res/drawable-xhdpi/exo_icon_pause.png similarity index 100% rename from library/ui/src/main/res/drawable-xhdpi/exo_controls_pause.png rename to library/ui/src/main/res/drawable-xhdpi/exo_icon_pause.png diff --git a/library/ui/src/main/res/drawable-xhdpi/exo_icon_play.png b/library/ui/src/main/res/drawable-xhdpi/exo_icon_play.png new file mode 100644 index 0000000000..381eabdccf Binary files /dev/null and b/library/ui/src/main/res/drawable-xhdpi/exo_icon_play.png differ diff --git a/library/ui/src/main/res/drawable-xhdpi/exo_icon_previous.png b/library/ui/src/main/res/drawable-xhdpi/exo_icon_previous.png new file mode 100644 index 0000000000..0a2ddd5e90 Binary files /dev/null and b/library/ui/src/main/res/drawable-xhdpi/exo_icon_previous.png differ diff --git a/library/ui/src/main/res/drawable-xhdpi/exo_icon_rewind.png b/library/ui/src/main/res/drawable-xhdpi/exo_icon_rewind.png new file mode 100644 index 0000000000..a798fee30e Binary files /dev/null and b/library/ui/src/main/res/drawable-xhdpi/exo_icon_rewind.png differ diff --git a/library/ui/src/main/res/drawable-xhdpi/exo_icon_stop.png b/library/ui/src/main/res/drawable-xhdpi/exo_icon_stop.png new file mode 100644 index 0000000000..8727a93480 Binary files /dev/null and b/library/ui/src/main/res/drawable-xhdpi/exo_icon_stop.png differ diff --git a/library/ui/src/main/res/drawable-xhdpi/exo_notification_small_icon.png b/library/ui/src/main/res/drawable-xhdpi/exo_notification_small_icon.png new file mode 100644 index 0000000000..dd31d608d4 Binary files /dev/null and b/library/ui/src/main/res/drawable-xhdpi/exo_notification_small_icon.png differ diff --git a/library/ui/src/main/res/drawable-xxhdpi/exo_controls_fastforward.png b/library/ui/src/main/res/drawable-xxhdpi/exo_controls_fastforward.png deleted file mode 100644 index e1c6cae292..0000000000 Binary files a/library/ui/src/main/res/drawable-xxhdpi/exo_controls_fastforward.png and /dev/null differ diff --git a/library/ui/src/main/res/drawable-xxhdpi/exo_controls_next.png b/library/ui/src/main/res/drawable-xxhdpi/exo_controls_next.png deleted file mode 100644 index 232f09e910..0000000000 Binary files a/library/ui/src/main/res/drawable-xxhdpi/exo_controls_next.png and /dev/null differ diff --git a/library/ui/src/main/res/drawable-xxhdpi/exo_controls_pause.png b/library/ui/src/main/res/drawable-xxhdpi/exo_controls_pause.png deleted file mode 100644 index 50a545db4d..0000000000 Binary files a/library/ui/src/main/res/drawable-xxhdpi/exo_controls_pause.png and /dev/null differ diff --git a/library/ui/src/main/res/drawable-xxhdpi/exo_controls_play.png b/library/ui/src/main/res/drawable-xxhdpi/exo_controls_play.png deleted file mode 100644 index 08508c5015..0000000000 Binary files a/library/ui/src/main/res/drawable-xxhdpi/exo_controls_play.png and /dev/null differ diff --git a/library/ui/src/main/res/drawable-xxhdpi/exo_controls_previous.png b/library/ui/src/main/res/drawable-xxhdpi/exo_controls_previous.png deleted file mode 100644 index f71acc4875..0000000000 Binary files a/library/ui/src/main/res/drawable-xxhdpi/exo_controls_previous.png and /dev/null differ diff --git a/library/ui/src/main/res/drawable-xxhdpi/exo_controls_rewind.png b/library/ui/src/main/res/drawable-xxhdpi/exo_controls_rewind.png deleted file mode 100644 index db0555f9e5..0000000000 Binary files a/library/ui/src/main/res/drawable-xxhdpi/exo_controls_rewind.png and /dev/null differ diff --git a/library/ui/src/main/res/drawable-xxhdpi/exo_icon_fastforward.png b/library/ui/src/main/res/drawable-xxhdpi/exo_icon_fastforward.png new file mode 100644 index 0000000000..1e8db0ec23 Binary files /dev/null and b/library/ui/src/main/res/drawable-xxhdpi/exo_icon_fastforward.png differ diff --git a/library/ui/src/main/res/drawable-xxhdpi/exo_icon_next.png b/library/ui/src/main/res/drawable-xxhdpi/exo_icon_next.png new file mode 100644 index 0000000000..131a531b37 Binary files /dev/null and b/library/ui/src/main/res/drawable-xxhdpi/exo_icon_next.png differ diff --git a/library/ui/src/main/res/drawable-xxhdpi/exo_icon_pause.png b/library/ui/src/main/res/drawable-xxhdpi/exo_icon_pause.png new file mode 100644 index 0000000000..ac8d4fcad5 Binary files /dev/null and b/library/ui/src/main/res/drawable-xxhdpi/exo_icon_pause.png differ diff --git a/library/ui/src/main/res/drawable-xxhdpi/exo_icon_play.png b/library/ui/src/main/res/drawable-xxhdpi/exo_icon_play.png new file mode 100644 index 0000000000..365b3dfee5 Binary files /dev/null and b/library/ui/src/main/res/drawable-xxhdpi/exo_icon_play.png differ diff --git a/library/ui/src/main/res/drawable-xxhdpi/exo_icon_previous.png b/library/ui/src/main/res/drawable-xxhdpi/exo_icon_previous.png new file mode 100644 index 0000000000..884cbdd407 Binary files /dev/null and b/library/ui/src/main/res/drawable-xxhdpi/exo_icon_previous.png differ diff --git a/library/ui/src/main/res/drawable-xxhdpi/exo_icon_rewind.png b/library/ui/src/main/res/drawable-xxhdpi/exo_icon_rewind.png new file mode 100644 index 0000000000..4bab545714 Binary files /dev/null and b/library/ui/src/main/res/drawable-xxhdpi/exo_icon_rewind.png differ diff --git a/library/ui/src/main/res/drawable-xxhdpi/exo_icon_stop.png b/library/ui/src/main/res/drawable-xxhdpi/exo_icon_stop.png new file mode 100644 index 0000000000..5239336671 Binary files /dev/null and b/library/ui/src/main/res/drawable-xxhdpi/exo_icon_stop.png differ diff --git a/library/ui/src/main/res/drawable-xxhdpi/exo_notification_small_icon.png b/library/ui/src/main/res/drawable-xxhdpi/exo_notification_small_icon.png new file mode 100644 index 0000000000..56ed071b2f Binary files /dev/null and b/library/ui/src/main/res/drawable-xxhdpi/exo_notification_small_icon.png differ diff --git a/library/ui/src/main/res/drawable-xxxhdpi/exo_notification_small_icon.png b/library/ui/src/main/res/drawable-xxxhdpi/exo_notification_small_icon.png new file mode 100644 index 0000000000..eabf12f38a Binary files /dev/null and b/library/ui/src/main/res/drawable-xxxhdpi/exo_notification_small_icon.png differ 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 f8b249d57d..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" - "Onderbreek" - "Speel" - "Stop" - "Spoel terug" - "Spoel vorentoe" - "Herhaal niks" - "Herhaal een" - "Herhaal alles" - "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 38e805eef6..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 98c1b555a2..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 e66605b599..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,30 +1,35 @@ - - - - - "Prethodna pesma" - "Sledeća pesma" - "Pauziraj" - "Pusti" - "Zaustavi" - "Premotaj unazad" - "Premotaj unapred" - "Ne ponavljaj nijednu" - "Ponovi jednu" - "Ponovi sve" - "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 6eb4e8881a..74bc85313d 100644 --- a/library/ui/src/main/res/values-bg/strings.xml +++ b/library/ui/src/main/res/values-bg/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-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 0e2512c061..10bf259418 100644 --- a/library/ui/src/main/res/values-ca/strings.xml +++ b/library/ui/src/main/res/values-ca/strings.xml @@ -1,30 +1,35 @@ - - - - - "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" + + + 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 e397968647..c910fd3483 100644 --- a/library/ui/src/main/res/values-cs/strings.xml +++ b/library/ui/src/main/res/values-cs/strings.xml @@ -1,30 +1,35 @@ - - - - - "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ě" + + + 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 7a909daba4..6a25bbf395 100644 --- a/library/ui/src/main/res/values-da/strings.xml +++ b/library/ui/src/main/res/values-da/strings.xml @@ -1,30 +1,35 @@ - - - - - "Afspil forrige" - "Afspil næste" - "Sæt på pause" - "Afspil" - "Stop" - "Spol tilbage" - "Spol frem" - "Gentag ingen" - "Gentag én" - "Gentag alle" - "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 b40fce1fb5..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" - "Wiedergeben" - "Beenden" - "Zurückspulen" - "Vorspulen" - "Keinen wiederholen" - "Einen wiederholen" - "Alle 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 4c86019d47..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 2a73a3e98e..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,30 +1,35 @@ - - - - - "Previous track" - "Next track" - "Pause" - "Play" - "Stop" - "Rewind" - "Fast-forward" - "Repeat none" - "Repeat one" - "Repeat all" - "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 2a73a3e98e..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 none" - "Repeat one" - "Repeat all" - "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-rIN/strings.xml b/library/ui/src/main/res/values-en-rIN/strings.xml index 2a73a3e98e..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 none" - "Repeat one" - "Repeat all" - "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-es-rUS/strings.xml b/library/ui/src/main/res/values-es-rUS/strings.xml index 5fca31959b..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,30 +1,35 @@ - - - - - "Pista anterior" - "Pista siguiente" - "Pausar" - "Reproducir" - "Detener" - "Retroceder" - "Avanzar" - "No repetir" - "Repetir uno" - "Repetir todo" - "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 b4f76b2828..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 @@ - - - - - "Pista anterior" - "Siguiente pista" - "Pausar" - "Reproducir" - "Detener" - "Rebobinar" - "Avanzar rápidamente" - "No repetir" - "Repetir uno" - "Repetir todo" - "Reproducir aleatoriamente" + + + 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 097680ae6b..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 ba93ad4d1e..d9b33f0977 100644 --- a/library/ui/src/main/res/values-fi/strings.xml +++ b/library/ui/src/main/res/values-fi/strings.xml @@ -1,30 +1,35 @@ - - - - - "Edellinen kappale" - "Seuraava kappale" - "Keskeytä" - "Toista" - "Lopeta" - "Kelaa taaksepäin" - "Kelaa eteenpäin" - "Ei uudelleentoistoa" - "Toista yksi uudelleen" - "Toista kaikki uudelleen" - "Satunnaistoisto" + + + 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 f08bd3d680..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,30 +1,35 @@ - - - - - "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" + + + 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 41dc7e9605..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 @@ - - - - - "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" + + + 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 a3a0c76b80..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 41a3c3f2e3..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 @@ - - - - - "Prethodni zapis" - "Sljedeći zapis" - "Pauza" - "Reproduciraj" - "Zaustavi" - "Unatrag" - "Brzo unaprijed" - "Bez ponavljanja" - "Ponovi jedno" - "Ponovi sve" - "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 3befaa70de..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ü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" + + + 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 636fbf0bbf..6cbd6a703a 100644 --- a/library/ui/src/main/res/values-in/strings.xml +++ b/library/ui/src/main/res/values-in/strings.xml @@ -1,30 +1,35 @@ - - - - - "Lagu sebelumnya" - "Lagu berikutnya" - "Jeda" - "Putar" - "Berhenti" - "Putar Ulang" - "Maju cepat" - "Jangan ulangi" - "Ulangi 1" - "Ulangi semua" - "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 a962f25072..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" - "Pausa" - "Riproduci" - "Interrompi" - "Riavvolgi" - "Avanti veloce" - "Non ripetere nulla" - "Ripeti uno" - "Ripeti tutto" - "Riproduzione casuale" + + + 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 56d85b90a5..9aec8e5834 100644 --- a/library/ui/src/main/res/values-iw/strings.xml +++ b/library/ui/src/main/res/values-iw/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-ja/strings.xml b/library/ui/src/main/res/values-ja/strings.xml index 9a6ff4f98f..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 59c0bd6647..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 4b01d4b3cd..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" - "Sustabdyti" - "Sukti atgal" - "Sukti pirmyn" - "Nekartoti nieko" - "Kartoti vieną" - "Kartoti viską" - "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 5920e8b1a2..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" - "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ā" + + + 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 da7de1f1dd..7e48146084 100644 --- a/library/ui/src/main/res/values-nb/strings.xml +++ b/library/ui/src/main/res/values-nb/strings.xml @@ -1,30 +1,35 @@ - - - - - "Forrige spor" - "Neste spor" - "Sett på pause" - "Spill av" - "Stopp" - "Spol tilbake" - "Spol forover" - "Ikke gjenta noen" - "Gjenta én" - "Gjenta alle" - "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 3ad2d002ca..1a2880ae1f 100644 --- a/library/ui/src/main/res/values-nl/strings.xml +++ b/library/ui/src/main/res/values-nl/strings.xml @@ -1,30 +1,35 @@ - - - - - "Vorige track" - "Volgende track" - "Pauzeren" - "Afspelen" - "Stoppen" - "Terugspoelen" - "Vooruitspoelen" - "Niets herhalen" - "Eén herhalen" - "Alles 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 9098e2bb9e..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" - "Nie powtarzaj" - "Powtórz jeden" - "Powtórz wszystkie" - "Odtwarzanie losowe" + + + 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 ede4093c94..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" - "Colocar em pausa" - "Reproduzir" - "Parar" - "Recuar" - "Avançar" - "Não repetir nenhum" - "Repetir um" - "Repetir tudo" - "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 f3c2ec2533..6ea00fed8d 100644 --- a/library/ui/src/main/res/values-pt/strings.xml +++ b/library/ui/src/main/res/values-pt/strings.xml @@ -1,30 +1,35 @@ - - - - - "Faixa anterior" - "Próxima faixa" - "Pausar" - "Reproduzir" - "Parar" - "Retroceder" - "Avançar" - "Não repetir" - "Repetir uma" - "Repetir tudo" - "Aleatório" + + + 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 398352d0d3..4ea18d4a58 100644 --- a/library/ui/src/main/res/values-ro/strings.xml +++ b/library/ui/src/main/res/values-ro/strings.xml @@ -1,30 +1,35 @@ - - - - - "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" + + + 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 779a2fc0f2..14c8badf99 100644 --- a/library/ui/src/main/res/values-ru/strings.xml +++ b/library/ui/src/main/res/values-ru/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-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 51dab688c3..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 skladba" - "Ďalšia skladba" - "Pozastaviť" - "Prehrať" - "Zastaviť" - "Pretočiť späť" - "Pretočiť dopredu" - "Neopakovať" - "Opakovať jednu" - "Opakovať všetko" - "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 832277bdc6..83d332103e 100644 --- a/library/ui/src/main/res/values-sl/strings.xml +++ b/library/ui/src/main/res/values-sl/strings.xml @@ -1,30 +1,35 @@ - - - - - "Prejšnja skladba" - "Naslednja skladba" - "Zaustavitev" - "Predvajanje" - "Ustavitev" - "Previjanje nazaj" - "Previjanje naprej" - "Brez ponavljanja" - "Ponavljanje ene" - "Ponavljanje vseh" - "Naključno predvajanje" + + + 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 8418a51767..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 acca62c10e..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" - "Stoppa" - "Spola tillbaka" - "Snabbspola framåt" - "Upprepa inga" - "Upprepa en" - "Upprepa alla" - "Blanda spår" + + + 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 a2441b8270..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" - "Sogeza mbele haraka" - "Usirudie yoyote" - "Rudia moja" - "Rudia zote" - "Changanya" + + + 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 02d5fff60c..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 b4a3e1136c..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" - "Walang uulitin" - "Mag-ulit ng isa" - "Ulitin lahat" - "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 e7f6c3c89b..cacb60c5a5 100644 --- a/library/ui/src/main/res/values-tr/strings.xml +++ b/library/ui/src/main/res/values-tr/strings.xml @@ -1,30 +1,35 @@ - - - - - "Ö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" + + + Ö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 114c7d9298..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 + Повторити всі + Перемішати + Повноекранний режим + Завантажити + Завантаження + Завантажується + Завантаження завершено + Не вдалося завантажити + Завантаження видаляються + Відео + Аудіо + Текст + Нічого + Автоматично + Невідомо + %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 25604f52fa..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" - "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" + + + 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 4624a9551e..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 5957c595a5..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 44275f85ff..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 6d8e67154f..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" - "Phumula" - "Dlala" - "Misa" - "Buyisela emuva" - "Dlulisela phambili" - "Phinda okungekho" - "Phinda okukodwa" - "Phinda konke" - "Shova" + + + 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/drawables.xml b/library/ui/src/main/res/values/drawables.xml new file mode 100644 index 0000000000..b528c9cc9a --- /dev/null +++ b/library/ui/src/main/res/values/drawables.xml @@ -0,0 +1,16 @@ + + + @drawable/exo_icon_play + @drawable/exo_icon_pause + @drawable/exo_icon_next + @drawable/exo_icon_previous + @drawable/exo_icon_fastforward + @drawable/exo_icon_rewind + @drawable/exo_icon_play + @drawable/exo_icon_pause + @drawable/exo_icon_next + @drawable/exo_icon_previous + @drawable/exo_icon_fastforward + @drawable/exo_icon_rewind + @drawable/exo_icon_stop + 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 007309deda..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 @@ -36,4 +36,46 @@ Repeat all Shuffle + + Fullscreen 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/playbacktests/src/androidTest/AndroidManifest.xml b/playbacktests/src/androidTest/AndroidManifest.xml index 328834e155..d4fd0b61f1 100644 --- a/playbacktests/src/androidTest/AndroidManifest.xml +++ b/playbacktests/src/androidTest/AndroidManifest.xml @@ -21,8 +21,6 @@ - - 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 e846a377dc..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 @@ -50,6 +50,7 @@ import com.google.android.exoplayer2.testutil.ExoHostedTest; import com.google.android.exoplayer2.testutil.HostActivity; import com.google.android.exoplayer2.testutil.HostActivity.HostedTest; import com.google.android.exoplayer2.testutil.MetricsLogger; +import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.FixedTrackSelection; import com.google.android.exoplayer2.trackselection.MappingTrackSelector; import com.google.android.exoplayer2.trackselection.RandomTrackSelection; @@ -264,8 +265,8 @@ public final class DashTestRunner { } @Override - protected MappingTrackSelector buildTrackSelector(HostActivity host, - BandwidthMeter bandwidthMeter) { + protected DefaultTrackSelector buildTrackSelector( + HostActivity host, BandwidthMeter bandwidthMeter) { return trackSelector; } @@ -279,7 +280,7 @@ public final class DashTestRunner { MediaDrmCallback drmCallback = new HttpMediaDrmCallback(widevineLicenseUrl, new DefaultHttpDataSourceFactory(userAgent)); DefaultDrmSessionManager drmSessionManager = - DefaultDrmSessionManager.newWidevineInstance(drmCallback, null, null, null); + DefaultDrmSessionManager.newWidevineInstance(drmCallback, null); if (!useL1Widevine) { drmSessionManager.setPropertyString( SECURITY_LEVEL_PROPERTY, WIDEVINE_SECURITY_LEVEL_3); @@ -298,8 +299,9 @@ public final class DashTestRunner { protected SimpleExoPlayer buildExoPlayer(HostActivity host, Surface surface, MappingTrackSelector trackSelector, DrmSessionManager drmSessionManager) { - SimpleExoPlayer player = ExoPlayerFactory.newSimpleInstance( - new DebugRenderersFactory(host, drmSessionManager), trackSelector); + SimpleExoPlayer player = + ExoPlayerFactory.newSimpleInstance( + new DebugRenderersFactory(host), trackSelector, drmSessionManager); player.setVideoSurface(surface); return player; } @@ -374,7 +376,7 @@ public final class DashTestRunner { } - private static final class DashTestTrackSelector extends MappingTrackSelector { + private static final class DashTestTrackSelector extends DefaultTrackSelector { private final String tag; private final String audioFormatId; @@ -392,32 +394,43 @@ public final class DashTestRunner { } @Override - protected TrackSelection[] selectTracks(RendererCapabilities[] rendererCapabilities, - TrackGroupArray[] rendererTrackGroupArrays, int[][][] rendererFormatSupports) + protected TrackSelection[] selectAllTracks( + 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(rendererTrackGroupArrays[VIDEO_RENDERER_INDEX].length == 1); - Assertions.checkState(rendererTrackGroupArrays[AUDIO_RENDERER_INDEX].length == 1); - TrackSelection[] selections = new TrackSelection[rendererCapabilities.length]; - selections[VIDEO_RENDERER_INDEX] = new RandomTrackSelection( - rendererTrackGroupArrays[VIDEO_RENDERER_INDEX].get(0), - getVideoTrackIndices(rendererTrackGroupArrays[VIDEO_RENDERER_INDEX].get(0), - rendererFormatSupports[VIDEO_RENDERER_INDEX][0], videoFormatIds, - canIncludeAdditionalVideoFormats), - 0 /* seed */); - selections[AUDIO_RENDERER_INDEX] = new FixedTrackSelection( - rendererTrackGroupArrays[AUDIO_RENDERER_INDEX].get(0), - getTrackIndex(rendererTrackGroupArrays[AUDIO_RENDERER_INDEX].get(0), audioFormatId)); + 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[mappedTrackInfo.getRendererCount()]; + selections[VIDEO_RENDERER_INDEX] = + new RandomTrackSelection( + videoTrackGroups.get(0), + getVideoTrackIndices( + videoTrackGroups.get(0), + rendererFormatSupports[VIDEO_RENDERER_INDEX][0], + videoFormatIds, + canIncludeAdditionalVideoFormats), + 0 /* seed */); + selections[AUDIO_RENDERER_INDEX] = + new FixedTrackSelection( + audioTrackGroups.get(0), getTrackIndex(audioTrackGroups.get(0), audioFormatId)); includedAdditionalVideoFormats = selections[VIDEO_RENDERER_INDEX].length() > videoFormatIds.length; 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. @@ -431,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 60cf6d278b..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 @@ -32,7 +32,8 @@ import com.google.android.exoplayer2.source.MediaSource; 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.MappingTrackSelector; +import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; +import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.Parameters; import com.google.android.exoplayer2.util.HandlerWrapper; /** @@ -64,7 +65,7 @@ public abstract class Action { */ public final void doActionAndScheduleNext( SimpleExoPlayer player, - MappingTrackSelector trackSelector, + DefaultTrackSelector trackSelector, Surface surface, HandlerWrapper handler, ActionNode nextAction) { @@ -75,7 +76,7 @@ public abstract class Action { } /** - * Called by {@link #doActionAndScheduleNext(SimpleExoPlayer, MappingTrackSelector, Surface, + * Called by {@link #doActionAndScheduleNext(SimpleExoPlayer, DefaultTrackSelector, Surface, * HandlerWrapper, ActionNode)} to perform the action and to schedule the next action node. * * @param player The player to which the action should be applied. @@ -86,7 +87,7 @@ public abstract class Action { */ protected void doActionAndScheduleNextImpl( SimpleExoPlayer player, - MappingTrackSelector trackSelector, + DefaultTrackSelector trackSelector, Surface surface, HandlerWrapper handler, ActionNode nextAction) { @@ -97,7 +98,7 @@ public abstract class Action { } /** - * Called by {@link #doActionAndScheduleNextImpl(SimpleExoPlayer, MappingTrackSelector, Surface, + * Called by {@link #doActionAndScheduleNextImpl(SimpleExoPlayer, DefaultTrackSelector, Surface, * HandlerWrapper, ActionNode)} to perform the action. * * @param player The player to which the action should be applied. @@ -105,7 +106,7 @@ public abstract class Action { * @param surface The surface to use when applying actions. */ protected abstract void doActionImpl( - SimpleExoPlayer player, MappingTrackSelector trackSelector, Surface surface); + SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface); /** * Calls {@link Player#seekTo(long)} or {@link Player#seekTo(int, long)}. @@ -141,8 +142,8 @@ public abstract class Action { } @Override - protected void doActionImpl(SimpleExoPlayer player, MappingTrackSelector trackSelector, - Surface surface) { + protected void doActionImpl( + SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) { if (windowIndex == null) { player.seekTo(positionMs); } else { @@ -183,8 +184,8 @@ public abstract class Action { } @Override - protected void doActionImpl(SimpleExoPlayer player, MappingTrackSelector trackSelector, - Surface surface) { + protected void doActionImpl( + SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) { if (reset == null) { player.stop(); } else { @@ -212,15 +213,16 @@ public abstract class Action { } @Override - protected void doActionImpl(SimpleExoPlayer player, MappingTrackSelector trackSelector, - Surface surface) { + protected void doActionImpl( + SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) { player.setPlayWhenReady(playWhenReady); } } /** - * Calls {@link MappingTrackSelector#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 { @@ -239,9 +241,10 @@ public abstract class Action { } @Override - protected void doActionImpl(SimpleExoPlayer player, MappingTrackSelector trackSelector, - Surface surface) { - trackSelector.setRendererDisabled(rendererIndex, disabled); + protected void doActionImpl( + SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) { + trackSelector.setParameters( + trackSelector.buildUponParameters().setRendererDisabled(rendererIndex, disabled)); } } @@ -259,8 +262,8 @@ public abstract class Action { } @Override - protected void doActionImpl(SimpleExoPlayer player, MappingTrackSelector trackSelector, - Surface surface) { + protected void doActionImpl( + SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) { player.clearVideoSurface(); } @@ -279,8 +282,8 @@ public abstract class Action { } @Override - protected void doActionImpl(SimpleExoPlayer player, MappingTrackSelector trackSelector, - Surface surface) { + protected void doActionImpl( + SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) { player.setVideoSurface(surface); } @@ -314,8 +317,8 @@ public abstract class Action { } @Override - protected void doActionImpl(SimpleExoPlayer player, MappingTrackSelector trackSelector, - Surface surface) { + protected void doActionImpl( + SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) { player.prepare(mediaSource, resetPosition, resetState); } @@ -337,8 +340,8 @@ public abstract class Action { } @Override - protected void doActionImpl(SimpleExoPlayer player, MappingTrackSelector trackSelector, - Surface surface) { + protected void doActionImpl( + SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) { player.setRepeatMode(repeatMode); } @@ -360,8 +363,8 @@ public abstract class Action { } @Override - protected void doActionImpl(SimpleExoPlayer player, MappingTrackSelector trackSelector, - Surface surface) { + protected void doActionImpl( + SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) { player.setShuffleModeEnabled(shuffleModeEnabled); } } @@ -407,7 +410,7 @@ public abstract class Action { @Override protected void doActionImpl( - final SimpleExoPlayer player, MappingTrackSelector trackSelector, Surface surface) { + final SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) { if (target instanceof PlayerTarget) { ((PlayerTarget) target).setPlayer(player); } @@ -440,8 +443,8 @@ public abstract class Action { } @Override - protected void doActionImpl(SimpleExoPlayer player, MappingTrackSelector trackSelector, - Surface surface) { + protected void doActionImpl( + SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) { player.setPlaybackParameters(playbackParameters); } @@ -463,7 +466,7 @@ public abstract class Action { @Override protected void doActionImpl( - SimpleExoPlayer player, MappingTrackSelector trackSelector, Surface surface) { + SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) { player .createMessage( new Target() { @@ -500,7 +503,7 @@ public abstract class Action { @Override protected void doActionAndScheduleNextImpl( final SimpleExoPlayer player, - final MappingTrackSelector trackSelector, + final DefaultTrackSelector trackSelector, final Surface surface, final HandlerWrapper handler, final ActionNode nextAction) { @@ -534,7 +537,7 @@ public abstract class Action { @Override protected void doActionImpl( - SimpleExoPlayer player, MappingTrackSelector trackSelector, Surface surface) { + SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) { // Not triggered. } } @@ -544,12 +547,16 @@ public abstract class Action { */ public static final class WaitForTimelineChanged extends Action { - private final Timeline expectedTimeline; + private final @Nullable Timeline expectedTimeline; /** + * Creates action waiting for a timeline change. + * * @param tag A tag to use for logging. + * @param expectedTimeline The expected timeline to wait for. If null, wait for any timeline + * change. */ - public WaitForTimelineChanged(String tag, Timeline expectedTimeline) { + public WaitForTimelineChanged(String tag, @Nullable Timeline expectedTimeline) { super(tag, "WaitForTimelineChanged"); this.expectedTimeline = expectedTimeline; } @@ -557,36 +564,36 @@ public abstract class Action { @Override protected void doActionAndScheduleNextImpl( final SimpleExoPlayer player, - final MappingTrackSelector trackSelector, + final DefaultTrackSelector trackSelector, final Surface surface, final HandlerWrapper handler, final ActionNode nextAction) { if (nextAction == null) { return; } - Player.EventListener listener = new Player.DefaultEventListener() { - @Override - public void onTimelineChanged(Timeline timeline, Object manifest, - @Player.TimelineChangeReason int reason) { - if (timeline.equals(expectedTimeline)) { - player.removeListener(this); - nextAction.schedule(player, trackSelector, surface, handler); - } - } - }; + Player.EventListener listener = + new Player.DefaultEventListener() { + @Override + public void onTimelineChanged( + Timeline timeline, Object manifest, @Player.TimelineChangeReason int reason) { + if (expectedTimeline == null || timeline.equals(expectedTimeline)) { + player.removeListener(this); + nextAction.schedule(player, trackSelector, surface, handler); + } + } + }; player.addListener(listener); - if (player.getCurrentTimeline().equals(expectedTimeline)) { + if (expectedTimeline != null && player.getCurrentTimeline().equals(expectedTimeline)) { player.removeListener(listener); nextAction.schedule(player, trackSelector, surface, handler); } } @Override - protected void doActionImpl(SimpleExoPlayer player, MappingTrackSelector trackSelector, - Surface surface) { + protected void doActionImpl( + SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) { // Not triggered. } - } /** @@ -604,7 +611,7 @@ public abstract class Action { @Override protected void doActionAndScheduleNextImpl( final SimpleExoPlayer player, - final MappingTrackSelector trackSelector, + final DefaultTrackSelector trackSelector, final Surface surface, final HandlerWrapper handler, final ActionNode nextAction) { @@ -621,11 +628,10 @@ public abstract class Action { } @Override - protected void doActionImpl(SimpleExoPlayer player, MappingTrackSelector trackSelector, - Surface surface) { + protected void doActionImpl( + SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) { // Not triggered. } - } /** @@ -647,7 +653,7 @@ public abstract class Action { @Override protected void doActionAndScheduleNextImpl( final SimpleExoPlayer player, - final MappingTrackSelector trackSelector, + final DefaultTrackSelector trackSelector, final Surface surface, final HandlerWrapper handler, final ActionNode nextAction) { @@ -670,11 +676,10 @@ public abstract class Action { } @Override - protected void doActionImpl(SimpleExoPlayer player, MappingTrackSelector trackSelector, - Surface surface) { + protected void doActionImpl( + SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) { // Not triggered. } - } /** @@ -692,7 +697,7 @@ public abstract class Action { @Override protected void doActionAndScheduleNextImpl( final SimpleExoPlayer player, - final MappingTrackSelector trackSelector, + final DefaultTrackSelector trackSelector, final Surface surface, final HandlerWrapper handler, final ActionNode nextAction) { @@ -709,11 +714,10 @@ public abstract class Action { } @Override - protected void doActionImpl(SimpleExoPlayer player, MappingTrackSelector trackSelector, - Surface surface) { + protected void doActionImpl( + SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) { // Not triggered. } - } /** @@ -732,8 +736,8 @@ public abstract class Action { } @Override - protected void doActionImpl(SimpleExoPlayer player, MappingTrackSelector trackSelector, - Surface surface) { + protected void doActionImpl( + SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) { if (runnable instanceof PlayerRunnable) { ((PlayerRunnable) runnable).setPlayer(player); } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java index 2ea8a50a84..74fa13ece1 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java @@ -45,7 +45,7 @@ import com.google.android.exoplayer2.testutil.Action.WaitForPlaybackState; import com.google.android.exoplayer2.testutil.Action.WaitForPositionDiscontinuity; import com.google.android.exoplayer2.testutil.Action.WaitForSeekProcessed; import com.google.android.exoplayer2.testutil.Action.WaitForTimelineChanged; -import com.google.android.exoplayer2.trackselection.MappingTrackSelector; +import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.HandlerWrapper; @@ -90,7 +90,7 @@ public final class ActionSchedule { */ /* package */ void start( SimpleExoPlayer player, - MappingTrackSelector trackSelector, + DefaultTrackSelector trackSelector, Surface surface, HandlerWrapper mainHandler, @Nullable Callback callback) { @@ -378,10 +378,11 @@ public final class ActionSchedule { /** * Schedules a delay until the timeline changed to a specified expected timeline. * - * @param expectedTimeline The expected timeline to wait for. + * @param expectedTimeline The expected timeline to wait for. If null, wait for any timeline + * change. * @return The builder, for convenience. */ - public Builder waitForTimelineChanged(Timeline expectedTimeline) { + public Builder waitForTimelineChanged(@Nullable Timeline expectedTimeline) { return apply(new WaitForTimelineChanged(tag, expectedTimeline)); } @@ -493,7 +494,7 @@ public final class ActionSchedule { private ActionNode next; private SimpleExoPlayer player; - private MappingTrackSelector trackSelector; + private DefaultTrackSelector trackSelector; private Surface surface; private HandlerWrapper mainHandler; @@ -537,7 +538,7 @@ public final class ActionSchedule { */ public void schedule( SimpleExoPlayer player, - MappingTrackSelector trackSelector, + DefaultTrackSelector trackSelector, Surface surface, HandlerWrapper mainHandler) { this.player = player; @@ -579,11 +580,10 @@ public final class ActionSchedule { } @Override - protected void doActionImpl(SimpleExoPlayer player, MappingTrackSelector trackSelector, - Surface surface) { + protected void doActionImpl( + SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) { // Do nothing. } - } /** @@ -604,7 +604,7 @@ public final class ActionSchedule { @Override protected void doActionAndScheduleNextImpl( SimpleExoPlayer player, - MappingTrackSelector trackSelector, + DefaultTrackSelector trackSelector, Surface surface, HandlerWrapper handler, ActionNode nextAction) { @@ -622,10 +622,9 @@ public final class ActionSchedule { @Override protected void doActionImpl( - SimpleExoPlayer player, MappingTrackSelector trackSelector, Surface surface) { + SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) { // Not triggered. } - } } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/DebugRenderersFactory.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/DebugRenderersFactory.java index 392a4907d4..4bbfef6bb8 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/DebugRenderersFactory.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/DebugRenderersFactory.java @@ -42,9 +42,8 @@ import java.util.ArrayList; @TargetApi(16) public class DebugRenderersFactory extends DefaultRenderersFactory { - public DebugRenderersFactory(Context context, - DrmSessionManager drmSessionManager) { - super(context, drmSessionManager, DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF, 0); + public DebugRenderersFactory(Context context) { + super(context, DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF, 0); } @Override diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java index 9071bef01d..5c8e87d38f 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java @@ -76,7 +76,7 @@ public abstract class ExoHostedTest extends Player.DefaultEventListener implemen private ActionSchedule pendingSchedule; private HandlerWrapper actionHandler; - private MappingTrackSelector trackSelector; + private DefaultTrackSelector trackSelector; private SimpleExoPlayer player; private Surface surface; private ExoPlaybackException playerError; @@ -359,8 +359,8 @@ public abstract class ExoHostedTest extends Player.DefaultEventListener implemen } @SuppressWarnings("unused") - protected MappingTrackSelector buildTrackSelector(HostActivity host, - BandwidthMeter bandwidthMeter) { + protected DefaultTrackSelector buildTrackSelector( + HostActivity host, BandwidthMeter bandwidthMeter) { return new DefaultTrackSelector(new AdaptiveTrackSelection.Factory(bandwidthMeter)); } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java index 8f06b82674..cf7470b80a 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java @@ -28,13 +28,16 @@ import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.RenderersFactory; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.analytics.AnalyticsCollector; +import com.google.android.exoplayer2.analytics.AnalyticsListener; import com.google.android.exoplayer2.audio.AudioRendererEventListener; +import com.google.android.exoplayer2.drm.DrmSessionManager; +import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.google.android.exoplayer2.metadata.MetadataOutput; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.text.TextOutput; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; -import com.google.android.exoplayer2.trackselection.MappingTrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.trackselection.TrackSelector; import com.google.android.exoplayer2.util.Clock; @@ -76,7 +79,7 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener private Timeline timeline; private Object manifest; private MediaSource mediaSource; - private MappingTrackSelector trackSelector; + private DefaultTrackSelector trackSelector; private LoadControl loadControl; private Format[] supportedFormats; private Renderer[] renderers; @@ -85,6 +88,7 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener private Player.EventListener eventListener; private VideoRendererEventListener videoRendererEventListener; private AudioRendererEventListener audioRendererEventListener; + private AnalyticsListener analyticsListener; private Integer expectedPlayerEndedCount; /** @@ -135,13 +139,13 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener } /** - * Sets a {@link MappingTrackSelector} to be used by the test runner. The default value is a - * {@link DefaultTrackSelector}. + * Sets a {@link DefaultTrackSelector} to be used by the test runner. The default value is a + * {@link DefaultTrackSelector} in its initial configuration. * - * @param trackSelector A {@link MappingTrackSelector} to be used by the test runner. + * @param trackSelector A {@link DefaultTrackSelector} to be used by the test runner. * @return This builder. */ - public Builder setTrackSelector(MappingTrackSelector trackSelector) { + public Builder setTrackSelector(DefaultTrackSelector trackSelector) { this.trackSelector = trackSelector; return this; } @@ -260,6 +264,17 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener return this; } + /** + * Sets an {@link AnalyticsListener} to be registered. + * + * @param analyticsListener An {@link AnalyticsListener} to be registered. + * @return This builder. + */ + public Builder setAnalyticsListener(AnalyticsListener analyticsListener) { + this.analyticsListener = analyticsListener; + return this; + } + /** * Sets the number of times the test runner is expected to reach the {@link Player#STATE_ENDED} * or {@link Player#STATE_IDLE}. The default is 1. This affects how long @@ -298,7 +313,8 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener VideoRendererEventListener videoRendererEventListener, AudioRendererEventListener audioRendererEventListener, TextOutput textRendererOutput, - MetadataOutput metadataRendererOutput) { + MetadataOutput metadataRendererOutput, + DrmSessionManager drmSessionManager) { return renderers; } }; @@ -328,6 +344,7 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener eventListener, videoRendererEventListener, audioRendererEventListener, + analyticsListener, expectedPlayerEndedCount); } } @@ -335,12 +352,13 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener private final Clock clock; private final MediaSource mediaSource; private final RenderersFactory renderersFactory; - private final MappingTrackSelector trackSelector; + private final DefaultTrackSelector trackSelector; private final LoadControl loadControl; private final @Nullable ActionSchedule actionSchedule; private final @Nullable Player.EventListener eventListener; private final @Nullable VideoRendererEventListener videoRendererEventListener; private final @Nullable AudioRendererEventListener audioRendererEventListener; + private final @Nullable AnalyticsListener analyticsListener; private final HandlerThread playerThread; private final HandlerWrapper handler; @@ -361,12 +379,13 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener Clock clock, MediaSource mediaSource, RenderersFactory renderersFactory, - MappingTrackSelector trackSelector, + DefaultTrackSelector trackSelector, LoadControl loadControl, @Nullable ActionSchedule actionSchedule, @Nullable Player.EventListener eventListener, @Nullable VideoRendererEventListener videoRendererEventListener, @Nullable AudioRendererEventListener audioRendererEventListener, + @Nullable AnalyticsListener analyticsListener, int expectedPlayerEndedCount) { this.clock = clock; this.mediaSource = mediaSource; @@ -377,6 +396,7 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener this.eventListener = eventListener; this.videoRendererEventListener = videoRendererEventListener; this.audioRendererEventListener = audioRendererEventListener; + this.analyticsListener = analyticsListener; this.timelines = new ArrayList<>(); this.manifests = new ArrayList<>(); this.timelineChangeReasons = new ArrayList<>(); @@ -415,6 +435,9 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener if (audioRendererEventListener != null) { player.addAudioDebugListener(audioRendererEventListener); } + if (analyticsListener != null) { + player.addAnalyticsListener(analyticsListener); + } player.setPlayWhenReady(true); if (actionSchedule != null) { actionSchedule.start( @@ -634,7 +657,13 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener TrackSelector trackSelector, LoadControl loadControl, Clock clock) { - super(renderersFactory, trackSelector, loadControl, clock); + super( + renderersFactory, + trackSelector, + loadControl, + /* drmSessionManager= */ null, + new AnalyticsCollector.Factory(), + clock); } } } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java index d32dda65f4..1008c0d561 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java @@ -35,7 +35,6 @@ import java.util.List; public class FakeAdaptiveMediaPeriod extends FakeMediaPeriod implements SequenceableLoader.Callback> { - private final EventDispatcher eventDispatcher; private final Allocator allocator; private final FakeChunkSource.Factory chunkSourceFactory; private final long durationUs; @@ -50,8 +49,7 @@ public class FakeAdaptiveMediaPeriod extends FakeMediaPeriod Allocator allocator, FakeChunkSource.Factory chunkSourceFactory, long durationUs) { - super(trackGroupArray); - this.eventDispatcher = eventDispatcher; + super(trackGroupArray, eventDispatcher); this.allocator = allocator; this.chunkSourceFactory = chunkSourceFactory; this.durationUs = durationUs; @@ -60,10 +58,10 @@ public class FakeAdaptiveMediaPeriod extends FakeMediaPeriod @Override public void release() { - super.release(); for (ChunkSampleStream sampleStream : sampleStreams) { sampleStream.release(); } + super.release(); } @Override diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaSource.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaSource.java index fbb2a83027..41488b2a3b 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaSource.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaSource.java @@ -30,7 +30,6 @@ import com.google.android.exoplayer2.upstream.Allocator; */ public class FakeAdaptiveMediaSource extends FakeMediaSource { - private final EventDispatcher eventDispatcher; private final FakeChunkSource.Factory chunkSourceFactory; public FakeAdaptiveMediaSource( @@ -41,16 +40,19 @@ public class FakeAdaptiveMediaSource extends FakeMediaSource { MediaSourceEventListener eventListener, FakeChunkSource.Factory chunkSourceFactory) { super(timeline, manifest, trackGroupArray); - this.eventDispatcher = new EventDispatcher(eventHandler, eventListener); this.chunkSourceFactory = chunkSourceFactory; + addEventListener(eventHandler, eventListener); } @Override - protected FakeMediaPeriod createFakeMediaPeriod(MediaPeriodId id, TrackGroupArray trackGroupArray, - Allocator allocator) { + protected FakeMediaPeriod createFakeMediaPeriod( + MediaPeriodId id, + TrackGroupArray trackGroupArray, + Allocator allocator, + EventDispatcher eventDispatcher) { Period period = timeline.getPeriod(id.periodIndex, new Period()); - return new FakeAdaptiveMediaPeriod(trackGroupArray, eventDispatcher, allocator, - chunkSourceFactory, period.durationUs); + return new FakeAdaptiveMediaPeriod( + trackGroupArray, eventDispatcher, allocator, chunkSourceFactory, period.durationUs); } } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java index ae2ed0d824..a251bd5ef0 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java @@ -17,50 +17,64 @@ package com.google.android.exoplayer2.testutil; import static com.google.common.truth.Truth.assertThat; +import android.net.Uri; import android.os.Handler; +import android.os.SystemClock; import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.source.MediaPeriod; +import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.TrackSelection; +import com.google.android.exoplayer2.upstream.DataSpec; import java.io.IOException; /** * Fake {@link MediaPeriod} that provides tracks from the given {@link TrackGroupArray}. Selecting - * tracks will give the player {@link FakeSampleStream}s. + * tracks will give the player {@link FakeSampleStream}s. Loading data completes immediately after + * the period has finished preparing. */ public class FakeMediaPeriod implements MediaPeriod { + public static final DataSpec FAKE_DATA_SPEC = new DataSpec(Uri.parse("http://fake.uri")); + private final TrackGroupArray trackGroupArray; + protected final EventDispatcher eventDispatcher; @Nullable private Handler playerHandler; @Nullable private Callback prepareCallback; private boolean deferOnPrepared; + private boolean notifiedReadingStarted; private boolean prepared; private long seekOffsetUs; private long discontinuityPositionUs; /** * @param trackGroupArray The track group array. + * @param eventDispatcher A dispatcher for media source events. */ - public FakeMediaPeriod(TrackGroupArray trackGroupArray) { - this(trackGroupArray, false); + public FakeMediaPeriod(TrackGroupArray trackGroupArray, EventDispatcher eventDispatcher) { + this(trackGroupArray, eventDispatcher, /* deferOnPrepared */ false); } /** * @param trackGroupArray The track group array. + * @param eventDispatcher A dispatcher for media source events. * @param deferOnPrepared Whether {@link MediaPeriod.Callback#onPrepared(MediaPeriod)} should be * called only after {@link #setPreparationComplete()} has been called. If {@code false} * preparation completes immediately. */ - public FakeMediaPeriod(TrackGroupArray trackGroupArray, boolean deferOnPrepared) { + public FakeMediaPeriod( + TrackGroupArray trackGroupArray, EventDispatcher eventDispatcher, boolean deferOnPrepared) { this.trackGroupArray = trackGroupArray; + this.eventDispatcher = eventDispatcher; this.deferOnPrepared = deferOnPrepared; discontinuityPositionUs = C.TIME_UNSET; + eventDispatcher.mediaPeriodCreated(); } /** @@ -79,13 +93,13 @@ public class FakeMediaPeriod implements MediaPeriod { public synchronized void setPreparationComplete() { deferOnPrepared = false; if (playerHandler != null && prepareCallback != null) { - playerHandler.post(new Runnable() { - @Override - public void run() { - prepared = true; - prepareCallback.onPrepared(FakeMediaPeriod.this); - } - }); + playerHandler.post( + new Runnable() { + @Override + public void run() { + finishPreparation(); + } + }); } } @@ -100,16 +114,26 @@ public class FakeMediaPeriod implements MediaPeriod { public void release() { prepared = false; + eventDispatcher.mediaPeriodReleased(); } @Override public synchronized void prepare(Callback callback, long positionUs) { + eventDispatcher.loadStarted( + FAKE_DATA_SPEC, + C.DATA_TYPE_MEDIA, + C.TRACK_TYPE_UNKNOWN, + /* trackFormat= */ null, + C.SELECTION_REASON_UNKNOWN, + /* trackSelectionData= */ null, + /* mediaStartTimeUs= */ 0, + /* mediaEndTimeUs = */ C.TIME_UNSET, + SystemClock.elapsedRealtime()); + prepareCallback = callback; if (deferOnPrepared) { playerHandler = new Handler(); - prepareCallback = callback; } else { - prepared = true; - callback.onPrepared(this); + finishPreparation(); } } @@ -161,6 +185,10 @@ public class FakeMediaPeriod implements MediaPeriod { @Override public long readDiscontinuity() { assertThat(prepared).isTrue(); + if (!notifiedReadingStarted) { + eventDispatcher.readingStarted(); + notifiedReadingStarted = true; + } long positionDiscontinuityUs = this.discontinuityPositionUs; this.discontinuityPositionUs = C.TIME_UNSET; return positionDiscontinuityUs; @@ -191,12 +219,28 @@ public class FakeMediaPeriod implements MediaPeriod { @Override public boolean continueLoading(long positionUs) { - assertThat(prepared).isTrue(); return false; } protected SampleStream createSampleStream(TrackSelection selection) { - return new FakeSampleStream(selection.getSelectedFormat()); + return new FakeSampleStream( + selection.getSelectedFormat(), eventDispatcher, /* shouldOutputSample= */ true); } + private void finishPreparation() { + prepared = true; + prepareCallback.onPrepared(this); + eventDispatcher.loadCompleted( + FAKE_DATA_SPEC, + C.DATA_TYPE_MEDIA, + C.TRACK_TYPE_UNKNOWN, + /* trackFormat= */ null, + C.SELECTION_REASON_UNKNOWN, + /* trackSelectionData= */ null, + /* mediaStartTimeUs= */ 0, + /* mediaEndTimeUs = */ C.TIME_UNSET, + SystemClock.elapsedRealtime(), + /* loadDurationMs= */ 0, + /* bytesLoaded= */ 100); + } } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java index da81bbb62c..905adae092 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java @@ -17,26 +17,38 @@ package com.google.android.exoplayer2.testutil; import static com.google.common.truth.Truth.assertThat; +import android.net.Uri; import android.os.Handler; +import android.os.SystemClock; import android.support.annotation.Nullable; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.Timeline.Period; +import com.google.android.exoplayer2.source.BaseMediaSource; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; +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.upstream.Allocator; +import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; import java.util.ArrayList; import java.util.List; /** - * Fake {@link MediaSource} that provides a given timeline. Creating the period will return a - * {@link FakeMediaPeriod} with a {@link TrackGroupArray} using the given {@link Format}s. + * Fake {@link MediaSource} that provides a given timeline. Creating the period will return a {@link + * FakeMediaPeriod} with a {@link TrackGroupArray} using the given {@link Format}s. */ -public class FakeMediaSource implements MediaSource { +public class FakeMediaSource extends BaseMediaSource { + + private static final DataSpec FAKE_DATA_SPEC = new DataSpec(Uri.parse("http://manifest.uri")); + private static final int MANIFEST_LOAD_BYTES = 100; private final TrackGroupArray trackGroupArray; private final ArrayList activeMediaPeriods; @@ -46,7 +58,6 @@ public class FakeMediaSource implements MediaSource { private Object manifest; private boolean preparedSource; private boolean releasedSource; - private Listener listener; private Handler sourceInfoRefreshHandler; /** @@ -75,15 +86,13 @@ public class FakeMediaSource implements MediaSource { } @Override - public synchronized void prepareSource( - ExoPlayer player, boolean isTopLevelSource, Listener listener) { + public synchronized void prepareSourceInternal(ExoPlayer player, boolean isTopLevelSource) { assertThat(preparedSource).isFalse(); preparedSource = true; releasedSource = false; - this.listener = listener; sourceInfoRefreshHandler = new Handler(); if (timeline != null) { - listener.onSourceInfoRefreshed(this, timeline, manifest); + finishSourcePreparation(); } } @@ -97,7 +106,11 @@ public class FakeMediaSource implements MediaSource { assertThat(preparedSource).isTrue(); assertThat(releasedSource).isFalse(); Assertions.checkIndex(id.periodIndex, 0, timeline.getPeriodCount()); - FakeMediaPeriod mediaPeriod = createFakeMediaPeriod(id, trackGroupArray, allocator); + Period period = timeline.getPeriod(id.periodIndex, new Period()); + EventDispatcher eventDispatcher = + createEventDispatcher(period.windowIndex, id, period.getPositionInWindowMs()); + FakeMediaPeriod mediaPeriod = + createFakeMediaPeriod(id, trackGroupArray, allocator, eventDispatcher); activeMediaPeriods.add(mediaPeriod); createdMediaPeriods.add(id); return mediaPeriod; @@ -113,7 +126,7 @@ public class FakeMediaSource implements MediaSource { } @Override - public void releaseSource() { + public void releaseSourceInternal() { assertThat(preparedSource).isTrue(); assertThat(releasedSource).isFalse(); assertThat(activeMediaPeriods.isEmpty()).isTrue(); @@ -121,7 +134,6 @@ public class FakeMediaSource implements MediaSource { preparedSource = false; sourceInfoRefreshHandler.removeCallbacksAndMessages(null); sourceInfoRefreshHandler = null; - listener = null; } /** @@ -138,7 +150,7 @@ public class FakeMediaSource implements MediaSource { assertThat(preparedSource).isTrue(); timeline = newTimeline; manifest = newManifest; - listener.onSourceInfoRefreshed(FakeMediaSource.this, timeline, manifest); + finishSourcePreparation(); } }); } else { @@ -166,9 +178,49 @@ public class FakeMediaSource implements MediaSource { return createdMediaPeriods; } - protected FakeMediaPeriod createFakeMediaPeriod(MediaPeriodId id, TrackGroupArray trackGroupArray, - Allocator allocator) { - return new FakeMediaPeriod(trackGroupArray); + /** + * Creates a {@link FakeMediaPeriod} for this media source. + * + * @param id The identifier of the period. + * @param trackGroupArray The {@link TrackGroupArray} supported by the media period. + * @param allocator An {@link Allocator} from which to obtain media buffer allocations. + * @param eventDispatcher An {@link EventDispatcher} to dispatch media source events. + * @return A new {@link FakeMediaPeriod}. + */ + protected FakeMediaPeriod createFakeMediaPeriod( + MediaPeriodId id, + TrackGroupArray trackGroupArray, + Allocator allocator, + EventDispatcher eventDispatcher) { + return new FakeMediaPeriod(trackGroupArray, eventDispatcher); + } + + private void finishSourcePreparation() { + refreshSourceInfo(timeline, manifest); + if (!timeline.isEmpty()) { + MediaLoadData mediaLoadData = + new MediaLoadData( + C.DATA_TYPE_MANIFEST, + C.TRACK_TYPE_UNKNOWN, + /* trackFormat= */ null, + C.SELECTION_REASON_UNKNOWN, + /* trackSelectionData= */ null, + /* mediaStartTimeMs= */ C.TIME_UNSET, + /* mediaEndTimeMs = */ C.TIME_UNSET); + long elapsedRealTimeMs = SystemClock.elapsedRealtime(); + EventDispatcher eventDispatcher = createEventDispatcher(/* mediaPeriodId= */ null); + eventDispatcher.loadStarted( + new LoadEventInfo( + FAKE_DATA_SPEC, elapsedRealTimeMs, /* loadDurationMs= */ 0, /* bytesLoaded= */ 0), + mediaLoadData); + eventDispatcher.loadCompleted( + new LoadEventInfo( + FAKE_DATA_SPEC, + elapsedRealTimeMs, + /* loadDurationMs= */ 0, + /* bytesLoaded= */ MANIFEST_LOAD_BYTES), + mediaLoadData); + } } private static TrackGroupArray buildTrackGroupArray(Format... formats) { diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeRenderer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeRenderer.java index 171e237fd1..0d65d7fcc7 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeRenderer.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeRenderer.java @@ -85,6 +85,7 @@ public class FakeRenderer extends BaseRenderer { if (result == C.RESULT_FORMAT_READ) { formatReadCount++; assertThat(expectedFormats).contains(formatHolder.format); + onFormatChanged(formatHolder.format); } else if (result == C.RESULT_BUFFER_READ) { if (buffer.isEndOfStream()) { isEnded = true; @@ -92,6 +93,7 @@ public class FakeRenderer extends BaseRenderer { } lastSamplePositionUs = buffer.timeUs; sampleBufferReadCount++; + onBufferRead(); } else { Assertions.checkState(result == C.RESULT_NOTHING_READ); return; @@ -115,4 +117,9 @@ public class FakeRenderer extends BaseRenderer { ? (FORMAT_HANDLED | ADAPTIVE_SEAMLESS) : FORMAT_UNSUPPORTED_TYPE; } + /** Called when the renderer reads a new format. */ + protected void onFormatChanged(Format format) {} + + /** Called when the renderer read a sample from the buffer. */ + protected void onBufferRead() {} } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSampleStream.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSampleStream.java index 228169b6b3..0575b261a9 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSampleStream.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSampleStream.java @@ -15,10 +15,12 @@ */ package com.google.android.exoplayer2.testutil; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; +import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.SampleStream; import java.io.IOException; @@ -29,16 +31,23 @@ import java.io.IOException; public final class FakeSampleStream implements SampleStream { private final Format format; + private final @Nullable EventDispatcher eventDispatcher; private boolean readFormat; private boolean readSample; - public FakeSampleStream(Format format) { - this(format, true); - } - - public FakeSampleStream(Format format, boolean shouldOutputSample) { + /** + * Creates fake sample stream which outputs the given {@link Format}, optionally one sample with + * zero bytes, then end of stream. + * + * @param format The {@link Format} to output. + * @param eventDispatcher An {@link EventDispatcher} to notify of read events. + * @param shouldOutputSample Whether the sample stream should output a sample. + */ + public FakeSampleStream( + Format format, @Nullable EventDispatcher eventDispatcher, boolean shouldOutputSample) { this.format = format; + this.eventDispatcher = eventDispatcher; readSample = !shouldOutputSample; } @@ -60,6 +69,14 @@ public final class FakeSampleStream implements SampleStream { buffer.data.put((byte) 0); buffer.flip(); readSample = true; + if (eventDispatcher != null) { + eventDispatcher.downstreamFormatChanged( + C.TRACK_TYPE_UNKNOWN, + format, + C.SELECTION_REASON_UNKNOWN, + /* trackSelectionData= */ null, + /* mediaTimeUs= */ 0); + } return C.RESULT_BUFFER_READ; } else { buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM); diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java index 7b27d3bd80..4e118366d7 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java @@ -42,14 +42,6 @@ public final class FakeTimeline extends Timeline { public final long durationUs; public final AdPlaybackState adPlaybackState; - /** - * Creates a seekable, non-dynamic window definition with one period with a duration of - * {@link #DEFAULT_WINDOW_DURATION_US}. - */ - public TimelineWindowDefinition() { - this(1, 0, true, false, DEFAULT_WINDOW_DURATION_US); - } - /** * Creates a seekable, non-dynamic window definition with a duration of * {@link #DEFAULT_WINDOW_DURATION_US}. @@ -172,13 +164,21 @@ public final class FakeTimeline extends Timeline { } @Override - public Window getWindow(int windowIndex, Window window, boolean setIds, - long defaultPositionProjectionUs) { + public Window getWindow( + int windowIndex, Window window, boolean setTag, long defaultPositionProjectionUs) { TimelineWindowDefinition windowDefinition = windowDefinitions[windowIndex]; - Object id = setIds ? windowDefinition.id : null; - return window.set(id, C.TIME_UNSET, C.TIME_UNSET, windowDefinition.isSeekable, - windowDefinition.isDynamic, 0, windowDefinition.durationUs, periodOffsets[windowIndex], - periodOffsets[windowIndex + 1] - 1, 0); + Object tag = setTag ? windowDefinition.id : null; + return window.set( + tag, + /* presentationStartTimeMs= */ C.TIME_UNSET, + /* windowStartTimeMs= */ C.TIME_UNSET, + windowDefinition.isSeekable, + windowDefinition.isDynamic, + /* defaultPositionUs= */ 0, + windowDefinition.durationUs, + periodOffsets[windowIndex], + periodOffsets[windowIndex + 1] - 1, + /* positionInFirstPeriodUs= */ 0); } @Override @@ -217,7 +217,9 @@ public final class FakeTimeline extends Timeline { private static TimelineWindowDefinition[] createDefaultWindowDefinitions(int windowCount) { TimelineWindowDefinition[] windowDefinitions = new TimelineWindowDefinition[windowCount]; - Arrays.fill(windowDefinitions, new TimelineWindowDefinition()); + for (int i = 0; i < windowCount; i++) { + windowDefinitions[i] = new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ i); + } return windowDefinitions; } diff --git a/testutils/src/test/AndroidManifest.xml b/testutils/src/test/AndroidManifest.xml index bea920852c..e30ea1c3ca 100644 --- a/testutils/src/test/AndroidManifest.xml +++ b/testutils/src/test/AndroidManifest.xml @@ -14,9 +14,4 @@ limitations under the License. --> - - - - - + 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/DummyMainThread.java b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/DummyMainThread.java new file mode 100644 index 0000000000..8f65dc876a --- /dev/null +++ b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/DummyMainThread.java @@ -0,0 +1,77 @@ +/* + * 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.testutil; + +import static com.google.common.truth.Truth.assertThat; + +import android.os.ConditionVariable; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; + +/** Helper class to simulate main/UI thread in tests. */ +public final class DummyMainThread { + + /** Default timeout value used for {@link #runOnMainThread(Runnable)}. */ + public static final int TIMEOUT_MS = 10000; + + private final HandlerThread thread; + private final Handler handler; + + public DummyMainThread() { + thread = new HandlerThread("DummyMainThread"); + thread.start(); + handler = new Handler(thread.getLooper()); + } + + /** + * Runs the provided {@link Runnable} on the main thread, blocking until execution completes or + * until {@link #TIMEOUT_MS} milliseconds have passed. + * + * @param runnable The {@link Runnable} to run. + */ + public void runOnMainThread(final Runnable runnable) { + runOnMainThread(TIMEOUT_MS, runnable); + } + + /** + * Runs the provided {@link Runnable} on the main thread, blocking until execution completes or + * until timeout milliseconds have passed. + * + * @param timeoutMs the maximum time to wait in milliseconds. + * @param runnable The {@link Runnable} to run. + */ + public void runOnMainThread(int timeoutMs, final Runnable runnable) { + if (Looper.myLooper() == handler.getLooper()) { + runnable.run(); + } else { + final ConditionVariable finishedCondition = new ConditionVariable(); + handler.post( + new Runnable() { + @Override + public void run() { + runnable.run(); + finishedCondition.open(); + } + }); + assertThat(finishedCondition.block(timeoutMs)).isTrue(); + } + } + + public void release() { + thread.quit(); + } +} 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 2daafbbb0b..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,18 +17,18 @@ 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; import com.google.android.exoplayer2.trackselection.MappingTrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelection; import java.util.ArrayList; import java.util.List; /** A fake {@link MappingTrackSelector} that returns {@link FakeTrackSelection}s. */ -public class FakeTrackSelector extends MappingTrackSelector { +public class FakeTrackSelector extends DefaultTrackSelector { - private final List selectedTrackSelections = new ArrayList<>(); + private final List trackSelections = new ArrayList<>(); private final boolean mayReuseTrackSelection; public FakeTrackSelector() { @@ -45,39 +45,38 @@ public class FakeTrackSelector extends MappingTrackSelector { } @Override - protected TrackSelection[] selectTracks( - RendererCapabilities[] rendererCapabilities, - TrackGroupArray[] rendererTrackGroupArrays, - int[][][] rendererFormatSupports) + protected TrackSelection[] selectAllTracks( + MappedTrackInfo mappedTrackInfo, + int[][][] rendererFormatSupports, + int[] rendererMixedMimeTypeAdaptationSupports, + Parameters params) throws ExoPlaybackException { - List resultList = new ArrayList<>(); - for (TrackGroupArray trackGroupArray : rendererTrackGroupArrays) { - TrackGroup trackGroup = trackGroupArray.get(0); - FakeTrackSelection trackSelectionForRenderer = reuseOrCreateTrackSelection(trackGroup); - resultList.add(trackSelectionForRenderer); + TrackSelection[] selections = new TrackSelection[mappedTrackInfo.length]; + for (int i = 0; i < mappedTrackInfo.length; i++) { + TrackGroupArray trackGroupArray = mappedTrackInfo.getTrackGroups(i); + boolean hasTracks = trackGroupArray.length > 0; + selections[i] = hasTracks ? reuseOrCreateTrackSelection(trackGroupArray.get(0)) : null; } - return resultList.toArray(new TrackSelection[resultList.size()]); + return selections; } @NonNull private FakeTrackSelection reuseOrCreateTrackSelection(TrackGroup trackGroup) { - FakeTrackSelection trackSelectionForRenderer = null; if (mayReuseTrackSelection) { - for (FakeTrackSelection selectedTrackSelection : selectedTrackSelections) { - if (selectedTrackSelection.getTrackGroup().equals(trackGroup)) { - trackSelectionForRenderer = selectedTrackSelection; + for (FakeTrackSelection trackSelection : trackSelections) { + if (trackSelection.getTrackGroup().equals(trackGroup)) { + return trackSelection; } } } - if (trackSelectionForRenderer == null) { - trackSelectionForRenderer = new FakeTrackSelection(trackGroup); - selectedTrackSelections.add(trackSelectionForRenderer); - } - return trackSelectionForRenderer; + FakeTrackSelection trackSelection = new FakeTrackSelection(trackGroup); + trackSelections.add(trackSelection); + return trackSelection; } /** Returns list of all {@link FakeTrackSelection}s that this track selector has made so far. */ - public List getSelectedTrackSelections() { - return selectedTrackSelections; + public List getAllTrackSelections() { + return trackSelections; } + } diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java index fbb48c9529..ee3a3a2d32 100644 --- a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java +++ b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.testutil; import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; import static org.junit.Assert.fail; import android.os.ConditionVariable; @@ -23,19 +24,30 @@ import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; import android.os.Message; +import android.support.annotation.Nullable; +import android.util.Pair; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.PlayerMessage; import com.google.android.exoplayer2.Timeline; 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.util.Assertions; import com.google.android.exoplayer2.util.Util; import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CountDownLatch; import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; /** A runner for {@link MediaSource} tests. */ public class MediaSourceTestRunner { @@ -50,6 +62,10 @@ public class MediaSourceTestRunner { private final Allocator allocator; private final LinkedBlockingDeque timelines; + private final CopyOnWriteArrayList> completedLoads; + private final AtomicReference lastCreatedMediaPeriod; + private final AtomicReference lastReleasedMediaPeriod; + private Timeline timeline; /** @@ -66,6 +82,10 @@ public class MediaSourceTestRunner { player = new EventHandlingExoPlayer(playbackLooper); mediaSourceListener = new MediaSourceListener(); timelines = new LinkedBlockingDeque<>(); + completedLoads = new CopyOnWriteArrayList<>(); + lastCreatedMediaPeriod = new AtomicReference<>(); + lastReleasedMediaPeriod = new AtomicReference<>(); + mediaSource.addEventListener(playbackHandler, mediaSourceListener); } /** @@ -193,13 +213,16 @@ public class MediaSourceTestRunner { }); } - /** Calls {@link MediaSource#releaseSource()} on the playback thread. */ + /** + * Calls {@link MediaSource#releaseSource(MediaSource.SourceInfoRefreshListener)} on the playback + * thread. + */ public void releaseSource() { runOnPlaybackThread( new Runnable() { @Override public void run() { - mediaSource.releaseSource(); + mediaSource.releaseSource(mediaSourceListener); } }); } @@ -265,16 +288,62 @@ public class MediaSourceTestRunner { private void assertPrepareAndReleasePeriod(MediaPeriodId mediaPeriodId) throws InterruptedException { MediaPeriod mediaPeriod = createPeriod(mediaPeriodId); + assertThat(lastCreatedMediaPeriod.getAndSet(/* newValue= */ null)).isEqualTo(mediaPeriodId); CountDownLatch preparedCondition = preparePeriod(mediaPeriod, 0); assertThat(preparedCondition.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue(); // MediaSource is supposed to support multiple calls to createPeriod with the same id without an // intervening call to releasePeriod. MediaPeriod secondMediaPeriod = createPeriod(mediaPeriodId); + assertThat(lastCreatedMediaPeriod.getAndSet(/* newValue= */ null)).isEqualTo(mediaPeriodId); CountDownLatch secondPreparedCondition = preparePeriod(secondMediaPeriod, 0); assertThat(secondPreparedCondition.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue(); // Release the periods. releasePeriod(mediaPeriod); + assertThat(lastReleasedMediaPeriod.getAndSet(/* newValue= */ null)).isEqualTo(mediaPeriodId); releasePeriod(secondMediaPeriod); + assertThat(lastReleasedMediaPeriod.getAndSet(/* newValue= */ null)).isEqualTo(mediaPeriodId); + } + + /** + * Asserts that the media source reported completed loads via {@link + * MediaSourceEventListener#onLoadCompleted(int, MediaPeriodId, LoadEventInfo, MediaLoadData)} for + * each specified window index and a null period id. Also asserts that no other loads with media + * period id null are reported. + */ + public void assertCompletedManifestLoads(Integer... windowIndices) { + List expectedWindowIndices = new ArrayList<>(Arrays.asList(windowIndices)); + for (Pair windowIndexAndMediaPeriodId : completedLoads) { + if (windowIndexAndMediaPeriodId.second == null) { + boolean loadExpected = expectedWindowIndices.remove(windowIndexAndMediaPeriodId.first); + assertThat(loadExpected).isTrue(); + } + } + assertWithMessage("Not all expected media source loads have been completed.") + .that(expectedWindowIndices) + .isEmpty(); + } + + /** + * Asserts that the media source reported completed loads via {@link + * MediaSourceEventListener#onLoadCompleted(int, MediaPeriodId, LoadEventInfo, MediaLoadData)} for + * each specified media period id, and asserts that the associated window index matches the one in + * the last known timeline returned from {@link #prepareSource()}, {@link #assertTimelineChange()} + * or {@link #assertTimelineChangeBlocking()}. + */ + public void assertCompletedMediaPeriodLoads(MediaPeriodId... mediaPeriodIds) { + Timeline.Period period = new Timeline.Period(); + HashSet expectedLoads = new HashSet<>(Arrays.asList(mediaPeriodIds)); + for (Pair windowIndexAndMediaPeriodId : completedLoads) { + int windowIndex = windowIndexAndMediaPeriodId.first; + MediaPeriodId mediaPeriodId = windowIndexAndMediaPeriodId.second; + if (expectedLoads.remove(mediaPeriodId)) { + assertThat(windowIndex) + .isEqualTo(timeline.getPeriod(mediaPeriodId.periodIndex, period).windowIndex); + } + } + assertWithMessage("Not all expected media source loads have been completed.") + .that(expectedLoads) + .isEmpty(); } /** Releases the runner. Should be called when the runner is no longer required. */ @@ -282,13 +351,86 @@ public class MediaSourceTestRunner { playbackThread.quit(); } - private class MediaSourceListener implements MediaSource.Listener { + private class MediaSourceListener + implements MediaSource.SourceInfoRefreshListener, MediaSourceEventListener { + + // SourceInfoRefreshListener methods. @Override public void onSourceInfoRefreshed(MediaSource source, Timeline timeline, Object manifest) { Assertions.checkState(Looper.myLooper() == playbackThread.getLooper()); timelines.addLast(timeline); } + + // MediaSourceEventListener methods. + + @Override + public void onMediaPeriodCreated(int windowIndex, MediaPeriodId mediaPeriodId) { + Assertions.checkState(Looper.myLooper() == playbackThread.getLooper()); + lastCreatedMediaPeriod.set(mediaPeriodId); + } + + @Override + public void onMediaPeriodReleased(int windowIndex, MediaPeriodId mediaPeriodId) { + Assertions.checkState(Looper.myLooper() == playbackThread.getLooper()); + lastReleasedMediaPeriod.set(mediaPeriodId); + } + + @Override + public void onLoadStarted( + int windowIndex, + @Nullable MediaPeriodId mediaPeriodId, + LoadEventInfo loadEventInfo, + MediaLoadData mediaLoadData) { + Assertions.checkState(Looper.myLooper() == playbackThread.getLooper()); + } + + @Override + public void onLoadCompleted( + int windowIndex, + @Nullable MediaPeriodId mediaPeriodId, + LoadEventInfo loadEventInfo, + MediaLoadData mediaLoadData) { + Assertions.checkState(Looper.myLooper() == playbackThread.getLooper()); + completedLoads.add(Pair.create(windowIndex, mediaPeriodId)); + } + + @Override + public void onLoadCanceled( + int windowIndex, + @Nullable MediaPeriodId mediaPeriodId, + LoadEventInfo loadEventInfo, + MediaLoadData mediaLoadData) { + Assertions.checkState(Looper.myLooper() == playbackThread.getLooper()); + } + + @Override + public void onLoadError( + int windowIndex, + @Nullable MediaPeriodId mediaPeriodId, + LoadEventInfo loadEventInfo, + MediaLoadData mediaLoadData, + IOException error, + boolean wasCanceled) { + Assertions.checkState(Looper.myLooper() == playbackThread.getLooper()); + } + + @Override + public void onReadingStarted(int windowIndex, MediaPeriodId mediaPeriodId) { + Assertions.checkState(Looper.myLooper() == playbackThread.getLooper()); + } + + @Override + public void onUpstreamDiscarded( + int windowIndex, @Nullable MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData) { + Assertions.checkState(Looper.myLooper() == playbackThread.getLooper()); + } + + @Override + public void onDownstreamFormatChanged( + int windowIndex, @Nullable MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData) { + Assertions.checkState(Looper.myLooper() == playbackThread.getLooper()); + } } private static class EventHandlingExoPlayer extends StubExoPlayer 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 af8b10e6d3..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 @@ -16,6 +16,8 @@ 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; @@ -62,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(); @@ -142,6 +149,11 @@ public abstract class StubExoPlayer implements ExoPlayer { throw new UnsupportedOperationException(); } + @Override + public @Nullable Object getCurrentTag() { + throw new UnsupportedOperationException(); + } + @Override public void stop() { throw new UnsupportedOperationException(); diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/TestDownloadManagerListener.java b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/TestDownloadManagerListener.java new file mode 100644 index 0000000000..b624c49350 --- /dev/null +++ b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/TestDownloadManagerListener.java @@ -0,0 +1,106 @@ +/* + * 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.testutil; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.android.exoplayer2.offline.DownloadAction; +import com.google.android.exoplayer2.offline.DownloadManager; +import java.util.HashMap; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +/** 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 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 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 + public synchronized void onIdle(DownloadManager downloadManager) { + if (downloadFinishedCondition != null) { + downloadFinishedCondition.countDown(); + } + } + + /** + * Blocks until all remove and download tasks are complete and throws an exception if there was an + * error. + */ + public void blockUntilTasksCompleteAndThrowAnyDownloadError() throws Throwable { + synchronized (this) { + downloadFinishedCondition = new CountDownLatch(1); + } + dummyMainThread.runOnMainThread( + new Runnable() { + @Override + public void run() { + if (downloadManager.isIdle()) { + downloadFinishedCondition.countDown(); + } + } + }); + assertThat(downloadFinishedCondition.await(TIMEOUT, TimeUnit.MILLISECONDS)).isTrue(); + if (downloadError != null) { + 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); + } + } +} diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java index abef8e06be..a0ca6af8a9 100644 --- a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java +++ b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java @@ -34,7 +34,7 @@ public final class TimelineAsserts { /** Assert that timeline is empty (i.e. has no windows or periods). */ public static void assertEmpty(Timeline timeline) { - assertWindowIds(timeline); + assertWindowTags(timeline); assertPeriodCounts(timeline); for (boolean shuffled : new boolean[] {false, true}) { assertThat(timeline.getFirstWindowIndex(shuffled)).isEqualTo(C.INDEX_UNSET); @@ -43,18 +43,18 @@ public final class TimelineAsserts { } /** - * Asserts that window IDs are set correctly. + * Asserts that window tags are set correctly. * - * @param expectedWindowIds A list of expected window IDs. If an ID is unknown or not important + * @param expectedWindowTags A list of expected window tags. If a tag is unknown or not important * {@code null} can be passed to skip this window. */ - public static void assertWindowIds(Timeline timeline, Object... expectedWindowIds) { + public static void assertWindowTags(Timeline timeline, Object... expectedWindowTags) { Window window = new Window(); - assertThat(timeline.getWindowCount()).isEqualTo(expectedWindowIds.length); + assertThat(timeline.getWindowCount()).isEqualTo(expectedWindowTags.length); for (int i = 0; i < timeline.getWindowCount(); i++) { timeline.getWindow(i, window, true); - if (expectedWindowIds[i] != null) { - assertThat(window.id).isEqualTo(expectedWindowIds[i]); + if (expectedWindowTags[i] != null) { + assertThat(window.tag).isEqualTo(expectedWindowTags[i]); } } } @@ -118,6 +118,7 @@ public final class TimelineAsserts { */ public static void assertPeriodCounts(Timeline timeline, int... expectedPeriodCounts) { int windowCount = timeline.getWindowCount(); + assertThat(windowCount).isEqualTo(expectedPeriodCounts.length); int[] accumulatedPeriodCounts = new int[windowCount + 1]; accumulatedPeriodCounts[0] = 0; for (int i = 0; i < windowCount; i++) {