diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 3c05471a89..3098bd9a76 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -39,6 +39,9 @@ * Scale up the initial video decoder maximum input size so playlist item transitions with small increases in maximum sample size don't require reinitialization ([#4510](https://github.com/google/ExoPlayer/issues/4510)). + * Propagate the end-of-stream signal directly in the renderer when using + tunneling, to fix an issue where the player would remain ready after the + stream ended. * Allow apps to pass a `CacheKeyFactory` for setting custom cache keys when creating a `CacheDataSource`. * Turned on Java 8 compiler support for the ExoPlayer library. Apps that depend @@ -70,6 +73,8 @@ * Allow configuration of the Loader retry delay ([#3370](https://github.com/google/ExoPlayer/issues/3370)). * HLS: + * Add support for variable substitution + ([#4422](https://github.com/google/ExoPlayer/issues/4422)). * Add support for PlayReady. * Add support for alternative EXT-X-KEY tags. * Set the bitrate on primary track sample formats @@ -91,7 +96,10 @@ * Allow setting the `Looper`, which is used to access the player, in `ExoPlayerFactory` ([#4278](https://github.com/google/ExoPlayer/issues/4278)). * Use default Deserializers if non given to DownloadManager. -* Add monoscopic 360 surface type to PlayerView. +* 360: + * Add monoscopic 360 surface type to PlayerView. + * Support + [VR180 video format](https://github.com/google/spatial-media/blob/master/docs/vr180.md). * Deprecate `Player.DefaultEventListener` as selective listener overrides can be directly made with the `Player.EventListener` interface. * Deprecate `DefaultAnalyticsListener` as selective listener overrides can be @@ -99,6 +107,10 @@ * Add uri field to `LoadEventInfo` in `MediaSourceEventListener` or `AnalyticsListener` callbacks. This uri is the redirected uri if redirection occurred ([#2054](https://github.com/google/ExoPlayer/issues/2054)). +* Add response headers field to `LoadEventInfo` in `MediaSourceEventListener` or + `AnalyticsListener` callbacks + ([#4361](https://github.com/google/ExoPlayer/issues/4361) and + [#4615](https://github.com/google/ExoPlayer/issues/4615)). * Allow `MediaCodecSelector`s to return multiple compatible decoders for `MediaCodecRenderer`, and provide an (optional) `MediaCodecSelector` that falls back to less preferred decoders like `MediaCodec.createDecoderByType` @@ -110,6 +122,14 @@ * Add option to show buffering view when playWhenReady is false ([#4304](https://github.com/google/ExoPlayer/issues/4304)). * Allow any `Drawable` to be used as `PlayerView` default artwork. +* IMA: + * Refine the previous fix for empty ad groups to avoid discarding ad breaks + unnecessarily ([#4030](https://github.com/google/ExoPlayer/issues/4030)), + ([#4280](https://github.com/google/ExoPlayer/issues/4280)). + * Fix handling of empty postrolls + ([#4681](https://github.com/google/ExoPlayer/issues/4681). + * Fix handling of postrolls with multiple ads + ([#4710](https://github.com/google/ExoPlayer/issues/4710). ### 2.8.4 ### diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java index 3e48ab2ab4..30968b8f85 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java @@ -30,8 +30,6 @@ import android.view.Menu; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; -import android.widget.AdapterView; -import android.widget.AdapterView.OnItemClickListener; import android.widget.ArrayAdapter; import android.widget.ListView; import android.widget.TextView; @@ -145,13 +143,9 @@ public class MainActivity extends AppCompatActivity implements OnClickListener, ListView sampleList = dialogList.findViewById(R.id.sample_list); sampleList.setAdapter(new SampleListAdapter(this)); sampleList.setOnItemClickListener( - new OnItemClickListener() { - - @Override - public void onItemClick(AdapterView parent, View view, int position, long id) { - playerManager.addItem(DemoUtil.SAMPLES.get(position)); - mediaQueueListAdapter.notifyItemInserted(playerManager.getMediaQueueSize() - 1); - } + (parent, view, position, id) -> { + playerManager.addItem(DemoUtil.SAMPLES.get(position)); + mediaQueueListAdapter.notifyItemInserted(playerManager.getMediaQueueSize() - 1); }); return dialogList; } 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 97e618ba52..d67c4549d8 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 @@ -76,9 +76,7 @@ import com.google.android.exoplayer2.util.Util; contentMediaSource, /* adMediaSourceFactory= */ this, adsLoader, - playerView.getOverlayFrameLayout(), - /* eventHandler= */ null, - /* eventListener= */ null); + playerView.getOverlayFrameLayout()); // Prepare the player with the source. player.seekTo(contentPosition); diff --git a/demos/main/src/main/AndroidManifest.xml b/demos/main/src/main/AndroidManifest.xml index 2234048ac1..e80e37688d 100644 --- a/demos/main/src/main/AndroidManifest.xml +++ b/demos/main/src/main/AndroidManifest.xml @@ -21,6 +21,7 @@ + diff --git a/demos/main/src/main/assets/media.exolist.json b/demos/main/src/main/assets/media.exolist.json index a366eeba05..732bb5f4f4 100644 --- a/demos/main/src/main/assets/media.exolist.json +++ b/demos/main/src/main/assets/media.exolist.json @@ -567,6 +567,11 @@ "uri": "https://storage.googleapis.com/exoplayer-test-media-1/360/congo.mp4", "spherical_stereo_mode": "top_bottom" }, + { + "name": "Sphericalv2 (180 top-bottom stereo)", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/360/sphericalv2.mp4", + "spherical_stereo_mode": "top_bottom" + }, { "name": "Iceland (360 top-bottom stereo ts)", "uri": "https://storage.googleapis.com/exoplayer-test-media-1/360/iceland0.ts", 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 index f20e41d8f7..be2dec71d5 100644 --- 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 @@ -175,14 +175,11 @@ public class DownloadTracker implements DownloadManager.Listener { } 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); - } + () -> { + try { + actionFile.store(actions); + } catch (IOException e) { + Log.e(TAG, "Failed to store tracked actions", e); } }); } 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 ad08fb990c..ffa9bafa4f 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 @@ -60,7 +60,7 @@ import com.google.android.exoplayer2.source.ads.AdsMediaSource; import com.google.android.exoplayer2.source.dash.DashMediaSource; import com.google.android.exoplayer2.source.dash.manifest.DashManifestParser; 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.DefaultHlsPlaylistParserFactory; import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifestParser; import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection; @@ -190,7 +190,7 @@ public class PlayerActivity extends Activity finish(); return; } - ((SphericalSurfaceView) playerView.getVideoSurfaceView()).setStereoMode(stereoMode); + ((SphericalSurfaceView) playerView.getVideoSurfaceView()).setDefaultStereoMode(stereoMode); } if (savedInstanceState != null) { @@ -490,8 +490,8 @@ public class PlayerActivity extends Activity .createMediaSource(uri); case C.TYPE_HLS: return new HlsMediaSource.Factory(dataSourceFactory) - .setPlaylistParser( - new FilteringManifestParser<>(new HlsPlaylistParser(), getOfflineStreamKeys(uri))) + .setPlaylistParserFactory( + new DefaultHlsPlaylistParserFactory(getOfflineStreamKeys(uri))) .createMediaSource(uri); case C.TYPE_OTHER: return new ExtractorMediaSource.Factory(dataSourceFactory).createMediaSource(uri); 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 f683e9900f..6817fab780 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 @@ -22,6 +22,7 @@ import android.content.res.AssetManager; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; +import android.support.annotation.Nullable; import android.util.JsonReader; import android.util.Log; import android.view.Menu; @@ -162,8 +163,8 @@ public class SampleChooserActivity extends Activity startActivity( sample.buildIntent( /* context= */ this, - preferExtensionDecodersMenuItem.isChecked(), - randomAbrMenuItem.isChecked() + isNonNullAndChecked(preferExtensionDecodersMenuItem), + isNonNullAndChecked(randomAbrMenuItem) ? PlayerActivity.ABR_ALGORITHM_RANDOM : PlayerActivity.ABR_ALGORITHM_DEFAULT)); return true; @@ -198,6 +199,11 @@ public class SampleChooserActivity extends Activity return 0; } + private static boolean isNonNullAndChecked(@Nullable MenuItem menuItem) { + // Temporary workaround for layouts that do not inflate the options menu. + return menuItem != null && menuItem.isChecked(); + } + private final class SampleListLoader extends AsyncTask> { private boolean sawError; @@ -207,7 +213,8 @@ public class SampleChooserActivity extends Activity List result = new ArrayList<>(); Context context = getApplicationContext(); String userAgent = Util.getUserAgent(context, "ExoPlayerDemo"); - DataSource dataSource = new DefaultDataSource(context, null, userAgent, false); + DataSource dataSource = + new DefaultDataSource(context, userAgent, /* allowCrossProtocolRedirects= */ false); for (String uri : uris) { DataSpec dataSpec = new DataSpec(Uri.parse(uri)); InputStream inputStream = new DataSourceInputStream(dataSource, dataSpec); diff --git a/extensions/cast/build.gradle b/extensions/cast/build.gradle index 35499f1c1d..bee73cac12 100644 --- a/extensions/cast/build.gradle +++ b/extensions/cast/build.gradle @@ -50,8 +50,6 @@ dependencies { api 'com.android.support:recyclerview-v7:' + supportLibraryVersion } -apply plugin: 'com.google.android.gms.strict-version-matcher-plugin' - ext { javadocTitle = 'Cast extension' } 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 21e853dd62..65ae097452 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 @@ -567,6 +567,11 @@ public final class CastPlayer implements Player { return C.INDEX_UNSET; } + @Override + public long getContentDuration() { + return getDuration(); + } + @Override public boolean isLoading() { return false; @@ -839,7 +844,6 @@ public final class CastPlayer implements Player { @Override public void onAdBreakStatusUpdated() {} - // SessionManagerListener implementation. @Override 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 fd6a3ce9ec..5c6f5dafd9 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 @@ -606,11 +606,9 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { if (request != currentUrlRequest) { return; } - if (currentDataSpec.postBody != null) { + if (currentDataSpec.httpMethod == DataSpec.HTTP_METHOD_POST) { int responseCode = info.getHttpStatusCode(); // The industry standard is to disregard POST redirects when the status code is 307 or 308. - // For other redirect response codes the POST request is converted to a GET request and the - // redirect is followed. if (responseCode == 307 || responseCode == 308) { exception = new InvalidResponseCodeException(responseCode, info.getAllHeaders(), currentDataSpec); @@ -627,7 +625,23 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { request.followRedirect(); } else { currentUrlRequest.cancel(); - DataSpec redirectUrlDataSpec = currentDataSpec.withUri(Uri.parse(newLocationUrl)); + DataSpec redirectUrlDataSpec; + if (currentDataSpec.httpMethod == DataSpec.HTTP_METHOD_POST) { + // For POST redirects that aren't 307 or 308, the redirect is followed but request is + // transformed into a GET. + redirectUrlDataSpec = + new DataSpec( + Uri.parse(newLocationUrl), + DataSpec.HTTP_METHOD_GET, + /* httpBody= */ null, + currentDataSpec.absoluteStreamPosition, + currentDataSpec.position, + currentDataSpec.length, + currentDataSpec.key, + currentDataSpec.flags); + } else { + redirectUrlDataSpec = currentDataSpec.withUri(Uri.parse(newLocationUrl)); + } UrlRequest.Builder requestBuilder; try { requestBuilder = buildRequestBuilder(redirectUrlDataSpec); 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 db1394c1d6..9257411e3c 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 @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.ext.cronet; import android.content.Context; 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; import java.lang.reflect.Field; @@ -39,7 +40,8 @@ public final class CronetEngineWrapper { private final @CronetEngineSource int cronetEngineSource; /** - * Source of {@link CronetEngine}. + * Source of {@link CronetEngine}. One of {@link #SOURCE_NATIVE}, {@link #SOURCE_GMS}, {@link + * #SOURCE_UNKNOWN}, {@link #SOURCE_USER_PROVIDED} or {@link #SOURCE_UNAVAILABLE}. */ @Retention(RetentionPolicy.SOURCE) @IntDef({SOURCE_NATIVE, SOURCE_GMS, SOURCE_UNKNOWN, SOURCE_USER_PROVIDED, SOURCE_UNAVAILABLE}) @@ -158,6 +160,8 @@ public final class CronetEngineWrapper { private final String gmsCoreCronetName; private final boolean preferGMSCoreCronet; + // Multi-catch can only be used for API 19+ in this case. + @SuppressWarnings("UseMultiCatch") public CronetProviderComparator(boolean preferGMSCoreCronet) { // GMSCore CronetProvider classes are only available in some configurations. // Thus, we use reflection to copy static name. @@ -218,8 +222,8 @@ public final class CronetEngineWrapper { if (versionLeft == null || versionRight == null) { return 0; } - String[] versionStringsLeft = versionLeft.split("\\."); - String[] versionStringsRight = versionRight.split("\\."); + String[] versionStringsLeft = Util.split(versionLeft, "\\."); + String[] versionStringsRight = Util.split(versionRight, "\\."); int minLength = Math.min(versionStringsLeft.length, versionStringsRight.length); for (int i = 0; i < minLength; i++) { if (!versionStringsLeft[i].equals(versionStringsRight[i])) { diff --git a/extensions/cronet/src/test/java/com/google/android/exoplayer2/ext/cronet/ByteArrayUploadDataProviderTest.java b/extensions/cronet/src/test/java/com/google/android/exoplayer2/ext/cronet/ByteArrayUploadDataProviderTest.java index 291e73fcc1..117518a1eb 100644 --- a/extensions/cronet/src/test/java/com/google/android/exoplayer2/ext/cronet/ByteArrayUploadDataProviderTest.java +++ b/extensions/cronet/src/test/java/com/google/android/exoplayer2/ext/cronet/ByteArrayUploadDataProviderTest.java @@ -60,7 +60,7 @@ public final class ByteArrayUploadDataProviderTest { @Test public void testReadPartialBuffer() throws IOException { - byte[] firstHalf = Arrays.copyOfRange(TEST_DATA, 0, TEST_DATA.length / 2); + byte[] firstHalf = Arrays.copyOf(TEST_DATA, TEST_DATA.length / 2); byte[] secondHalf = Arrays.copyOfRange(TEST_DATA, TEST_DATA.length / 2, TEST_DATA.length); byteBuffer = ByteBuffer.allocate(TEST_DATA.length / 2); // Read half of the data. diff --git a/extensions/cronet/src/test/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java b/extensions/cronet/src/test/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java index 3e2242826c..7d47b0da64 100644 --- a/extensions/cronet/src/test/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java +++ b/extensions/cronet/src/test/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java @@ -38,6 +38,7 @@ import com.google.android.exoplayer2.upstream.HttpDataSource.HttpDataSourceExcep import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.util.Clock; import com.google.android.exoplayer2.util.Predicate; +import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.net.SocketTimeoutException; import java.net.UnknownHostException; @@ -50,6 +51,7 @@ import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; import org.chromium.net.CronetEngine; import org.chromium.net.NetworkException; import org.chromium.net.UrlRequest; @@ -60,10 +62,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; import org.robolectric.RobolectricTestRunner; -import org.robolectric.shadows.ShadowSystemClock; /** Tests for {@link CronetDataSource}. */ @RunWith(RobolectricTestRunner.class) @@ -73,7 +72,7 @@ public final class CronetDataSourceTest { private static final int TEST_READ_TIMEOUT_MS = 100; private static final String TEST_URL = "http://google.com"; private static final String TEST_CONTENT_TYPE = "test/test"; - private static final byte[] TEST_POST_BODY = "test post body".getBytes(); + private static final byte[] TEST_POST_BODY = Util.getUtf8Bytes("test post body"); private static final long TEST_CONTENT_LENGTH = 16000L; private static final int TEST_CONNECTION_STATUS = 5; private static final int TEST_INVALID_CONNECTION_STATUS = -1; @@ -170,16 +169,13 @@ public final class CronetDataSourceTest { final UrlRequest mockUrlRequest2 = mock(UrlRequest.class); when(mockUrlRequestBuilder.build()).thenReturn(mockUrlRequest2); doAnswer( - new Answer() { - @Override - public Object answer(InvocationOnMock invocation) throws Throwable { - // Invoke the callback for the previous request. - dataSourceUnderTest.urlRequestCallback.onFailed( - mockUrlRequest, testUrlResponseInfo, mockNetworkException); - dataSourceUnderTest.urlRequestCallback.onResponseStarted( - mockUrlRequest2, testUrlResponseInfo); - return null; - } + invocation -> { + // Invoke the callback for the previous request. + dataSourceUnderTest.urlRequestCallback.onFailed( + mockUrlRequest, testUrlResponseInfo, mockNetworkException); + dataSourceUnderTest.urlRequestCallback.onResponseStarted( + mockUrlRequest2, testUrlResponseInfo); + return null; }) .when(mockUrlRequest2) .start(); @@ -582,10 +578,10 @@ public final class CronetDataSourceTest { // We should still be trying to open. assertNotCountedDown(timedOutLatch); // We should still be trying to open as we approach the timeout. - ShadowSystemClock.setCurrentTimeMillis(startTimeMs + TEST_CONNECT_TIMEOUT_MS - 1); + SystemClock.setCurrentTimeMillis(startTimeMs + TEST_CONNECT_TIMEOUT_MS - 1); assertNotCountedDown(timedOutLatch); // Now we timeout. - ShadowSystemClock.setCurrentTimeMillis(startTimeMs + TEST_CONNECT_TIMEOUT_MS + 10); + SystemClock.setCurrentTimeMillis(startTimeMs + TEST_CONNECT_TIMEOUT_MS + 10); timedOutLatch.await(); verify(mockTransferListener, never()) @@ -621,7 +617,7 @@ public final class CronetDataSourceTest { // We should still be trying to open. assertNotCountedDown(timedOutLatch); // We should still be trying to open as we approach the timeout. - ShadowSystemClock.setCurrentTimeMillis(startTimeMs + TEST_CONNECT_TIMEOUT_MS - 1); + SystemClock.setCurrentTimeMillis(startTimeMs + TEST_CONNECT_TIMEOUT_MS - 1); assertNotCountedDown(timedOutLatch); // Now we interrupt. thread.interrupt(); @@ -637,14 +633,16 @@ public final class CronetDataSourceTest { final ConditionVariable startCondition = buildUrlRequestStartedCondition(); final CountDownLatch openLatch = new CountDownLatch(1); + AtomicReference exceptionOnTestThread = new AtomicReference<>(); new Thread() { @Override public void run() { try { dataSourceUnderTest.open(testDataSpec); - openLatch.countDown(); } catch (HttpDataSourceException e) { - fail(); + exceptionOnTestThread.set(e); + } finally { + openLatch.countDown(); } } }.start(); @@ -653,11 +651,12 @@ public final class CronetDataSourceTest { // We should still be trying to open. assertNotCountedDown(openLatch); // We should still be trying to open as we approach the timeout. - ShadowSystemClock.setCurrentTimeMillis(startTimeMs + TEST_CONNECT_TIMEOUT_MS - 1); + SystemClock.setCurrentTimeMillis(startTimeMs + TEST_CONNECT_TIMEOUT_MS - 1); assertNotCountedDown(openLatch); // The response arrives just in time. dataSourceUnderTest.urlRequestCallback.onResponseStarted(mockUrlRequest, testUrlResponseInfo); openLatch.await(); + assertThat(exceptionOnTestThread.get()).isNull(); } @Test @@ -687,14 +686,14 @@ public final class CronetDataSourceTest { // We should still be trying to open. assertNotCountedDown(timedOutLatch); // We should still be trying to open as we approach the timeout. - ShadowSystemClock.setCurrentTimeMillis(startTimeMs + TEST_CONNECT_TIMEOUT_MS - 1); + SystemClock.setCurrentTimeMillis(startTimeMs + TEST_CONNECT_TIMEOUT_MS - 1); assertNotCountedDown(timedOutLatch); // A redirect arrives just in time. dataSourceUnderTest.urlRequestCallback.onRedirectReceived( mockUrlRequest, testUrlResponseInfo, "RandomRedirectedUrl1"); long newTimeoutMs = 2 * TEST_CONNECT_TIMEOUT_MS - 1; - ShadowSystemClock.setCurrentTimeMillis(startTimeMs + newTimeoutMs - 1); + SystemClock.setCurrentTimeMillis(startTimeMs + newTimeoutMs - 1); // We should still be trying to open as we approach the new timeout. assertNotCountedDown(timedOutLatch); // A redirect arrives just in time. @@ -702,11 +701,11 @@ public final class CronetDataSourceTest { mockUrlRequest, testUrlResponseInfo, "RandomRedirectedUrl2"); newTimeoutMs = 3 * TEST_CONNECT_TIMEOUT_MS - 2; - ShadowSystemClock.setCurrentTimeMillis(startTimeMs + newTimeoutMs - 1); + SystemClock.setCurrentTimeMillis(startTimeMs + newTimeoutMs - 1); // We should still be trying to open as we approach the new timeout. assertNotCountedDown(timedOutLatch); // Now we timeout. - ShadowSystemClock.setCurrentTimeMillis(startTimeMs + newTimeoutMs + 10); + SystemClock.setCurrentTimeMillis(startTimeMs + newTimeoutMs + 10); timedOutLatch.await(); verify(mockTransferListener, never()) @@ -900,14 +899,11 @@ public final class CronetDataSourceTest { private void mockStatusResponse() { doAnswer( - new Answer() { - @Override - public Object answer(InvocationOnMock invocation) throws Throwable { - UrlRequest.StatusListener statusListener = - (UrlRequest.StatusListener) invocation.getArguments()[0]; - statusListener.onStatus(TEST_CONNECTION_STATUS); - return null; - } + invocation -> { + UrlRequest.StatusListener statusListener = + (UrlRequest.StatusListener) invocation.getArguments()[0]; + statusListener.onStatus(TEST_CONNECTION_STATUS); + return null; }) .when(mockUrlRequest) .getStatus(any(UrlRequest.StatusListener.class)); @@ -915,13 +911,10 @@ public final class CronetDataSourceTest { private void mockResponseStartSuccess() { doAnswer( - new Answer() { - @Override - public Object answer(InvocationOnMock invocation) throws Throwable { - dataSourceUnderTest.urlRequestCallback.onResponseStarted( - mockUrlRequest, testUrlResponseInfo); - return null; - } + invocation -> { + dataSourceUnderTest.urlRequestCallback.onResponseStarted( + mockUrlRequest, testUrlResponseInfo); + return null; }) .when(mockUrlRequest) .start(); @@ -929,15 +922,12 @@ public final class CronetDataSourceTest { private void mockResponseStartRedirect() { doAnswer( - new Answer() { - @Override - public Object answer(InvocationOnMock invocation) throws Throwable { - dataSourceUnderTest.urlRequestCallback.onRedirectReceived( - mockUrlRequest, - createUrlResponseInfo(307), // statusCode - "http://redirect.location.com"); - return null; - } + invocation -> { + dataSourceUnderTest.urlRequestCallback.onRedirectReceived( + mockUrlRequest, + createUrlResponseInfo(307), // statusCode + "http://redirect.location.com"); + return null; }) .when(mockUrlRequest) .start(); @@ -945,21 +935,18 @@ public final class CronetDataSourceTest { private void mockSingleRedirectSuccess() { doAnswer( - new Answer() { - @Override - public Object answer(InvocationOnMock invocation) throws Throwable { - if (!redirectCalled) { - redirectCalled = true; - dataSourceUnderTest.urlRequestCallback.onRedirectReceived( - mockUrlRequest, - createUrlResponseInfoWithUrl("http://example.com/video", 300), - "http://example.com/video/redirect"); - } else { - dataSourceUnderTest.urlRequestCallback.onResponseStarted( - mockUrlRequest, testUrlResponseInfo); - } - return null; + invocation -> { + if (!redirectCalled) { + redirectCalled = true; + dataSourceUnderTest.urlRequestCallback.onRedirectReceived( + mockUrlRequest, + createUrlResponseInfoWithUrl("http://example.com/video", 300), + "http://example.com/video/redirect"); + } else { + dataSourceUnderTest.urlRequestCallback.onResponseStarted( + mockUrlRequest, testUrlResponseInfo); } + return null; }) .when(mockUrlRequest) .start(); @@ -967,13 +954,10 @@ public final class CronetDataSourceTest { private void mockFollowRedirectSuccess() { doAnswer( - new Answer() { - @Override - public Object answer(InvocationOnMock invocation) throws Throwable { - dataSourceUnderTest.urlRequestCallback.onResponseStarted( - mockUrlRequest, testUrlResponseInfo); - return null; - } + invocation -> { + dataSourceUnderTest.urlRequestCallback.onResponseStarted( + mockUrlRequest, testUrlResponseInfo); + return null; }) .when(mockUrlRequest) .followRedirect(); @@ -981,15 +965,12 @@ public final class CronetDataSourceTest { private void mockResponseStartFailure() { doAnswer( - new Answer() { - @Override - public Object answer(InvocationOnMock invocation) throws Throwable { - dataSourceUnderTest.urlRequestCallback.onFailed( - mockUrlRequest, - createUrlResponseInfo(500), // statusCode - mockNetworkException); - return null; - } + invocation -> { + dataSourceUnderTest.urlRequestCallback.onFailed( + mockUrlRequest, + createUrlResponseInfo(500), // statusCode + mockNetworkException); + return null; }) .when(mockUrlRequest) .start(); @@ -998,23 +979,20 @@ public final class CronetDataSourceTest { private void mockReadSuccess(int position, int length) { final int[] positionAndRemaining = new int[] {position, length}; doAnswer( - new Answer() { - @Override - public Void answer(InvocationOnMock invocation) throws Throwable { - if (positionAndRemaining[1] == 0) { - dataSourceUnderTest.urlRequestCallback.onSucceeded( - mockUrlRequest, testUrlResponseInfo); - } else { - ByteBuffer inputBuffer = (ByteBuffer) invocation.getArguments()[0]; - int readLength = Math.min(positionAndRemaining[1], inputBuffer.remaining()); - inputBuffer.put(buildTestDataBuffer(positionAndRemaining[0], readLength)); - positionAndRemaining[0] += readLength; - positionAndRemaining[1] -= readLength; - dataSourceUnderTest.urlRequestCallback.onReadCompleted( - mockUrlRequest, testUrlResponseInfo, inputBuffer); - } - return null; + invocation -> { + if (positionAndRemaining[1] == 0) { + dataSourceUnderTest.urlRequestCallback.onSucceeded( + mockUrlRequest, testUrlResponseInfo); + } else { + ByteBuffer inputBuffer = (ByteBuffer) invocation.getArguments()[0]; + int readLength = Math.min(positionAndRemaining[1], inputBuffer.remaining()); + inputBuffer.put(buildTestDataBuffer(positionAndRemaining[0], readLength)); + positionAndRemaining[0] += readLength; + positionAndRemaining[1] -= readLength; + dataSourceUnderTest.urlRequestCallback.onReadCompleted( + mockUrlRequest, testUrlResponseInfo, inputBuffer); } + return null; }) .when(mockUrlRequest) .read(any(ByteBuffer.class)); @@ -1022,15 +1000,12 @@ public final class CronetDataSourceTest { private void mockReadFailure() { doAnswer( - new Answer() { - @Override - public Object answer(InvocationOnMock invocation) throws Throwable { - dataSourceUnderTest.urlRequestCallback.onFailed( - mockUrlRequest, - createUrlResponseInfo(500), // statusCode - mockNetworkException); - return null; - } + invocation -> { + dataSourceUnderTest.urlRequestCallback.onFailed( + mockUrlRequest, + createUrlResponseInfo(500), // statusCode + mockNetworkException); + return null; }) .when(mockUrlRequest) .read(any(ByteBuffer.class)); @@ -1039,12 +1014,9 @@ public final class CronetDataSourceTest { private ConditionVariable buildReadStartedCondition() { final ConditionVariable startedCondition = new ConditionVariable(); doAnswer( - new Answer() { - @Override - public Object answer(InvocationOnMock invocation) throws Throwable { - startedCondition.open(); - return null; - } + invocation -> { + startedCondition.open(); + return null; }) .when(mockUrlRequest) .read(any(ByteBuffer.class)); @@ -1054,12 +1026,9 @@ public final class CronetDataSourceTest { private ConditionVariable buildUrlRequestStartedCondition() { final ConditionVariable startedCondition = new ConditionVariable(); doAnswer( - new Answer() { - @Override - public Object answer(InvocationOnMock invocation) throws Throwable { - startedCondition.open(); - return null; - } + invocation -> { + startedCondition.open(); + return null; }) .when(mockUrlRequest) .start(); diff --git a/extensions/flac/build.gradle b/extensions/flac/build.gradle index 98b81d911a..e5261902c6 100644 --- a/extensions/flac/build.gradle +++ b/extensions/flac/build.gradle @@ -27,6 +27,7 @@ android { minSdkVersion project.ext.minSdkVersion targetSdkVersion project.ext.targetSdkVersion consumerProguardFiles 'proguard-rules.txt' + testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' } sourceSets.main { @@ -38,6 +39,7 @@ android { dependencies { implementation 'com.android.support:support-annotations:' + supportLibraryVersion implementation project(modulePrefix + 'library-core') + androidTestImplementation 'androidx.test:runner:' + testRunnerVersion androidTestImplementation project(modulePrefix + 'testutils') testImplementation project(modulePrefix + 'testutils-robolectric') } diff --git a/extensions/flac/src/androidTest/AndroidManifest.xml b/extensions/flac/src/androidTest/AndroidManifest.xml index 4e3925d8e3..cfc90117ac 100644 --- a/extensions/flac/src/androidTest/AndroidManifest.xml +++ b/extensions/flac/src/androidTest/AndroidManifest.xml @@ -26,6 +26,6 @@ + android:name="androidx.test.runner.AndroidJUnitRunner"/> 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 fc9bdac2ea..29a597daa4 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 @@ -16,9 +16,7 @@ package com.google.android.exoplayer2.ext.flac; import android.test.InstrumentationTestCase; -import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.testutil.ExtractorAsserts; -import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory; /** * Unit test for {@link FlacExtractor}. @@ -35,25 +33,11 @@ public class FlacExtractorTest extends InstrumentationTestCase { public void testExtractFlacSample() throws Exception { ExtractorAsserts.assertBehavior( - new ExtractorFactory() { - @Override - public Extractor create() { - return new FlacExtractor(); - } - }, - "bear.flac", - getInstrumentation().getContext()); + FlacExtractor::new, "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()); + FlacExtractor::new, "bear_with_id3.flac", getInstrumentation().getContext()); } } diff --git a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java index 07b7a0ccdb..99ddba55c4 100644 --- a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java +++ b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java @@ -15,10 +15,13 @@ */ package com.google.android.exoplayer2.ext.flac; +import static androidx.test.InstrumentationRegistry.getContext; +import static org.junit.Assert.fail; + import android.content.Context; import android.net.Uri; import android.os.Looper; -import android.test.InstrumentationTestCase; +import androidx.test.runner.AndroidJUnit4; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayerFactory; @@ -29,36 +32,34 @@ import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; -/** - * Playback tests using {@link LibflacAudioRenderer}. - */ -public class FlacPlaybackTest extends InstrumentationTestCase { +/** Playback tests using {@link LibflacAudioRenderer}. */ +@RunWith(AndroidJUnit4.class) +public class FlacPlaybackTest { private static final String BEAR_FLAC_URI = "asset:///bear-flac.mka"; - @Override - protected void setUp() throws Exception { - super.setUp(); + @Before + public void setUp() { if (!FlacLibrary.isAvailable()) { fail("Flac library not available."); } } - public void testBasicPlayback() throws ExoPlaybackException { + @Test + public void testBasicPlayback() throws Exception { playUri(BEAR_FLAC_URI); } - private void playUri(String uri) throws ExoPlaybackException { - TestPlaybackRunnable testPlaybackRunnable = new TestPlaybackRunnable(Uri.parse(uri), - getInstrumentation().getContext()); + private void playUri(String uri) throws Exception { + TestPlaybackRunnable testPlaybackRunnable = + new TestPlaybackRunnable(Uri.parse(uri), getContext()); Thread thread = new Thread(testPlaybackRunnable); thread.start(); - try { - thread.join(); - } catch (InterruptedException e) { - fail(); // Should never happen. - } + thread.join(); if (testPlaybackRunnable.playbackException != null) { throw testPlaybackRunnable.playbackException; } diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java index 69c0d082ee..de038921aa 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java @@ -155,6 +155,7 @@ import java.nio.ByteBuffer; } /** Decodes and consumes the next sample from the FLAC stream into the given byte buffer. */ + @SuppressWarnings("ByteBufferBackingArray") public void decodeSample(ByteBuffer output) throws IOException, InterruptedException, FlacFrameDecodeException { output.clear(); 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 b6eec765d1..a1fbcc69d6 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 @@ -50,7 +50,10 @@ public final class FlacExtractor implements Extractor { /** Factory that returns one extractor which is a {@link FlacExtractor}. */ public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new FlacExtractor()}; - /** Flags controlling the behavior of the extractor. */ + /** + * Flags controlling the behavior of the extractor. Possible flag value is {@link + * #FLAG_DISABLE_ID3_METADATA}. + */ @Retention(RetentionPolicy.SOURCE) @IntDef( flag = true, diff --git a/extensions/flac/src/test/java/com/google/android/exoplayer2/ext/flac/DefaultExtractorsFactoryTest.java b/extensions/flac/src/test/java/com/google/android/exoplayer2/ext/flac/DefaultExtractorsFactoryTest.java index e08f4dc28c..79c4452928 100644 --- a/extensions/flac/src/test/java/com/google/android/exoplayer2/ext/flac/DefaultExtractorsFactoryTest.java +++ b/extensions/flac/src/test/java/com/google/android/exoplayer2/ext/flac/DefaultExtractorsFactoryTest.java @@ -46,13 +46,13 @@ public final class DefaultExtractorsFactoryTest { DefaultExtractorsFactory defaultExtractorsFactory = new DefaultExtractorsFactory(); Extractor[] extractors = defaultExtractorsFactory.createExtractors(); - List listCreatedExtractorClasses = new ArrayList<>(); + List> listCreatedExtractorClasses = new ArrayList<>(); for (Extractor extractor : extractors) { listCreatedExtractorClasses.add(extractor.getClass()); } - Class[] expectedExtractorClassses = - new Class[] { + Class[] expectedExtractorClassses = + new Class[] { MatroskaExtractor.class, FragmentedMp4Extractor.class, Mp4Extractor.class, diff --git a/extensions/ima/build.gradle b/extensions/ima/build.gradle index cf6938a2b1..7fc7935cac 100644 --- a/extensions/ima/build.gradle +++ b/extensions/ima/build.gradle @@ -48,8 +48,6 @@ dependencies { testImplementation project(modulePrefix + 'testutils-robolectric') } -apply plugin: 'com.google.android.gms.strict-version-matcher-plugin' - ext { javadocTitle = 'IMA extension' } diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index bf1cdfe02c..56d62f26a9 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -631,11 +631,8 @@ public final class ImaAdsLoader } else if (fakeContentProgressElapsedRealtimeMs != C.TIME_UNSET) { long elapsedSinceEndMs = SystemClock.elapsedRealtime() - fakeContentProgressElapsedRealtimeMs; contentPositionMs = fakeContentProgressOffsetMs + elapsedSinceEndMs; - int adGroupIndexForPosition = + expectedAdGroupIndex = adPlaybackState.getAdGroupIndexForPositionUs(C.msToUs(contentPositionMs)); - if (adGroupIndexForPosition != C.INDEX_UNSET) { - expectedAdGroupIndex = adGroupIndexForPosition; - } } else if (imaAdState == IMA_AD_STATE_NONE && !playingAd && hasContentDuration) { contentPositionMs = player.getCurrentPosition(); // Update the expected ad group index for the current content position. The update is delayed @@ -867,8 +864,6 @@ public final class ImaAdsLoader && playWhenReady) { checkForContentComplete(); } else if (imaAdState != IMA_AD_STATE_NONE && playbackState == Player.STATE_ENDED) { - // IMA is waiting for the ad playback to finish so invoke the callback now. - // Either CONTENT_RESUME_REQUESTED will be passed next, or playAd will be called again. for (int i = 0; i < adCallbacks.size(); i++) { adCallbacks.get(i).onEnded(); } @@ -1044,26 +1039,24 @@ public final class ImaAdsLoader int oldPlayingAdIndexInAdGroup = playingAdIndexInAdGroup; playingAd = player.isPlayingAd(); playingAdIndexInAdGroup = playingAd ? player.getCurrentAdIndexInAdGroup() : C.INDEX_UNSET; - if (!sentContentComplete) { - boolean adFinished = wasPlayingAd && playingAdIndexInAdGroup != oldPlayingAdIndexInAdGroup; - if (adFinished) { - // IMA is waiting for the ad playback to finish so invoke the callback now. - // Either CONTENT_RESUME_REQUESTED will be passed next, or playAd will be called again. - for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onEnded(); - } - if (DEBUG) { - Log.d(TAG, "VideoAdPlayerCallback.onEnded in onTimelineChanged/onPositionDiscontinuity"); - } + boolean adFinished = wasPlayingAd && playingAdIndexInAdGroup != oldPlayingAdIndexInAdGroup; + if (adFinished) { + // IMA is waiting for the ad playback to finish so invoke the callback now. + // Either CONTENT_RESUME_REQUESTED will be passed next, or playAd will be called again. + for (int i = 0; i < adCallbacks.size(); i++) { + adCallbacks.get(i).onEnded(); } - if (!wasPlayingAd && playingAd && imaAdState == IMA_AD_STATE_NONE) { - int adGroupIndex = player.getCurrentAdGroupIndex(); - // IMA hasn't called playAd yet, so fake the content position. - fakeContentProgressElapsedRealtimeMs = SystemClock.elapsedRealtime(); - fakeContentProgressOffsetMs = C.usToMs(adPlaybackState.adGroupTimesUs[adGroupIndex]); - if (fakeContentProgressOffsetMs == C.TIME_END_OF_SOURCE) { - fakeContentProgressOffsetMs = contentDurationMs; - } + if (DEBUG) { + Log.d(TAG, "VideoAdPlayerCallback.onEnded in onTimelineChanged/onPositionDiscontinuity"); + } + } + if (!sentContentComplete && !wasPlayingAd && playingAd && imaAdState == IMA_AD_STATE_NONE) { + int adGroupIndex = player.getCurrentAdGroupIndex(); + // IMA hasn't called playAd yet, so fake the content position. + fakeContentProgressElapsedRealtimeMs = SystemClock.elapsedRealtime(); + fakeContentProgressOffsetMs = C.usToMs(adPlaybackState.adGroupTimesUs[adGroupIndex]); + if (fakeContentProgressOffsetMs == C.TIME_END_OF_SOURCE) { + fakeContentProgressOffsetMs = contentDurationMs; } } } @@ -1127,16 +1120,8 @@ public final class ImaAdsLoader if (pendingAdLoadError == null) { pendingAdLoadError = AdLoadException.createForAdGroup(error, adGroupIndex); } - // Discard the ad break, which makes sure we don't receive duplicate load error events. - adsManager.discardAdBreak(); - // Set the next expected ad group index so we can handle multiple load errors in a row. - adGroupIndex++; - if (adGroupIndex < adPlaybackState.adGroupCount) { - expectedAdGroupIndex = adGroupIndex; - } else { - expectedAdGroupIndex = C.INDEX_UNSET; - } pendingContentPositionMs = C.TIME_UNSET; + fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET; } private void handleAdPrepareError(int adGroupIndex, int adIndexInAdGroup, Exception exception) { @@ -1184,6 +1169,10 @@ public final class ImaAdsLoader Log.d(TAG, "adsLoader.contentComplete"); } sentContentComplete = true; + // After sending content complete IMA will not poll the content position, so set the expected + // ad group index. + expectedAdGroupIndex = + adPlaybackState.getAdGroupIndexForPositionUs(C.msToUs(contentDurationMs)); } } diff --git a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakePlayer.java b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakePlayer.java index 11ed214279..c7026bab5f 100644 --- a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakePlayer.java +++ b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakePlayer.java @@ -27,9 +27,9 @@ import java.util.ArrayList; private final ArrayList listeners; private final Timeline.Window window; private final Timeline.Period period; + private final Timeline timeline; private boolean prepared; - private Timeline timeline; private int state; private boolean playWhenReady; private long position; diff --git a/extensions/jobdispatcher/src/main/java/com/google/android/exoplayer2/ext/jobdispatcher/JobDispatcherScheduler.java b/extensions/jobdispatcher/src/main/java/com/google/android/exoplayer2/ext/jobdispatcher/JobDispatcherScheduler.java index f75607f268..d6759245c0 100644 --- a/extensions/jobdispatcher/src/main/java/com/google/android/exoplayer2/ext/jobdispatcher/JobDispatcherScheduler.java +++ b/extensions/jobdispatcher/src/main/java/com/google/android/exoplayer2/ext/jobdispatcher/JobDispatcherScheduler.java @@ -23,7 +23,6 @@ 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; @@ -38,6 +37,7 @@ import com.google.android.exoplayer2.util.Util; * *
{@literal
  * 
+ * 
  *
  *  queue = mediaController.getQueue();
     for (int i = 0; i < queue.size(); i++) {
       if (equalityChecker.equals(queue.get(i).getDescription(), description)) {
-        onRemoveQueueItemAt(player, i);
+        queueDataAdapter.remove(i);
+        queueMediaSource.removeMediaSource(i);
         return;
       }
     }
   }
 
-  @Override
-  public void onRemoveQueueItemAt(Player player, int index) {
-    queueDataAdapter.remove(index);
-    queueMediaSource.removeMediaSource(index);
-  }
-
   // CommandReceiver implementation.
 
   @NonNull
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 1d0dfddb3f..ba5640c4e0 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
@@ -161,8 +161,8 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource {
       responseBody = Assertions.checkNotNull(response.body());
       responseByteStream = responseBody.byteStream();
     } catch (IOException e) {
-      throw new HttpDataSourceException("Unable to connect to " + dataSpec.uri.toString(), e,
-          dataSpec, HttpDataSourceException.TYPE_OPEN);
+      throw new HttpDataSourceException(
+          "Unable to connect to " + dataSpec.uri, e, dataSpec, HttpDataSourceException.TYPE_OPEN);
     }
 
     int responseCode = response.code();
diff --git a/extensions/opus/build.gradle b/extensions/opus/build.gradle
index dc530d05aa..cb12442de8 100644
--- a/extensions/opus/build.gradle
+++ b/extensions/opus/build.gradle
@@ -27,6 +27,7 @@ android {
         minSdkVersion project.ext.minSdkVersion
         targetSdkVersion project.ext.targetSdkVersion
         consumerProguardFiles 'proguard-rules.txt'
+        testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
     }
 
     sourceSets.main {
@@ -37,6 +38,7 @@ android {
 
 dependencies {
     implementation project(modulePrefix + 'library-core')
+    androidTestImplementation 'androidx.test:runner:' + testRunnerVersion
 }
 
 ext {
diff --git a/extensions/opus/src/androidTest/AndroidManifest.xml b/extensions/opus/src/androidTest/AndroidManifest.xml
index 9e7f05051e..5ba0f3c0f4 100644
--- a/extensions/opus/src/androidTest/AndroidManifest.xml
+++ b/extensions/opus/src/androidTest/AndroidManifest.xml
@@ -26,6 +26,6 @@
 
   
+    android:name="androidx.test.runner.AndroidJUnitRunner"/>
 
 
diff --git a/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java b/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java
index 8e3a213af1..c457514c87 100644
--- a/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java
+++ b/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java
@@ -15,10 +15,13 @@
  */
 package com.google.android.exoplayer2.ext.opus;
 
+import static androidx.test.InstrumentationRegistry.getContext;
+import static org.junit.Assert.fail;
+
 import android.content.Context;
 import android.net.Uri;
 import android.os.Looper;
-import android.test.InstrumentationTestCase;
+import androidx.test.runner.AndroidJUnit4;
 import com.google.android.exoplayer2.ExoPlaybackException;
 import com.google.android.exoplayer2.ExoPlayer;
 import com.google.android.exoplayer2.ExoPlayerFactory;
@@ -29,36 +32,34 @@ import com.google.android.exoplayer2.source.ExtractorMediaSource;
 import com.google.android.exoplayer2.source.MediaSource;
 import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
 import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
 
-/**
- * Playback tests using {@link LibopusAudioRenderer}.
- */
-public class OpusPlaybackTest extends InstrumentationTestCase {
+/** Playback tests using {@link LibopusAudioRenderer}. */
+@RunWith(AndroidJUnit4.class)
+public class OpusPlaybackTest {
 
   private static final String BEAR_OPUS_URI = "asset:///bear-opus.webm";
 
-  @Override
-  protected void setUp() throws Exception {
-    super.setUp();
+  @Before
+  public void setUp() {
     if (!OpusLibrary.isAvailable()) {
       fail("Opus library not available.");
     }
   }
 
-  public void testBasicPlayback() throws ExoPlaybackException {
+  @Test
+  public void testBasicPlayback() throws Exception {
     playUri(BEAR_OPUS_URI);
   }
 
-  private void playUri(String uri) throws ExoPlaybackException {
-    TestPlaybackRunnable testPlaybackRunnable = new TestPlaybackRunnable(Uri.parse(uri),
-        getInstrumentation().getContext());
+  private void playUri(String uri) throws Exception {
+    TestPlaybackRunnable testPlaybackRunnable =
+        new TestPlaybackRunnable(Uri.parse(uri), getContext());
     Thread thread = new Thread(testPlaybackRunnable);
     thread.start();
-    try {
-      thread.join();
-    } catch (InterruptedException e) {
-      fail(); // Should never happen.
-    }
+    thread.join();
     if (testPlaybackRunnable.playbackException != null) {
       throw testPlaybackRunnable.playbackException;
     }
diff --git a/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSourceFactory.java b/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSourceFactory.java
index 3cf9b8de37..d1350276f2 100644
--- a/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSourceFactory.java
+++ b/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSourceFactory.java
@@ -38,7 +38,11 @@ public final class RtmpDataSourceFactory implements DataSource.Factory {
 
   @Override
   public DataSource createDataSource() {
-    return new RtmpDataSource(listener);
+    RtmpDataSource dataSource = new RtmpDataSource();
+    if (listener != null) {
+      dataSource.addTransferListener(listener);
+    }
+    return dataSource;
   }
 
 }
diff --git a/extensions/vp9/build.gradle b/extensions/vp9/build.gradle
index 3fb627fd77..96c58d7a57 100644
--- a/extensions/vp9/build.gradle
+++ b/extensions/vp9/build.gradle
@@ -27,6 +27,7 @@ android {
         minSdkVersion project.ext.minSdkVersion
         targetSdkVersion project.ext.targetSdkVersion
         consumerProguardFiles 'proguard-rules.txt'
+        testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
     }
 
     sourceSets.main {
@@ -38,6 +39,7 @@ android {
 dependencies {
     implementation project(modulePrefix + 'library-core')
     implementation 'com.android.support:support-annotations:' + supportLibraryVersion
+    androidTestImplementation 'androidx.test:runner:' + testRunnerVersion
     androidTestImplementation 'com.google.truth:truth:' + truthVersion
 }
 
diff --git a/extensions/vp9/src/androidTest/AndroidManifest.xml b/extensions/vp9/src/androidTest/AndroidManifest.xml
index c7ed3d7fb2..214427c4f0 100644
--- a/extensions/vp9/src/androidTest/AndroidManifest.xml
+++ b/extensions/vp9/src/androidTest/AndroidManifest.xml
@@ -26,6 +26,6 @@
 
   
+      android:name="androidx.test.runner.AndroidJUnitRunner"/>
 
 
diff --git a/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java b/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java
index bab7cb6fd7..d06e2934fb 100644
--- a/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java
+++ b/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java
@@ -15,13 +15,15 @@
  */
 package com.google.android.exoplayer2.ext.vp9;
 
+import static androidx.test.InstrumentationRegistry.getContext;
 import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
 
 import android.content.Context;
 import android.net.Uri;
 import android.os.Looper;
-import android.test.InstrumentationTestCase;
 import android.util.Log;
+import androidx.test.runner.AndroidJUnit4;
 import com.google.android.exoplayer2.ExoPlaybackException;
 import com.google.android.exoplayer2.ExoPlayer;
 import com.google.android.exoplayer2.ExoPlayerFactory;
@@ -32,11 +34,13 @@ import com.google.android.exoplayer2.source.ExtractorMediaSource;
 import com.google.android.exoplayer2.source.MediaSource;
 import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
 import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
 
-/**
- * Playback tests using {@link LibvpxVideoRenderer}.
- */
-public class VpxPlaybackTest extends InstrumentationTestCase {
+/** Playback tests using {@link LibvpxVideoRenderer}. */
+@RunWith(AndroidJUnit4.class)
+public class VpxPlaybackTest {
 
   private static final String BEAR_URI = "asset:///bear-vp9.webm";
   private static final String BEAR_ODD_DIMENSIONS_URI = "asset:///bear-vp9-odd-dimensions.webm";
@@ -45,23 +49,25 @@ public class VpxPlaybackTest extends InstrumentationTestCase {
 
   private static final String TAG = "VpxPlaybackTest";
 
-  @Override
-  protected void setUp() throws Exception {
-    super.setUp();
+  @Before
+  public void setUp() {
     if (!VpxLibrary.isAvailable()) {
       fail("Vpx library not available.");
     }
   }
 
-  public void testBasicPlayback() throws ExoPlaybackException {
+  @Test
+  public void testBasicPlayback() throws Exception {
     playUri(BEAR_URI);
   }
 
-  public void testOddDimensionsPlayback() throws ExoPlaybackException {
+  @Test
+  public void testOddDimensionsPlayback() throws Exception {
     playUri(BEAR_ODD_DIMENSIONS_URI);
   }
 
-  public void test10BitProfile2Playback() throws ExoPlaybackException {
+  @Test
+  public void test10BitProfile2Playback() throws Exception {
     if (VpxLibrary.isHighBitDepthSupported()) {
       Log.d(TAG, "High Bit Depth supported.");
       playUri(ROADTRIP_10BIT_URI);
@@ -70,6 +76,7 @@ public class VpxPlaybackTest extends InstrumentationTestCase {
     Log.d(TAG, "High Bit Depth not supported.");
   }
 
+  @Test
   public void testInvalidBitstream() {
     try {
       playUri(INVALID_BITSTREAM_URI);
@@ -80,16 +87,12 @@ public class VpxPlaybackTest extends InstrumentationTestCase {
     }
   }
 
-  private void playUri(String uri) throws ExoPlaybackException {
-    TestPlaybackRunnable testPlaybackRunnable = new TestPlaybackRunnable(Uri.parse(uri),
-        getInstrumentation().getContext());
+  private void playUri(String uri) throws Exception {
+    TestPlaybackRunnable testPlaybackRunnable =
+        new TestPlaybackRunnable(Uri.parse(uri), getContext());
     Thread thread = new Thread(testPlaybackRunnable);
     thread.start();
-    try {
-      thread.join();
-    } catch (InterruptedException e) {
-      fail(); // Should never happen.
-    }
+    thread.join();
     if (testPlaybackRunnable.playbackException != null) {
       throw testPlaybackRunnable.playbackException;
     }
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 08c413aba7..f0986d08be 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
@@ -39,8 +39,10 @@ import com.google.android.exoplayer2.drm.DrmSessionManager;
 import com.google.android.exoplayer2.drm.ExoMediaCrypto;
 import com.google.android.exoplayer2.util.Assertions;
 import com.google.android.exoplayer2.util.MimeTypes;
+import com.google.android.exoplayer2.util.TimedValueQueue;
 import com.google.android.exoplayer2.util.TraceUtil;
 import com.google.android.exoplayer2.util.Util;
+import com.google.android.exoplayer2.video.VideoFrameMetadataListener;
 import com.google.android.exoplayer2.video.VideoRendererEventListener;
 import com.google.android.exoplayer2.video.VideoRendererEventListener.EventDispatcher;
 import java.lang.annotation.Retention;
@@ -109,11 +111,14 @@ public class LibvpxVideoRenderer extends BaseRenderer {
   private final boolean playClearSamplesWithoutKeys;
   private final EventDispatcher eventDispatcher;
   private final FormatHolder formatHolder;
+  private final TimedValueQueue formatQueue;
   private final DecoderInputBuffer flagsOnlyBuffer;
   private final DrmSessionManager drmSessionManager;
   private final boolean useSurfaceYuvOutput;
 
   private Format format;
+  private Format pendingFormat;
+  private Format outputFormat;
   private VpxDecoder decoder;
   private VpxInputBuffer inputBuffer;
   private VpxOutputBuffer outputBuffer;
@@ -142,6 +147,8 @@ public class LibvpxVideoRenderer extends BaseRenderer {
   private int consecutiveDroppedFrameCount;
   private int buffersInCodecCount;
   private long lastRenderTimeUs;
+  private long outputStreamOffsetUs;
+  private VideoFrameMetadataListener frameMetadataListener;
 
   protected DecoderCounters decoderCounters;
 
@@ -219,6 +226,7 @@ public class LibvpxVideoRenderer extends BaseRenderer {
     joiningDeadlineMs = C.TIME_UNSET;
     clearReportedVideoSize();
     formatHolder = new FormatHolder();
+    formatQueue = new TimedValueQueue<>();
     flagsOnlyBuffer = DecoderInputBuffer.newFlagsOnlyInstance();
     eventDispatcher = new EventDispatcher(eventHandler, eventListener);
     outputMode = VpxDecoder.OUTPUT_MODE_NONE;
@@ -328,6 +336,7 @@ public class LibvpxVideoRenderer extends BaseRenderer {
     } else {
       joiningDeadlineMs = C.TIME_UNSET;
     }
+    formatQueue.clear();
   }
 
   @Override
@@ -371,6 +380,12 @@ public class LibvpxVideoRenderer extends BaseRenderer {
     }
   }
 
+  @Override
+  protected void onStreamChanged(Format[] formats, long offsetUs) throws ExoPlaybackException {
+    outputStreamOffsetUs = offsetUs;
+    super.onStreamChanged(formats, offsetUs);
+  }
+
   /**
    * Called when a decoder has been created and configured.
    *
@@ -437,6 +452,7 @@ public class LibvpxVideoRenderer extends BaseRenderer {
   protected void onInputFormatChanged(Format newFormat) throws ExoPlaybackException {
     Format oldFormat = format;
     format = newFormat;
+    pendingFormat = newFormat;
 
     boolean drmInitDataChanged = !Util.areEqual(format.drmInitData, oldFormat == null ? null
         : oldFormat.drmInitData);
@@ -629,6 +645,8 @@ public class LibvpxVideoRenderer extends BaseRenderer {
       setOutput((Surface) message, null);
     } else if (messageType == MSG_SET_OUTPUT_BUFFER_RENDERER) {
       setOutput(null, (VpxOutputBufferRenderer) message);
+    } else if (messageType == C.MSG_SET_VIDEO_FRAME_METADATA_LISTENER) {
+      frameMetadataListener = (VideoFrameMetadataListener) message;
     } else {
       super.handleMessage(messageType, message);
     }
@@ -772,6 +790,10 @@ public class LibvpxVideoRenderer extends BaseRenderer {
     if (waitingForKeys) {
       return false;
     }
+    if (pendingFormat != null) {
+      formatQueue.add(inputBuffer.timeUs, pendingFormat);
+      pendingFormat = null;
+    }
     inputBuffer.flip();
     inputBuffer.colorInfo = formatHolder.format.colorInfo;
     onQueueInputBuffer(inputBuffer);
@@ -851,11 +873,21 @@ public class LibvpxVideoRenderer extends BaseRenderer {
       return false;
     }
 
+    long presentationTimeUs = outputBuffer.timeUs - outputStreamOffsetUs;
+    Format format = formatQueue.pollFloor(presentationTimeUs);
+    if (format != null) {
+      outputFormat = format;
+    }
+
     long elapsedRealtimeNowUs = SystemClock.elapsedRealtime() * 1000;
     boolean isStarted = getState() == STATE_STARTED;
     if (!renderedFirstFrame
         || (isStarted
             && shouldForceRenderOutputBuffer(earlyUs, elapsedRealtimeNowUs - lastRenderTimeUs))) {
+      if (frameMetadataListener != null) {
+        frameMetadataListener.onVideoFrameAboutToBeRendered(
+            presentationTimeUs, System.nanoTime(), outputFormat);
+      }
       renderOutputBuffer(outputBuffer);
       return true;
     }
@@ -873,6 +905,10 @@ public class LibvpxVideoRenderer extends BaseRenderer {
     }
 
     if (earlyUs < 30000) {
+      if (frameMetadataListener != null) {
+        frameMetadataListener.onVideoFrameAboutToBeRendered(
+            presentationTimeUs, System.nanoTime(), outputFormat);
+      }
       renderOutputBuffer(outputBuffer);
       return true;
     }
diff --git a/javadoc_combined.gradle b/javadoc_combined.gradle
index aea65d4d97..209ad3a1a3 100644
--- a/javadoc_combined.gradle
+++ b/javadoc_combined.gradle
@@ -39,7 +39,7 @@ class CombinedJavadocPlugin implements Plugin {
             libraryModules.each { libraryModule ->
               libraryModule.android.libraryVariants.all { variant ->
                 def name = variant.buildType.name
-                if (name.equals("release")) {
+                if (name == "release") {
                   classpath +=
                       libraryModule.project.files(
                           variant.javaCompile.classpath.files,
@@ -63,7 +63,7 @@ class CombinedJavadocPlugin implements Plugin {
   }
 
   // Returns Android library modules that declare a generateJavadoc task.
-  private Set getLibraryModules(Project project) {
+  private static Set getLibraryModules(Project project) {
     project.subprojects.findAll {
       it.plugins.findPlugin("com.android.library") &&
       it.tasks.findByName("generateJavadoc")
diff --git a/library/core/build.gradle b/library/core/build.gradle
index 947972392f..606033fdea 100644
--- a/library/core/build.gradle
+++ b/library/core/build.gradle
@@ -28,7 +28,7 @@ android {
         targetSdkVersion project.ext.targetSdkVersion
         consumerProguardFiles 'proguard-rules.txt'
 
-        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+        testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
 
         // The following argument makes the Android Test Orchestrator run its
         // "pm clear" command after each test invocation. This command ensures
@@ -39,11 +39,11 @@ android {
     // Workaround to prevent circular dependency on project :testutils.
     sourceSets {
         androidTest {
-            java.srcDirs += "../../testutils/src/main/java/"
+            java.srcDirs += '../../testutils/src/main/java/'
         }
         test {
-            java.srcDirs += "../../testutils/src/main/java/"
-            java.srcDirs += "../../testutils_robolectric/src/main/java/"
+            java.srcDirs += '../../testutils/src/main/java/'
+            java.srcDirs += '../../testutils_robolectric/src/main/java/'
         }
     }
 
@@ -60,12 +60,12 @@ dependencies {
     implementation 'com.android.support:support-annotations:' + supportLibraryVersion
     compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
     compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion
+    androidTestImplementation 'androidx.test:runner:' + testRunnerVersion
+    androidTestImplementation 'com.google.auto.value:auto-value-annotations:' + autoValueVersion
     androidTestImplementation 'com.google.dexmaker:dexmaker:' + dexmakerVersion
     androidTestImplementation 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion
     androidTestImplementation 'com.google.truth:truth:' + truthVersion
     androidTestImplementation 'org.mockito:mockito-core:' + mockitoVersion
-    androidTestImplementation 'androidx.test:runner:' + testRunnerVersion
-    androidTestImplementation 'com.google.auto.value:auto-value-annotations:' + autoValueVersion
     androidTestAnnotationProcessor 'com.google.auto.value:auto-value:' + autoValueVersion
     testImplementation 'com.google.truth:truth:' + truthVersion
     testImplementation 'junit:junit:' + junitVersion
diff --git a/library/core/src/androidTest/AndroidManifest.xml b/library/core/src/androidTest/AndroidManifest.xml
index 1aa47c10f6..d9104b1077 100644
--- a/library/core/src/androidTest/AndroidManifest.xml
+++ b/library/core/src/androidTest/AndroidManifest.xml
@@ -29,6 +29,6 @@
 
   
+      android:name="androidx.test.runner.AndroidJUnitRunner"/>
 
 
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java
index 49329c38c0..45b784e30f 100644
--- a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java
+++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java
@@ -87,7 +87,7 @@ public final class ContentDataSourceTest {
       fail();
     } catch (ContentDataSource.ContentDataSourceException e) {
       // Expected.
-      assertThat(e.getCause()).isInstanceOf(FileNotFoundException.class);
+      assertThat(e).hasCauseThat().isInstanceOf(FileNotFoundException.class);
     } finally {
       dataSource.close();
     }
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 87499a9cb1..0cbdc14b1c 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
@@ -26,6 +26,8 @@ import android.view.Surface;
 import com.google.android.exoplayer2.PlayerMessage.Target;
 import com.google.android.exoplayer2.audio.AuxEffectInfo;
 import com.google.android.exoplayer2.util.Util;
+import com.google.android.exoplayer2.video.VideoFrameMetadataListener;
+import com.google.android.exoplayer2.video.spherical.CameraMotionListener;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.UUID;
@@ -109,7 +111,8 @@ public final class C {
   public static final String SANS_SERIF_NAME = "sans-serif";
 
   /**
-   * Crypto modes for a codec.
+   * Crypto modes for a codec. One of {@link #CRYPTO_MODE_UNENCRYPTED}, {@link #CRYPTO_MODE_AES_CTR}
+   * or {@link #CRYPTO_MODE_AES_CBC}.
    */
   @Retention(RetentionPolicy.SOURCE)
   @IntDef({CRYPTO_MODE_UNENCRYPTED, CRYPTO_MODE_AES_CTR, CRYPTO_MODE_AES_CBC})
@@ -133,7 +136,14 @@ public final class C {
    */
   public static final int AUDIO_SESSION_ID_UNSET = AudioManager.AUDIO_SESSION_ID_GENERATE;
 
-  /** Represents an audio encoding, or an invalid or unset value. */
+  /**
+   * Represents an audio encoding, or an invalid or unset value. One of {@link Format#NO_VALUE},
+   * {@link #ENCODING_INVALID}, {@link #ENCODING_PCM_8BIT}, {@link #ENCODING_PCM_16BIT}, {@link
+   * #ENCODING_PCM_24BIT}, {@link #ENCODING_PCM_32BIT}, {@link #ENCODING_PCM_FLOAT}, {@link
+   * #ENCODING_PCM_MU_LAW}, {@link #ENCODING_PCM_A_LAW}, {@link #ENCODING_AC3}, {@link
+   * #ENCODING_E_AC3}, {@link #ENCODING_DTS}, {@link #ENCODING_DTS_HD} or {@link
+   * #ENCODING_DOLBY_TRUEHD}.
+   */
   @Retention(RetentionPolicy.SOURCE)
   @IntDef({
     Format.NO_VALUE,
@@ -153,7 +163,12 @@ public final class C {
   })
   public @interface Encoding {}
 
-  /** Represents a PCM audio encoding, or an invalid or unset value. */
+  /**
+   * Represents a PCM audio encoding, or an invalid or unset value. One of {@link Format#NO_VALUE},
+   * {@link #ENCODING_INVALID}, {@link #ENCODING_PCM_8BIT}, {@link #ENCODING_PCM_16BIT}, {@link
+   * #ENCODING_PCM_24BIT}, {@link #ENCODING_PCM_32BIT}, {@link #ENCODING_PCM_FLOAT}, {@link
+   * #ENCODING_PCM_MU_LAW} or {@link #ENCODING_PCM_A_LAW}.
+   */
   @Retention(RetentionPolicy.SOURCE)
   @IntDef({
     Format.NO_VALUE,
@@ -195,11 +210,22 @@ public final class C {
   public static final int ENCODING_DOLBY_TRUEHD = AudioFormat.ENCODING_DOLBY_TRUEHD;
 
   /**
-   * Stream types for an {@link android.media.AudioTrack}.
+   * Stream types for an {@link android.media.AudioTrack}. One of {@link #STREAM_TYPE_ALARM}, {@link
+   * #STREAM_TYPE_DTMF}, {@link #STREAM_TYPE_MUSIC}, {@link #STREAM_TYPE_NOTIFICATION}, {@link
+   * #STREAM_TYPE_RING}, {@link #STREAM_TYPE_SYSTEM}, {@link #STREAM_TYPE_VOICE_CALL} or {@link
+   * #STREAM_TYPE_USE_DEFAULT}.
    */
   @Retention(RetentionPolicy.SOURCE)
-  @IntDef({STREAM_TYPE_ALARM, STREAM_TYPE_DTMF, STREAM_TYPE_MUSIC, STREAM_TYPE_NOTIFICATION,
-      STREAM_TYPE_RING, STREAM_TYPE_SYSTEM, STREAM_TYPE_VOICE_CALL, STREAM_TYPE_USE_DEFAULT})
+  @IntDef({
+    STREAM_TYPE_ALARM,
+    STREAM_TYPE_DTMF,
+    STREAM_TYPE_MUSIC,
+    STREAM_TYPE_NOTIFICATION,
+    STREAM_TYPE_RING,
+    STREAM_TYPE_SYSTEM,
+    STREAM_TYPE_VOICE_CALL,
+    STREAM_TYPE_USE_DEFAULT
+  })
   public @interface StreamType {}
   /**
    * @see AudioManager#STREAM_ALARM
@@ -239,11 +265,18 @@ public final class C {
   public static final int STREAM_TYPE_DEFAULT = STREAM_TYPE_MUSIC;
 
   /**
-   * Content types for {@link com.google.android.exoplayer2.audio.AudioAttributes}.
+   * Content types for {@link com.google.android.exoplayer2.audio.AudioAttributes}. One of {@link
+   * #CONTENT_TYPE_MOVIE}, {@link #CONTENT_TYPE_MUSIC}, {@link #CONTENT_TYPE_SONIFICATION}, {@link
+   * #CONTENT_TYPE_SPEECH} or {@link #CONTENT_TYPE_UNKNOWN}.
    */
   @Retention(RetentionPolicy.SOURCE)
-  @IntDef({CONTENT_TYPE_MOVIE, CONTENT_TYPE_MUSIC, CONTENT_TYPE_SONIFICATION, CONTENT_TYPE_SPEECH,
-      CONTENT_TYPE_UNKNOWN})
+  @IntDef({
+    CONTENT_TYPE_MOVIE,
+    CONTENT_TYPE_MUSIC,
+    CONTENT_TYPE_SONIFICATION,
+    CONTENT_TYPE_SPEECH,
+    CONTENT_TYPE_UNKNOWN
+  })
   public @interface AudioContentType {}
   /**
    * @see android.media.AudioAttributes#CONTENT_TYPE_MOVIE
@@ -270,13 +303,16 @@ public final class C {
       android.media.AudioAttributes.CONTENT_TYPE_UNKNOWN;
 
   /**
-   * Flags for {@link com.google.android.exoplayer2.audio.AudioAttributes}.
-   * 

- * Note that {@code FLAG_HW_AV_SYNC} is not available because the player takes care of setting the - * flag when tunneling is enabled via a track selector. + * Flags for {@link com.google.android.exoplayer2.audio.AudioAttributes}. Possible flag value is + * {@link #FLAG_AUDIBILITY_ENFORCED}. + * + *

Note that {@code FLAG_HW_AV_SYNC} is not available because the player takes care of setting + * the flag when tunneling is enabled via a track selector. */ @Retention(RetentionPolicy.SOURCE) - @IntDef(flag = true, value = {FLAG_AUDIBILITY_ENFORCED}) + @IntDef( + flag = true, + value = {FLAG_AUDIBILITY_ENFORCED}) public @interface AudioFlags {} /** * @see android.media.AudioAttributes#FLAG_AUDIBILITY_ENFORCED @@ -284,7 +320,17 @@ public final class C { public static final int FLAG_AUDIBILITY_ENFORCED = android.media.AudioAttributes.FLAG_AUDIBILITY_ENFORCED; - /** Usage types for {@link com.google.android.exoplayer2.audio.AudioAttributes}. */ + /** + * Usage types for {@link com.google.android.exoplayer2.audio.AudioAttributes}. One of {@link + * #USAGE_ALARM}, {@link #USAGE_ASSISTANCE_ACCESSIBILITY}, {@link + * #USAGE_ASSISTANCE_NAVIGATION_GUIDANCE}, {@link #USAGE_ASSISTANCE_SONIFICATION}, {@link + * #USAGE_ASSISTANT}, {@link #USAGE_GAME}, {@link #USAGE_MEDIA}, {@link #USAGE_NOTIFICATION}, + * {@link #USAGE_NOTIFICATION_COMMUNICATION_DELAYED}, {@link + * #USAGE_NOTIFICATION_COMMUNICATION_INSTANT}, {@link #USAGE_NOTIFICATION_COMMUNICATION_REQUEST}, + * {@link #USAGE_NOTIFICATION_EVENT}, {@link #USAGE_NOTIFICATION_RINGTONE}, {@link + * #USAGE_UNKNOWN}, {@link #USAGE_VOICE_COMMUNICATION} or {@link + * #USAGE_VOICE_COMMUNICATION_SIGNALLING}. + */ @Retention(RetentionPolicy.SOURCE) @IntDef({ USAGE_ALARM, @@ -376,7 +422,11 @@ public final class C { public static final int USAGE_VOICE_COMMUNICATION_SIGNALLING = android.media.AudioAttributes.USAGE_VOICE_COMMUNICATION_SIGNALLING; - /** Audio focus types. */ + /** + * Audio focus types. One of {@link #AUDIOFOCUS_NONE}, {@link #AUDIOFOCUS_GAIN}, {@link + * #AUDIOFOCUS_GAIN_TRANSIENT}, {@link #AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK} or {@link + * #AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE}. + */ @Retention(RetentionPolicy.SOURCE) @IntDef({ AUDIOFOCUS_NONE, @@ -400,11 +450,19 @@ public final class C { AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE; /** - * Flags which can apply to a buffer containing a media sample. + * Flags which can apply to a buffer containing a media sample. Possible flag values are {@link + * #BUFFER_FLAG_KEY_FRAME}, {@link #BUFFER_FLAG_END_OF_STREAM}, {@link #BUFFER_FLAG_ENCRYPTED} and + * {@link #BUFFER_FLAG_DECODE_ONLY}. */ @Retention(RetentionPolicy.SOURCE) - @IntDef(flag = true, value = {BUFFER_FLAG_KEY_FRAME, BUFFER_FLAG_END_OF_STREAM, - BUFFER_FLAG_ENCRYPTED, BUFFER_FLAG_DECODE_ONLY}) + @IntDef( + flag = true, + value = { + BUFFER_FLAG_KEY_FRAME, + BUFFER_FLAG_END_OF_STREAM, + BUFFER_FLAG_ENCRYPTED, + BUFFER_FLAG_DECODE_ONLY + }) public @interface BufferFlags {} /** * Indicates that a buffer holds a synchronization sample. @@ -417,10 +475,12 @@ public final class C { /** Indicates that a buffer is (at least partially) encrypted. */ public static final int BUFFER_FLAG_ENCRYPTED = 1 << 30; // 0x40000000 /** Indicates that a buffer should be decoded but not rendered. */ + @SuppressWarnings("NumericOverflow") public static final int BUFFER_FLAG_DECODE_ONLY = 1 << 31; // 0x80000000 /** - * Video scaling modes for {@link MediaCodec}-based {@link Renderer}s. + * Video scaling modes for {@link MediaCodec}-based {@link Renderer}s. One of {@link + * #VIDEO_SCALING_MODE_SCALE_TO_FIT} or {@link #VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING}. */ @Retention(RetentionPolicy.SOURCE) @IntDef(value = {VIDEO_SCALING_MODE_SCALE_TO_FIT, VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING}) @@ -441,11 +501,13 @@ public final class C { public static final int VIDEO_SCALING_MODE_DEFAULT = VIDEO_SCALING_MODE_SCALE_TO_FIT; /** - * Track selection flags. + * Track selection flags. Possible flag values are {@link #SELECTION_FLAG_DEFAULT}, {@link + * #SELECTION_FLAG_FORCED} and {@link #SELECTION_FLAG_AUTOSELECT}. */ @Retention(RetentionPolicy.SOURCE) - @IntDef(flag = true, value = {SELECTION_FLAG_DEFAULT, SELECTION_FLAG_FORCED, - SELECTION_FLAG_AUTOSELECT}) + @IntDef( + flag = true, + value = {SELECTION_FLAG_DEFAULT, SELECTION_FLAG_FORCED, SELECTION_FLAG_AUTOSELECT}) public @interface SelectionFlags {} /** * Indicates that the track should be selected if user preferences do not state otherwise. @@ -465,7 +527,8 @@ public final class C { public static final String LANGUAGE_UNDETERMINED = "und"; /** - * Represents a streaming or other media type. + * Represents a streaming or other media type. One of {@link #TYPE_DASH}, {@link #TYPE_SS}, {@link + * #TYPE_HLS} or {@link #TYPE_OTHER}. */ @Retention(RetentionPolicy.SOURCE) @IntDef({TYPE_DASH, TYPE_SS, TYPE_HLS, TYPE_OTHER}) @@ -533,34 +596,22 @@ public final class C { */ public static final int DATA_TYPE_CUSTOM_BASE = 10000; - /** - * A type constant for tracks of unknown type. - */ + /** A type constant for tracks of unknown type. */ public static final int TRACK_TYPE_UNKNOWN = -1; - /** - * A type constant for tracks of some default type, where the type itself is unknown. - */ + /** A type constant for tracks of some default type, where the type itself is unknown. */ public static final int TRACK_TYPE_DEFAULT = 0; - /** - * A type constant for audio tracks. - */ + /** A type constant for audio tracks. */ public static final int TRACK_TYPE_AUDIO = 1; - /** - * A type constant for video tracks. - */ + /** A type constant for video tracks. */ public static final int TRACK_TYPE_VIDEO = 2; - /** - * A type constant for text tracks. - */ + /** A type constant for text tracks. */ public static final int TRACK_TYPE_TEXT = 3; - /** - * A type constant for metadata tracks. - */ + /** A type constant for metadata tracks. */ public static final int TRACK_TYPE_METADATA = 4; - /** - * A type constant for a dummy or empty track. - */ - public static final int TRACK_TYPE_NONE = 5; + /** A type constant for camera motion tracks. */ + public static final int TRACK_TYPE_CAMERA_MOTION = 5; + /** A type constant for a dummy or empty track. */ + public static final int TRACK_TYPE_NONE = 6; /** * Applications or extensions may define custom {@code TRACK_TYPE_*} constants greater than or * equal to this value. @@ -593,55 +644,42 @@ public final class C { */ public static final int SELECTION_REASON_CUSTOM_BASE = 10000; - /** - * A default size in bytes for an individual allocation that forms part of a larger buffer. - */ + /** A default size in bytes for an individual allocation that forms part of a larger buffer. */ public static final int DEFAULT_BUFFER_SEGMENT_SIZE = 64 * 1024; - /** - * A default size in bytes for a video buffer. - */ + /** A default size in bytes for a video buffer. */ public static final int DEFAULT_VIDEO_BUFFER_SIZE = 200 * DEFAULT_BUFFER_SEGMENT_SIZE; - /** - * A default size in bytes for an audio buffer. - */ + /** A default size in bytes for an audio buffer. */ public static final int DEFAULT_AUDIO_BUFFER_SIZE = 54 * DEFAULT_BUFFER_SEGMENT_SIZE; - /** - * A default size in bytes for a text buffer. - */ + /** A default size in bytes for a text buffer. */ public static final int DEFAULT_TEXT_BUFFER_SIZE = 2 * DEFAULT_BUFFER_SEGMENT_SIZE; - /** - * A default size in bytes for a metadata buffer. - */ + /** A default size in bytes for a metadata buffer. */ public static final int DEFAULT_METADATA_BUFFER_SIZE = 2 * DEFAULT_BUFFER_SEGMENT_SIZE; - /** - * A default size in bytes for a muxed buffer (e.g. containing video, audio and text). - */ - public static final int DEFAULT_MUXED_BUFFER_SIZE = DEFAULT_VIDEO_BUFFER_SIZE - + DEFAULT_AUDIO_BUFFER_SIZE + DEFAULT_TEXT_BUFFER_SIZE; + /** A default size in bytes for a camera motion buffer. */ + public static final int DEFAULT_CAMERA_MOTION_BUFFER_SIZE = 2 * DEFAULT_BUFFER_SEGMENT_SIZE; - /** - * "cenc" scheme type name as defined in ISO/IEC 23001-7:2016. - */ + /** A default size in bytes for a muxed buffer (e.g. containing video, audio and text). */ + public static final int DEFAULT_MUXED_BUFFER_SIZE = + DEFAULT_VIDEO_BUFFER_SIZE + DEFAULT_AUDIO_BUFFER_SIZE + DEFAULT_TEXT_BUFFER_SIZE; + + /** "cenc" scheme type name as defined in ISO/IEC 23001-7:2016. */ + @SuppressWarnings("ConstantField") public static final String CENC_TYPE_cenc = "cenc"; - /** - * "cbc1" scheme type name as defined in ISO/IEC 23001-7:2016. - */ + /** "cbc1" scheme type name as defined in ISO/IEC 23001-7:2016. */ + @SuppressWarnings("ConstantField") public static final String CENC_TYPE_cbc1 = "cbc1"; - /** - * "cens" scheme type name as defined in ISO/IEC 23001-7:2016. - */ + /** "cens" scheme type name as defined in ISO/IEC 23001-7:2016. */ + @SuppressWarnings("ConstantField") public static final String CENC_TYPE_cens = "cens"; - /** - * "cbcs" scheme type name as defined in ISO/IEC 23001-7:2016. - */ + /** "cbcs" scheme type name as defined in ISO/IEC 23001-7:2016. */ + @SuppressWarnings("ConstantField") public static final String CENC_TYPE_cbcs = "cbcs"; /** @@ -733,6 +771,20 @@ public final class C { */ public static final int MSG_SET_AUX_EFFECT_INFO = 5; + /** + * The type of a message that can be passed to a video {@link Renderer} via {@link + * ExoPlayer#createMessage(Target)}. The message payload should be a {@link + * VideoFrameMetadataListener} instance, or null. + */ + public static final int MSG_SET_VIDEO_FRAME_METADATA_LISTENER = 6; + + /** + * The type of a message that can be passed to a camera motion {@link Renderer} via {@link + * ExoPlayer#createMessage(Target)}. The message payload should be a {@link CameraMotionListener} + * instance, or null. + */ + public static final int MSG_SET_CAMERA_MOTION_LISTENER = 7; + /** * Applications or extensions may define custom {@code MSG_*} constants that can be passed to * {@link Renderer}s. These custom constants must be greater than or equal to this value. @@ -740,15 +792,17 @@ public final class C { public static final int MSG_CUSTOM_BASE = 10000; /** - * The stereo mode for 360/3D/VR videos. + * The stereo mode for 360/3D/VR videos. One of {@link Format#NO_VALUE}, {@link + * #STEREO_MODE_MONO}, {@link #STEREO_MODE_TOP_BOTTOM}, {@link #STEREO_MODE_LEFT_RIGHT} or {@link + * #STEREO_MODE_STEREO_MESH}. */ @Retention(RetentionPolicy.SOURCE) @IntDef({ - Format.NO_VALUE, - STEREO_MODE_MONO, - STEREO_MODE_TOP_BOTTOM, - STEREO_MODE_LEFT_RIGHT, - STEREO_MODE_STEREO_MESH + Format.NO_VALUE, + STEREO_MODE_MONO, + STEREO_MODE_TOP_BOTTOM, + STEREO_MODE_LEFT_RIGHT, + STEREO_MODE_STEREO_MESH }) public @interface StereoMode {} /** @@ -770,7 +824,8 @@ public final class C { public static final int STEREO_MODE_STEREO_MESH = 3; /** - * Video colorspaces. + * Video colorspaces. One of {@link Format#NO_VALUE}, {@link #COLOR_SPACE_BT709}, {@link + * #COLOR_SPACE_BT601} or {@link #COLOR_SPACE_BT2020}. */ @Retention(RetentionPolicy.SOURCE) @IntDef({Format.NO_VALUE, COLOR_SPACE_BT709, COLOR_SPACE_BT601, COLOR_SPACE_BT2020}) @@ -789,7 +844,8 @@ public final class C { public static final int COLOR_SPACE_BT2020 = MediaFormat.COLOR_STANDARD_BT2020; /** - * Video color transfer characteristics. + * Video color transfer characteristics. One of {@link Format#NO_VALUE}, {@link + * #COLOR_TRANSFER_SDR}, {@link #COLOR_TRANSFER_ST2084} or {@link #COLOR_TRANSFER_HLG}. */ @Retention(RetentionPolicy.SOURCE) @IntDef({Format.NO_VALUE, COLOR_TRANSFER_SDR, COLOR_TRANSFER_ST2084, COLOR_TRANSFER_HLG}) @@ -808,7 +864,8 @@ public final class C { public static final int COLOR_TRANSFER_HLG = MediaFormat.COLOR_TRANSFER_HLG; /** - * Video color range. + * Video color range. One of {@link Format#NO_VALUE}, {@link #COLOR_RANGE_LIMITED} or {@link + * #COLOR_RANGE_FULL}. */ @Retention(RetentionPolicy.SOURCE) @IntDef({Format.NO_VALUE, COLOR_RANGE_LIMITED, COLOR_RANGE_FULL}) @@ -836,7 +893,12 @@ public final class C { */ public static final int PRIORITY_DOWNLOAD = PRIORITY_PLAYBACK - 1000; - /** Network connection type. */ + /** + * Network connection type. One of {@link #NETWORK_TYPE_UNKNOWN}, {@link #NETWORK_TYPE_OFFLINE}, + * {@link #NETWORK_TYPE_WIFI}, {@link #NETWORK_TYPE_2G}, {@link #NETWORK_TYPE_3G}, {@link + * #NETWORK_TYPE_4G}, {@link #NETWORK_TYPE_CELLULAR_UNKNOWN}, {@link #NETWORK_TYPE_ETHERNET} or + * {@link #NETWORK_TYPE_OTHER}. + */ @Retention(RetentionPolicy.SOURCE) @IntDef({ NETWORK_TYPE_UNKNOWN, 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 f8b7f5f5c2..c466815c79 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 @@ -154,6 +154,7 @@ public class DefaultLoadControl implements LoadControl { } /** Creates a {@link DefaultLoadControl}. */ + @SuppressWarnings("deprecation") public DefaultLoadControl createDefaultLoadControl() { if (allocator == null) { allocator = new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE); @@ -183,15 +184,15 @@ public class DefaultLoadControl implements LoadControl { private int targetBufferSize; private boolean isBuffering; - /** - * Constructs a new instance, using the {@code DEFAULT_*} constants defined in this class. - */ + /** Constructs a new instance, using the {@code DEFAULT_*} constants defined in this class. */ + @SuppressWarnings("deprecation") public DefaultLoadControl() { this(new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE)); } /** @deprecated Use {@link Builder} instead. */ @Deprecated + @SuppressWarnings("deprecation") public DefaultLoadControl(DefaultAllocator allocator) { this( allocator, @@ -205,6 +206,7 @@ public class DefaultLoadControl implements LoadControl { /** @deprecated Use {@link Builder} instead. */ @Deprecated + @SuppressWarnings("deprecation") public DefaultLoadControl( DefaultAllocator allocator, int minBufferMs, 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 6cab53b78a..c0a117c241 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 @@ -35,6 +35,7 @@ import com.google.android.exoplayer2.text.TextRenderer; import com.google.android.exoplayer2.trackselection.TrackSelector; import com.google.android.exoplayer2.video.MediaCodecVideoRenderer; import com.google.android.exoplayer2.video.VideoRendererEventListener; +import com.google.android.exoplayer2.video.spherical.CameraMotionRenderer; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.reflect.Constructor; @@ -52,11 +53,11 @@ public class DefaultRenderersFactory implements RenderersFactory { public static final long DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS = 5000; /** - * Modes for using extension renderers. + * Modes for using extension renderers. One of {@link #EXTENSION_RENDERER_MODE_OFF}, {@link + * #EXTENSION_RENDERER_MODE_ON} or {@link #EXTENSION_RENDERER_MODE_PREFER}. */ @Retention(RetentionPolicy.SOURCE) - @IntDef({EXTENSION_RENDERER_MODE_OFF, EXTENSION_RENDERER_MODE_ON, - EXTENSION_RENDERER_MODE_PREFER}) + @IntDef({EXTENSION_RENDERER_MODE_OFF, EXTENSION_RENDERER_MODE_ON, EXTENSION_RENDERER_MODE_PREFER}) public @interface ExtensionRendererMode {} /** * Do not allow use of extension renderers. @@ -82,7 +83,7 @@ public class DefaultRenderersFactory implements RenderersFactory { protected static final int MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY = 50; private final Context context; - @Nullable private final DrmSessionManager drmSessionManager; + private final @Nullable DrmSessionManager drmSessionManager; private final @ExtensionRendererMode int extensionRendererMode; private final long allowedVideoJoiningTimeMs; @@ -98,6 +99,7 @@ public class DefaultRenderersFactory implements RenderersFactory { * directly to {@link SimpleExoPlayer} or {@link ExoPlayerFactory}. */ @Deprecated + @SuppressWarnings("deprecation") public DefaultRenderersFactory( Context context, @Nullable DrmSessionManager drmSessionManager) { this(context, drmSessionManager, EXTENSION_RENDERER_MODE_OFF); @@ -111,7 +113,7 @@ public class DefaultRenderersFactory implements RenderersFactory { */ public DefaultRenderersFactory( Context context, @ExtensionRendererMode int extensionRendererMode) { - this(context, null, extensionRendererMode, DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS); + this(context, extensionRendererMode, DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS); } /** @@ -119,6 +121,7 @@ public class DefaultRenderersFactory implements RenderersFactory { * DrmSessionManager} directly to {@link SimpleExoPlayer} or {@link ExoPlayerFactory}. */ @Deprecated + @SuppressWarnings("deprecation") public DefaultRenderersFactory( Context context, @Nullable DrmSessionManager drmSessionManager, @@ -138,7 +141,10 @@ public class DefaultRenderersFactory implements RenderersFactory { Context context, @ExtensionRendererMode int extensionRendererMode, long allowedVideoJoiningTimeMs) { - this(context, null, extensionRendererMode, allowedVideoJoiningTimeMs); + this.context = context; + this.extensionRendererMode = extensionRendererMode; + this.allowedVideoJoiningTimeMs = allowedVideoJoiningTimeMs; + this.drmSessionManager = null; } /** @@ -177,6 +183,7 @@ public class DefaultRenderersFactory implements RenderersFactory { extensionRendererMode, renderersList); buildMetadataRenderers(context, metadataRendererOutput, eventHandler.getLooper(), extensionRendererMode, renderersList); + buildCameraMotionRenderers(context, extensionRendererMode, renderersList); buildMiscellaneousRenderers(context, eventHandler, extensionRendererMode, renderersList); return renderersList.toArray(new Renderer[renderersList.size()]); } @@ -355,12 +362,14 @@ public class DefaultRenderersFactory implements RenderersFactory { * * @param context The {@link Context} associated with the player. * @param output An output for the renderers. - * @param outputLooper The looper associated with the thread on which the output should be - * called. + * @param outputLooper The looper associated with the thread on which the output should be called. * @param extensionRendererMode The extension renderer mode. * @param out An array to which the built renderers should be appended. */ - protected void buildTextRenderers(Context context, TextOutput output, Looper outputLooper, + protected void buildTextRenderers( + Context context, + TextOutput output, + Looper outputLooper, @ExtensionRendererMode int extensionRendererMode, ArrayList out) { out.add(new TextRenderer(output, outputLooper)); @@ -371,16 +380,31 @@ public class DefaultRenderersFactory implements RenderersFactory { * * @param context The {@link Context} associated with the player. * @param output An output for the renderers. - * @param outputLooper The looper associated with the thread on which the output should be - * called. + * @param outputLooper The looper associated with the thread on which the output should be called. * @param extensionRendererMode The extension renderer mode. * @param out An array to which the built renderers should be appended. */ - protected void buildMetadataRenderers(Context context, MetadataOutput output, Looper outputLooper, - @ExtensionRendererMode int extensionRendererMode, ArrayList out) { + protected void buildMetadataRenderers( + Context context, + MetadataOutput output, + Looper outputLooper, + @ExtensionRendererMode int extensionRendererMode, + ArrayList out) { out.add(new MetadataRenderer(output, outputLooper)); } + /** + * Builds camera motion renderers for use by the player. + * + * @param context The {@link Context} associated with the player. + * @param extensionRendererMode The extension renderer mode. + * @param out An array to which the built renderers should be appended. + */ + protected void buildCameraMotionRenderers( + Context context, @ExtensionRendererMode int extensionRendererMode, ArrayList out) { + out.add(new CameraMotionRenderer()); + } + /** * Builds any miscellaneous renderers used by the player. * diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlaybackException.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlaybackException.java index ca7367f1b0..ba00d1163f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlaybackException.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlaybackException.java @@ -28,7 +28,8 @@ import java.lang.annotation.RetentionPolicy; public final class ExoPlaybackException extends Exception { /** - * The type of source that produced the error. + * The type of source that produced the error. One of {@link #TYPE_SOURCE}, {@link #TYPE_RENDERER} + * or {@link #TYPE_UNEXPECTED}. */ @Retention(RetentionPolicy.SOURCE) @IntDef({TYPE_SOURCE, TYPE_RENDERER, TYPE_UNEXPECTED}) 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 5780f7b418..452c1043a3 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 @@ -227,6 +227,7 @@ public interface ExoPlayer extends Player { /** @deprecated Use {@link #createMessage(PlayerMessage.Target)} instead. */ @Deprecated + @SuppressWarnings("deprecation") void sendMessages(ExoPlayerMessage... messages); /** @@ -234,6 +235,7 @@ public interface ExoPlayer extends Player { * PlayerMessage#blockUntilDelivered()}. */ @Deprecated + @SuppressWarnings("deprecation") void blockingSendMessages(ExoPlayerMessage... messages); /** 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 648168816f..5e0dd905b9 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 @@ -327,10 +327,10 @@ import java.util.concurrent.CopyOnWriteArraySet; } else { long windowPositionUs = positionMs == C.TIME_UNSET ? timeline.getWindow(windowIndex, window).getDefaultPositionUs() : C.msToUs(positionMs); - Pair periodIndexAndPosition = + Pair periodUidAndPosition = timeline.getPeriodPosition(window, period, windowIndex, windowPositionUs); maskingWindowPositionMs = C.usToMs(windowPositionUs); - maskingPeriodIndex = periodIndexAndPosition.first; + maskingPeriodIndex = timeline.getIndexOfPeriod(periodUidAndPosition.first); } internalPlayer.seekTo(timeline, windowIndex, C.msToUs(positionMs)); for (Player.EventListener listener : listeners) { @@ -415,6 +415,8 @@ import java.util.concurrent.CopyOnWriteArraySet; } @Override + @Deprecated + @SuppressWarnings("deprecation") public void sendMessages(ExoPlayerMessage... messages) { for (ExoPlayerMessage message : messages) { createMessage(message.target).setType(message.messageType).setPayload(message.message).send(); @@ -432,6 +434,8 @@ import java.util.concurrent.CopyOnWriteArraySet; } @Override + @Deprecated + @SuppressWarnings("deprecation") public void blockingSendMessages(ExoPlayerMessage... messages) { List playerMessages = new ArrayList<>(); for (ExoPlayerMessage message : messages) { @@ -464,7 +468,7 @@ import java.util.concurrent.CopyOnWriteArraySet; if (shouldMaskPosition()) { return maskingPeriodIndex; } else { - return playbackInfo.periodId.periodIndex; + return playbackInfo.timeline.getIndexOfPeriod(playbackInfo.periodId.periodUid); } } @@ -473,7 +477,8 @@ import java.util.concurrent.CopyOnWriteArraySet; if (shouldMaskPosition()) { return maskingWindowIndex; } else { - return playbackInfo.timeline.getPeriod(playbackInfo.periodId.periodIndex, period).windowIndex; + return playbackInfo.timeline.getPeriodByUid(playbackInfo.periodId.periodUid, period) + .windowIndex; } } @@ -493,18 +498,13 @@ import java.util.concurrent.CopyOnWriteArraySet; @Override public long getDuration() { - Timeline timeline = playbackInfo.timeline; - if (timeline.isEmpty()) { - return C.TIME_UNSET; - } if (isPlayingAd()) { MediaPeriodId periodId = playbackInfo.periodId; - timeline.getPeriod(periodId.periodIndex, period); + playbackInfo.timeline.getPeriodByUid(periodId.periodUid, period); long adDurationUs = period.getAdDurationUs(periodId.adGroupIndex, periodId.adIndexInAdGroup); return C.usToMs(adDurationUs); - } else { - return timeline.getWindow(getCurrentWindowIndex(), window).getDurationMs(); } + return getContentDuration(); } @Override @@ -569,10 +569,17 @@ import java.util.concurrent.CopyOnWriteArraySet; return isPlayingAd() ? playbackInfo.periodId.adIndexInAdGroup : C.INDEX_UNSET; } + @Override + public long getContentDuration() { + return playbackInfo.timeline.isEmpty() + ? C.TIME_UNSET + : playbackInfo.timeline.getWindow(getCurrentWindowIndex(), window).getDurationMs(); + } + @Override public long getContentPosition() { if (isPlayingAd()) { - playbackInfo.timeline.getPeriod(playbackInfo.periodId.periodIndex, period); + playbackInfo.timeline.getPeriodByUid(playbackInfo.periodId.periodUid, period); return period.getPositionInWindowMs() + C.usToMs(playbackInfo.contentPositionUs); } else { return getCurrentPosition(); @@ -591,7 +598,7 @@ import java.util.concurrent.CopyOnWriteArraySet; long contentBufferedPositionUs = playbackInfo.bufferedPositionUs; if (playbackInfo.loadingMediaPeriodId.isAd()) { Timeline.Period loadingPeriod = - playbackInfo.timeline.getPeriod(playbackInfo.loadingMediaPeriodId.periodIndex, period); + playbackInfo.timeline.getPeriodByUid(playbackInfo.loadingMediaPeriodId.periodUid, period); contentBufferedPositionUs = loadingPeriod.getAdGroupTimeUs(playbackInfo.loadingMediaPeriodId.adGroupIndex); if (contentBufferedPositionUs == C.TIME_END_OF_SOURCE) { @@ -761,7 +768,7 @@ import java.util.concurrent.CopyOnWriteArraySet; private long periodPositionUsToWindowPositionMs(MediaPeriodId periodId, long positionUs) { long positionMs = C.usToMs(positionUs); - playbackInfo.timeline.getPeriod(periodId.periodIndex, period); + playbackInfo.timeline.getPeriodByUid(periodId.periodUid, period); positionMs += period.getPositionInWindowMs(); return positionMs; } 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 7e54726daf..f2aeb7321d 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 @@ -594,7 +594,7 @@ import java.util.Collections; long periodPositionUs; long contentPositionUs; boolean seekPositionAdjusted; - Pair resolvedSeekPosition = + Pair resolvedSeekPosition = resolveSeekPosition(seekPosition, /* trySubsequentPeriods= */ true); if (resolvedSeekPosition == null) { // The seek position was valid for the timeline that it was performed into, but the @@ -605,9 +605,9 @@ import java.util.Collections; seekPositionAdjusted = true; } else { // Update the resolved seek position to take ads into account. - int periodIndex = resolvedSeekPosition.first; + Object periodUid = resolvedSeekPosition.first; contentPositionUs = resolvedSeekPosition.second; - periodId = queue.resolveMediaPeriodIdForAds(periodIndex, contentPositionUs); + periodId = queue.resolveMediaPeriodIdForAds(periodUid, contentPositionUs); if (periodId.isAd()) { periodPositionUs = 0; seekPositionAdjusted = true; @@ -760,7 +760,7 @@ import java.util.Collections; int firstPeriodIndex = timeline.getWindow(timeline.getFirstWindowIndex(shuffleModeEnabled), window) .firstPeriodIndex; - return new MediaPeriodId(firstPeriodIndex); + return new MediaPeriodId(timeline.getUidOfPeriod(firstPeriodIndex)); } private void resetInternal( @@ -853,15 +853,12 @@ import java.util.Collections; private void sendMessageToTargetThread(final PlayerMessage message) { Handler handler = message.getHandler(); handler.post( - new Runnable() { - @Override - public void run() { - try { - deliverMessage(message); - } catch (ExoPlaybackException e) { - Log.e(TAG, "Unexpected error delivering message on external thread.", e); - throw new RuntimeException(e); - } + () -> { + try { + deliverMessage(message); + } catch (ExoPlaybackException e) { + Log.e(TAG, "Unexpected error delivering message on external thread.", e); + throw new RuntimeException(e); } }); } @@ -892,7 +889,7 @@ import java.util.Collections; private boolean resolvePendingMessagePosition(PendingMessageInfo pendingMessageInfo) { if (pendingMessageInfo.resolvedPeriodUid == null) { // Position is still unresolved. Try to find window in current timeline. - Pair periodPosition = + Pair periodPosition = resolveSeekPosition( new SeekPosition( pendingMessageInfo.message.getTimeline(), @@ -903,9 +900,9 @@ import java.util.Collections; return false; } pendingMessageInfo.setResolvedPosition( - periodPosition.first, + playbackInfo.timeline.getIndexOfPeriod(periodPosition.first), periodPosition.second, - playbackInfo.timeline.getUidOfPeriod(periodPosition.first)); + periodPosition.first); } else { // Position has been resolved for a previous timeline. Try to find the updated period index. int index = playbackInfo.timeline.getIndexOfPeriod(pendingMessageInfo.resolvedPeriodUid); @@ -928,7 +925,8 @@ import java.util.Collections; oldPeriodPositionUs--; } // Correct next index if necessary (e.g. after seeking, timeline changes, or new messages) - int currentPeriodIndex = playbackInfo.periodId.periodIndex; + int currentPeriodIndex = + playbackInfo.timeline.getIndexOfPeriod(playbackInfo.periodId.periodUid); PendingMessageInfo previousInfo = nextPendingMessageIndex > 0 ? pendingMessages.get(nextPendingMessageIndex - 1) : null; while (previousInfo != null @@ -1156,7 +1154,7 @@ import java.util.Collections; playbackInfoUpdate.incrementPendingOperationAcks(pendingPrepareCount); pendingPrepareCount = 0; if (pendingInitialSeekPosition != null) { - Pair periodPosition; + Pair periodPosition; try { periodPosition = resolveSeekPosition(pendingInitialSeekPosition, /* trySubsequentPeriods= */ true); @@ -1171,9 +1169,9 @@ import java.util.Collections; // timeline has changed and a suitable seek position could not be resolved in the new one. handleSourceInfoRefreshEndedPlayback(); } else { - int periodIndex = periodPosition.first; + Object periodUid = periodPosition.first; long positionUs = periodPosition.second; - MediaPeriodId periodId = queue.resolveMediaPeriodIdForAds(periodIndex, positionUs); + MediaPeriodId periodId = queue.resolveMediaPeriodIdForAds(periodUid, positionUs); playbackInfo = playbackInfo.fromNewPosition( periodId, periodId.isAd() ? 0 : positionUs, /* contentPositionUs= */ positionUs); @@ -1182,11 +1180,12 @@ import java.util.Collections; if (timeline.isEmpty()) { handleSourceInfoRefreshEndedPlayback(); } else { - Pair defaultPosition = getPeriodPosition(timeline, - timeline.getFirstWindowIndex(shuffleModeEnabled), C.TIME_UNSET); - int periodIndex = defaultPosition.first; + Pair defaultPosition = + getPeriodPosition( + timeline, timeline.getFirstWindowIndex(shuffleModeEnabled), C.TIME_UNSET); + Object periodUid = defaultPosition.first; long startPositionUs = defaultPosition.second; - MediaPeriodId periodId = queue.resolveMediaPeriodIdForAds(periodIndex, startPositionUs); + MediaPeriodId periodId = queue.resolveMediaPeriodIdForAds(periodUid, startPositionUs); playbackInfo = playbackInfo.fromNewPosition( periodId, @@ -1200,12 +1199,12 @@ import java.util.Collections; if (oldTimeline.isEmpty()) { // If the old timeline is empty, the period queue is also empty. if (!timeline.isEmpty()) { - Pair defaultPosition = + Pair defaultPosition = getPeriodPosition( timeline, timeline.getFirstWindowIndex(shuffleModeEnabled), C.TIME_UNSET); - int periodIndex = defaultPosition.first; + Object periodUid = defaultPosition.first; long startPositionUs = defaultPosition.second; - MediaPeriodId periodId = queue.resolveMediaPeriodIdForAds(periodIndex, startPositionUs); + MediaPeriodId periodId = queue.resolveMediaPeriodIdForAds(periodUid, startPositionUs); playbackInfo = playbackInfo.fromNewPosition( periodId, @@ -1215,37 +1214,32 @@ import java.util.Collections; return; } MediaPeriodHolder periodHolder = queue.getFrontPeriod(); - int playingPeriodIndex = playbackInfo.periodId.periodIndex; long contentPositionUs = playbackInfo.contentPositionUs; Object playingPeriodUid = - periodHolder == null ? oldTimeline.getUidOfPeriod(playingPeriodIndex) : periodHolder.uid; + periodHolder == null ? playbackInfo.periodId.periodUid : periodHolder.uid; int periodIndex = timeline.getIndexOfPeriod(playingPeriodUid); if (periodIndex == C.INDEX_UNSET) { // We didn't find the current period in the new timeline. Attempt to resolve a subsequent // period whose window we can restart from. - int newPeriodIndex = resolveSubsequentPeriod(playingPeriodIndex, oldTimeline, timeline); - if (newPeriodIndex == C.INDEX_UNSET) { + Object newPeriodUid = resolveSubsequentPeriod(playingPeriodUid, oldTimeline, timeline); + if (newPeriodUid == null) { // We failed to resolve a suitable restart position. handleSourceInfoRefreshEndedPlayback(); return; } // We resolved a subsequent period. Seek to the default position in the corresponding window. - Pair defaultPosition = getPeriodPosition(timeline, - timeline.getPeriod(newPeriodIndex, period).windowIndex, C.TIME_UNSET); - newPeriodIndex = defaultPosition.first; + Pair defaultPosition = + getPeriodPosition( + timeline, timeline.getPeriodByUid(newPeriodUid, period).windowIndex, C.TIME_UNSET); + newPeriodUid = defaultPosition.first; contentPositionUs = defaultPosition.second; - MediaPeriodId periodId = queue.resolveMediaPeriodIdForAds(newPeriodIndex, contentPositionUs); + MediaPeriodId periodId = queue.resolveMediaPeriodIdForAds(newPeriodUid, contentPositionUs); if (periodHolder != null) { - // Clear the index of each holder that doesn't contain the default position. If a holder - // contains the default position then update its index so it can be re-used when seeking. - Object newPeriodUid = timeline.getUidOfPeriod(newPeriodIndex); - periodHolder.info = periodHolder.info.copyWithPeriodIndex(C.INDEX_UNSET); + // Update the new playing media period info if it already exists. while (periodHolder.next != null) { periodHolder = periodHolder.next; - if (periodHolder.uid.equals(newPeriodUid)) { - periodHolder.info = queue.getUpdatedMediaPeriodInfo(periodHolder.info, newPeriodIndex); - } else { - periodHolder.info = periodHolder.info.copyWithPeriodIndex(C.INDEX_UNSET); + if (periodHolder.info.id.equals(periodId)) { + periodHolder.info = queue.getUpdatedMediaPeriodInfo(periodHolder.info); } } } @@ -1255,14 +1249,10 @@ import java.util.Collections; return; } - // The current period is in the new timeline. Update the playback info. - if (periodIndex != playingPeriodIndex) { - playbackInfo = playbackInfo.copyWithPeriodIndex(periodIndex); - } - MediaPeriodId playingPeriodId = playbackInfo.periodId; if (playingPeriodId.isAd()) { - MediaPeriodId periodId = queue.resolveMediaPeriodIdForAds(periodIndex, contentPositionUs); + MediaPeriodId periodId = + queue.resolveMediaPeriodIdForAds(playingPeriodUid, contentPositionUs); if (!periodId.equals(playingPeriodId)) { // The previously playing ad should no longer be played, so skip it. long seekPositionUs = @@ -1287,16 +1277,17 @@ import java.util.Collections; /** * Given a period index into an old timeline, finds the first subsequent period that also exists - * in a new timeline. The index of this period in the new timeline is returned. + * in a new timeline. The uid of this period in the new timeline is returned. * - * @param oldPeriodIndex The index of the period in the old timeline. + * @param oldPeriodUid The index of the period in the old timeline. * @param oldTimeline The old timeline. * @param newTimeline The new timeline. - * @return The index in the new timeline of the first subsequent period, or {@link C#INDEX_UNSET} - * if no such period was found. + * @return The uid in the new timeline of the first subsequent period, or null if no such period + * was found. */ - private int resolveSubsequentPeriod( - int oldPeriodIndex, Timeline oldTimeline, Timeline newTimeline) { + private @Nullable Object resolveSubsequentPeriod( + Object oldPeriodUid, Timeline oldTimeline, Timeline newTimeline) { + int oldPeriodIndex = oldTimeline.getIndexOfPeriod(oldPeriodUid); int newPeriodIndex = C.INDEX_UNSET; int maxIterations = oldTimeline.getPeriodCount(); for (int i = 0; i < maxIterations && newPeriodIndex == C.INDEX_UNSET; i++) { @@ -1308,11 +1299,11 @@ import java.util.Collections; } newPeriodIndex = newTimeline.getIndexOfPeriod(oldTimeline.getUidOfPeriod(oldPeriodIndex)); } - return newPeriodIndex; + return newPeriodIndex == C.INDEX_UNSET ? null : newTimeline.getUidOfPeriod(newPeriodIndex); } /** - * Converts a {@link SeekPosition} into the corresponding (periodIndex, periodPositionUs) for the + * Converts a {@link SeekPosition} into the corresponding (periodUid, periodPositionUs) for the * internal timeline. * * @param seekPosition The position to resolve. @@ -1322,7 +1313,7 @@ import java.util.Collections; * @throws IllegalSeekPositionException If the window index of the seek position is outside the * bounds of the timeline. */ - private Pair resolveSeekPosition( + private Pair resolveSeekPosition( SeekPosition seekPosition, boolean trySubsequentPeriods) { Timeline timeline = playbackInfo.timeline; Timeline seekTimeline = seekPosition.timeline; @@ -1336,7 +1327,7 @@ import java.util.Collections; seekTimeline = timeline; } // Map the SeekPosition to a position in the corresponding timeline. - Pair periodPosition; + Pair periodPosition; try { periodPosition = seekTimeline.getPeriodPosition(window, period, seekPosition.windowIndex, seekPosition.windowPositionUs); @@ -1350,15 +1341,15 @@ import java.util.Collections; return periodPosition; } // Attempt to find the mapped period in the internal timeline. - int periodIndex = timeline.getIndexOfPeriod(seekTimeline.getUidOfPeriod(periodPosition.first)); + int periodIndex = timeline.getIndexOfPeriod(periodPosition.first); if (periodIndex != C.INDEX_UNSET) { // We successfully located the period in the internal timeline. - return Pair.create(periodIndex, periodPosition.second); + return periodPosition; } if (trySubsequentPeriods) { // Try and find a subsequent period from the seek timeline in the internal timeline. - periodIndex = resolveSubsequentPeriod(periodPosition.first, seekTimeline, timeline); - if (periodIndex != C.INDEX_UNSET) { + Object periodUid = resolveSubsequentPeriod(periodPosition.first, seekTimeline, timeline); + if (periodUid != null) { // We found one. Map the SeekPosition onto the corresponding default position. return getPeriodPosition( timeline, timeline.getPeriod(periodIndex, period).windowIndex, C.TIME_UNSET); @@ -1372,7 +1363,7 @@ import java.util.Collections; * Calls {@link Timeline#getPeriodPosition(Timeline.Window, Timeline.Period, int, long)} using the * current timeline. */ - private Pair getPeriodPosition( + private Pair getPeriodPosition( Timeline timeline, int windowIndex, long windowPositionUs) { return timeline.getPeriodPosition(window, period, windowIndex, windowPositionUs); } @@ -1509,14 +1500,12 @@ import java.util.Collections; if (info == null) { mediaSource.maybeThrowSourceInfoRefreshError(); } else { - Object uid = playbackInfo.timeline.getUidOfPeriod(info.id.periodIndex); MediaPeriod mediaPeriod = queue.enqueueNextMediaPeriod( rendererCapabilities, trackSelector, loadControl.getAllocator(), mediaSource, - uid, info); mediaPeriod.prepare(this, info.startPositionUs); setIsLoading(true); 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 a74a2ac1ca..e42dd03dbe 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 @@ -62,7 +62,6 @@ import com.google.android.exoplayer2.util.Assertions; * @param trackSelector The track selector. * @param allocator The allocator. * @param mediaSource The media source that produced the media period. - * @param uid The unique identifier for the containing timeline period. * @param info Information used to identify this media period in its timeline period. */ public MediaPeriodHolder( @@ -71,13 +70,12 @@ import com.google.android.exoplayer2.util.Assertions; TrackSelector trackSelector, Allocator allocator, MediaSource mediaSource, - Object uid, MediaPeriodInfo info) { this.rendererCapabilities = rendererCapabilities; this.rendererPositionOffsetUs = rendererPositionOffsetUs - info.startPositionUs; this.trackSelector = trackSelector; this.mediaSource = mediaSource; - this.uid = Assertions.checkNotNull(uid); + this.uid = Assertions.checkNotNull(info.id.periodUid); this.info = info; sampleStreams = new SampleStream[rendererCapabilities.length]; mayRetainStreamFlags = new boolean[rendererCapabilities.length]; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfo.java index b887e8222e..ba19b54c3f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfo.java @@ -62,20 +62,6 @@ import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; this.isFinal = isFinal; } - /** - * Returns a copy of this instance with the period identifier's period index set to the specified - * value. - */ - public MediaPeriodInfo copyWithPeriodIndex(int periodIndex) { - return new MediaPeriodInfo( - id.copyWithPeriodIndex(periodIndex), - startPositionUs, - contentPositionUs, - durationUs, - isLastInTimelinePeriod, - isFinal); - } - /** Returns a copy of this instance with the start position set to the specified value. */ public MediaPeriodInfo copyWithStartPositionUs(long startPositionUs) { return new MediaPeriodInfo( 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 e9be2d985e..2edf7bb8c6 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 @@ -30,7 +30,6 @@ import com.google.android.exoplayer2.util.Assertions; * loading media period at the end of the queue, with methods for controlling loading and updating * the queue. Also has a reference to the media period currently being read. */ -@SuppressWarnings("UngroupedOverloads") /* package */ final class MediaPeriodQueue { /** @@ -135,7 +134,6 @@ import com.google.android.exoplayer2.util.Assertions; * @param trackSelector The track selector. * @param allocator The allocator. * @param mediaSource The media source that produced the media period. - * @param uid The unique identifier for the containing timeline period. * @param info Information used to identify this media period in its timeline period. */ public MediaPeriod enqueueNextMediaPeriod( @@ -143,7 +141,6 @@ import com.google.android.exoplayer2.util.Assertions; TrackSelector trackSelector, Allocator allocator, MediaSource mediaSource, - Object uid, MediaPeriodInfo info) { long rendererPositionOffsetUs = loading == null @@ -156,7 +153,6 @@ import com.google.android.exoplayer2.util.Assertions; trackSelector, allocator, mediaSource, - uid, info); if (loading != null) { Assertions.checkState(hasPlayingPeriod()); @@ -304,14 +300,14 @@ import com.google.android.exoplayer2.util.Assertions; // TODO: Merge this into setTimeline so that the queue gets updated as soon as the new timeline // is set, once all cases handled by ExoPlayerImplInternal.handleSourceInfoRefreshed can be // handled here. - int periodIndex = playingPeriodId.periodIndex; + int periodIndex = timeline.getIndexOfPeriod(playingPeriodId.periodUid); // The front period is either playing now, or is being loaded and will become the playing // period. MediaPeriodHolder previousPeriodHolder = null; MediaPeriodHolder periodHolder = getFrontPeriod(); while (periodHolder != null) { if (previousPeriodHolder == null) { - periodHolder.info = getUpdatedMediaPeriodInfo(periodHolder.info, periodIndex); + periodHolder.info = getUpdatedMediaPeriodInfo(periodHolder.info); } else { // Check this period holder still follows the previous one, based on the new timeline. if (periodIndex == C.INDEX_UNSET @@ -325,8 +321,8 @@ import com.google.android.exoplayer2.util.Assertions; // We've loaded a next media period that is not in the new timeline. return !removeAfter(previousPeriodHolder); } - // Update the period index. - periodHolder.info = getUpdatedMediaPeriodInfo(periodHolder.info, periodIndex); + // Update the period holder. + periodHolder.info = getUpdatedMediaPeriodInfo(periodHolder.info); // Check the media period information matches the new timeline. if (!canKeepMediaPeriodHolder(periodHolder, periodInfo)) { return !removeAfter(previousPeriodHolder); @@ -348,16 +344,29 @@ import com.google.android.exoplayer2.util.Assertions; /** * Returns new media period info based on specified {@code mediaPeriodInfo} but taking into - * account the current timeline, and with the period index updated to {@code newPeriodIndex}. + * account the current timeline. This method must only be called if the period is still part of + * the current timeline. * - * @param mediaPeriodInfo Media period info for a media period based on an old timeline. - * @param newPeriodIndex The new period index in the new timeline for the existing media period. + * @param info Media period info for a media period based on an old timeline. * @return The updated media period info for the current timeline. */ - public MediaPeriodInfo getUpdatedMediaPeriodInfo( - MediaPeriodInfo mediaPeriodInfo, int newPeriodIndex) { - return getUpdatedMediaPeriodInfo( - mediaPeriodInfo, mediaPeriodInfo.id.copyWithPeriodIndex(newPeriodIndex)); + public MediaPeriodInfo getUpdatedMediaPeriodInfo(MediaPeriodInfo info) { + boolean isLastInPeriod = isLastInPeriod(info.id); + boolean isLastInTimeline = isLastInTimeline(info.id, isLastInPeriod); + timeline.getPeriodByUid(info.id.periodUid, period); + long durationUs = + info.id.isAd() + ? period.getAdDurationUs(info.id.adGroupIndex, info.id.adIndexInAdGroup) + : (info.id.endPositionUs == C.TIME_END_OF_SOURCE + ? period.getDurationUs() + : info.id.endPositionUs); + return new MediaPeriodInfo( + info.id, + info.startPositionUs, + info.contentPositionUs, + durationUs, + isLastInPeriod, + isLastInTimeline); } /** @@ -365,13 +374,13 @@ import com.google.android.exoplayer2.util.Assertions; * played, returning an identifier for an ad group if one needs to be played before the specified * position, or an identifier for a content media period if not. * - * @param periodIndex The index of the timeline period to play. + * @param periodUid The uid of the timeline period to play. * @param positionUs The next content position in the period to play. * @return The identifier for the first media period to play, taking into account unplayed ads. */ - public MediaPeriodId resolveMediaPeriodIdForAds(int periodIndex, long positionUs) { - long windowSequenceNumber = resolvePeriodIndexToWindowSequenceNumber(periodIndex); - return resolveMediaPeriodIdForAds(periodIndex, positionUs, windowSequenceNumber); + public MediaPeriodId resolveMediaPeriodIdForAds(Object periodUid, long positionUs) { + long windowSequenceNumber = resolvePeriodIndexToWindowSequenceNumber(periodUid); + return resolveMediaPeriodIdForAds(periodUid, positionUs, windowSequenceNumber); } // Internal methods. @@ -381,15 +390,15 @@ import com.google.android.exoplayer2.util.Assertions; * played, returning an identifier for an ad group if one needs to be played before the specified * position, or an identifier for a content media period if not. * - * @param periodIndex The index of the timeline period to play. + * @param periodUid The uid of the timeline period to play. * @param positionUs The next content position in the period to play. * @param windowSequenceNumber The sequence number of the window in the buffered sequence of * windows this period is part of. * @return The identifier for the first media period to play, taking into account unplayed ads. */ private MediaPeriodId resolveMediaPeriodIdForAds( - int periodIndex, long positionUs, long windowSequenceNumber) { - timeline.getPeriod(periodIndex, period); + Object periodUid, long positionUs, long windowSequenceNumber) { + timeline.getPeriodByUid(periodUid, period); int adGroupIndex = period.getAdGroupIndexForPositionUs(positionUs); if (adGroupIndex == C.INDEX_UNSET) { int nextAdGroupIndex = period.getAdGroupIndexAfterPositionUs(positionUs); @@ -397,24 +406,23 @@ import com.google.android.exoplayer2.util.Assertions; nextAdGroupIndex == C.INDEX_UNSET ? C.TIME_END_OF_SOURCE : period.getAdGroupTimeUs(nextAdGroupIndex); - return new MediaPeriodId(periodIndex, windowSequenceNumber, endPositionUs); + return new MediaPeriodId(periodUid, windowSequenceNumber, endPositionUs); } else { int adIndexInAdGroup = period.getFirstAdIndexToPlay(adGroupIndex); - return new MediaPeriodId(periodIndex, adGroupIndex, adIndexInAdGroup, windowSequenceNumber); + return new MediaPeriodId(periodUid, adGroupIndex, adIndexInAdGroup, windowSequenceNumber); } } /** - * Resolves the specified period index to a corresponding window sequence number. Either by - * reusing the window sequence number of an existing matching media period or by creating a new - * window sequence number. + * Resolves the specified period uid to a corresponding window sequence number. Either by reusing + * the window sequence number of an existing matching media period or by creating a new window + * sequence number. * - * @param periodIndex The index of the timeline period. + * @param periodUid The uid of the timeline period. * @return A window sequence number for a media period created for this timeline period. */ - private long resolvePeriodIndexToWindowSequenceNumber(int periodIndex) { - Object periodUid = timeline.getPeriod(periodIndex, period, /* setIds= */ true).uid; - int windowIndex = period.windowIndex; + private long resolvePeriodIndexToWindowSequenceNumber(Object periodUid) { + int windowIndex = timeline.getPeriodByUid(periodUid, period).windowIndex; if (oldFrontPeriodUid != null) { int oldFrontPeriodIndex = timeline.getIndexOfPeriod(oldFrontPeriodUid); if (oldFrontPeriodIndex != C.INDEX_UNSET) { @@ -469,32 +477,32 @@ import com.google.android.exoplayer2.util.Assertions; if (lastValidPeriodHolder == null) { return true; } + int currentPeriodIndex = timeline.getIndexOfPeriod(lastValidPeriodHolder.uid); while (true) { int nextPeriodIndex = timeline.getNextPeriodIndex( - lastValidPeriodHolder.info.id.periodIndex, - period, - window, - repeatMode, - shuffleModeEnabled); + currentPeriodIndex, period, window, repeatMode, shuffleModeEnabled); while (lastValidPeriodHolder.next != null && !lastValidPeriodHolder.info.isLastInTimelinePeriod) { lastValidPeriodHolder = lastValidPeriodHolder.next; } - if (nextPeriodIndex == C.INDEX_UNSET - || lastValidPeriodHolder.next == null - || lastValidPeriodHolder.next.info.id.periodIndex != nextPeriodIndex) { + + if (nextPeriodIndex == C.INDEX_UNSET || lastValidPeriodHolder.next == null) { + break; + } + int nextPeriodHolderPeriodIndex = timeline.getIndexOfPeriod(lastValidPeriodHolder.next.uid); + if (nextPeriodHolderPeriodIndex != nextPeriodIndex) { break; } lastValidPeriodHolder = lastValidPeriodHolder.next; + currentPeriodIndex = nextPeriodIndex; } // Release any period holders that don't match the new period order. boolean readingPeriodRemoved = removeAfter(lastValidPeriodHolder); // Update the period info for the last holder, as it may now be the last period in the timeline. - lastValidPeriodHolder.info = - getUpdatedMediaPeriodInfo(lastValidPeriodHolder.info, lastValidPeriodHolder.info.id); + lastValidPeriodHolder.info = getUpdatedMediaPeriodInfo(lastValidPeriodHolder.info); // If renderers may have read from a period that's been removed, it is necessary to restart. return !readingPeriodRemoved || !hasPlayingPeriod(); @@ -525,9 +533,10 @@ import com.google.android.exoplayer2.util.Assertions; // timeline is updated, to avoid repeatedly checking the same timeline. MediaPeriodInfo mediaPeriodInfo = mediaPeriodHolder.info; if (mediaPeriodInfo.isLastInTimelinePeriod) { + int currentPeriodIndex = timeline.getIndexOfPeriod(mediaPeriodInfo.id.periodUid); int nextPeriodIndex = timeline.getNextPeriodIndex( - mediaPeriodInfo.id.periodIndex, period, window, repeatMode, shuffleModeEnabled); + currentPeriodIndex, period, window, repeatMode, shuffleModeEnabled); if (nextPeriodIndex == C.INDEX_UNSET) { // We can't create a next period yet. return null; @@ -546,7 +555,7 @@ import com.google.android.exoplayer2.util.Assertions; // the buffer, and start buffering from this point. long defaultPositionProjectionUs = mediaPeriodHolder.getRendererOffset() + mediaPeriodInfo.durationUs - rendererPositionUs; - Pair defaultPosition = + Pair defaultPosition = timeline.getPeriodPosition( window, period, @@ -556,7 +565,7 @@ import com.google.android.exoplayer2.util.Assertions; if (defaultPosition == null) { return null; } - nextPeriodIndex = defaultPosition.first; + nextPeriodUid = defaultPosition.first; startPositionUs = defaultPosition.second; if (mediaPeriodHolder.next != null && mediaPeriodHolder.next.uid.equals(nextPeriodUid)) { windowSequenceNumber = mediaPeriodHolder.next.info.id.windowSequenceNumber; @@ -567,12 +576,12 @@ import com.google.android.exoplayer2.util.Assertions; startPositionUs = 0; } MediaPeriodId periodId = - resolveMediaPeriodIdForAds(nextPeriodIndex, startPositionUs, windowSequenceNumber); + resolveMediaPeriodIdForAds(nextPeriodUid, startPositionUs, windowSequenceNumber); return getMediaPeriodInfo(periodId, startPositionUs, startPositionUs); } MediaPeriodId currentPeriodId = mediaPeriodInfo.id; - timeline.getPeriod(currentPeriodId.periodIndex, period); + timeline.getPeriodByUid(currentPeriodId.periodUid, period); if (currentPeriodId.isAd()) { int adGroupIndex = currentPeriodId.adGroupIndex; int adCountInCurrentAdGroup = period.getAdCountInAdGroup(adGroupIndex); @@ -586,7 +595,7 @@ import com.google.android.exoplayer2.util.Assertions; return !period.isAdAvailable(adGroupIndex, nextAdIndexInAdGroup) ? null : getMediaPeriodInfoForAd( - currentPeriodId.periodIndex, + currentPeriodId.periodUid, adGroupIndex, nextAdIndexInAdGroup, mediaPeriodInfo.contentPositionUs, @@ -594,7 +603,7 @@ import com.google.android.exoplayer2.util.Assertions; } else { // Play content from the ad group position. return getMediaPeriodInfoForContent( - currentPeriodId.periodIndex, + currentPeriodId.periodUid, mediaPeriodInfo.contentPositionUs, currentPeriodId.windowSequenceNumber); } @@ -604,7 +613,7 @@ import com.google.android.exoplayer2.util.Assertions; if (nextAdGroupIndex == C.INDEX_UNSET) { // The next ad group can't be played. Play content from the ad group position instead. return getMediaPeriodInfoForContent( - currentPeriodId.periodIndex, + currentPeriodId.periodUid, mediaPeriodInfo.id.endPositionUs, currentPeriodId.windowSequenceNumber); } @@ -612,7 +621,7 @@ import com.google.android.exoplayer2.util.Assertions; return !period.isAdAvailable(nextAdGroupIndex, adIndexInAdGroup) ? null : getMediaPeriodInfoForAd( - currentPeriodId.periodIndex, + currentPeriodId.periodUid, nextAdGroupIndex, adIndexInAdGroup, mediaPeriodInfo.id.endPositionUs, @@ -634,7 +643,7 @@ import com.google.android.exoplayer2.util.Assertions; } long contentDurationUs = period.getDurationUs(); return getMediaPeriodInfoForAd( - currentPeriodId.periodIndex, + currentPeriodId.periodUid, adGroupIndex, adIndexInAdGroup, contentDurationUs, @@ -642,57 +651,37 @@ import com.google.android.exoplayer2.util.Assertions; } } - private MediaPeriodInfo getUpdatedMediaPeriodInfo(MediaPeriodInfo info, MediaPeriodId newId) { - long startPositionUs = info.startPositionUs; - boolean isLastInPeriod = isLastInPeriod(newId); - boolean isLastInTimeline = isLastInTimeline(newId, isLastInPeriod); - timeline.getPeriod(newId.periodIndex, period); - long durationUs = - newId.isAd() - ? period.getAdDurationUs(newId.adGroupIndex, newId.adIndexInAdGroup) - : (newId.endPositionUs == C.TIME_END_OF_SOURCE - ? period.getDurationUs() - : newId.endPositionUs); - return new MediaPeriodInfo( - newId, - startPositionUs, - info.contentPositionUs, - durationUs, - isLastInPeriod, - isLastInTimeline); - } - private MediaPeriodInfo getMediaPeriodInfo( MediaPeriodId id, long contentPositionUs, long startPositionUs) { - timeline.getPeriod(id.periodIndex, period); + timeline.getPeriodByUid(id.periodUid, period); if (id.isAd()) { if (!period.isAdAvailable(id.adGroupIndex, id.adIndexInAdGroup)) { return null; } return getMediaPeriodInfoForAd( - id.periodIndex, + id.periodUid, id.adGroupIndex, id.adIndexInAdGroup, contentPositionUs, id.windowSequenceNumber); } else { - return getMediaPeriodInfoForContent(id.periodIndex, startPositionUs, id.windowSequenceNumber); + return getMediaPeriodInfoForContent(id.periodUid, startPositionUs, id.windowSequenceNumber); } } private MediaPeriodInfo getMediaPeriodInfoForAd( - int periodIndex, + Object periodUid, int adGroupIndex, int adIndexInAdGroup, long contentPositionUs, long windowSequenceNumber) { MediaPeriodId id = - new MediaPeriodId(periodIndex, adGroupIndex, adIndexInAdGroup, windowSequenceNumber); + new MediaPeriodId(periodUid, adGroupIndex, adIndexInAdGroup, windowSequenceNumber); boolean isLastInPeriod = isLastInPeriod(id); boolean isLastInTimeline = isLastInTimeline(id, isLastInPeriod); long durationUs = timeline - .getPeriod(id.periodIndex, period) + .getPeriodByUid(id.periodUid, period) .getAdDurationUs(id.adGroupIndex, id.adIndexInAdGroup); long startPositionUs = adIndexInAdGroup == period.getFirstAdIndexToPlay(adGroupIndex) @@ -708,14 +697,14 @@ import com.google.android.exoplayer2.util.Assertions; } private MediaPeriodInfo getMediaPeriodInfoForContent( - int periodIndex, long startPositionUs, long windowSequenceNumber) { + Object periodUid, long startPositionUs, long windowSequenceNumber) { int nextAdGroupIndex = period.getAdGroupIndexAfterPositionUs(startPositionUs); long endPositionUs = nextAdGroupIndex == C.INDEX_UNSET ? C.TIME_END_OF_SOURCE : period.getAdGroupTimeUs(nextAdGroupIndex); - MediaPeriodId id = new MediaPeriodId(periodIndex, windowSequenceNumber, endPositionUs); - timeline.getPeriod(id.periodIndex, period); + MediaPeriodId id = new MediaPeriodId(periodUid, windowSequenceNumber, endPositionUs); + timeline.getPeriodByUid(id.periodUid, period); boolean isLastInPeriod = isLastInPeriod(id); boolean isLastInTimeline = isLastInTimeline(id, isLastInPeriod); long durationUs = @@ -725,7 +714,7 @@ import com.google.android.exoplayer2.util.Assertions; } private boolean isLastInPeriod(MediaPeriodId id) { - int adGroupCount = timeline.getPeriod(id.periodIndex, period).getAdGroupCount(); + int adGroupCount = timeline.getPeriodByUid(id.periodUid, period).getAdGroupCount(); if (adGroupCount == 0) { return true; } @@ -749,9 +738,10 @@ import com.google.android.exoplayer2.util.Assertions; } private boolean isLastInTimeline(MediaPeriodId id, boolean isLastMediaPeriodInPeriod) { - int windowIndex = timeline.getPeriod(id.periodIndex, period).windowIndex; + int periodIndex = timeline.getIndexOfPeriod(id.periodUid); + int windowIndex = timeline.getPeriod(periodIndex, period).windowIndex; return !timeline.getWindow(windowIndex, window).isDynamic - && timeline.isLastPeriod(id.periodIndex, period, window, repeatMode, shuffleModeEnabled) + && timeline.isLastPeriod(periodIndex, period, window, repeatMode, shuffleModeEnabled) && isLastMediaPeriodInPeriod; } } 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 b338de15b4..02058c0484 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 @@ -29,7 +29,8 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; * Dummy media period id used while the timeline is empty and no period id is specified. This id * is used when playback infos are created with {@link #createDummy(long, TrackSelectorResult)}. */ - public static final MediaPeriodId DUMMY_MEDIA_PERIOD_ID = new MediaPeriodId(/* periodIndex= */ 0); + public static final MediaPeriodId DUMMY_MEDIA_PERIOD_ID = + new MediaPeriodId(/* periodUid= */ new Object()); /** The current {@link Timeline}. */ public final Timeline timeline; @@ -149,23 +150,6 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; startPositionUs); } - public PlaybackInfo copyWithPeriodIndex(int periodIndex) { - return new PlaybackInfo( - timeline, - manifest, - periodId.copyWithPeriodIndex(periodIndex), - startPositionUs, - contentPositionUs, - playbackState, - isLoading, - trackGroups, - trackSelectorResult, - loadingMediaPeriodId, - bufferedPositionUs, - totalBufferedDurationUs, - positionUs); - } - public PlaybackInfo copyWithTimeline(Timeline timeline, Object manifest) { return new PlaybackInfo( timeline, 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 87aec0c253..e99d62a417 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 @@ -29,7 +29,9 @@ import com.google.android.exoplayer2.source.TrackGroupArray; 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.VideoFrameMetadataListener; import com.google.android.exoplayer2.video.VideoListener; +import com.google.android.exoplayer2.video.spherical.CameraMotionListener; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -165,12 +167,54 @@ public interface Player { */ void removeVideoListener(VideoListener listener); + /** + * Sets a listener to receive video frame metadata events. + * + *

This method is intended to be called by the same component that sets the {@link Surface} + * onto which video will be rendered. If using ExoPlayer's standard UI components, this method + * should not be called directly from application code. + * + * @param listener The listener. + */ + void setVideoFrameMetadataListener(VideoFrameMetadataListener listener); + + /** + * Clears the listener which receives video frame metadata events if it matches the one passed. + * Else does nothing. + * + * @param listener The listener to clear. + */ + void clearVideoFrameMetadataListener(VideoFrameMetadataListener listener); + + /** + * Sets a listener of camera motion events. + * + * @param listener The listener. + */ + void setCameraMotionListener(CameraMotionListener listener); + + /** + * Clears the listener which receives camera motion events if it matches the one passed. Else + * does nothing. + * + * @param listener The listener to clear. + */ + void clearCameraMotionListener(CameraMotionListener listener); + /** * Clears any {@link Surface}, {@link SurfaceHolder}, {@link SurfaceView} or {@link TextureView} * currently set on the player. */ void clearVideoSurface(); + /** + * Clears the {@link Surface} onto which video is being rendered if it matches the one passed. + * Else does nothing. + * + * @param surface The surface to clear. + */ + void clearVideoSurface(Surface surface); + /** * Sets the {@link Surface} onto which video will be rendered. The caller is responsible for * tracking the lifecycle of the surface, and must clear the surface by calling {@code @@ -186,14 +230,6 @@ public interface Player { */ void setVideoSurface(@Nullable Surface surface); - /** - * Clears the {@link Surface} onto which video is being rendered if it matches the one passed. - * Else does nothing. - * - * @param surface The surface to clear. - */ - void clearVideoSurface(Surface surface); - /** * Sets the {@link SurfaceHolder} that holds the {@link Surface} onto which video will be * rendered. The player will track the lifecycle of the surface automatically. @@ -372,6 +408,7 @@ public interface Player { abstract class DefaultEventListener implements EventListener { @Override + @SuppressWarnings("deprecation") public void onTimelineChanged( Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason) { // Call deprecated version. Otherwise, do nothing. @@ -405,11 +442,12 @@ public interface Player { int STATE_ENDED = 4; /** - * Repeat modes for playback. + * Repeat modes for playback. One of {@link #REPEAT_MODE_OFF}, {@link #REPEAT_MODE_ONE} or {@link + * #REPEAT_MODE_ALL}. */ @Retention(RetentionPolicy.SOURCE) @IntDef({REPEAT_MODE_OFF, REPEAT_MODE_ONE, REPEAT_MODE_ALL}) - public @interface RepeatMode {} + @interface RepeatMode {} /** * Normal playback without repetition. */ @@ -423,7 +461,11 @@ public interface Player { */ int REPEAT_MODE_ALL = 2; - /** Reasons for position discontinuities. */ + /** + * Reasons for position discontinuities. One of {@link #DISCONTINUITY_REASON_PERIOD_TRANSITION}, + * {@link #DISCONTINUITY_REASON_SEEK}, {@link #DISCONTINUITY_REASON_SEEK_ADJUSTMENT}, {@link + * #DISCONTINUITY_REASON_AD_INSERTION} or {@link #DISCONTINUITY_REASON_INTERNAL}. + */ @Retention(RetentionPolicy.SOURCE) @IntDef({ DISCONTINUITY_REASON_PERIOD_TRANSITION, @@ -432,7 +474,7 @@ public interface Player { DISCONTINUITY_REASON_AD_INSERTION, DISCONTINUITY_REASON_INTERNAL }) - public @interface DiscontinuityReason {} + @interface DiscontinuityReason {} /** * Automatic playback transition from one period in the timeline to the next. The period index may * be the same as it was before the discontinuity in case the current period is repeated. @@ -451,12 +493,16 @@ public interface Player { int DISCONTINUITY_REASON_INTERNAL = 4; /** - * Reasons for timeline and/or manifest changes. + * Reasons for timeline and/or manifest changes. One of {@link #TIMELINE_CHANGE_REASON_PREPARED}, + * {@link #TIMELINE_CHANGE_REASON_RESET} or {@link #TIMELINE_CHANGE_REASON_DYNAMIC}. */ @Retention(RetentionPolicy.SOURCE) - @IntDef({TIMELINE_CHANGE_REASON_PREPARED, TIMELINE_CHANGE_REASON_RESET, - TIMELINE_CHANGE_REASON_DYNAMIC}) - public @interface TimelineChangeReason {} + @IntDef({ + TIMELINE_CHANGE_REASON_PREPARED, + TIMELINE_CHANGE_REASON_RESET, + TIMELINE_CHANGE_REASON_DYNAMIC + }) + @interface TimelineChangeReason {} /** * Timeline and manifest changed as a result of a player initialization with new media. */ @@ -719,8 +765,8 @@ public interface Player { @Nullable Object getCurrentTag(); /** - * Returns the duration of the current window in milliseconds, or {@link C#TIME_UNSET} if the - * duration is not known. + * Returns the duration of the current content window or ad in milliseconds, or {@link + * C#TIME_UNSET} if the duration is not known. */ long getDuration(); @@ -778,6 +824,13 @@ public interface Player { */ int getCurrentAdIndexInAdGroup(); + /** + * If {@link #isPlayingAd()} returns {@code true}, returns the duration of the current content + * window in milliseconds, or {@link C#TIME_UNSET} if the duration is not known. If there is no ad + * playing, the returned duration is the same as that returned by {@link #getDuration()}. + */ + long getContentDuration(); + /** * If {@link #isPlayingAd()} returns {@code true}, returns the content position that will be * played once all ads in the ad group have finished playing, in milliseconds. If there is no ad diff --git a/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java b/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java index 2c7aee834e..d2b8d72b1e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java @@ -156,6 +156,14 @@ public final class PlayerMessage { return handler; } + /** + * Returns position in window at {@link #getWindowIndex()} at which the message will be delivered, + * in milliseconds. If {@link C#TIME_UNSET}, the message will be delivered immediately. + */ + public long getPositionMs() { + return positionMs; + } + /** * Sets a position in the current window at which the message will be delivered. * @@ -170,14 +178,6 @@ public final class PlayerMessage { return this; } - /** - * Returns position in window at {@link #getWindowIndex()} at which the message will be delivered, - * in milliseconds. If {@link C#TIME_UNSET}, the message will be delivered immediately. - */ - public long getPositionMs() { - return positionMs; - } - /** * Sets a position in a window at which the message will be delivered. * @@ -231,7 +231,7 @@ public final class PlayerMessage { * Player.EventListener#onPlayerError(ExoPlaybackException)}. * * @return This message. - * @throws IllegalStateException If {@link #send()} has already been called. + * @throws IllegalStateException If this message has already been sent. */ public PlayerMessage send() { Assertions.checkState(!isSent); 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 c29017856f..d1e1541cdc 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 @@ -34,7 +34,10 @@ import java.lang.annotation.RetentionPolicy; */ public interface Renderer extends PlayerMessage.Target { - /** The renderer states. */ + /** + * The renderer states. One of {@link #STATE_DISABLED}, {@link #STATE_ENABLED} or {@link + * #STATE_STARTED}. + */ @Retention(RetentionPolicy.SOURCE) @IntDef({STATE_DISABLED, STATE_ENABLED, STATE_STARTED}) @interface State {} @@ -202,7 +205,7 @@ public interface Renderer extends PlayerMessage.Target { * @param operatingRate The operating rate. * @throws ExoPlaybackException If an error occurs handling the operating rate. */ - default void setOperatingRate(float operatingRate) throws ExoPlaybackException {}; + default void setOperatingRate(float operatingRate) throws ExoPlaybackException {} /** * Incrementally renders the {@link SampleStream}. 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 055cf1de17..c29055ddc7 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 @@ -51,7 +51,9 @@ import com.google.android.exoplayer2.trackselection.TrackSelector; import com.google.android.exoplayer2.upstream.BandwidthMeter; import com.google.android.exoplayer2.util.Clock; import com.google.android.exoplayer2.util.Util; +import com.google.android.exoplayer2.video.VideoFrameMetadataListener; import com.google.android.exoplayer2.video.VideoRendererEventListener; +import com.google.android.exoplayer2.video.spherical.CameraMotionListener; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -105,39 +107,8 @@ public class SimpleExoPlayer private float audioVolume; private MediaSource mediaSource; private List currentCues; - - /** - * @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 bandwidthMeter The {@link BandwidthMeter} 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 looper The {@link Looper} which must be used for all calls to the player and which is - * used to call listeners on. - * @deprecated Use {@link #SimpleExoPlayer(Context, RenderersFactory, TrackSelector, LoadControl, - * BandwidthMeter, DrmSessionManager, Looper)}. The use of {@link - * SimpleExoPlayer#setAudioAttributes(AudioAttributes, boolean)} to manage audio focus will be - * unavailable for a player created with this constructor. - */ - @Deprecated - protected SimpleExoPlayer( - RenderersFactory renderersFactory, - TrackSelector trackSelector, - LoadControl loadControl, - BandwidthMeter bandwidthMeter, - @Nullable DrmSessionManager drmSessionManager, - Looper looper) { - this( - /* context= */ null, - renderersFactory, - trackSelector, - loadControl, - drmSessionManager, - bandwidthMeter, - new AnalyticsCollector.Factory(), - looper); - } + private VideoFrameMetadataListener videoFrameMetadataListener; + private CameraMotionListener cameraMotionListener; /** * @param context A {@link Context}. @@ -317,6 +288,13 @@ public class SimpleExoPlayer setVideoSurface(null); } + @Override + public void clearVideoSurface(Surface surface) { + if (surface != null && surface == this.surface) { + setVideoSurface(null); + } + } + @Override public void setVideoSurface(Surface surface) { removeSurfaceCallbacks(); @@ -325,13 +303,6 @@ public class SimpleExoPlayer maybeNotifySurfaceSizeChanged(/* width= */ newSurfaceSize, /* height= */ newSurfaceSize); } - @Override - public void clearVideoSurface(Surface surface) { - if (surface != null && surface == this.surface) { - setVideoSurface(null); - } - } - @Override public void setVideoSurfaceHolder(SurfaceHolder surfaceHolder) { removeSurfaceCallbacks(); @@ -598,6 +569,66 @@ public class SimpleExoPlayer videoListeners.remove(listener); } + @Override + public void setVideoFrameMetadataListener(VideoFrameMetadataListener listener) { + videoFrameMetadataListener = listener; + for (Renderer renderer : renderers) { + if (renderer.getTrackType() == C.TRACK_TYPE_VIDEO) { + player + .createMessage(renderer) + .setType(C.MSG_SET_VIDEO_FRAME_METADATA_LISTENER) + .setPayload(listener) + .send(); + } + } + } + + @Override + public void clearVideoFrameMetadataListener(VideoFrameMetadataListener listener) { + if (videoFrameMetadataListener != listener) { + return; + } + for (Renderer renderer : renderers) { + if (renderer.getTrackType() == C.TRACK_TYPE_VIDEO) { + player + .createMessage(renderer) + .setType(C.MSG_SET_VIDEO_FRAME_METADATA_LISTENER) + .setPayload(null) + .send(); + } + } + } + + @Override + public void setCameraMotionListener(CameraMotionListener listener) { + cameraMotionListener = listener; + for (Renderer renderer : renderers) { + if (renderer.getTrackType() == C.TRACK_TYPE_CAMERA_MOTION) { + player + .createMessage(renderer) + .setType(C.MSG_SET_CAMERA_MOTION_LISTENER) + .setPayload(listener) + .send(); + } + } + } + + @Override + public void clearCameraMotionListener(CameraMotionListener listener) { + if (cameraMotionListener != listener) { + return; + } + for (Renderer renderer : renderers) { + if (renderer.getTrackType() == C.TRACK_TYPE_CAMERA_MOTION) { + player + .createMessage(renderer) + .setType(C.MSG_SET_CAMERA_MOTION_LISTENER) + .setPayload(null) + .send(); + } + } + } + /** * Sets a listener to receive video events, removing all existing listeners. * @@ -605,6 +636,7 @@ public class SimpleExoPlayer * @deprecated Use {@link #addVideoListener(com.google.android.exoplayer2.video.VideoListener)}. */ @Deprecated + @SuppressWarnings("deprecation") public void setVideoListener(VideoListener listener) { videoListeners.clear(); if (listener != null) { @@ -620,6 +652,7 @@ public class SimpleExoPlayer * #removeVideoListener(com.google.android.exoplayer2.video.VideoListener)}. */ @Deprecated + @SuppressWarnings("deprecation") public void clearVideoListener(VideoListener listener) { removeVideoListener(listener); } @@ -710,6 +743,7 @@ public class SimpleExoPlayer * information. */ @Deprecated + @SuppressWarnings("deprecation") public void setVideoDebugListener(VideoRendererEventListener listener) { videoDebugListeners.retainAll(Collections.singleton(analyticsCollector)); if (listener != null) { @@ -740,6 +774,7 @@ public class SimpleExoPlayer * information. */ @Deprecated + @SuppressWarnings("deprecation") public void setAudioDebugListener(AudioRendererEventListener listener) { audioDebugListeners.retainAll(Collections.singleton(analyticsCollector)); if (listener != null) { @@ -940,6 +975,8 @@ public class SimpleExoPlayer } @Override + @Deprecated + @SuppressWarnings("deprecation") public void sendMessages(ExoPlayerMessage... messages) { player.sendMessages(messages); } @@ -950,6 +987,8 @@ public class SimpleExoPlayer } @Override + @Deprecated + @SuppressWarnings("deprecation") public void blockingSendMessages(ExoPlayerMessage... messages) { player.blockingSendMessages(messages); } @@ -1054,6 +1093,11 @@ public class SimpleExoPlayer return player.getCurrentAdIndexInAdGroup(); } + @Override + public long getContentDuration() { + return player.getContentDuration(); + } + @Override public long getContentPosition() { return player.getContentPosition(); 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 a1a0e9b152..1639920aaa 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 @@ -702,13 +702,13 @@ public abstract class Timeline { * Calls {@link #getPeriodPosition(Window, Period, int, long, long)} with a zero default position * projection. */ - public final Pair getPeriodPosition(Window window, Period period, int windowIndex, - long windowPositionUs) { + public final Pair getPeriodPosition( + Window window, Period period, int windowIndex, long windowPositionUs) { return getPeriodPosition(window, period, windowIndex, windowPositionUs, 0); } /** - * Converts (windowIndex, windowPositionUs) to the corresponding (periodIndex, periodPositionUs). + * Converts (windowIndex, windowPositionUs) to the corresponding (periodUid, periodPositionUs). * * @param window A {@link Window} that may be overwritten. * @param period A {@link Period} that may be overwritten. @@ -717,12 +717,16 @@ public abstract class Timeline { * start position. * @param defaultPositionProjectionUs If {@code windowPositionUs} is {@link C#TIME_UNSET}, the * duration into the future by which the window's position should be projected. - * @return The corresponding (periodIndex, periodPositionUs), or null if {@code #windowPositionUs} + * @return The corresponding (periodUid, periodPositionUs), or null if {@code #windowPositionUs} * is {@link C#TIME_UNSET}, {@code defaultPositionProjectionUs} is non-zero, and the window's * position could not be projected by {@code defaultPositionProjectionUs}. */ - public final Pair getPeriodPosition(Window window, Period period, int windowIndex, - long windowPositionUs, long defaultPositionProjectionUs) { + public final Pair getPeriodPosition( + Window window, + Period period, + int windowIndex, + long windowPositionUs, + long defaultPositionProjectionUs) { Assertions.checkIndex(windowIndex, 0, getWindowCount()); getWindow(windowIndex, window, false, defaultPositionProjectionUs); if (windowPositionUs == C.TIME_UNSET) { @@ -733,13 +737,13 @@ public abstract class Timeline { } int periodIndex = window.firstPeriodIndex; long periodPositionUs = window.getPositionInFirstPeriodUs() + windowPositionUs; - long periodDurationUs = getPeriod(periodIndex, period).getDurationUs(); + long periodDurationUs = getPeriod(periodIndex, period, /* setIds= */ true).getDurationUs(); while (periodDurationUs != C.TIME_UNSET && periodPositionUs >= periodDurationUs && periodIndex < window.lastPeriodIndex) { periodPositionUs -= periodDurationUs; - periodDurationUs = getPeriod(++periodIndex, period).getDurationUs(); + periodDurationUs = getPeriod(++periodIndex, period, /* setIds= */ true).getDurationUs(); } - return Pair.create(periodIndex, periodPositionUs); + return Pair.create(period.uid, periodPositionUs); } /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java index 262187586b..f1a4b31e13 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java @@ -304,6 +304,11 @@ public class AnalyticsCollector // VideoListener implementation. + @Override + public final void onRenderedFirstFrame() { + // Do nothing. Already reported in VideoRendererEventListener.onRenderedFirstFrame. + } + @Override public final void onVideoSizeChanged( int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { @@ -322,11 +327,6 @@ public class AnalyticsCollector } } - @Override - public final void onRenderedFirstFrame() { - // Do nothing. Already reported in VideoRendererEventListener.onRenderedFirstFrame. - } - // MediaSourceEventListener implementation. @Override @@ -705,11 +705,10 @@ public class AnalyticsCollector 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 + int periodIndex = timeline.getIndexOfPeriod(mediaPeriod.mediaPeriodId.periodUid); + if (periodIndex != C.INDEX_UNSET && timeline.getPeriod(periodIndex, period).windowIndex == windowIndex) { if (match != null) { // Ambiguous match. @@ -731,10 +730,10 @@ public class AnalyticsCollector public void onTimelineChanged(Timeline timeline) { for (int i = 0; i < activeMediaPeriods.size(); i++) { activeMediaPeriods.set( - i, updateMediaPeriodToNewTimeline(activeMediaPeriods.get(i), timeline)); + i, updateWindowIndexToNewTimeline(activeMediaPeriods.get(i), timeline)); } if (readingMediaPeriod != null) { - readingMediaPeriod = updateMediaPeriodToNewTimeline(readingMediaPeriod, timeline); + readingMediaPeriod = updateWindowIndexToNewTimeline(readingMediaPeriod, timeline); } this.timeline = timeline; updateLastReportedPlayingMediaPeriod(); @@ -779,19 +778,17 @@ public class AnalyticsCollector } } - private WindowAndMediaPeriodId updateMediaPeriodToNewTimeline( + private WindowAndMediaPeriodId updateWindowIndexToNewTimeline( WindowAndMediaPeriodId mediaPeriod, Timeline newTimeline) { if (newTimeline.isEmpty() || timeline.isEmpty()) { return mediaPeriod; } - Object uid = timeline.getUidOfPeriod(mediaPeriod.mediaPeriodId.periodIndex); - int newPeriodIndex = newTimeline.getIndexOfPeriod(uid); + int newPeriodIndex = newTimeline.getIndexOfPeriod(mediaPeriod.mediaPeriodId.periodUid); if (newPeriodIndex == C.INDEX_UNSET) { return mediaPeriod; } int newWindowIndex = newTimeline.getPeriod(newPeriodIndex, period).windowIndex; - return new WindowAndMediaPeriodId( - newWindowIndex, mediaPeriod.mediaPeriodId.copyWithPeriodIndex(newPeriodIndex)); + return new WindowAndMediaPeriodId(newWindowIndex, mediaPeriod.mediaPeriodId); } } 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 94fe759a9b..b02fc1b1b0 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 @@ -33,7 +33,10 @@ public final class Ac3Util { /** Holds sample format information as presented by a syncframe header. */ public static final class SyncFrameInfo { - /** AC3 stream types. See also ETSI TS 102 366 E.1.3.1.1. */ + /** + * AC3 stream types. See also ETSI TS 102 366 E.1.3.1.1. One of {@link #STREAM_TYPE_UNDEFINED}, + * {@link #STREAM_TYPE_TYPE0}, {@link #STREAM_TYPE_TYPE1} or {@link #STREAM_TYPE_TYPE2}. + */ @Retention(RetentionPolicy.SOURCE) @IntDef({STREAM_TYPE_UNDEFINED, STREAM_TYPE_TYPE0, STREAM_TYPE_TYPE1, STREAM_TYPE_TYPE2}) public @interface StreamType {} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioFocusManager.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioFocusManager.java index d078cddcc1..f1c24d0027 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioFocusManager.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioFocusManager.java @@ -52,7 +52,10 @@ public final class AudioFocusManager { void executePlayerCommand(@PlayerCommand int playerCommand); } - /** Player commands. */ + /** + * Player commands. One of {@link #PLAYER_COMMAND_DO_NOT_PLAY}, {@link + * #PLAYER_COMMAND_WAIT_FOR_CALLBACK} or {@link #PLAYER_COMMAND_PLAY_WHEN_READY}. + */ @Retention(RetentionPolicy.SOURCE) @IntDef({ PLAYER_COMMAND_DO_NOT_PLAY, @@ -134,8 +137,7 @@ public final class AudioFocusManager { * managed automatically. * @param playWhenReady The current state of {@link ExoPlayer#getPlayWhenReady()}. * @param playerState The current player state; {@link ExoPlayer#getPlaybackState()}. - * @return A command to execute on the player. One of {@link #PLAYER_COMMAND_DO_NOT_PLAY}, {@link - * #PLAYER_COMMAND_WAIT_FOR_CALLBACK}, and {@link #PLAYER_COMMAND_PLAY_WHEN_READY}. + * @return A {@link PlayerCommand} to execute on the player. */ public @PlayerCommand int setAudioAttributes( @Nullable AudioAttributes audioAttributes, boolean playWhenReady, int playerState) { @@ -169,8 +171,7 @@ public final class AudioFocusManager { * Called by a player as part of {@link ExoPlayer#prepare(MediaSource, boolean, boolean)}. * * @param playWhenReady The current state of {@link ExoPlayer#getPlayWhenReady()}. - * @return A command to execute on the player. One of {@link #PLAYER_COMMAND_DO_NOT_PLAY}, {@link - * #PLAYER_COMMAND_WAIT_FOR_CALLBACK}, and {@link #PLAYER_COMMAND_PLAY_WHEN_READY}. + * @return A {@link PlayerCommand} to execute on the player. */ public @PlayerCommand int handlePrepare(boolean playWhenReady) { if (audioManager == null) { @@ -185,8 +186,7 @@ public final class AudioFocusManager { * * @param playWhenReady The desired value of playWhenReady. * @param playerState The current state of the player. - * @return A command to execute on the player. One of {@link #PLAYER_COMMAND_DO_NOT_PLAY}, {@link - * #PLAYER_COMMAND_WAIT_FOR_CALLBACK}, and {@link #PLAYER_COMMAND_PLAY_WHEN_READY}. + * @return A {@link PlayerCommand} to execute on the player. */ public @PlayerCommand int handleSetPlayWhenReady(boolean playWhenReady, int playerState) { if (audioManager == null) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioRendererEventListener.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioRendererEventListener.java index 7a4958a61a..7c3c1481fc 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioRendererEventListener.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioRendererEventListener.java @@ -104,12 +104,7 @@ public interface AudioRendererEventListener { */ public void enabled(final DecoderCounters decoderCounters) { if (listener != null) { - handler.post(new Runnable() { - @Override - public void run() { - listener.onAudioEnabled(decoderCounters); - } - }); + handler.post(() -> listener.onAudioEnabled(decoderCounters)); } } @@ -119,13 +114,10 @@ public interface AudioRendererEventListener { public void decoderInitialized(final String decoderName, final long initializedTimestampMs, final long initializationDurationMs) { if (listener != null) { - handler.post(new Runnable() { - @Override - public void run() { - listener.onAudioDecoderInitialized(decoderName, initializedTimestampMs, - initializationDurationMs); - } - }); + handler.post( + () -> + listener.onAudioDecoderInitialized( + decoderName, initializedTimestampMs, initializationDurationMs)); } } @@ -134,12 +126,7 @@ public interface AudioRendererEventListener { */ public void inputFormatChanged(final Format format) { if (listener != null) { - handler.post(new Runnable() { - @Override - public void run() { - listener.onAudioInputFormatChanged(format); - } - }); + handler.post(() -> listener.onAudioInputFormatChanged(format)); } } @@ -149,12 +136,8 @@ public interface AudioRendererEventListener { public void audioTrackUnderrun(final int bufferSize, final long bufferSizeMs, final long elapsedSinceLastFeedMs) { if (listener != null) { - handler.post(new Runnable() { - @Override - public void run() { - listener.onAudioSinkUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs); - } - }); + handler.post( + () -> listener.onAudioSinkUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs)); } } @@ -163,13 +146,11 @@ public interface AudioRendererEventListener { */ public void disabled(final DecoderCounters counters) { if (listener != null) { - handler.post(new Runnable() { - @Override - public void run() { - counters.ensureUpdated(); - listener.onAudioDisabled(counters); - } - }); + handler.post( + () -> { + counters.ensureUpdated(); + listener.onAudioDisabled(counters); + }); } } @@ -178,12 +159,7 @@ public interface AudioRendererEventListener { */ public void audioSessionId(final int audioSessionId) { if (listener != null) { - handler.post(new Runnable() { - @Override - public void run() { - listener.onAudioSessionId(audioSessionId); - } - }); + handler.post(() -> listener.onAudioSessionId(audioSessionId)); } } 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 1197cb5a71..1d3e65f7ff 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 @@ -547,9 +547,17 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media } @Override - protected boolean processOutputBuffer(long positionUs, long elapsedRealtimeUs, MediaCodec codec, - ByteBuffer buffer, int bufferIndex, int bufferFlags, long bufferPresentationTimeUs, - boolean shouldSkip) throws ExoPlaybackException { + protected boolean processOutputBuffer( + long positionUs, + long elapsedRealtimeUs, + MediaCodec codec, + ByteBuffer buffer, + int bufferIndex, + int bufferFlags, + long bufferPresentationTimeUs, + boolean shouldSkip, + Format format) + throws ExoPlaybackException { if (passthroughEnabled && (bufferFlags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) { // Discard output buffers from the passthrough (raw) decoder containing codec specific data. codec.releaseOutputBuffer(bufferIndex, false); 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 eac0bffd65..3ae1a393f9 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 @@ -98,6 +98,8 @@ import java.nio.ByteOrder; break; case C.ENCODING_PCM_16BIT: case C.ENCODING_PCM_FLOAT: + case C.ENCODING_PCM_A_LAW: + case C.ENCODING_PCM_MU_LAW: case C.ENCODING_INVALID: case Format.NO_VALUE: default: @@ -134,6 +136,8 @@ import java.nio.ByteOrder; break; case C.ENCODING_PCM_16BIT: case C.ENCODING_PCM_FLOAT: + case C.ENCODING_PCM_A_LAW: + case C.ENCODING_PCM_MU_LAW: case C.ENCODING_INVALID: case Format.NO_VALUE: default: diff --git a/library/core/src/main/java/com/google/android/exoplayer2/decoder/DecoderInputBuffer.java b/library/core/src/main/java/com/google/android/exoplayer2/decoder/DecoderInputBuffer.java index d22a45ce88..7a32ef128b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/decoder/DecoderInputBuffer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/decoder/DecoderInputBuffer.java @@ -27,11 +27,16 @@ import java.nio.ByteBuffer; public class DecoderInputBuffer extends Buffer { /** - * The buffer replacement mode, which may disable replacement. + * The buffer replacement mode, which may disable replacement. One of {@link + * #BUFFER_REPLACEMENT_MODE_DISABLED}, {@link #BUFFER_REPLACEMENT_MODE_NORMAL} or {@link + * #BUFFER_REPLACEMENT_MODE_DIRECT}. */ @Retention(RetentionPolicy.SOURCE) - @IntDef({BUFFER_REPLACEMENT_MODE_DISABLED, BUFFER_REPLACEMENT_MODE_NORMAL, - BUFFER_REPLACEMENT_MODE_DIRECT}) + @IntDef({ + BUFFER_REPLACEMENT_MODE_DISABLED, + BUFFER_REPLACEMENT_MODE_NORMAL, + BUFFER_REPLACEMENT_MODE_DIRECT + }) public @interface BufferReplacementMode {} /** * Disallows buffer replacement. @@ -85,8 +90,8 @@ public class DecoderInputBuffer extends Buffer { /** * Ensures that {@link #data} is large enough to accommodate a write of a given length at its * current position. - *

- * If the capacity of {@link #data} is sufficient this method does nothing. If the capacity is + * + *

If the capacity of {@link #data} is sufficient this method does nothing. If the capacity is * insufficient then an attempt is made to replace {@link #data} with a new {@link ByteBuffer} * whose capacity is sufficient. Data up to the current position is copied to the new buffer. * @@ -94,7 +99,7 @@ public class DecoderInputBuffer extends Buffer { * @throws IllegalStateException If there is insufficient capacity to accommodate the write and * the buffer replacement mode of the holder is {@link #BUFFER_REPLACEMENT_MODE_DISABLED}. */ - public void ensureSpaceForWrite(int length) throws IllegalStateException { + public void ensureSpaceForWrite(int length) { if (data == null) { data = createReplacementByteBuffer(length); return; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/decoder/SimpleDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/decoder/SimpleDecoder.java index 98b1c7ca0f..7e5ae694ce 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/decoder/SimpleDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/decoder/SimpleDecoder.java @@ -20,11 +20,11 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.Assertions; import java.util.ArrayDeque; -/** - * Base class for {@link Decoder}s that use their own decode thread. - */ -public abstract class SimpleDecoder implements Decoder { +/** Base class for {@link Decoder}s that use their own decode thread. */ +@SuppressWarnings("UngroupedOverloads") +public abstract class SimpleDecoder< + I extends DecoderInputBuffer, O extends OutputBuffer, E extends Exception> + implements Decoder { private final Thread decodeThread; 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 895c27ad93..e7d23017d2 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 @@ -67,7 +67,10 @@ public class DefaultDrmSessionManager implements DrmSe */ public static final String PLAYREADY_CUSTOM_DATA_KEY = "PRCustomData"; - /** Determines the action to be done after a session acquired. */ + /** + * Determines the action to be done after a session acquired. One of {@link #MODE_PLAYBACK}, + * {@link #MODE_QUERY}, {@link #MODE_DOWNLOAD} or {@link #MODE_RELEASE}. + */ @Retention(RetentionPolicy.SOURCE) @IntDef({MODE_PLAYBACK, MODE_QUERY, MODE_DOWNLOAD, MODE_RELEASE}) public @interface Mode {} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java index a3ae1d8b71..bed3545d78 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java @@ -40,11 +40,12 @@ public interface DrmSession { } /** - * The state of the DRM session. + * The state of the DRM session. One of {@link #STATE_RELEASED}, {@link #STATE_ERROR}, {@link + * #STATE_OPENING}, {@link #STATE_OPENED} or {@link #STATE_OPENED_WITH_KEYS}. */ @Retention(RetentionPolicy.SOURCE) @IntDef({STATE_RELEASED, STATE_ERROR, STATE_OPENING, STATE_OPENED, STATE_OPENED_WITH_KEYS}) - public @interface State {} + @interface State {} /** * The session has been released. */ diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java index 8937768ff4..255541971e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java @@ -24,8 +24,6 @@ import android.media.MediaDrm; import android.media.MediaDrmException; import android.media.NotProvisionedException; import android.media.UnsupportedSchemeException; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.extractor.mp4.PsshAtomUtil; import com.google.android.exoplayer2.util.Assertions; @@ -68,10 +66,10 @@ public final class FrameworkMediaDrm implements ExoMediaDrm listener) { - mediaDrm.setOnEventListener(listener == null ? null : new MediaDrm.OnEventListener() { - @Override - public void onEvent(@NonNull MediaDrm md, @Nullable byte[] sessionId, int event, int extra, - byte[] data) { - listener.onEvent(FrameworkMediaDrm.this, sessionId, event, extra, data); - } - }); + mediaDrm.setOnEventListener( + listener == null + ? null + : (mediaDrm, sessionId, event, extra, data) -> + listener.onEvent(FrameworkMediaDrm.this, sessionId, event, extra, data)); } @Override @@ -99,20 +95,13 @@ public final class FrameworkMediaDrm implements ExoMediaDrm keyInfo, - boolean hasNewUsableKey) { - List exoKeyInfo = new ArrayList<>(); - for (MediaDrm.KeyStatus keyStatus : keyInfo) { - exoKeyInfo.add(new KeyStatus(keyStatus.getStatusCode(), keyStatus.getKeyId())); - } - listener.onKeyStatusChange( - FrameworkMediaDrm.this, sessionId, exoKeyInfo, hasNewUsableKey); + : (mediaDrm, sessionId, keyInfo, hasNewUsableKey) -> { + List exoKeyInfo = new ArrayList<>(); + for (MediaDrm.KeyStatus keyStatus : keyInfo) { + exoKeyInfo.add(new KeyStatus(keyStatus.getStatusCode(), keyStatus.getKeyId())); } + listener.onKeyStatusChange( + FrameworkMediaDrm.this, sessionId, exoKeyInfo, hasNewUsableKey); }, null); } @@ -238,7 +227,7 @@ public final class FrameworkMediaDrm implements ExoMediaDrm + id0 == 'C' && id1 == 'O' && id2 == 'M' && (id3 == 'M' || majorVersion == 2); private static final String GAPLESS_DOMAIN = "com.apple.iTunes"; private static final String GAPLESS_DESCRIPTION = "iTunSMPB"; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/MpegAudioHeader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/MpegAudioHeader.java index f394a7415c..ab49ca5454 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/MpegAudioHeader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/MpegAudioHeader.java @@ -153,7 +153,9 @@ public final class MpegAudioHeader { } int padding = (headerData >>> 9) & 1; - int bitrate, frameSize, samplesPerFrame; + int bitrate; + int frameSize; + int samplesPerFrame; if (layer == 3) { // Layer I (layer == 3) bitrate = version == 3 ? BITRATE_V1_L1[bitrateIndex - 1] : BITRATE_V2_L1[bitrateIndex - 1]; 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 index b94ea7cb58..dfdce02450 100644 --- 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 @@ -47,7 +47,10 @@ public final class AmrExtractor implements Extractor { /** Factory for {@link AmrExtractor} instances. */ public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new AmrExtractor()}; - /** Flags controlling the behavior of the extractor. */ + /** + * Flags controlling the behavior of the extractor. Possible flag value is {@link + * #FLAG_ENABLE_CONSTANT_BITRATE_SEEKING}. + */ @Retention(RetentionPolicy.SOURCE) @IntDef( flag = true, 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 b1cd508c8e..067c88b552 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 @@ -27,7 +27,10 @@ import java.lang.annotation.RetentionPolicy; */ /* package */ interface EbmlReaderOutput { - /** EBML element types. */ + /** + * EBML element types. One of {@link #TYPE_UNKNOWN}, {@link #TYPE_MASTER}, {@link + * #TYPE_UNSIGNED_INT}, {@link #TYPE_STRING}, {@link #TYPE_BINARY} or {@link #TYPE_FLOAT}. + */ @Retention(RetentionPolicy.SOURCE) @IntDef({TYPE_UNKNOWN, TYPE_MASTER, TYPE_UNSIGNED_INT, TYPE_STRING, TYPE_BINARY, TYPE_FLOAT}) @interface ElementType {} 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 355e299325..8e00628536 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 @@ -65,10 +65,13 @@ public final class MatroskaExtractor implements Extractor { public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new MatroskaExtractor()}; /** - * Flags controlling the behavior of the extractor. + * Flags controlling the behavior of the extractor. Possible flag value is {@link + * #FLAG_DISABLE_SEEK_FOR_CUES}. */ @Retention(RetentionPolicy.SOURCE) - @IntDef(flag = true, value = {FLAG_DISABLE_SEEK_FOR_CUES}) + @IntDef( + flag = true, + value = {FLAG_DISABLE_SEEK_FOR_CUES}) public @interface Flags {} /** * Flag to disable seeking for cues. 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 73dd0ec218..26a8bcce75 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 @@ -47,10 +47,13 @@ public final class Mp3Extractor implements Extractor { public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new Mp3Extractor()}; /** - * Flags controlling the behavior of the extractor. + * Flags controlling the behavior of the extractor. Possible flag values are {@link + * #FLAG_ENABLE_CONSTANT_BITRATE_SEEKING} and {@link #FLAG_DISABLE_ID3_METADATA}. */ @Retention(RetentionPolicy.SOURCE) - @IntDef(flag = true, value = {FLAG_ENABLE_CONSTANT_BITRATE_SEEKING, FLAG_DISABLE_ID3_METADATA}) + @IntDef( + flag = true, + value = {FLAG_ENABLE_CONSTANT_BITRATE_SEEKING, FLAG_DISABLE_ID3_METADATA}) public @interface Flags {} /** * Flag to force enable seeking using a constant bitrate assumption in cases where seeking would diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java index f59214fc37..3d33e105e0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java @@ -22,6 +22,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +@SuppressWarnings("ConstantField") /* package*/ abstract class Atom { /** 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 fe79185697..6fa3a5fe2b 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 @@ -38,9 +38,8 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; -/** - * Utility methods for parsing MP4 format atom payloads according to ISO 14496-12. - */ +/** Utility methods for parsing MP4 format atom payloads according to ISO 14496-12. */ +@SuppressWarnings("ConstantField") /* package */ final class AtomParsers { /** Thrown if an edit list couldn't be applied. */ @@ -619,9 +618,11 @@ import java.util.List; long timescale = mdhd.readUnsignedInt(); mdhd.skipBytes(version == 0 ? 4 : 8); int languageCode = mdhd.readUnsignedShort(); - String language = "" + (char) (((languageCode >> 10) & 0x1F) + 0x60) - + (char) (((languageCode >> 5) & 0x1F) + 0x60) - + (char) (((languageCode) & 0x1F) + 0x60); + String language = + "" + + (char) (((languageCode >> 10) & 0x1F) + 0x60) + + (char) (((languageCode >> 5) & 0x1F) + 0x60) + + (char) ((languageCode & 0x1F) + 0x60); return Pair.create(timescale, language); } 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 12da11fd6b..e06eee8515 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 @@ -62,12 +62,21 @@ public final class FragmentedMp4Extractor implements Extractor { () -> new Extractor[] {new FragmentedMp4Extractor()}; /** - * Flags controlling the behavior of the extractor. + * Flags controlling the behavior of the extractor. Possible flag values are {@link + * #FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME}, {@link #FLAG_WORKAROUND_IGNORE_TFDT_BOX}, + * {@link #FLAG_ENABLE_EMSG_TRACK}, {@link #FLAG_SIDELOADED} and {@link + * #FLAG_WORKAROUND_IGNORE_EDIT_LISTS}. */ @Retention(RetentionPolicy.SOURCE) - @IntDef(flag = true, value = {FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME, - FLAG_WORKAROUND_IGNORE_TFDT_BOX, FLAG_ENABLE_EMSG_TRACK, FLAG_SIDELOADED, - FLAG_WORKAROUND_IGNORE_EDIT_LISTS}) + @IntDef( + flag = true, + value = { + FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME, + FLAG_WORKAROUND_IGNORE_TFDT_BOX, + FLAG_ENABLE_EMSG_TRACK, + FLAG_SIDELOADED, + FLAG_WORKAROUND_IGNORE_EDIT_LISTS + }) public @interface Flags {} /** * Flag to work around an issue in some video streams where every frame is marked as a sync frame. @@ -93,7 +102,10 @@ public final class FragmentedMp4Extractor implements Extractor { public static final int FLAG_WORKAROUND_IGNORE_EDIT_LISTS = 1 << 4; // 16 private static final String TAG = "FragmentedMp4Extractor"; + + @SuppressWarnings("ConstantField") private static final int SAMPLE_GROUP_TYPE_seig = Util.getIntegerCodeForString("seig"); + private static final byte[] PIFF_SAMPLE_ENCRYPTION_BOX_EXTENDED_TYPE = new byte[] {-94, 57, 79, 82, 90, -101, 79, 20, -94, 68, 108, 66, 124, 100, -115, -12}; private static final Format EMSG_FORMAT = diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java index 5bb5e214c9..2aa9b86444 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java @@ -50,10 +50,13 @@ public final class Mp4Extractor implements Extractor, SeekMap { public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new Mp4Extractor()}; /** - * Flags controlling the behavior of the extractor. + * Flags controlling the behavior of the extractor. Possible flag value is {@link + * #FLAG_WORKAROUND_IGNORE_EDIT_LISTS}. */ @Retention(RetentionPolicy.SOURCE) - @IntDef(flag = true, value = {FLAG_WORKAROUND_IGNORE_EDIT_LISTS}) + @IntDef( + flag = true, + value = {FLAG_WORKAROUND_IGNORE_EDIT_LISTS}) public @interface Flags {} /** * Flag to ignore any edit lists in the stream. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Track.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Track.java index 3adc5a8972..867e037f4b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Track.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Track.java @@ -28,7 +28,8 @@ import java.lang.annotation.RetentionPolicy; public final class Track { /** - * The transformation to apply to samples in the track, if any. + * The transformation to apply to samples in the track, if any. One of {@link + * #TRANSFORMATION_NONE} or {@link #TRANSFORMATION_CEA608_CDAT}. */ @Retention(RetentionPolicy.SOURCE) @IntDef({TRANSFORMATION_NONE, TRANSFORMATION_CEA608_CDAT}) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/StreamReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/StreamReader.java index d136468faa..8872c6359f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/StreamReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/StreamReader.java @@ -26,9 +26,8 @@ import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.util.ParsableByteArray; import java.io.IOException; -/** - * StreamReader abstract class. - */ +/** StreamReader abstract class. */ +@SuppressWarnings("UngroupedOverloads") /* package */ abstract class StreamReader { private static final int STATE_READ_HEADERS = 0; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/VorbisReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/VorbisReader.java index 31ac6858be..147ad5a20b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/VorbisReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/VorbisReader.java @@ -153,7 +153,7 @@ import java.util.ArrayList; buffer.setLimit(buffer.limit() + 4); // The vorbis decoder expects the number of samples in the packet // to be appended to the audio data as an int32 - buffer.data[buffer.limit() - 4] = (byte) ((packetSampleCount) & 0xFF); + buffer.data[buffer.limit() - 4] = (byte) (packetSampleCount & 0xFF); buffer.data[buffer.limit() - 3] = (byte) ((packetSampleCount >>> 8) & 0xFF); buffer.data[buffer.limit() - 2] = (byte) ((packetSampleCount >>> 16) & 0xFF); buffer.data[buffer.limit() - 1] = (byte) ((packetSampleCount >>> 24) & 0xFF); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java index ef7b763306..0c2a0545dc 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java @@ -43,7 +43,10 @@ public final class AdtsExtractor implements Extractor { /** Factory for {@link AdtsExtractor} instances. */ public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new AdtsExtractor()}; - /** Flags controlling the behavior of the extractor. */ + /** + * Flags controlling the behavior of the extractor. Possible flag value is {@link + * #FLAG_ENABLE_CONSTANT_BITRATE_SEEKING}. + */ @Retention(RetentionPolicy.SOURCE) @IntDef( flag = true, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java index 7f6a22b58b..316c17bb34 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java @@ -81,7 +81,6 @@ public final class AdtsReader implements ElementaryStreamReader { private int firstFrameSampleRateIndex; private int currentFrameVersion; - private int currentFrameSampleRateIndex; // Used when parsing the header. private boolean hasOutputFormat; @@ -170,6 +169,8 @@ public final class AdtsReader implements ElementaryStreamReader { case STATE_READING_SAMPLE: readSample(data); break; + default: + throw new IllegalStateException(); } } } @@ -327,7 +328,7 @@ public final class AdtsReader implements ElementaryStreamReader { adtsScratch.data[0] = buffer.data[buffer.getPosition()]; adtsScratch.setPosition(2); - currentFrameSampleRateIndex = adtsScratch.readBits(4); + int currentFrameSampleRateIndex = adtsScratch.readBits(4); if (firstFrameSampleRateIndex != C.INDEX_UNSET && currentFrameSampleRateIndex != firstFrameSampleRateIndex) { // Invalid header. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java index 085e3443c1..06a60776c2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java @@ -34,13 +34,24 @@ import java.util.List; public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Factory { /** - * Flags controlling elementary stream readers' behavior. + * Flags controlling elementary stream readers' behavior. Possible flag values are {@link + * #FLAG_ALLOW_NON_IDR_KEYFRAMES}, {@link #FLAG_IGNORE_AAC_STREAM}, {@link + * #FLAG_IGNORE_H264_STREAM}, {@link #FLAG_DETECT_ACCESS_UNITS}, {@link + * #FLAG_IGNORE_SPLICE_INFO_STREAM} and {@link #FLAG_OVERRIDE_CAPTION_DESCRIPTORS}. */ @Retention(RetentionPolicy.SOURCE) - @IntDef(flag = true, value = {FLAG_ALLOW_NON_IDR_KEYFRAMES, FLAG_IGNORE_AAC_STREAM, - FLAG_IGNORE_H264_STREAM, FLAG_DETECT_ACCESS_UNITS, FLAG_IGNORE_SPLICE_INFO_STREAM, - FLAG_OVERRIDE_CAPTION_DESCRIPTORS}) + @IntDef( + flag = true, + value = { + FLAG_ALLOW_NON_IDR_KEYFRAMES, + FLAG_IGNORE_AAC_STREAM, + FLAG_IGNORE_H264_STREAM, + FLAG_DETECT_ACCESS_UNITS, + FLAG_IGNORE_SPLICE_INFO_STREAM, + FLAG_OVERRIDE_CAPTION_DESCRIPTORS + }) public @interface Flags {} + public static final int FLAG_ALLOW_NON_IDR_KEYFRAMES = 1; public static final int FLAG_IGNORE_AAC_STREAM = 1 << 1; public static final int FLAG_IGNORE_H264_STREAM = 1 << 2; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DtsReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DtsReader.java index 0fc3383015..2e45853951 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DtsReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DtsReader.java @@ -111,6 +111,8 @@ public final class DtsReader implements ElementaryStreamReader { state = STATE_FINDING_SYNC; } break; + default: + throw new IllegalStateException(); } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/LatmReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/LatmReader.java index 313e556764..f401a6e736 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/LatmReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/LatmReader.java @@ -134,6 +134,8 @@ public final class LatmReader implements ElementaryStreamReader { state = STATE_FINDING_SYNC_1; } break; + default: + throw new IllegalStateException(); } } } @@ -250,6 +252,8 @@ public final class LatmReader implements ElementaryStreamReader { case 7: data.skipBits(1); // HVXCframeLengthTableIndex. break; + default: + throw new IllegalStateException(); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java index 82fb84b291..effa7d7c96 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java @@ -100,6 +100,8 @@ public final class MpegAudioReader implements ElementaryStreamReader { case STATE_READING_FRAME: readFrameRemainder(data); break; + default: + throw new IllegalStateException(); } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PesReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PesReader.java index 4863df42eb..6ae810a27d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PesReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PesReader.java @@ -100,6 +100,8 @@ public final class PesReader implements TsPayloadReader { // Either way, notify the reader that it has now finished. reader.packetFinished(); break; + default: + throw new IllegalStateException(); } setState(STATE_READING_HEADER); } @@ -140,6 +142,8 @@ public final class PesReader implements TsPayloadReader { } } break; + default: + throw new IllegalStateException(); } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java index f677dc008f..cef0eb8363 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java @@ -54,7 +54,8 @@ public final class TsExtractor implements Extractor { public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new TsExtractor()}; /** - * Modes for the extractor. + * Modes for the extractor. One of {@link #MODE_MULTI_PMT}, {@link #MODE_SINGLE_PMT} or {@link + * #MODE_HLS}. */ @Retention(RetentionPolicy.SOURCE) @IntDef({MODE_MULTI_PMT, MODE_SINGLE_PMT, MODE_HLS}) @@ -243,8 +244,8 @@ public final class TsExtractor implements Extractor { @Override public @ReadResult int read(ExtractorInput input, PositionHolder seekPosition) throws IOException, InterruptedException { + long inputLength = input.getLength(); if (tracksEnded) { - long inputLength = input.getLength(); boolean canReadDuration = inputLength != C.LENGTH_UNSET && mode != MODE_HLS; if (canReadDuration && !durationReader.isDurationReadFinished()) { return durationReader.readDuration(input, seekPosition, pcrPid); @@ -324,10 +325,10 @@ public final class TsExtractor implements Extractor { payloadReader.consume(tsPacketBuffer, payloadUnitStartIndicator); tsPacketBuffer.setLimit(limit); } - if (mode != MODE_HLS && !wereTracksEnded && tracksEnded) { - // We have read all tracks from all PMTs in this stream. Now seek to the beginning and read - // again to make sure we output all media, including any contained in packets prior to those - // containing the track information. + if (mode != MODE_HLS && !wereTracksEnded && tracksEnded && inputLength != C.LENGTH_UNSET) { + // We have read all tracks from all PMTs in this non-live stream. Now seek to the beginning + // and read again to make sure we output all media, including any contained in packets prior + // to those containing the track information. pendingSeekToStart = true; } 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 3630977fca..7daafe98ec 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 @@ -43,6 +43,7 @@ import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryExcep import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.NalUnitUtil; +import com.google.android.exoplayer2.util.TimedValueQueue; import com.google.android.exoplayer2.util.TraceUtil; import com.google.android.exoplayer2.util.Util; import java.lang.annotation.Retention; @@ -272,10 +273,13 @@ public abstract class MediaCodecRenderer extends BaseRenderer { private final DecoderInputBuffer buffer; private final DecoderInputBuffer flagsOnlyBuffer; private final FormatHolder formatHolder; + private final TimedValueQueue formatQueue; private final List decodeOnlyPresentationTimestamps; private final MediaCodec.BufferInfo outputBufferInfo; private Format format; + private Format pendingFormat; + private Format outputFormat; private DrmSession drmSession; private DrmSession pendingDrmSession; private MediaCodec codec; @@ -288,12 +292,12 @@ public abstract class MediaCodecRenderer extends BaseRenderer { private @AdaptationWorkaroundMode int codecAdaptationWorkaroundMode; private boolean codecNeedsDiscardToSpsWorkaround; private boolean codecNeedsFlushWorkaround; - private boolean codecNeedsEosPropagationWorkaround; private boolean codecNeedsEosFlushWorkaround; private boolean codecNeedsEosOutputExceptionWorkaround; private boolean codecNeedsMonoChannelCountWorkaround; private boolean codecNeedsAdaptationWorkaroundBuffer; private boolean shouldSkipAdaptationWorkaroundOutputBuffer; + private boolean codecNeedsEosPropagation; private ByteBuffer[] inputBuffers; private ByteBuffer[] outputBuffers; private long codecHotswapDeadlineMs; @@ -344,6 +348,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { buffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED); flagsOnlyBuffer = DecoderInputBuffer.newFlagsOnlyInstance(); formatHolder = new FormatHolder(); + formatQueue = new TimedValueQueue<>(); decodeOnlyPresentationTimestamps = new ArrayList<>(); outputBufferInfo = new MediaCodec.BufferInfo(); codecReconfigurationState = RECONFIGURATION_STATE_NONE; @@ -463,10 +468,11 @@ public abstract class MediaCodecRenderer extends BaseRenderer { codecAdaptationWorkaroundMode = codecAdaptationWorkaroundMode(codecName); codecNeedsDiscardToSpsWorkaround = codecNeedsDiscardToSpsWorkaround(codecName, format); codecNeedsFlushWorkaround = codecNeedsFlushWorkaround(codecName); - codecNeedsEosPropagationWorkaround = codecNeedsEosPropagationWorkaround(codecInfo); codecNeedsEosFlushWorkaround = codecNeedsEosFlushWorkaround(codecName); codecNeedsEosOutputExceptionWorkaround = codecNeedsEosOutputExceptionWorkaround(codecName); codecNeedsMonoChannelCountWorkaround = codecNeedsMonoChannelCountWorkaround(codecName, format); + codecNeedsEosPropagation = + codecNeedsEosPropagationWorkaround(codecInfo) || getCodecNeedsEosPropagation(); codecHotswapDeadlineMs = getState() == STATE_STARTED ? (SystemClock.elapsedRealtime() + MAX_CODEC_HOTSWAP_TIME_MS) @@ -481,11 +487,19 @@ public abstract class MediaCodecRenderer extends BaseRenderer { return true; } + /** + * Returns whether the codec needs the renderer to propagate the end-of-stream signal directly, + * rather than by using an end-of-stream buffer queued to the codec. + */ + protected boolean getCodecNeedsEosPropagation() { + return false; + } + protected final MediaCodec getCodec() { return codec; } - protected final MediaCodecInfo getCodecInfo() { + protected final @Nullable MediaCodecInfo getCodecInfo() { return codecInfo; } @@ -501,6 +515,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { if (codec != null) { flushCodec(); } + formatQueue.clear(); } @Override @@ -547,11 +562,11 @@ public abstract class MediaCodecRenderer extends BaseRenderer { codecNeedsDiscardToSpsWorkaround = false; codecNeedsFlushWorkaround = false; codecAdaptationWorkaroundMode = ADAPTATION_WORKAROUND_MODE_NEVER; - codecNeedsEosPropagationWorkaround = false; codecNeedsEosFlushWorkaround = false; codecNeedsMonoChannelCountWorkaround = false; codecNeedsAdaptationWorkaroundBuffer = false; shouldSkipAdaptationWorkaroundOutputBuffer = false; + codecNeedsEosPropagation = false; codecReceivedEos = false; codecReconfigurationState = RECONFIGURATION_STATE_NONE; codecReinitializationState = REINITIALIZATION_STATE_NONE; @@ -849,7 +864,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { if (codecReinitializationState == REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM) { // We need to re-initialize the codec. Send an end of stream signal to the existing codec so // that it outputs any remaining buffers before we release it. - if (codecNeedsEosPropagationWorkaround) { + if (codecNeedsEosPropagation) { // Do nothing. } else { codecReceivedEos = true; @@ -917,7 +932,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { return false; } try { - if (codecNeedsEosPropagationWorkaround) { + if (codecNeedsEosPropagation) { // Do nothing. } else { codecReceivedEos = true; @@ -956,6 +971,10 @@ public abstract class MediaCodecRenderer extends BaseRenderer { if (buffer.isDecodeOnly()) { decodeOnlyPresentationTimestamps.add(presentationTimeUs); } + if (pendingFormat != null) { + formatQueue.add(presentationTimeUs, pendingFormat); + pendingFormat = null; + } buffer.flip(); onQueueInputBuffer(buffer); @@ -1012,6 +1031,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { protected void onInputFormatChanged(Format newFormat) throws ExoPlaybackException { Format oldFormat = format; format = newFormat; + pendingFormat = newFormat; boolean drmInitDataChanged = !Util.areEqual(format.drmInitData, oldFormat == null ? null : oldFormat.drmInitData); @@ -1234,42 +1254,48 @@ public abstract class MediaCodecRenderer extends BaseRenderer { codec.dequeueOutputBuffer(outputBufferInfo, getDequeueOutputBufferTimeoutUs()); } - if (outputIndex >= 0) { - // We've dequeued a buffer. - if (shouldSkipAdaptationWorkaroundOutputBuffer) { - shouldSkipAdaptationWorkaroundOutputBuffer = false; - codec.releaseOutputBuffer(outputIndex, false); + if (outputIndex < 0) { + if (outputIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED /* (-2) */) { + processOutputFormat(); + return true; + } else if (outputIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED /* (-3) */) { + processOutputBuffersChanged(); return true; - } 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; - } else { - this.outputIndex = outputIndex; - outputBuffer = getOutputBuffer(outputIndex); - // The dequeued buffer is a media buffer. Do some initial setup. - // It will be processed by calling processOutputBuffer (possibly multiple times). - if (outputBuffer != null) { - outputBuffer.position(outputBufferInfo.offset); - outputBuffer.limit(outputBufferInfo.offset + outputBufferInfo.size); - } - shouldSkipOutputBuffer = shouldSkipOutputBuffer(outputBufferInfo.presentationTimeUs); } - } else if (outputIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED /* (-2) */) { - processOutputFormat(); - return true; - } else if (outputIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED /* (-3) */) { - processOutputBuffersChanged(); - return true; - } else /* MediaCodec.INFO_TRY_AGAIN_LATER (-1) or unknown negative return value */ { - if (codecNeedsEosPropagationWorkaround + /* MediaCodec.INFO_TRY_AGAIN_LATER (-1) or unknown negative return value */ + if (codecNeedsEosPropagation && (inputStreamEnded || codecReinitializationState == REINITIALIZATION_STATE_WAIT_END_OF_STREAM)) { processEndOfStream(); } return false; } + + // We've dequeued a buffer. + if (shouldSkipAdaptationWorkaroundOutputBuffer) { + shouldSkipAdaptationWorkaroundOutputBuffer = false; + codec.releaseOutputBuffer(outputIndex, false); + return true; + } 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; + } + + this.outputIndex = outputIndex; + outputBuffer = getOutputBuffer(outputIndex); + // The dequeued buffer is a media buffer. Do some initial setup. + // It will be processed by calling processOutputBuffer (possibly multiple times). + if (outputBuffer != null) { + outputBuffer.position(outputBufferInfo.offset); + outputBuffer.limit(outputBufferInfo.offset + outputBufferInfo.size); + } + shouldSkipOutputBuffer = shouldSkipOutputBuffer(outputBufferInfo.presentationTimeUs); + Format format = formatQueue.pollFloor(outputBufferInfo.presentationTimeUs); + if (format != null) { + outputFormat = format; + } } boolean processedOutputBuffer; @@ -1284,7 +1310,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer { outputIndex, outputBufferInfo.flags, outputBufferInfo.presentationTimeUs, - shouldSkipOutputBuffer); + shouldSkipOutputBuffer, + outputFormat); } catch (IllegalStateException e) { processEndOfStream(); if (outputStreamEnded) { @@ -1303,7 +1330,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer { outputIndex, outputBufferInfo.flags, outputBufferInfo.presentationTimeUs, - shouldSkipOutputBuffer); + shouldSkipOutputBuffer, + outputFormat); } if (processedOutputBuffer) { @@ -1348,36 +1376,43 @@ public abstract class MediaCodecRenderer extends BaseRenderer { /** * Processes an output media buffer. - *

- * When a new {@link ByteBuffer} is passed to this method its position and limit delineate the + * + *

When a new {@link ByteBuffer} is passed to this method its position and limit delineate the * data to be processed. The return value indicates whether the buffer was processed in full. If * true is returned then the next call to this method will receive a new buffer to be processed. * If false is returned then the same buffer will be passed to the next call. An implementation of * this method is free to modify the buffer and can assume that the buffer will not be externally * modified between successive calls. Hence an implementation can, for example, modify the * buffer's position to keep track of how much of the data it has processed. - *

- * Note that the first call to this method following a call to - * {@link #onPositionReset(long, boolean)} will always receive a new {@link ByteBuffer} to be - * processed. * - * @param positionUs The current media time in microseconds, measured at the start of the - * current iteration of the rendering loop. - * @param elapsedRealtimeUs {@link android.os.SystemClock#elapsedRealtime()} in microseconds, - * measured at the start of the current iteration of the rendering loop. + *

Note that the first call to this method following a call to {@link #onPositionReset(long, + * boolean)} will always receive a new {@link ByteBuffer} to be processed. + * + * @param positionUs The current media time in microseconds, measured at the start of the current + * iteration of the rendering loop. + * @param elapsedRealtimeUs {@link SystemClock#elapsedRealtime()} in microseconds, measured at the + * start of the current iteration of the rendering loop. * @param codec The {@link MediaCodec} instance. * @param buffer The output buffer to process. * @param bufferIndex The index of the output buffer. * @param bufferFlags The flags attached to the output buffer. * @param bufferPresentationTimeUs The presentation time of the output buffer in microseconds. * @param shouldSkip Whether the buffer should be skipped (i.e. not rendered). - * + * @param format The format associated with the buffer. * @return Whether the output buffer was fully processed (e.g. rendered or skipped). * @throws ExoPlaybackException If an error occurs processing the output buffer. */ - protected abstract boolean processOutputBuffer(long positionUs, long elapsedRealtimeUs, - MediaCodec codec, ByteBuffer buffer, int bufferIndex, int bufferFlags, - long bufferPresentationTimeUs, boolean shouldSkip) throws ExoPlaybackException; + protected abstract boolean processOutputBuffer( + long positionUs, + long elapsedRealtimeUs, + MediaCodec codec, + ByteBuffer buffer, + int bufferIndex, + int bufferFlags, + long bufferPresentationTimeUs, + boolean shouldSkip, + Format format) + throws ExoPlaybackException; /** * Incrementally renders any remaining output. 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 570d5074b7..65207401ff 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 @@ -499,23 +499,34 @@ public final class MediaCodecUtil { */ private static int avcLevelToMaxFrameSize(int avcLevel) { switch (avcLevel) { - case CodecProfileLevel.AVCLevel1: return 99 * 16 * 16; - case CodecProfileLevel.AVCLevel1b: return 99 * 16 * 16; - case CodecProfileLevel.AVCLevel12: return 396 * 16 * 16; - case CodecProfileLevel.AVCLevel13: return 396 * 16 * 16; - case CodecProfileLevel.AVCLevel2: return 396 * 16 * 16; - case CodecProfileLevel.AVCLevel21: return 792 * 16 * 16; - case CodecProfileLevel.AVCLevel22: return 1620 * 16 * 16; - case CodecProfileLevel.AVCLevel3: return 1620 * 16 * 16; - case CodecProfileLevel.AVCLevel31: return 3600 * 16 * 16; - case CodecProfileLevel.AVCLevel32: return 5120 * 16 * 16; - case CodecProfileLevel.AVCLevel4: return 8192 * 16 * 16; - case CodecProfileLevel.AVCLevel41: return 8192 * 16 * 16; - case CodecProfileLevel.AVCLevel42: return 8704 * 16 * 16; - case CodecProfileLevel.AVCLevel5: return 22080 * 16 * 16; - case CodecProfileLevel.AVCLevel51: return 36864 * 16 * 16; - case CodecProfileLevel.AVCLevel52: return 36864 * 16 * 16; - default: return -1; + case CodecProfileLevel.AVCLevel1: + case CodecProfileLevel.AVCLevel1b: + return 99 * 16 * 16; + case CodecProfileLevel.AVCLevel12: + case CodecProfileLevel.AVCLevel13: + case CodecProfileLevel.AVCLevel2: + return 396 * 16 * 16; + case CodecProfileLevel.AVCLevel21: + return 792 * 16 * 16; + case CodecProfileLevel.AVCLevel22: + case CodecProfileLevel.AVCLevel3: + return 1620 * 16 * 16; + case CodecProfileLevel.AVCLevel31: + return 3600 * 16 * 16; + case CodecProfileLevel.AVCLevel32: + return 5120 * 16 * 16; + case CodecProfileLevel.AVCLevel4: + case CodecProfileLevel.AVCLevel41: + return 8192 * 16 * 16; + case CodecProfileLevel.AVCLevel42: + return 8704 * 16 * 16; + case CodecProfileLevel.AVCLevel5: + return 22080 * 16 * 16; + case CodecProfileLevel.AVCLevel51: + case CodecProfileLevel.AVCLevel52: + return 36864 * 16 * 16; + default: + return -1; } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java index 7e5125e71c..c2adda88e5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java @@ -32,6 +32,7 @@ import java.util.Arrays; */ public final class EventMessageDecoder implements MetadataDecoder { + @SuppressWarnings("ByteBufferBackingArray") @Override public Metadata decode(MetadataInputBuffer inputBuffer) { ByteBuffer buffer = inputBuffer.data; 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 914fca5eef..289bcc3f1a 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 @@ -56,13 +56,7 @@ 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; - } - }; + (majorVersion, id0, id1, id2, id3) -> false; private static final String TAG = "Id3Decoder"; @@ -102,6 +96,7 @@ public final class Id3Decoder implements MetadataDecoder { this.framePredicate = framePredicate; } + @SuppressWarnings("ByteBufferBackingArray") @Override public @Nullable Metadata decode(MetadataInputBuffer inputBuffer) { ByteBuffer buffer = inputBuffer.data; @@ -702,14 +697,13 @@ public final class Id3Decoder implements MetadataDecoder { */ private static String getCharsetName(int encodingByte) { switch (encodingByte) { - case ID3_TEXT_ENCODING_ISO_8859_1: - return "ISO-8859-1"; case ID3_TEXT_ENCODING_UTF_16: return "UTF-16"; case ID3_TEXT_ENCODING_UTF_16BE: return "UTF-16BE"; case ID3_TEXT_ENCODING_UTF_8: return "UTF-8"; + case ID3_TEXT_ENCODING_ISO_8859_1: default: return "ISO-8859-1"; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoder.java index d6fc4f6c19..1153f918fc 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoder.java @@ -44,6 +44,7 @@ public final class SpliceInfoDecoder implements MetadataDecoder { sectionHeader = new ParsableBitArray(); } + @SuppressWarnings("ByteBufferBackingArray") @Override public Metadata decode(MetadataInputBuffer inputBuffer) { // Internal timestamps adjustment. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadAction.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadAction.java index 20b7860784..efe537a014 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadAction.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadAction.java @@ -189,6 +189,7 @@ public abstract class DownloadAction { public abstract Downloader createDownloader( DownloaderConstructorHelper downloaderConstructorHelper); + @SuppressWarnings("EqualsGetClass") @Override public boolean equals(@Nullable Object o) { if (o == null || getClass() != o.getClass()) { 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 index f6157c1dc3..bb82df7a48 100644 --- 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 @@ -59,21 +59,9 @@ public abstract class DownloadHelper { public void run() { try { prepareInternal(); - handler.post( - new Runnable() { - @Override - public void run() { - callback.onPrepared(DownloadHelper.this); - } - }); + handler.post(() -> callback.onPrepared(DownloadHelper.this)); } catch (final IOException e) { - handler.post( - new Runnable() { - @Override - public void run() { - callback.onPrepareError(DownloadHelper.this, e); - } - }); + handler.post(() -> callback.onPrepareError(DownloadHelper.this, e)); } } }.start(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java index 3b825bb14a..5b5ba6a095 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java @@ -336,12 +336,7 @@ public final class DownloadManager { tasks.get(i).stop(); } final ConditionVariable fileIOFinishedCondition = new ConditionVariable(); - fileIOHandler.post(new Runnable() { - @Override - public void run() { - fileIOFinishedCondition.open(); - } - }); + fileIOHandler.post(fileIOFinishedCondition::open); fileIOFinishedCondition.block(); fileIOThread.quit(); logd("Released"); @@ -451,51 +446,45 @@ public final class DownloadManager { 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()) { - tasks.addAll(pendingTasks); - saveActions(); - } - maybeStartTasks(); - for (int i = 0; i < tasks.size(); i++) { - Task task = tasks.get(i); - 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); - } - } - } - }); + () -> { + 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( + () -> { + 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()) { + tasks.addAll(pendingTasks); + saveActions(); + } + maybeStartTasks(); + for (int i = 0; i < tasks.size(); i++) { + Task task = tasks.get(i); + 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); + } + } + }); }); } @@ -507,17 +496,15 @@ public final class DownloadManager { 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); - } - } - }); + fileIOHandler.post( + () -> { + try { + actionFile.store(actions); + logd("Actions persisted."); + } catch (IOException e) { + Log.e(TAG, "Persisting actions failed.", e); + } + }); } private static void logd(String message) { @@ -534,7 +521,8 @@ public final class DownloadManager { public static final class TaskState { /** - * Task states. + * Task states. One of {@link #STATE_QUEUED}, {@link #STATE_STARTED}, {@link #STATE_COMPLETED}, + * {@link #STATE_CANCELED} or {@link #STATE_FAILED}. * *

Transition diagram: * @@ -614,7 +602,10 @@ public final class DownloadManager { private static final class Task implements Runnable { /** - * Task states. + * Task states. One of {@link TaskState#STATE_QUEUED}, {@link TaskState#STATE_STARTED}, {@link + * TaskState#STATE_COMPLETED}, {@link TaskState#STATE_CANCELED}, {@link TaskState#STATE_FAILED}, + * {@link #STATE_QUEUED_CANCELING}, {@link #STATE_STARTED_CANCELING} or {@link + * #STATE_STARTED_STOPPING}. * *

Transition map (vertical states are source states): * @@ -771,12 +762,7 @@ public final class DownloadManager { 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); - } - }); + () -> changeStateAndNotify(STATE_QUEUED_CANCELING, STATE_CANCELED)); } else if (changeStateAndNotify(STATE_STARTED, STATE_STARTED_CANCELING)) { cancelDownload(); } @@ -851,19 +837,14 @@ public final class DownloadManager { } 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(); + () -> { + 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(); }); } 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 9aa7afd7cd..625ec4f5e7 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 @@ -25,6 +25,7 @@ import com.google.android.exoplayer2.upstream.cache.CacheDataSource; 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 com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; @@ -54,8 +55,7 @@ public abstract class SegmentDownloader> impleme @Override public int compareTo(@NonNull Segment other) { - long startOffsetDiff = startTimeUs - other.startTimeUs; - return startOffsetDiff == 0 ? 0 : ((startOffsetDiff < 0) ? -1 : 1); + return Util.compareLong(startTimeUs, other.startTimeUs); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/PlatformScheduler.java b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/PlatformScheduler.java index b3737eb8bc..20254c87ae 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/PlatformScheduler.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/PlatformScheduler.java @@ -34,6 +34,7 @@ import com.google.android.exoplayer2.util.Util; * *

{@literal
  * 
+ * 
  *
  * ) concatenatedUid).first;
+  }
+
+  /**
+   * Returns UID of the period in the child timeline from a concatenated period UID.
+   *
+   * @param concatenatedUid UID of a period in a concatenated timeline.
+   * @return UID of the period in the child timeline.
+   */
+  public static Object getChildPeriodUidFromConcatenatedUid(Object concatenatedUid) {
+    return ((Pair) concatenatedUid).second;
+  }
+
+  /**
+   * Returns concatenated UID for a period in a child timeline.
+   *
+   * @param childTimelineUid UID of the child timeline this period belongs to.
+   * @param childPeriodUid UID of the period in the child timeline.
+   * @return UID of the period in the concatenated timeline.
+   */
+  public static Object getConcatenatedUid(Object childTimelineUid, Object childPeriodUid) {
+    return Pair.create(childTimelineUid, childPeriodUid);
+  }
+
   /**
    * Sets up a concatenated timeline with a shuffle order of child timelines.
    *
@@ -170,9 +201,8 @@ import com.google.android.exoplayer2.Timeline;
 
   @Override
   public final Period getPeriodByUid(Object uid, Period period) {
-    Pair childUidAndPeriodUid = (Pair) uid;
-    Object childUid = childUidAndPeriodUid.first;
-    Object periodUid = childUidAndPeriodUid.second;
+    Object childUid = getChildTimelineUidFromConcatenatedUid(uid);
+    Object periodUid = getChildPeriodUidFromConcatenatedUid(uid);
     int childIndex = getChildIndexByChildUid(childUid);
     int firstWindowIndexInChild = getFirstWindowIndexByChildIndex(childIndex);
     getTimelineByChildIndex(childIndex).getPeriodByUid(periodUid, period);
@@ -190,7 +220,7 @@ import com.google.android.exoplayer2.Timeline;
         setIds);
     period.windowIndex += firstWindowIndexInChild;
     if (setIds) {
-      period.uid = Pair.create(getChildUidByChildIndex(childIndex), period.uid);
+      period.uid = getConcatenatedUid(getChildUidByChildIndex(childIndex), period.uid);
     }
     return period;
   }
@@ -200,9 +230,8 @@ import com.google.android.exoplayer2.Timeline;
     if (!(uid instanceof Pair)) {
       return C.INDEX_UNSET;
     }
-    Pair childUidAndPeriodUid = (Pair) uid;
-    Object childUid = childUidAndPeriodUid.first;
-    Object periodUid = childUidAndPeriodUid.second;
+    Object childUid = getChildTimelineUidFromConcatenatedUid(uid);
+    Object periodUid = getChildPeriodUidFromConcatenatedUid(uid);
     int childIndex = getChildIndexByChildUid(childUid);
     if (childIndex == C.INDEX_UNSET) {
       return C.INDEX_UNSET;
@@ -218,7 +247,7 @@ import com.google.android.exoplayer2.Timeline;
     int firstPeriodIndexInChild = getFirstPeriodIndexByChildIndex(childIndex);
     Object periodUidInChild =
         getTimelineByChildIndex(childIndex).getUidOfPeriod(periodIndex - firstPeriodIndexInChild);
-    return Pair.create(getChildUidByChildIndex(childIndex), periodUidInChild);
+    return getConcatenatedUid(getChildUidByChildIndex(childIndex), periodUidInChild);
   }
 
   /**
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 f494856509..88e98e811f 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
@@ -37,7 +37,10 @@ public final class ClippingMediaSource extends CompositeMediaSource {
   /** Thrown when a {@link ClippingMediaSource} cannot clip its wrapped source. */
   public static final class IllegalClippingException extends IOException {
 
-    /** The reason clipping failed. */
+    /**
+     * The reason clipping failed. One of {@link #REASON_INVALID_PERIOD_COUNT}, {@link
+     * #REASON_NOT_SEEKABLE_TO_START} or {@link #REASON_START_EXCEEDS_END}.
+     */
     @Retention(RetentionPolicy.SOURCE)
     @IntDef({REASON_INVALID_PERIOD_COUNT, REASON_NOT_SEEKABLE_TO_START, REASON_START_EXCEEDS_END})
     public @interface Reason {}
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 2ef5186224..69fa4b094b 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
@@ -101,13 +101,7 @@ public abstract class CompositeMediaSource extends BaseMediaSource {
   protected final void prepareChildSource(final T id, MediaSource mediaSource) {
     Assertions.checkArgument(!childSources.containsKey(id));
     SourceInfoRefreshListener sourceListener =
-        new SourceInfoRefreshListener() {
-          @Override
-          public void onSourceInfoRefreshed(
-              MediaSource source, Timeline timeline, @Nullable Object manifest) {
-            onChildSourceInfoRefreshed(id, source, timeline, manifest);
-          }
-        };
+        (source, timeline, manifest) -> onChildSourceInfoRefreshed(id, source, timeline, manifest);
     MediaSourceEventListener eventListener = new ForwardingEventListener(id);
     childSources.put(id, new MediaSourceAndListener(mediaSource, sourceListener, eventListener));
     mediaSource.addEventListener(Assertions.checkNotNull(eventHandler), eventListener);
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 8987e9cb56..9850427063 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
@@ -60,8 +60,8 @@ public class ConcatenatingMediaSource extends CompositeMediaSource mediaSourceHolders;
-  private final MediaSourceHolder query;
   private final Map mediaSourceByMediaPeriod;
+  private final Map mediaSourceByUid;
   private final List pendingOnCompletionActions;
   private final boolean isAtomic;
   private final boolean useLazyPreparation;
@@ -125,10 +125,10 @@ public class ConcatenatingMediaSource extends CompositeMediaSource 0 ? shuffleOrder.cloneAndClear() : shuffleOrder;
     this.mediaSourceByMediaPeriod = new IdentityHashMap<>();
+    this.mediaSourceByUid = new HashMap<>();
     this.mediaSourcesPublic = new ArrayList<>();
     this.mediaSourceHolders = new ArrayList<>();
     this.pendingOnCompletionActions = new ArrayList<>();
-    this.query = new MediaSourceHolder(/* mediaSource= */ null);
     this.isAtomic = isAtomic;
     this.useLazyPreparation = useLazyPreparation;
     window = new Timeline.Window();
@@ -451,8 +451,8 @@ public class ConcatenatingMediaSource extends CompositeMediaSource {
   private final Map childMediaPeriodIdToMediaPeriodId;
   private final Map mediaPeriodToChildMediaPeriodId;
 
-  private int childPeriodCount;
-
   /**
    * Loops the provided source indefinitely. Note that it is usually better to use
    * {@link ExoPlayer#setRepeatMode(int)}.
@@ -80,7 +78,8 @@ public final class LoopingMediaSource extends CompositeMediaSource {
     if (loopCount == Integer.MAX_VALUE) {
       return childSource.createPeriod(id, allocator);
     }
-    MediaPeriodId childMediaPeriodId = id.copyWithPeriodIndex(id.periodIndex % childPeriodCount);
+    Object childPeriodUid = LoopingTimeline.getChildPeriodUidFromConcatenatedUid(id.periodUid);
+    MediaPeriodId childMediaPeriodId = id.copyWithPeriodUid(childPeriodUid);
     childMediaPeriodIdToMediaPeriodId.put(childMediaPeriodId, id);
     MediaPeriod mediaPeriod = childSource.createPeriod(childMediaPeriodId, allocator);
     mediaPeriodToChildMediaPeriodId.put(mediaPeriod, childMediaPeriodId);
@@ -96,16 +95,9 @@ public final class LoopingMediaSource extends CompositeMediaSource {
     }
   }
 
-  @Override
-  public void releaseSourceInternal() {
-    super.releaseSourceInternal();
-    childPeriodCount = 0;
-  }
-
   @Override
   protected void onChildSourceInfoRefreshed(
       Void id, MediaSource mediaSource, Timeline timeline, @Nullable Object manifest) {
-    childPeriodCount = timeline.getPeriodCount();
     Timeline loopingTimeline =
         loopCount != Integer.MAX_VALUE
             ? new LoopingTimeline(timeline, loopCount)
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 fb4c64ae6e..6b0f5c8eeb 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
@@ -66,10 +66,8 @@ public interface MediaSource {
    */
   final class MediaPeriodId {
 
-    /**
-     * The timeline period index.
-     */
-    public final int periodIndex;
+    /** The unique id of the timeline period. */
+    public final Object periodUid;
 
     /**
      * If the media period is in an ad group, the index of the ad group in the period.
@@ -103,72 +101,70 @@ public interface MediaSource {
      * Creates a media period identifier for a dummy period which is not part of a buffered sequence
      * of windows.
      *
-     * @param periodIndex The period index.
+     * @param periodUid The unique id of the timeline period.
      */
-    public MediaPeriodId(int periodIndex) {
-      this(periodIndex, C.INDEX_UNSET);
+    public MediaPeriodId(Object periodUid) {
+      this(periodUid, C.INDEX_UNSET);
     }
 
     /**
      * Creates a media period identifier for the specified period in the timeline.
      *
-     * @param periodIndex The timeline period index.
+     * @param periodUid The unique id of the timeline period.
      * @param windowSequenceNumber The sequence number of the window in the buffered sequence of
      *     windows this media period is part of.
      */
-    public MediaPeriodId(int periodIndex, long windowSequenceNumber) {
-      this(periodIndex, C.INDEX_UNSET, C.INDEX_UNSET, windowSequenceNumber, C.TIME_END_OF_SOURCE);
+    public MediaPeriodId(Object periodUid, long windowSequenceNumber) {
+      this(periodUid, C.INDEX_UNSET, C.INDEX_UNSET, windowSequenceNumber, C.TIME_END_OF_SOURCE);
     }
 
     /**
      * Creates a media period identifier for the specified clipped period in the timeline.
      *
-     * @param periodIndex The timeline period index.
+     * @param periodUid The unique id of the timeline period.
      * @param windowSequenceNumber The sequence number of the window in the buffered sequence of
      *     windows this media period is part of.
      * @param endPositionUs The end position of the media period within the timeline period, in
      *     microseconds.
      */
-    public MediaPeriodId(int periodIndex, long windowSequenceNumber, long endPositionUs) {
-      this(periodIndex, C.INDEX_UNSET, C.INDEX_UNSET, windowSequenceNumber, endPositionUs);
+    public MediaPeriodId(Object periodUid, long windowSequenceNumber, long endPositionUs) {
+      this(periodUid, C.INDEX_UNSET, C.INDEX_UNSET, windowSequenceNumber, endPositionUs);
     }
 
     /**
      * Creates a media period identifier that identifies an ad within an ad group at the specified
      * timeline period.
      *
-     * @param periodIndex The index of the timeline period that contains the ad group.
+     * @param periodUid The unique id of the timeline period that contains the ad group.
      * @param adGroupIndex The index of the ad group.
      * @param adIndexInAdGroup The index of the ad in the ad group.
      * @param windowSequenceNumber The sequence number of the window in the buffered sequence of
      *     windows this media period is part of.
      */
     public MediaPeriodId(
-        int periodIndex, int adGroupIndex, int adIndexInAdGroup, long windowSequenceNumber) {
-      this(periodIndex, adGroupIndex, adIndexInAdGroup, windowSequenceNumber, C.TIME_END_OF_SOURCE);
+        Object periodUid, int adGroupIndex, int adIndexInAdGroup, long windowSequenceNumber) {
+      this(periodUid, adGroupIndex, adIndexInAdGroup, windowSequenceNumber, C.TIME_END_OF_SOURCE);
     }
 
     private MediaPeriodId(
-        int periodIndex,
+        Object periodUid,
         int adGroupIndex,
         int adIndexInAdGroup,
         long windowSequenceNumber,
         long endPositionUs) {
-      this.periodIndex = periodIndex;
+      this.periodUid = periodUid;
       this.adGroupIndex = adGroupIndex;
       this.adIndexInAdGroup = adIndexInAdGroup;
       this.windowSequenceNumber = windowSequenceNumber;
       this.endPositionUs = endPositionUs;
     }
 
-    /**
-     * Returns a copy of this period identifier but with {@code newPeriodIndex} as its period index.
-     */
-    public MediaPeriodId copyWithPeriodIndex(int newPeriodIndex) {
-      return periodIndex == newPeriodIndex
+    /** Returns a copy of this period identifier but with {@code newPeriodUid} as its period uid. */
+    public MediaPeriodId copyWithPeriodUid(Object newPeriodUid) {
+      return periodUid.equals(newPeriodUid)
           ? this
           : new MediaPeriodId(
-              newPeriodIndex, adGroupIndex, adIndexInAdGroup, windowSequenceNumber, endPositionUs);
+              newPeriodUid, adGroupIndex, adIndexInAdGroup, windowSequenceNumber, endPositionUs);
     }
 
     /**
@@ -188,7 +184,7 @@ public interface MediaSource {
       }
 
       MediaPeriodId periodId = (MediaPeriodId) obj;
-      return periodIndex == periodId.periodIndex
+      return periodUid.equals(periodId.periodUid)
           && adGroupIndex == periodId.adGroupIndex
           && adIndexInAdGroup == periodId.adIndexInAdGroup
           && windowSequenceNumber == periodId.windowSequenceNumber
@@ -198,14 +194,13 @@ public interface MediaSource {
     @Override
     public int hashCode() {
       int result = 17;
-      result = 31 * result + periodIndex;
+      result = 31 * result + periodUid.hashCode();
       result = 31 * result + adGroupIndex;
       result = 31 * result + adIndexInAdGroup;
       result = 31 * result + (int) windowSequenceNumber;
       result = 31 * result + (int) endPositionUs;
       return result;
     }
-
   }
 
   /**
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 844534a43d..98d1d0a2ab 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
@@ -28,6 +28,9 @@ 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.Collections;
+import java.util.List;
+import java.util.Map;
 import java.util.concurrent.CopyOnWriteArrayList;
 
 /** Interface for callbacks to be notified of {@link MediaSource} events. */
@@ -44,6 +47,8 @@ public interface MediaSourceEventListener {
      * after redirection.
      */
     public final Uri uri;
+    /** The response headers associated with the load, or an empty map if unavailable. */
+    public final Map> responseHeaders;
     /** 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. */
@@ -58,6 +63,8 @@ public interface MediaSourceEventListener {
      * @param uri The {@link Uri} from which data is being read. The uri must be identical to the
      *     one in {@code dataSpec.uri} unless redirection has occurred. If redirection has occurred,
      *     this is the uri after redirection.
+     * @param responseHeaders The response headers associated with the load, or an empty map if
+     *     unavailable.
      * @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.
@@ -65,9 +72,15 @@ public interface MediaSourceEventListener {
      *     network responses, this is the decompressed size.
      */
     public LoadEventInfo(
-        DataSpec dataSpec, Uri uri, long elapsedRealtimeMs, long loadDurationMs, long bytesLoaded) {
+        DataSpec dataSpec,
+        Uri uri,
+        Map> responseHeaders,
+        long elapsedRealtimeMs,
+        long loadDurationMs,
+        long bytesLoaded) {
       this.dataSpec = dataSpec;
       this.uri = uri;
+      this.responseHeaders = responseHeaders;
       this.elapsedRealtimeMs = elapsedRealtimeMs;
       this.loadDurationMs = loadDurationMs;
       this.bytesLoaded = bytesLoaded;
@@ -168,7 +181,8 @@ public interface MediaSourceEventListener {
    * @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} corresponding to the event. The value of {@link
-   *     LoadEventInfo#uri} won't reflect potential redirection yet.
+   *     LoadEventInfo#uri} won't reflect potential redirection yet and {@link
+   *     LoadEventInfo#responseHeaders} will be empty.
    * @param mediaLoadData The {@link MediaLoadData} defining the data being loaded.
    */
   void onLoadStarted(
@@ -370,10 +384,9 @@ public interface MediaSourceEventListener {
     }
 
     /** Dispatches {@link #onLoadStarted(int, MediaPeriodId, LoadEventInfo, MediaLoadData)}. */
-    public void loadStarted(DataSpec dataSpec, Uri uri, int dataType, long elapsedRealtimeMs) {
+    public void loadStarted(DataSpec dataSpec, int dataType, long elapsedRealtimeMs) {
       loadStarted(
           dataSpec,
-          uri,
           dataType,
           C.TRACK_TYPE_UNKNOWN,
           null,
@@ -387,7 +400,6 @@ public interface MediaSourceEventListener {
     /** Dispatches {@link #onLoadStarted(int, MediaPeriodId, LoadEventInfo, MediaLoadData)}. */
     public void loadStarted(
         DataSpec dataSpec,
-        Uri uri,
         int dataType,
         int trackType,
         @Nullable Format trackFormat,
@@ -398,7 +410,12 @@ public interface MediaSourceEventListener {
         long elapsedRealtimeMs) {
       loadStarted(
           new LoadEventInfo(
-              dataSpec, uri, elapsedRealtimeMs, /* loadDurationMs= */ 0, /* bytesLoaded= */ 0),
+              dataSpec,
+              dataSpec.uri,
+              /* responseHeaders= */ Collections.emptyMap(),
+              elapsedRealtimeMs,
+              /* loadDurationMs= */ 0,
+              /* bytesLoaded= */ 0),
           new MediaLoadData(
               dataType,
               trackType,
@@ -423,6 +440,7 @@ public interface MediaSourceEventListener {
     public void loadCompleted(
         DataSpec dataSpec,
         Uri uri,
+        Map> responseHeaders,
         int dataType,
         long elapsedRealtimeMs,
         long loadDurationMs,
@@ -430,6 +448,7 @@ public interface MediaSourceEventListener {
       loadCompleted(
           dataSpec,
           uri,
+          responseHeaders,
           dataType,
           C.TRACK_TYPE_UNKNOWN,
           null,
@@ -446,6 +465,7 @@ public interface MediaSourceEventListener {
     public void loadCompleted(
         DataSpec dataSpec,
         Uri uri,
+        Map> responseHeaders,
         int dataType,
         int trackType,
         @Nullable Format trackFormat,
@@ -457,7 +477,8 @@ public interface MediaSourceEventListener {
         long loadDurationMs,
         long bytesLoaded) {
       loadCompleted(
-          new LoadEventInfo(dataSpec, uri, elapsedRealtimeMs, loadDurationMs, bytesLoaded),
+          new LoadEventInfo(
+              dataSpec, uri, responseHeaders, elapsedRealtimeMs, loadDurationMs, bytesLoaded),
           new MediaLoadData(
               dataType,
               trackType,
@@ -483,6 +504,7 @@ public interface MediaSourceEventListener {
     public void loadCanceled(
         DataSpec dataSpec,
         Uri uri,
+        Map> responseHeaders,
         int dataType,
         long elapsedRealtimeMs,
         long loadDurationMs,
@@ -490,6 +512,7 @@ public interface MediaSourceEventListener {
       loadCanceled(
           dataSpec,
           uri,
+          responseHeaders,
           dataType,
           C.TRACK_TYPE_UNKNOWN,
           null,
@@ -506,6 +529,7 @@ public interface MediaSourceEventListener {
     public void loadCanceled(
         DataSpec dataSpec,
         Uri uri,
+        Map> responseHeaders,
         int dataType,
         int trackType,
         @Nullable Format trackFormat,
@@ -517,7 +541,8 @@ public interface MediaSourceEventListener {
         long loadDurationMs,
         long bytesLoaded) {
       loadCanceled(
-          new LoadEventInfo(dataSpec, uri, elapsedRealtimeMs, loadDurationMs, bytesLoaded),
+          new LoadEventInfo(
+              dataSpec, uri, responseHeaders, elapsedRealtimeMs, loadDurationMs, bytesLoaded),
           new MediaLoadData(
               dataType,
               trackType,
@@ -546,6 +571,7 @@ public interface MediaSourceEventListener {
     public void loadError(
         DataSpec dataSpec,
         Uri uri,
+        Map> responseHeaders,
         int dataType,
         long elapsedRealtimeMs,
         long loadDurationMs,
@@ -555,6 +581,7 @@ public interface MediaSourceEventListener {
       loadError(
           dataSpec,
           uri,
+          responseHeaders,
           dataType,
           C.TRACK_TYPE_UNKNOWN,
           null,
@@ -576,6 +603,7 @@ public interface MediaSourceEventListener {
     public void loadError(
         DataSpec dataSpec,
         Uri uri,
+        Map> responseHeaders,
         int dataType,
         int trackType,
         @Nullable Format trackFormat,
@@ -589,7 +617,8 @@ public interface MediaSourceEventListener {
         IOException error,
         boolean wasCanceled) {
       loadError(
-          new LoadEventInfo(dataSpec, uri, elapsedRealtimeMs, loadDurationMs, bytesLoaded),
+          new LoadEventInfo(
+              dataSpec, uri, responseHeaders, elapsedRealtimeMs, loadDurationMs, bytesLoaded),
           new MediaLoadData(
               dataType,
               trackType,
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 d33cfb8abd..746af5719e 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
@@ -40,9 +40,7 @@ public final class MergingMediaSource extends CompositeMediaSource {
    */
   public static final class IllegalMergeException extends IOException {
 
-    /**
-     * The reason the merge failed.
-     */
+    /** The reason the merge failed. One of {@link #REASON_PERIOD_COUNT_MISMATCH}. */
     @Retention(RetentionPolicy.SOURCE)
     @IntDef({REASON_PERIOD_COUNT_MISMATCH})
     public @interface Reason {}
@@ -68,10 +66,10 @@ public final class MergingMediaSource extends CompositeMediaSource {
   private static final int PERIOD_COUNT_UNSET = -1;
 
   private final MediaSource[] mediaSources;
+  private final Timeline[] timelines;
   private final ArrayList pendingTimelineSources;
   private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
 
-  private Timeline primaryTimeline;
   private Object primaryManifest;
   private int periodCount;
   private IllegalMergeException mergeError;
@@ -95,6 +93,7 @@ public final class MergingMediaSource extends CompositeMediaSource {
     this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory;
     pendingTimelineSources = new ArrayList<>(Arrays.asList(mediaSources));
     periodCount = PERIOD_COUNT_UNSET;
+    timelines = new Timeline[mediaSources.length];
   }
 
   @Override
@@ -119,8 +118,11 @@ public final class MergingMediaSource extends CompositeMediaSource {
   @Override
   public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) {
     MediaPeriod[] periods = new MediaPeriod[mediaSources.length];
+    int periodIndex = timelines[0].getIndexOfPeriod(id.periodUid);
     for (int i = 0; i < periods.length; i++) {
-      periods[i] = mediaSources[i].createPeriod(id, allocator);
+      MediaPeriodId childMediaPeriodId =
+          id.copyWithPeriodUid(timelines[i].getUidOfPeriod(periodIndex));
+      periods[i] = mediaSources[i].createPeriod(childMediaPeriodId, allocator);
     }
     return new MergingMediaPeriod(compositeSequenceableLoaderFactory, periods);
   }
@@ -136,7 +138,7 @@ public final class MergingMediaSource extends CompositeMediaSource {
   @Override
   public void releaseSourceInternal() {
     super.releaseSourceInternal();
-    primaryTimeline = null;
+    Arrays.fill(timelines, null);
     primaryManifest = null;
     periodCount = PERIOD_COUNT_UNSET;
     mergeError = null;
@@ -154,12 +156,12 @@ public final class MergingMediaSource extends CompositeMediaSource {
       return;
     }
     pendingTimelineSources.remove(mediaSource);
+    timelines[id] = timeline;
     if (mediaSource == mediaSources[0]) {
-      primaryTimeline = timeline;
       primaryManifest = manifest;
     }
     if (pendingTimelineSources.isEmpty()) {
-      refreshSourceInfo(primaryTimeline, primaryManifest);
+      refreshSourceInfo(timelines[0], primaryManifest);
     }
   }
 
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 458148499a..f53dd594e6 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
@@ -155,7 +155,6 @@ import java.util.Arrays;
             loadErrorHandlingPolicy.getMinimumLoadableRetryCount(C.DATA_TYPE_MEDIA));
     eventDispatcher.loadStarted(
         dataSpec,
-        dataSpec.uri,
         C.DATA_TYPE_MEDIA,
         C.TRACK_TYPE_UNKNOWN,
         format,
@@ -211,6 +210,7 @@ import java.util.Arrays;
     eventDispatcher.loadCompleted(
         loadable.dataSpec,
         loadable.dataSource.getLastOpenedUri(),
+        loadable.dataSource.getLastResponseHeaders(),
         C.DATA_TYPE_MEDIA,
         C.TRACK_TYPE_UNKNOWN,
         format,
@@ -229,6 +229,7 @@ import java.util.Arrays;
     eventDispatcher.loadCanceled(
         loadable.dataSpec,
         loadable.dataSource.getLastOpenedUri(),
+        loadable.dataSource.getLastResponseHeaders(),
         C.DATA_TYPE_MEDIA,
         C.TRACK_TYPE_UNKNOWN,
         /* trackFormat= */ null,
@@ -269,6 +270,7 @@ import java.util.Arrays;
     eventDispatcher.loadError(
         loadable.dataSpec,
         loadable.dataSource.getLastOpenedUri(),
+        loadable.dataSource.getLastResponseHeaders(),
         C.DATA_TYPE_MEDIA,
         C.TRACK_TYPE_UNKNOWN,
         format,
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 24f49cb086..dc46b12b2e 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
@@ -197,6 +197,7 @@ public final class SingleSampleMediaSource extends BaseMediaSource {
    * @deprecated Use {@link Factory} instead.
    */
   @Deprecated
+  @SuppressWarnings("deprecation")
   public SingleSampleMediaSource(
       Uri uri, DataSource.Factory dataSourceFactory, Format format, long durationUs) {
     this(
@@ -249,6 +250,7 @@ public final class SingleSampleMediaSource extends BaseMediaSource {
    * @deprecated Use {@link Factory} instead.
    */
   @Deprecated
+  @SuppressWarnings("deprecation")
   public SingleSampleMediaSource(
       Uri uri,
       DataSource.Factory dataSourceFactory,
@@ -308,7 +310,6 @@ public final class SingleSampleMediaSource extends BaseMediaSource {
 
   @Override
   public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) {
-    Assertions.checkArgument(id.periodIndex == 0);
     return new SingleSampleMediaPeriod(
         dataSpec,
         dataSourceFactory,
@@ -334,6 +335,8 @@ public final class SingleSampleMediaSource extends BaseMediaSource {
    * Wraps a deprecated {@link EventListener}, invoking its callback from the equivalent callback in
    * {@link MediaSourceEventListener}.
    */
+  @Deprecated
+  @SuppressWarnings("deprecation")
   private static final class EventListenerWrapper extends DefaultMediaSourceEventListener {
 
     private final EventListener eventListener;
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdPlaybackState.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdPlaybackState.java
index 53f0a418be..72fc162bc3 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdPlaybackState.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdPlaybackState.java
@@ -234,7 +234,11 @@ public final class AdPlaybackState {
     }
   }
 
-  /** Represents the state of an ad in an ad group. */
+  /**
+   * Represents the state of an ad in an ad group. One of {@link #AD_STATE_UNAVAILABLE}, {@link
+   * #AD_STATE_AVAILABLE}, {@link #AD_STATE_SKIPPED}, {@link #AD_STATE_PLAYED} or {@link
+   * #AD_STATE_ERROR}.
+   */
   @Retention(RetentionPolicy.SOURCE)
   @IntDef({
     AD_STATE_UNAVAILABLE,
@@ -311,8 +315,7 @@ public final class AdPlaybackState {
     // Use a linear search as the array elements may not be increasing due to TIME_END_OF_SOURCE.
     // In practice we expect there to be few ad groups so the search shouldn't be expensive.
     int index = adGroupTimesUs.length - 1;
-    while (index >= 0
-        && (adGroupTimesUs[index] == C.TIME_END_OF_SOURCE || adGroupTimesUs[index] > positionUs)) {
+    while (index >= 0 && isPositionBeforeAdGroup(positionUs, index)) {
       index--;
     }
     return index >= 0 && adGroups[index].hasUnplayedAds() ? index : C.INDEX_UNSET;
@@ -450,4 +453,13 @@ public final class AdPlaybackState {
     result = 31 * result + Arrays.hashCode(adGroups);
     return result;
   }
+
+  private boolean isPositionBeforeAdGroup(long positionUs, int adGroupIndex) {
+    long adGroupPositionUs = adGroupTimesUs[adGroupIndex];
+    if (adGroupPositionUs == C.TIME_END_OF_SOURCE) {
+      return contentDurationUs == C.TIME_UNSET || positionUs < contentDurationUs;
+    } else {
+      return positionUs < adGroupPositionUs;
+    }
+  }
 }
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 66370828b7..3d5c41e8bc 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
@@ -43,6 +43,7 @@ import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -82,7 +83,10 @@ public final class AdsMediaSource extends CompositeMediaSource {
    */
   public static final class AdLoadException extends IOException {
 
-    /** Types of ad load exceptions. */
+    /**
+     * Types of ad load exceptions. One of {@link #TYPE_AD}, {@link #TYPE_AD_GROUP}, {@link
+     * #TYPE_ALL_ADS} or {@link #TYPE_UNEXPECTED}.
+     */
     @Retention(RetentionPolicy.SOURCE)
     @IntDef({TYPE_AD, TYPE_AD_GROUP, TYPE_ALL_ADS, TYPE_UNEXPECTED})
     public @interface Type {}
@@ -176,7 +180,7 @@ public final class AdsMediaSource extends CompositeMediaSource {
 
   // Used to identify the content "child" source for CompositeMediaSource.
   private static final MediaPeriodId DUMMY_CONTENT_MEDIA_PERIOD_ID =
-      new MediaPeriodId(/* periodIndex= */ 0);
+      new MediaPeriodId(/* periodUid= */ new Object());
 
   private final MediaSource contentMediaSource;
   private final MediaSourceFactory adMediaSourceFactory;
@@ -194,7 +198,7 @@ public final class AdsMediaSource extends CompositeMediaSource {
   private Object contentManifest;
   private AdPlaybackState adPlaybackState;
   private MediaSource[][] adGroupMediaSources;
-  private long[][] adDurationsUs;
+  private Timeline[][] adGroupTimelines;
 
   /**
    * Constructs a new source that inserts ads linearly with the content specified by {@code
@@ -309,7 +313,7 @@ public final class AdsMediaSource extends CompositeMediaSource {
     deferredMediaPeriodByAdMediaSource = new HashMap<>();
     period = new Timeline.Period();
     adGroupMediaSources = new MediaSource[0][];
-    adDurationsUs = new long[0][];
+    adGroupTimelines = new Timeline[0][];
     adsLoader.setSupportedContentTypes(adMediaSourceFactory.getSupportedTypes());
   }
 
@@ -325,12 +329,7 @@ public final class AdsMediaSource extends CompositeMediaSource {
     final ComponentListener componentListener = new ComponentListener();
     this.componentListener = componentListener;
     prepareChildSource(DUMMY_CONTENT_MEDIA_PERIOD_ID, contentMediaSource);
-    mainHandler.post(new Runnable() {
-      @Override
-      public void run() {
-        adsLoader.attachPlayer(player, componentListener, adUiViewGroup);
-      }
-    });
+    mainHandler.post(() -> adsLoader.attachPlayer(player, componentListener, adUiViewGroup));
   }
 
   @Override
@@ -346,8 +345,7 @@ public final class AdsMediaSource extends CompositeMediaSource {
           int adCount = adIndexInAdGroup + 1;
           adGroupMediaSources[adGroupIndex] =
               Arrays.copyOf(adGroupMediaSources[adGroupIndex], adCount);
-          adDurationsUs[adGroupIndex] = Arrays.copyOf(adDurationsUs[adGroupIndex], adCount);
-          Arrays.fill(adDurationsUs[adGroupIndex], oldAdCount, adCount, C.TIME_UNSET);
+          adGroupTimelines[adGroupIndex] = Arrays.copyOf(adGroupTimelines[adGroupIndex], adCount);
         }
         adGroupMediaSources[adGroupIndex][adIndexInAdGroup] = adMediaSource;
         deferredMediaPeriodByAdMediaSource.put(adMediaSource, new ArrayList<>());
@@ -359,8 +357,9 @@ public final class AdsMediaSource extends CompositeMediaSource {
           new AdPrepareErrorListener(adUri, adGroupIndex, adIndexInAdGroup));
       List mediaPeriods = deferredMediaPeriodByAdMediaSource.get(mediaSource);
       if (mediaPeriods == null) {
-        MediaPeriodId adSourceMediaPeriodId =
-            new MediaPeriodId(/* periodIndex= */ 0, id.windowSequenceNumber);
+        Object periodUid =
+            adGroupTimelines[adGroupIndex][adIndexInAdGroup].getUidOfPeriod(/* periodIndex= */ 0);
+        MediaPeriodId adSourceMediaPeriodId = new MediaPeriodId(periodUid, id.windowSequenceNumber);
         deferredMediaPeriod.createPeriod(adSourceMediaPeriodId);
       } else {
         // Keep track of the deferred media period so it can be populated with the real media period
@@ -396,13 +395,8 @@ public final class AdsMediaSource extends CompositeMediaSource {
     contentManifest = null;
     adPlaybackState = null;
     adGroupMediaSources = new MediaSource[0][];
-    adDurationsUs = new long[0][];
-    mainHandler.post(new Runnable() {
-      @Override
-      public void run() {
-        adsLoader.detachPlayer();
-      }
-    });
+    adGroupTimelines = new Timeline[0][];
+    mainHandler.post(adsLoader::detachPlayer);
   }
 
   @Override
@@ -434,8 +428,8 @@ public final class AdsMediaSource extends CompositeMediaSource {
     if (this.adPlaybackState == null) {
       adGroupMediaSources = new MediaSource[adPlaybackState.adGroupCount][];
       Arrays.fill(adGroupMediaSources, new MediaSource[0]);
-      adDurationsUs = new long[adPlaybackState.adGroupCount][];
-      Arrays.fill(adDurationsUs, new long[0]);
+      adGroupTimelines = new Timeline[adPlaybackState.adGroupCount][];
+      Arrays.fill(adGroupTimelines, new Timeline[0]);
     }
     this.adPlaybackState = adPlaybackState;
     maybeUpdateSourceInfo();
@@ -450,13 +444,14 @@ public final class AdsMediaSource extends CompositeMediaSource {
   private void onAdSourceInfoRefreshed(MediaSource mediaSource, int adGroupIndex,
       int adIndexInAdGroup, Timeline timeline) {
     Assertions.checkArgument(timeline.getPeriodCount() == 1);
-    adDurationsUs[adGroupIndex][adIndexInAdGroup] = timeline.getPeriod(0, period).getDurationUs();
+    adGroupTimelines[adGroupIndex][adIndexInAdGroup] = timeline;
     List mediaPeriods = deferredMediaPeriodByAdMediaSource.remove(mediaSource);
     if (mediaPeriods != null) {
+      Object periodUid = timeline.getUidOfPeriod(/* periodIndex= */ 0);
       for (int i = 0; i < mediaPeriods.size(); i++) {
         DeferredMediaPeriod mediaPeriod = mediaPeriods.get(i);
         MediaPeriodId adSourceMediaPeriodId =
-            new MediaPeriodId(/* periodIndex= */ 0, mediaPeriod.id.windowSequenceNumber);
+            new MediaPeriodId(periodUid, mediaPeriod.id.windowSequenceNumber);
         mediaPeriod.createPeriod(adSourceMediaPeriodId);
       }
     }
@@ -465,7 +460,7 @@ public final class AdsMediaSource extends CompositeMediaSource {
 
   private void maybeUpdateSourceInfo() {
     if (adPlaybackState != null && contentTimeline != null) {
-      adPlaybackState = adPlaybackState.withAdDurationsUs(adDurationsUs);
+      adPlaybackState = adPlaybackState.withAdDurationsUs(getAdDurations(adGroupTimelines, period));
       Timeline timeline =
           adPlaybackState.adGroupCount == 0
               ? contentTimeline
@@ -474,6 +469,20 @@ public final class AdsMediaSource extends CompositeMediaSource {
     }
   }
 
+  private static long[][] getAdDurations(Timeline[][] adTimelines, Timeline.Period period) {
+    long[][] adDurations = new long[adTimelines.length][];
+    for (int i = 0; i < adTimelines.length; i++) {
+      adDurations[i] = new long[adTimelines[i].length];
+      for (int j = 0; j < adTimelines[i].length; j++) {
+        adDurations[i][j] =
+            adTimelines[i][j] == null
+                ? C.TIME_UNSET
+                : adTimelines[i][j].getPeriod(/* periodIndex= */ 0, period).getDurationUs();
+      }
+    }
+    return adDurations;
+  }
+
   /** Listener for component events. All methods are called on the main thread. */
   private final class ComponentListener implements AdsLoader.EventListener {
 
@@ -500,15 +509,13 @@ public final class AdsMediaSource extends CompositeMediaSource {
       if (released) {
         return;
       }
-      playerHandler.post(new Runnable() {
-        @Override
-        public void run() {
-          if (released) {
-            return;
-          }
-          AdsMediaSource.this.onAdPlaybackState(adPlaybackState);
-        }
-      });
+      playerHandler.post(
+          () -> {
+            if (released) {
+              return;
+            }
+            AdsMediaSource.this.onAdPlaybackState(adPlaybackState);
+          });
     }
 
     @Override
@@ -517,14 +524,12 @@ public final class AdsMediaSource extends CompositeMediaSource {
         return;
       }
       if (eventHandler != null && eventListener != null) {
-        eventHandler.post(new Runnable() {
-          @Override
-          public void run() {
-            if (!released) {
-              eventListener.onAdClicked();
-            }
-          }
-        });
+        eventHandler.post(
+            () -> {
+              if (!released) {
+                eventListener.onAdClicked();
+              }
+            });
       }
     }
 
@@ -534,14 +539,12 @@ public final class AdsMediaSource extends CompositeMediaSource {
         return;
       }
       if (eventHandler != null && eventListener != null) {
-        eventHandler.post(new Runnable() {
-          @Override
-          public void run() {
-            if (!released) {
-              eventListener.onAdTapped();
-            }
-          }
-        });
+        eventHandler.post(
+            () -> {
+              if (!released) {
+                eventListener.onAdTapped();
+              }
+            });
       }
     }
 
@@ -554,6 +557,7 @@ public final class AdsMediaSource extends CompositeMediaSource {
           .loadError(
               dataSpec,
               dataSpec.uri,
+              /* responseHeaders= */ Collections.emptyMap(),
               C.DATA_TYPE_AD,
               C.TRACK_TYPE_UNKNOWN,
               /* loadDurationMs= */ 0,
@@ -562,15 +566,12 @@ public final class AdsMediaSource extends CompositeMediaSource {
               /* wasCanceled= */ true);
       if (eventHandler != null && eventListener != null) {
         eventHandler.post(
-            new Runnable() {
-              @Override
-              public void run() {
-                if (!released) {
-                  if (error.type == AdLoadException.TYPE_UNEXPECTED) {
-                    eventListener.onInternalAdLoadError(error.getRuntimeExceptionForUnexpected());
-                  } else {
-                    eventListener.onAdLoadError(error);
-                  }
+            () -> {
+              if (!released) {
+                if (error.type == AdLoadException.TYPE_UNEXPECTED) {
+                  eventListener.onInternalAdLoadError(error.getRuntimeExceptionForUnexpected());
+                } else {
+                  eventListener.onAdLoadError(error);
                 }
               }
             });
@@ -596,6 +597,7 @@ public final class AdsMediaSource extends CompositeMediaSource {
           .loadError(
               new DataSpec(adUri),
               adUri,
+              /* responseHeaders= */ Collections.emptyMap(),
               C.DATA_TYPE_AD,
               C.TRACK_TYPE_UNKNOWN,
               /* loadDurationMs= */ 0,
@@ -603,12 +605,7 @@ public final class AdsMediaSource extends CompositeMediaSource {
               AdLoadException.createForAd(exception),
               /* wasCanceled= */ true);
       mainHandler.post(
-          new Runnable() {
-            @Override
-            public void run() {
-              adsLoader.handlePrepareError(adGroupIndex, adIndexInAdGroup, exception);
-            }
-          });
+          () -> adsLoader.handlePrepareError(adGroupIndex, adIndexInAdGroup, exception));
     }
   }
 }
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 78914e9f33..383f3dfc15 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
@@ -432,6 +432,7 @@ public class ChunkSampleStream implements SampleStream, S
     eventDispatcher.loadCompleted(
         loadable.dataSpec,
         loadable.getUri(),
+        loadable.getResponseHeaders(),
         loadable.type,
         primaryTrackType,
         loadable.trackFormat,
@@ -451,6 +452,7 @@ public class ChunkSampleStream implements SampleStream, S
     eventDispatcher.loadCanceled(
         loadable.dataSpec,
         loadable.getUri(),
+        loadable.getResponseHeaders(),
         loadable.type,
         primaryTrackType,
         loadable.trackFormat,
@@ -518,6 +520,7 @@ public class ChunkSampleStream implements SampleStream, S
     eventDispatcher.loadError(
         loadable.dataSpec,
         loadable.getUri(),
+        loadable.getResponseHeaders(),
         loadable.type,
         primaryTrackType,
         loadable.trackFormat,
@@ -585,7 +588,6 @@ public class ChunkSampleStream implements SampleStream, S
             loadable, this, loadErrorHandlingPolicy.getMinimumLoadableRetryCount(loadable.type));
     eventDispatcher.loadStarted(
         loadable.dataSpec,
-        loadable.dataSpec.uri,
         loadable.type,
         primaryTrackType,
         loadable.trackFormat,
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/CaptionStyleCompat.java b/library/core/src/main/java/com/google/android/exoplayer2/text/CaptionStyleCompat.java
index 51f5ad0a64..87dcb97a81 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/text/CaptionStyleCompat.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/text/CaptionStyleCompat.java
@@ -31,11 +31,18 @@ import java.lang.annotation.RetentionPolicy;
 public final class CaptionStyleCompat {
 
   /**
-   * The type of edge, which may be none.
+   * The type of edge, which may be none. One of {@link #EDGE_TYPE_NONE}, {@link
+   * #EDGE_TYPE_OUTLINE}, {@link #EDGE_TYPE_DROP_SHADOW}, {@link #EDGE_TYPE_RAISED} or {@link
+   * #EDGE_TYPE_DEPRESSED}.
    */
   @Retention(RetentionPolicy.SOURCE)
-  @IntDef({EDGE_TYPE_NONE, EDGE_TYPE_OUTLINE, EDGE_TYPE_DROP_SHADOW, EDGE_TYPE_RAISED,
-      EDGE_TYPE_DEPRESSED})
+  @IntDef({
+    EDGE_TYPE_NONE,
+    EDGE_TYPE_OUTLINE,
+    EDGE_TYPE_DROP_SHADOW,
+    EDGE_TYPE_RAISED,
+    EDGE_TYPE_DEPRESSED
+  })
   public @interface EdgeType {}
   /**
    * Edge type value specifying no character edges.
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/Cue.java b/library/core/src/main/java/com/google/android/exoplayer2/text/Cue.java
index 8bc0b8e136..e1305acd14 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/text/Cue.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/text/Cue.java
@@ -33,7 +33,8 @@ public class Cue {
   public static final float DIMEN_UNSET = Float.MIN_VALUE;
 
   /**
-   * The type of anchor, which may be unset.
+   * The type of anchor, which may be unset. One of {@link #TYPE_UNSET}, {@link #ANCHOR_TYPE_START},
+   * {@link #ANCHOR_TYPE_MIDDLE} or {@link #ANCHOR_TYPE_END}.
    */
   @Retention(RetentionPolicy.SOURCE)
   @IntDef({TYPE_UNSET, ANCHOR_TYPE_START, ANCHOR_TYPE_MIDDLE, ANCHOR_TYPE_END})
@@ -62,7 +63,8 @@ public class Cue {
   public static final int ANCHOR_TYPE_END = 2;
 
   /**
-   * The type of line, which may be unset.
+   * The type of line, which may be unset. One of {@link #TYPE_UNSET}, {@link #LINE_TYPE_FRACTION}
+   * or {@link #LINE_TYPE_NUMBER}.
    */
   @Retention(RetentionPolicy.SOURCE)
   @IntDef({TYPE_UNSET, LINE_TYPE_FRACTION, LINE_TYPE_NUMBER})
@@ -78,7 +80,11 @@ public class Cue {
    */
   public static final int LINE_TYPE_NUMBER = 1;
 
-  /** The type of default text size for this cue, which may be unset. */
+  /**
+   * The type of default text size for this cue, which may be unset. One of {@link #TYPE_UNSET},
+   * {@link #TEXT_SIZE_TYPE_FRACTIONAL}, {@link #TEXT_SIZE_TYPE_FRACTIONAL_IGNORE_PADDING} or {@link
+   * #TEXT_SIZE_TYPE_ABSOLUTE}.
+   */
   @Retention(RetentionPolicy.SOURCE)
   @IntDef({
     TYPE_UNSET,
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/SimpleSubtitleDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/SimpleSubtitleDecoder.java
index 997f750b61..38d6ff25cb 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/text/SimpleSubtitleDecoder.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/text/SimpleSubtitleDecoder.java
@@ -67,9 +67,10 @@ public abstract class SimpleSubtitleDecoder extends
     super.releaseOutputBuffer(buffer);
   }
 
+  @SuppressWarnings("ByteBufferBackingArray")
   @Override
-  protected final SubtitleDecoderException decode(SubtitleInputBuffer inputBuffer,
-      SubtitleOutputBuffer outputBuffer, boolean reset) {
+  protected final SubtitleDecoderException decode(
+      SubtitleInputBuffer inputBuffer, SubtitleOutputBuffer outputBuffer, boolean reset) {
     try {
       ByteBuffer inputData = inputBuffer.data;
       Subtitle subtitle = decode(inputData.array(), inputData.limit(), reset);
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java
index 725321e53f..60cdda06c4 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java
@@ -250,6 +250,7 @@ public final class Cea608Decoder extends CeaDecoder {
     return new CeaSubtitle(cues);
   }
 
+  @SuppressWarnings("ByteBufferBackingArray")
   @Override
   protected void decode(SubtitleInputBuffer inputBuffer) {
     ccData.reset(inputBuffer.data.array(), inputBuffer.data.limit());
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/tx3g/Tx3gDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/tx3g/Tx3gDecoder.java
index ebc38bcd70..9211dc51ce 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/text/tx3g/Tx3gDecoder.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/text/tx3g/Tx3gDecoder.java
@@ -56,8 +56,8 @@ public final class Tx3gDecoder extends SimpleSubtitleDecoder {
   private static final int FONT_FACE_ITALIC = 0x0002;
   private static final int FONT_FACE_UNDERLINE = 0x0004;
 
-  private static final int SPAN_PRIORITY_LOW = (0xFF << Spanned.SPAN_PRIORITY_SHIFT);
-  private static final int SPAN_PRIORITY_HIGH = (0x00 << Spanned.SPAN_PRIORITY_SHIFT);
+  private static final int SPAN_PRIORITY_LOW = 0xFF << Spanned.SPAN_PRIORITY_SHIFT;
+  private static final int SPAN_PRIORITY_HIGH = 0;
 
   private static final int DEFAULT_FONT_FACE = 0;
   private static final int DEFAULT_COLOR = Color.WHITE;
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.java b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.java
index a78c5afa78..0e46fa0d2f 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.java
@@ -35,20 +35,29 @@ public final class WebvttCssStyle {
 
   public static final int UNSPECIFIED = -1;
 
-  /** Style flag enum */
+  /**
+   * Style flag enum. Possible flag values are {@link #UNSPECIFIED}, {@link #STYLE_NORMAL}, {@link
+   * #STYLE_BOLD}, {@link #STYLE_ITALIC} and {@link #STYLE_BOLD_ITALIC}.
+   */
   @Retention(RetentionPolicy.SOURCE)
-  @IntDef(flag = true, value = {UNSPECIFIED, STYLE_NORMAL, STYLE_BOLD, STYLE_ITALIC,
-      STYLE_BOLD_ITALIC})
+  @IntDef(
+      flag = true,
+      value = {UNSPECIFIED, STYLE_NORMAL, STYLE_BOLD, STYLE_ITALIC, STYLE_BOLD_ITALIC})
   public @interface StyleFlags {}
+
   public static final int STYLE_NORMAL = Typeface.NORMAL;
   public static final int STYLE_BOLD = Typeface.BOLD;
   public static final int STYLE_ITALIC = Typeface.ITALIC;
   public static final int STYLE_BOLD_ITALIC = Typeface.BOLD_ITALIC;
 
-  /** Font size unit enum */
+  /**
+   * Font size unit enum. One of {@link #UNSPECIFIED}, {@link #FONT_SIZE_UNIT_PIXEL}, {@link
+   * #FONT_SIZE_UNIT_EM} or {@link #FONT_SIZE_UNIT_PERCENT}.
+   */
   @Retention(RetentionPolicy.SOURCE)
   @IntDef({UNSPECIFIED, FONT_SIZE_UNIT_PIXEL, FONT_SIZE_UNIT_EM, FONT_SIZE_UNIT_PERCENT})
   public @interface FontSizeUnit {}
+
   public static final int FONT_SIZE_UNIT_PIXEL = 1;
   public static final int FONT_SIZE_UNIT_EM = 2;
   public static final int FONT_SIZE_UNIT_PERCENT = 3;
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java
index 3f201bccea..9a15cbae14 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java
@@ -186,7 +186,7 @@ public abstract class BaseTrackSelection implements TrackSelection {
 
   // Track groups are compared by identity not value, as distinct groups may have the same value.
   @Override
-  @SuppressWarnings("ReferenceEquality")
+  @SuppressWarnings({"ReferenceEquality", "EqualsGetClass"})
   public boolean equals(@Nullable Object obj) {
     if (this == obj) {
       return true;
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 58784e4c5a..5e4aef958c 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
@@ -19,6 +19,7 @@ import android.content.Context;
 import android.graphics.Point;
 import android.os.Parcel;
 import android.os.Parcelable;
+import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 import android.text.TextUtils;
 import android.util.Pair;
@@ -167,6 +168,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
     private boolean selectUndeterminedTextLanguage;
     private int disabledTextTrackSelectionFlags;
     private boolean forceLowestBitrate;
+    private boolean forceHighestSupportedBitrate;
     private boolean allowMixedMimeAdaptiveness;
     private boolean allowNonSeamlessAdaptiveness;
     private int maxVideoWidth;
@@ -196,6 +198,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
       selectUndeterminedTextLanguage = initialValues.selectUndeterminedTextLanguage;
       disabledTextTrackSelectionFlags = initialValues.disabledTextTrackSelectionFlags;
       forceLowestBitrate = initialValues.forceLowestBitrate;
+      forceHighestSupportedBitrate = initialValues.forceHighestSupportedBitrate;
       allowMixedMimeAdaptiveness = initialValues.allowMixedMimeAdaptiveness;
       allowNonSeamlessAdaptiveness = initialValues.allowNonSeamlessAdaptiveness;
       maxVideoWidth = initialValues.maxVideoWidth;
@@ -261,6 +264,16 @@ public class DefaultTrackSelector extends MappingTrackSelector {
       return this;
     }
 
+    /**
+     * See {@link Parameters#forceHighestSupportedBitrate}.
+     *
+     * @return This builder.
+     */
+    public ParametersBuilder setForceHighestSupportedBitrate(boolean forceHighestSupportedBitrate) {
+      this.forceHighestSupportedBitrate = forceHighestSupportedBitrate;
+      return this;
+    }
+
     /**
      * See {@link Parameters#allowMixedMimeAdaptiveness}.
      *
@@ -519,6 +532,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
           selectUndeterminedTextLanguage,
           disabledTextTrackSelectionFlags,
           forceLowestBitrate,
+          forceHighestSupportedBitrate,
           allowMixedMimeAdaptiveness,
           allowNonSeamlessAdaptiveness,
           maxVideoWidth,
@@ -633,6 +647,11 @@ public class DefaultTrackSelector extends MappingTrackSelector {
      * with all other constraints. The default value is {@code false}.
      */
     public final boolean forceLowestBitrate;
+    /**
+     * Whether to force selection of the highest bitrate audio and video tracks that comply with all
+     * other constraints. The default value is {@code false}.
+     */
+    public final boolean forceHighestSupportedBitrate;
     /**
      * Whether to allow adaptive selections containing mixed mime types. The default value is {@code
      * false}.
@@ -669,6 +688,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
           /* selectUndeterminedTextLanguage= */ false,
           /* disabledTextTrackSelectionFlags= */ 0,
           /* forceLowestBitrate= */ false,
+          /* forceHighestSupportedBitrate= */ false,
           /* allowMixedMimeAdaptiveness= */ false,
           /* allowNonSeamlessAdaptiveness= */ true,
           /* maxVideoWidth= */ Integer.MAX_VALUE,
@@ -690,6 +710,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
         boolean selectUndeterminedTextLanguage,
         int disabledTextTrackSelectionFlags,
         boolean forceLowestBitrate,
+        boolean forceHighestSupportedBitrate,
         boolean allowMixedMimeAdaptiveness,
         boolean allowNonSeamlessAdaptiveness,
         int maxVideoWidth,
@@ -708,6 +729,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
       this.selectUndeterminedTextLanguage = selectUndeterminedTextLanguage;
       this.disabledTextTrackSelectionFlags = disabledTextTrackSelectionFlags;
       this.forceLowestBitrate = forceLowestBitrate;
+      this.forceHighestSupportedBitrate = forceHighestSupportedBitrate;
       this.allowMixedMimeAdaptiveness = allowMixedMimeAdaptiveness;
       this.allowNonSeamlessAdaptiveness = allowNonSeamlessAdaptiveness;
       this.maxVideoWidth = maxVideoWidth;
@@ -729,6 +751,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
       this.selectUndeterminedTextLanguage = Util.readBoolean(in);
       this.disabledTextTrackSelectionFlags = in.readInt();
       this.forceLowestBitrate = Util.readBoolean(in);
+      this.forceHighestSupportedBitrate = Util.readBoolean(in);
       this.allowMixedMimeAdaptiveness = Util.readBoolean(in);
       this.allowNonSeamlessAdaptiveness = Util.readBoolean(in);
       this.maxVideoWidth = in.readInt();
@@ -796,6 +819,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
       return selectUndeterminedTextLanguage == other.selectUndeterminedTextLanguage
           && disabledTextTrackSelectionFlags == other.disabledTextTrackSelectionFlags
           && forceLowestBitrate == other.forceLowestBitrate
+          && forceHighestSupportedBitrate == other.forceHighestSupportedBitrate
           && allowMixedMimeAdaptiveness == other.allowMixedMimeAdaptiveness
           && allowNonSeamlessAdaptiveness == other.allowNonSeamlessAdaptiveness
           && maxVideoWidth == other.maxVideoWidth
@@ -818,6 +842,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
       int result = selectUndeterminedTextLanguage ? 1 : 0;
       result = 31 * result + disabledTextTrackSelectionFlags;
       result = 31 * result + (forceLowestBitrate ? 1 : 0);
+      result = 31 * result + (forceHighestSupportedBitrate ? 1 : 0);
       result = 31 * result + (allowMixedMimeAdaptiveness ? 1 : 0);
       result = 31 * result + (allowNonSeamlessAdaptiveness ? 1 : 0);
       result = 31 * result + maxVideoWidth;
@@ -851,6 +876,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
       Util.writeBoolean(dest, selectUndeterminedTextLanguage);
       dest.writeInt(disabledTextTrackSelectionFlags);
       Util.writeBoolean(dest, forceLowestBitrate);
+      Util.writeBoolean(dest, forceHighestSupportedBitrate);
       Util.writeBoolean(dest, allowMixedMimeAdaptiveness);
       Util.writeBoolean(dest, allowNonSeamlessAdaptiveness);
       dest.writeInt(maxVideoWidth);
@@ -1069,6 +1095,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
    *     directly passed to the player in ExoPlayerFactory.
    */
   @Deprecated
+  @SuppressWarnings("deprecation")
   public DefaultTrackSelector(BandwidthMeter bandwidthMeter) {
     this(new AdaptiveTrackSelection.Factory(bandwidthMeter));
   }
@@ -1354,7 +1381,9 @@ public class DefaultTrackSelector extends MappingTrackSelector {
       @Nullable TrackSelection.Factory adaptiveTrackSelectionFactory)
       throws ExoPlaybackException {
     TrackSelection selection = null;
-    if (!params.forceLowestBitrate && adaptiveTrackSelectionFactory != null) {
+    if (!params.forceHighestSupportedBitrate
+        && !params.forceLowestBitrate
+        && adaptiveTrackSelectionFactory != null) {
       selection =
           selectAdaptiveVideoTrack(
               groups,
@@ -1605,7 +1634,9 @@ public class DefaultTrackSelector extends MappingTrackSelector {
     }
 
     TrackGroup selectedGroup = groups.get(selectedGroupIndex);
-    if (!params.forceLowestBitrate && adaptiveTrackSelectionFactory != null) {
+    if (!params.forceHighestSupportedBitrate
+        && !params.forceLowestBitrate
+        && adaptiveTrackSelectionFactory != null) {
       // If the group of the track with the highest score allows it, try to enable adaptation.
       int[] adaptiveTracks =
           getAdaptiveAudioTracks(
@@ -2032,7 +2063,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
      *     negative integer if this score is worse than the other.
      */
     @Override
-    public int compareTo(AudioTrackScore other) {
+    public int compareTo(@NonNull AudioTrackScore other) {
       if (this.withinRendererCapabilitiesScore != other.withinRendererCapabilitiesScore) {
         return compareInts(this.withinRendererCapabilitiesScore,
             other.withinRendererCapabilitiesScore);
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 99e4e58c4a..c2fda67728 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
@@ -43,7 +43,11 @@ public abstract class MappingTrackSelector extends TrackSelector {
    */
   public static final class MappedTrackInfo {
 
-    /** Levels of renderer support. Higher numerical values indicate higher levels of support. */
+    /**
+     * Levels of renderer support. Higher numerical values indicate higher levels of support. One of
+     * {@link #RENDERER_SUPPORT_NO_TRACKS}, {@link #RENDERER_SUPPORT_UNSUPPORTED_TRACKS}, {@link
+     * #RENDERER_SUPPORT_EXCEEDS_CAPABILITIES_TRACKS} or {@link #RENDERER_SUPPORT_PLAYABLE_TRACKS}.
+     */
     @Retention(RetentionPolicy.SOURCE)
     @IntDef({
       RENDERER_SUPPORT_NO_TRACKS,
@@ -95,6 +99,7 @@ public abstract class MappingTrackSelector extends TrackSelector {
      *     each mapped track, indexed by renderer, track group and track (in that order).
      * @param unmappedTrackGroups {@link TrackGroup}s not mapped to any renderer.
      */
+    @SuppressWarnings("deprecation")
     /* package */ MappedTrackInfo(
         int[] rendererTrackTypes,
         TrackGroupArray[] rendererTrackGroups,
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/BaseDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/BaseDataSource.java
index 18a7dcea49..5ed2e33d2b 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/BaseDataSource.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/BaseDataSource.java
@@ -47,8 +47,10 @@ public abstract class BaseDataSource implements DataSource {
 
   @Override
   public final void addTransferListener(TransferListener transferListener) {
-    listeners.add(transferListener);
-    listenerCount++;
+    if (!listeners.contains(transferListener)) {
+      listeners.add(transferListener);
+      listenerCount++;
+    }
   }
 
   /**
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java
index 273509e0d4..30051a53e6 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java
@@ -148,6 +148,7 @@ public final class ContentDataSource extends BaseDataSource {
     return uri;
   }
 
+  @SuppressWarnings("Finally")
   @Override
   public void close() throws ContentDataSourceException {
     uri = null;
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java
index 366b6d8c67..c968921822 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java
@@ -30,10 +30,13 @@ import java.util.Arrays;
 public final class DataSpec {
 
   /**
-   * The flags that apply to any request for data.
+   * The flags that apply to any request for data. Possible flag values are {@link #FLAG_ALLOW_GZIP}
+   * and {@link #FLAG_ALLOW_CACHING_UNKNOWN_LENGTH}.
    */
   @Retention(RetentionPolicy.SOURCE)
-  @IntDef(flag = true, value = {FLAG_ALLOW_GZIP, FLAG_ALLOW_CACHING_UNKNOWN_LENGTH})
+  @IntDef(
+      flag = true,
+      value = {FLAG_ALLOW_GZIP, FLAG_ALLOW_CACHING_UNKNOWN_LENGTH})
   public @interface Flags {}
   /**
    * Permits an underlying network stack to request that the server use gzip compression.
@@ -54,7 +57,11 @@ public final class DataSpec {
    */
   public static final int FLAG_ALLOW_CACHING_UNKNOWN_LENGTH = 1 << 1; // 2
 
-  /** The set of HTTP methods that are supported by ExoPlayer {@link HttpDataSource}s. */
+  /**
+   * The set of HTTP methods that are supported by ExoPlayer {@link HttpDataSource}s. One of {@link
+   * #HTTP_METHOD_GET}, {@link #HTTP_METHOD_POST} or {@link #HTTP_METHOD_HEAD}.
+   */
+  @Retention(RetentionPolicy.SOURCE)
   @IntDef({HTTP_METHOD_GET, HTTP_METHOD_POST, HTTP_METHOD_HEAD})
   public @interface HttpMethod {}
 
@@ -219,6 +226,7 @@ public final class DataSpec {
    * @param key {@link #key}.
    * @param flags {@link #flags}.
    */
+  @SuppressWarnings("deprecation")
   public DataSpec(
       Uri uri,
       @HttpMethod int httpMethod,
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSource.java
index 23d6cc368f..411240d56c 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSource.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSource.java
@@ -142,6 +142,7 @@ public final class DefaultDataSource implements DataSource {
    *     #addTransferListener(TransferListener)}.
    */
   @Deprecated
+  @SuppressWarnings("deprecation")
   public DefaultDataSource(
       Context context,
       @Nullable TransferListener listener,
@@ -167,6 +168,7 @@ public final class DefaultDataSource implements DataSource {
    *     #addTransferListener(TransferListener)}.
    */
   @Deprecated
+  @SuppressWarnings("deprecation")
   public DefaultDataSource(
       Context context,
       @Nullable TransferListener listener,
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSourceFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSourceFactory.java
index 293ba7f17b..8183a89064 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSourceFactory.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSourceFactory.java
@@ -75,6 +75,11 @@ public final class DefaultDataSourceFactory implements Factory {
 
   @Override
   public DefaultDataSource createDataSource() {
-    return new DefaultDataSource(context, listener, baseDataSourceFactory.createDataSource());
+    DefaultDataSource dataSource =
+        new DefaultDataSource(context, baseDataSourceFactory.createDataSource());
+    if (listener != null) {
+      dataSource.addTransferListener(listener);
+    }
+    return dataSource;
   }
 }
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 87ea36bd18..f02f1cc7c4 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
@@ -168,6 +168,7 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou
    *     #addTransferListener(TransferListener)}.
    */
   @Deprecated
+  @SuppressWarnings("deprecation")
   public DefaultHttpDataSource(
       String userAgent,
       @Nullable Predicate contentTypePredicate,
@@ -190,6 +191,7 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou
    *     #addTransferListener(TransferListener)}.
    */
   @Deprecated
+  @SuppressWarnings("deprecation")
   public DefaultHttpDataSource(
       String userAgent,
       @Nullable Predicate contentTypePredicate,
@@ -379,7 +381,7 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou
    *
    * @return The current open connection, or null.
    */
-  protected final HttpURLConnection getConnection() {
+  protected final @Nullable HttpURLConnection getConnection() {
     return connection;
   }
 
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceFactory.java
index aa0ac7b97e..95ea49132d 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceFactory.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceFactory.java
@@ -103,7 +103,17 @@ public final class DefaultHttpDataSourceFactory extends BaseFactory {
   @Override
   protected DefaultHttpDataSource createDataSourceInternal(
       HttpDataSource.RequestProperties defaultRequestProperties) {
-    return new DefaultHttpDataSource(userAgent, null, listener, connectTimeoutMillis,
-        readTimeoutMillis, allowCrossProtocolRedirects, defaultRequestProperties);
+    DefaultHttpDataSource dataSource =
+        new DefaultHttpDataSource(
+            userAgent,
+            /* contentTypePredicate= */ null,
+            connectTimeoutMillis,
+            readTimeoutMillis,
+            allowCrossProtocolRedirects,
+            defaultRequestProperties);
+    if (listener != null) {
+      dataSource.addTransferListener(listener);
+    }
+    return dataSource;
   }
 }
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DummyDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DummyDataSource.java
index 06dc79e345..13c5732a62 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DummyDataSource.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DummyDataSource.java
@@ -27,12 +27,7 @@ public final class DummyDataSource implements DataSource {
   public static final DummyDataSource INSTANCE = new DummyDataSource();
 
   /** A factory that produces {@link DummyDataSource}. */
-  public static final Factory FACTORY = new Factory() {
-    @Override
-    public DataSource createDataSource() {
-      return new DummyDataSource();
-    }
-  };
+  public static final Factory FACTORY = DummyDataSource::new;
 
   private DummyDataSource() {}
 
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/FileDataSourceFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/FileDataSourceFactory.java
index f69adeb8c2..fd1920991e 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/FileDataSourceFactory.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/FileDataSourceFactory.java
@@ -34,7 +34,11 @@ public final class FileDataSourceFactory implements DataSource.Factory {
 
   @Override
   public DataSource createDataSource() {
-    return new FileDataSource(listener);
+    FileDataSource dataSource = new FileDataSource();
+    if (listener != null) {
+      dataSource.addTransferListener(listener);
+    }
+    return dataSource;
   }
 
 }
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/HttpDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/HttpDataSource.java
index 71a0e68260..daf5d3281a 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/HttpDataSource.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/HttpDataSource.java
@@ -211,20 +211,15 @@ public interface HttpDataSource extends DataSource {
 
   }
 
-  /**
-   * A {@link Predicate} that rejects content types often used for pay-walls.
-   */
-  Predicate REJECT_PAYWALL_TYPES = new Predicate() {
-
-    @Override
-    public boolean evaluate(String contentType) {
-      contentType = Util.toLowerInvariant(contentType);
-      return !TextUtils.isEmpty(contentType)
-          && (!contentType.contains("text") || contentType.contains("text/vtt"))
-          && !contentType.contains("html") && !contentType.contains("xml");
-    }
-
-  };
+  /** A {@link Predicate} that rejects content types often used for pay-walls. */
+  Predicate REJECT_PAYWALL_TYPES =
+      contentType -> {
+        contentType = Util.toLowerInvariant(contentType);
+        return !TextUtils.isEmpty(contentType)
+            && (!contentType.contains("text") || contentType.contains("text/vtt"))
+            && !contentType.contains("html")
+            && !contentType.contains("xml");
+      };
 
   /**
    * Thrown when an error is encountered when trying to read from a {@link HttpDataSource}.
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 17d479daab..cdcb3787fa 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
@@ -24,6 +24,8 @@ import com.google.android.exoplayer2.util.Assertions;
 import com.google.android.exoplayer2.util.Util;
 import java.io.IOException;
 import java.io.InputStream;
+import java.util.List;
+import java.util.Map;
 
 /**
  * A {@link Loadable} for objects that can be parsed from binary data using a {@link Parser}.
@@ -132,6 +134,14 @@ public final class ParsingLoadable implements Loadable {
     return dataSource.getLastOpenedUri();
   }
 
+  /**
+   * Returns the response headers associated with the load. Must only be called after the load
+   * completed, failed, or was canceled.
+   */
+  public Map> getResponseHeaders() {
+    return dataSource.getLastResponseHeaders();
+  }
+
   @Override
   public final void cancelLoad() {
     // Do nothing.
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/RawResourceDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/RawResourceDataSource.java
index f86ed87c19..7f51efda0f 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/RawResourceDataSource.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/RawResourceDataSource.java
@@ -171,6 +171,7 @@ public final class RawResourceDataSource extends BaseDataSource {
     return uri;
   }
 
+  @SuppressWarnings("Finally")
   @Override
   public void close() throws RawResourceDataSourceException {
     uri = null;
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/UdpDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/UdpDataSource.java
index 47677d2c47..8d6b39fa98 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/UdpDataSource.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/UdpDataSource.java
@@ -95,6 +95,7 @@ public final class UdpDataSource extends BaseDataSource {
    * @deprecated Use {@link #UdpDataSource()} and {@link #addTransferListener(TransferListener)}.
    */
   @Deprecated
+  @SuppressWarnings("deprecation")
   public UdpDataSource(@Nullable TransferListener listener) {
     this(listener, DEFAULT_MAX_PACKET_SIZE);
   }
@@ -107,6 +108,7 @@ public final class UdpDataSource extends BaseDataSource {
    * @deprecated Use {@link #UdpDataSource(int)} and {@link #addTransferListener(TransferListener)}.
    */
   @Deprecated
+  @SuppressWarnings("deprecation")
   public UdpDataSource(@Nullable TransferListener listener, int maxPacketSize) {
     this(listener, maxPacketSize, DEFAULT_SOCKET_TIMEOUT_MILLIS);
   }
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 584939fdc7..a769e9acac 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
@@ -15,7 +15,6 @@
  */
 package com.google.android.exoplayer2.upstream.cache;
 
-import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 import java.io.File;
 import java.io.IOException;
@@ -96,7 +95,6 @@ public interface Cache {
    * @param listener The listener to add.
    * @return The current spans for the key.
    */
-  @NonNull
   NavigableSet addListener(String key, Listener listener);
 
   /**
@@ -113,7 +111,6 @@ public interface Cache {
    * @param key The key for which spans should be returned.
    * @return The spans for the key.
    */
-  @NonNull
   NavigableSet getCachedSpans(String key);
 
   /**
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 222d5385d3..a91e3246cc 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
@@ -56,11 +56,17 @@ public final class CacheDataSource implements DataSource {
   public static final long DEFAULT_MAX_CACHE_FILE_SIZE = 2 * 1024 * 1024;
 
   /**
-   * Flags controlling the cache's behavior.
+   * Flags controlling the cache's behavior. Possible flag values are {@link #FLAG_BLOCK_ON_CACHE},
+   * {@link #FLAG_IGNORE_CACHE_ON_ERROR} and {@link #FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS}.
    */
   @Retention(RetentionPolicy.SOURCE)
-  @IntDef(flag = true, value = {FLAG_BLOCK_ON_CACHE, FLAG_IGNORE_CACHE_ON_ERROR,
-      FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS})
+  @IntDef(
+      flag = true,
+      value = {
+        FLAG_BLOCK_ON_CACHE,
+        FLAG_IGNORE_CACHE_ON_ERROR,
+        FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS
+      })
   public @interface Flags {}
   /**
    * A flag indicating whether we will block reads if the cache key is locked. If unset then data is
@@ -81,7 +87,10 @@ public final class CacheDataSource implements DataSource {
    */
   public static final int FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS = 1 << 2; // 4
 
-  /** Reasons the cache may be ignored. */
+  /**
+   * Reasons the cache may be ignored. One of {@link #CACHE_IGNORED_REASON_ERROR} or {@link
+   * #CACHE_IGNORED_REASON_UNSET_LENGTH}.
+   */
   @Retention(RetentionPolicy.SOURCE)
   @IntDef({CACHE_IGNORED_REASON_ERROR, CACHE_IGNORED_REASON_UNSET_LENGTH})
   public @interface CacheIgnoredReason {}
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 1bdaa8e3fa..cee22375a9 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
@@ -55,13 +55,7 @@ public final class CacheUtil {
   public static final int DEFAULT_BUFFER_SIZE_BYTES = 128 * 1024;
 
   /** Default {@link CacheKeyFactory} that calls through to {@link #getKey}. */
-  public static final CacheKeyFactory DEFAULT_CACHE_KEY_FACTORY =
-      new CacheKeyFactory() {
-        @Override
-        public String buildCacheKey(DataSpec dataSpec) {
-          return getKey(dataSpec);
-        }
-      };
+  public static final CacheKeyFactory DEFAULT_CACHE_KEY_FACTORY = CacheUtil::getKey;
 
   /**
    * Generates a cache key out of the given {@link Uri}.
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 89835f31de..97a7828a22 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
@@ -185,16 +185,13 @@ import java.util.TreeSet;
    * @throws CacheException If renaming of the underlying span file failed.
    */
   public SimpleCacheSpan touch(SimpleCacheSpan cacheSpan) throws CacheException {
-    // Remove the old span from the in-memory representation.
-    Assertions.checkState(cachedSpans.remove(cacheSpan));
-    // Obtain a new span with updated last access timestamp.
     SimpleCacheSpan newCacheSpan = cacheSpan.copyWithUpdatedLastAccessTime(id);
-    // Rename the cache file
     if (!cacheSpan.file.renameTo(newCacheSpan.file)) {
       throw new CacheException("Renaming of " + cacheSpan.file + " to " + newCacheSpan.file
           + " failed.");
     }
-    // Add the updated span back into the in-memory representation.
+    // Replace the in-memory representation of the span.
+    Assertions.checkState(cachedSpans.remove(cacheSpan));
     cachedSpans.add(newCacheSpan);
     return newCacheSpan;
   }
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
index cf63bcc4f6..e16ff5483a 100644
--- 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
@@ -188,9 +188,12 @@ public final class DefaultContentMetadata implements ContentMetadata {
       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));
+            "The size of "
+                + name
+                + " ("
+                + bytes.length
+                + ") is greater than maximum allowed: "
+                + MAX_VALUE_LENGTH);
       }
       metadata.put(name, bytes);
     }
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 7d2d5b79a9..adaf523e7c 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
@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.upstream.cache;
 
 import android.os.ConditionVariable;
 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.util.Assertions;
@@ -190,7 +191,7 @@ public final class SimpleCache implements Cache {
     Assertions.checkState(!released);
     CachedContent cachedContent = index.get(key);
     return cachedContent == null || cachedContent.isEmpty()
-        ? new TreeSet()
+        ? new TreeSet<>()
         : new TreeSet(cachedContent.getSpans());
   }
 
@@ -224,17 +225,23 @@ public final class SimpleCache implements Cache {
   }
 
   @Override
-  public synchronized SimpleCacheSpan startReadWriteNonBlocking(String key, long position)
+  public synchronized @Nullable SimpleCacheSpan startReadWriteNonBlocking(String key, long position)
       throws CacheException {
     Assertions.checkState(!released);
     SimpleCacheSpan cacheSpan = getSpan(key, position);
 
     // Read case.
     if (cacheSpan.isCached) {
-      // Obtain a new span with updated last access timestamp.
-      SimpleCacheSpan newCacheSpan = index.get(key).touch(cacheSpan);
-      notifySpanTouched(cacheSpan, newCacheSpan);
-      return newCacheSpan;
+      try {
+        // Obtain a new span with updated last access timestamp.
+        SimpleCacheSpan newCacheSpan = index.get(key).touch(cacheSpan);
+        notifySpanTouched(cacheSpan, newCacheSpan);
+        return newCacheSpan;
+      } catch (CacheException e) {
+        // Ignore. In worst case the cache span is evicted early.
+        // This happens very rarely [Internal: b/38351639]
+        return cacheSpan;
+      }
     }
 
     CachedContent cachedContent = index.getOrAdd(key);
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/EGLSurfaceTexture.java b/library/core/src/main/java/com/google/android/exoplayer2/util/EGLSurfaceTexture.java
index 7e831f0512..90e37de828 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/util/EGLSurfaceTexture.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/util/EGLSurfaceTexture.java
@@ -39,7 +39,10 @@ public final class EGLSurfaceTexture implements SurfaceTexture.OnFrameAvailableL
     void onFrameAvailable();
   }
 
-  /** Secure mode to be used by the EGL surface and context. */
+  /**
+   * Secure mode to be used by the EGL surface and context. One of {@link #SECURE_MODE_NONE}, {@link
+   * #SECURE_MODE_SURFACELESS_CONTEXT} or {@link #SECURE_MODE_PROTECTED_PBUFFER}.
+   */
   @Retention(RetentionPolicy.SOURCE)
   @IntDef({SECURE_MODE_NONE, SECURE_MODE_SURFACELESS_CONTEXT, SECURE_MODE_PROTECTED_PBUFFER})
   public @interface SecureMode {}
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 3ca463e5e4..626464ec69 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
@@ -44,7 +44,7 @@ import java.util.Locale;
 /** Logs events from {@link Player} and other core components using {@link Log}. */
 public class EventLogger implements AnalyticsListener {
 
-  private static final String TAG = "EventLogger";
+  private static final String DEFAULT_TAG = "EventLogger";
   private static final int MAX_TIMELINE_ITEM_LINES = 3;
   private static final NumberFormat TIME_FORMAT;
   static {
@@ -55,6 +55,7 @@ public class EventLogger implements AnalyticsListener {
   }
 
   private final @Nullable MappingTrackSelector trackSelector;
+  private final String tag;
   private final Timeline.Window window;
   private final Timeline.Period period;
   private final long startTimeMs;
@@ -66,7 +67,19 @@ public class EventLogger implements AnalyticsListener {
    *     logging of track mapping is not required.
    */
   public EventLogger(@Nullable MappingTrackSelector trackSelector) {
+    this(trackSelector, DEFAULT_TAG);
+  }
+
+  /**
+   * 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.
+   * @param tag The tag used for logging.
+   */
+  public EventLogger(@Nullable MappingTrackSelector trackSelector, String tag) {
     this.trackSelector = trackSelector;
+    this.tag = tag;
     window = new Timeline.Window();
     period = new Timeline.Period();
     startTimeMs = SystemClock.elapsedRealtime();
@@ -403,7 +416,7 @@ public class EventLogger implements AnalyticsListener {
    * @param msg The message to log.
    */
   protected void logd(String msg) {
-    Log.d(TAG, msg);
+    Log.d(tag, msg);
   }
 
   /**
@@ -413,7 +426,7 @@ public class EventLogger implements AnalyticsListener {
    * @param tr The exception to log.
    */
   protected void loge(String msg, Throwable tr) {
-    Log.e(TAG, msg, tr);
+    Log.e(tag, msg, tr);
   }
 
   // Internal methods
@@ -456,7 +469,8 @@ public class EventLogger implements AnalyticsListener {
   private String getEventTimeString(EventTime eventTime) {
     String windowPeriodString = "window=" + eventTime.windowIndex;
     if (eventTime.mediaPeriodId != null) {
-      windowPeriodString += ", period=" + eventTime.mediaPeriodId.periodIndex;
+      windowPeriodString +=
+          ", period=" + eventTime.timeline.getIndexOfPeriod(eventTime.mediaPeriodId.periodUid);
       if (eventTime.mediaPeriodId.isAd()) {
         windowPeriodString += ", adGroup=" + eventTime.mediaPeriodId.adGroupIndex;
         windowPeriodString += ", ad=" + eventTime.mediaPeriodId.adIndexInAdGroup;
@@ -585,6 +599,8 @@ public class EventLogger implements AnalyticsListener {
         return "default";
       case C.TRACK_TYPE_METADATA:
         return "metadata";
+      case C.TRACK_TYPE_CAMERA_MOTION:
+        return "camera motion";
       case C.TRACK_TYPE_NONE:
         return "none";
       case C.TRACK_TYPE_TEXT:
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 e0b1df7739..f56aac7c70 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
@@ -328,9 +328,10 @@ public final class MimeTypes {
       return C.TRACK_TYPE_TEXT;
     } else if (APPLICATION_ID3.equals(mimeType)
         || APPLICATION_EMSG.equals(mimeType)
-        || APPLICATION_SCTE35.equals(mimeType)
-        || APPLICATION_CAMERA_MOTION.equals(mimeType)) {
+        || APPLICATION_SCTE35.equals(mimeType)) {
       return C.TRACK_TYPE_METADATA;
+    } else if (APPLICATION_CAMERA_MOTION.equals(mimeType)) {
+      return C.TRACK_TYPE_CAMERA_MOTION;
     } else {
       return getTrackTypeForCustomMimeType(mimeType);
     }
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
index c93d7cd72e..e70f576754 100644
--- 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
@@ -31,7 +31,11 @@ import java.lang.annotation.RetentionPolicy;
 @SuppressLint("InlinedApi")
 public final class NotificationUtil {
 
-  /** Notification channel importance levels. */
+  /**
+   * Notification channel importance levels. One of {@link #IMPORTANCE_UNSPECIFIED}, {@link
+   * #IMPORTANCE_NONE}, {@link #IMPORTANCE_MIN}, {@link #IMPORTANCE_LOW}, {@link
+   * #IMPORTANCE_DEFAULT} or {@link #IMPORTANCE_HIGH}.
+   */
   @Retention(RetentionPolicy.SOURCE)
   @IntDef({
     IMPORTANCE_UNSPECIFIED,
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/RepeatModeUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/util/RepeatModeUtil.java
index d386206bdd..de92e1ad93 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/util/RepeatModeUtil.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/util/RepeatModeUtil.java
@@ -26,11 +26,14 @@ import java.lang.annotation.RetentionPolicy;
 public final class RepeatModeUtil {
 
   /**
-   * Set of repeat toggle modes. Can be combined using bit-wise operations.
+   * Set of repeat toggle modes. Can be combined using bit-wise operations. Possible flag values are
+   * {@link #REPEAT_TOGGLE_MODE_NONE}, {@link #REPEAT_TOGGLE_MODE_ONE} and {@link
+   * #REPEAT_TOGGLE_MODE_ALL}.
    */
   @Retention(RetentionPolicy.SOURCE)
-  @IntDef(flag = true, value = {REPEAT_TOGGLE_MODE_NONE, REPEAT_TOGGLE_MODE_ONE,
-      REPEAT_TOGGLE_MODE_ALL})
+  @IntDef(
+      flag = true,
+      value = {REPEAT_TOGGLE_MODE_NONE, REPEAT_TOGGLE_MODE_ONE, REPEAT_TOGGLE_MODE_ALL})
   public @interface RepeatToggleModes {}
   /**
    * All repeat mode buttons disabled.
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/SlidingPercentile.java b/library/core/src/main/java/com/google/android/exoplayer2/util/SlidingPercentile.java
index c43b1929cb..f9be1a53b2 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/util/SlidingPercentile.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/util/SlidingPercentile.java
@@ -35,19 +35,9 @@ import java.util.Comparator;
 public class SlidingPercentile {
 
   // Orderings.
-  private static final Comparator INDEX_COMPARATOR = new Comparator() {
-    @Override
-    public int compare(Sample a, Sample b) {
-      return a.index - b.index;
-    }
-  };
-
-  private static final Comparator VALUE_COMPARATOR = new Comparator() {
-    @Override
-    public int compare(Sample a, Sample b) {
-      return a.value < b.value ? -1 : b.value < a.value ? 1 : 0;
-    }
-  };
+  private static final Comparator INDEX_COMPARATOR = (a, b) -> a.index - b.index;
+  private static final Comparator VALUE_COMPARATOR =
+      (a, b) -> Float.compare(a.value, b.value);
 
   private static final int SORT_ORDER_NONE = -1;
   private static final int SORT_ORDER_BY_VALUE = 0;
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/TimedValueQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/util/TimedValueQueue.java
new file mode 100644
index 0000000000..3fe3c56c15
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/util/TimedValueQueue.java
@@ -0,0 +1,160 @@
+/*
+ * 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.support.annotation.Nullable;
+import java.util.Arrays;
+import org.checkerframework.checker.nullness.compatqual.NullableType;
+
+/** A utility class to keep a queue of values with timestamps. This class is thread safe. */
+public final class TimedValueQueue {
+  private static final int INITIAL_BUFFER_SIZE = 10;
+
+  // Looping buffer for timestamps and values
+  private long[] timestamps;
+  private @NullableType V[] values;
+  private int first;
+  private int size;
+
+  public TimedValueQueue() {
+    this(INITIAL_BUFFER_SIZE);
+  }
+
+  /** Creates a TimedValueBuffer with the given initial buffer size. */
+  public TimedValueQueue(int initialBufferSize) {
+    timestamps = new long[initialBufferSize];
+    values = newArray(initialBufferSize);
+  }
+
+  /**
+   * Associates the specified value with the specified timestamp. All new values should have a
+   * greater timestamp than the previously added values. Otherwise all values are removed before
+   * adding the new one.
+   */
+  public synchronized void add(long timestamp, V value) {
+    clearBufferOnTimeDiscontinuity(timestamp);
+    doubleCapacityIfFull();
+    addUnchecked(timestamp, value);
+  }
+
+  /** Removes all of the values. */
+  public synchronized void clear() {
+    first = 0;
+    size = 0;
+    Arrays.fill(values, null);
+  }
+
+  /** Returns number of the values buffered. */
+  public synchronized int size() {
+    return size;
+  }
+
+  /**
+   * Returns the value with the greatest timestamp which is less than or equal to the given
+   * timestamp. Removes all older values and the returned one from the buffer.
+   *
+   * @param timestamp The timestamp value.
+   * @return The value with the greatest timestamp which is less than or equal to the given
+   *     timestamp or null if there is no such value.
+   * @see #poll(long)
+   */
+  public synchronized @Nullable V pollFloor(long timestamp) {
+    return poll(timestamp, /* onlyOlder= */ true);
+  }
+
+  /**
+   * Returns the value with the closest timestamp to the given timestamp. Removes all older values
+   * including the returned one from the buffer.
+   *
+   * @param timestamp The timestamp value.
+   * @return The value with the closest timestamp or null if the buffer is empty.
+   * @see #pollFloor(long)
+   */
+  public synchronized @Nullable V poll(long timestamp) {
+    return poll(timestamp, /* onlyOlder= */ false);
+  }
+
+  /**
+   * Returns the value with the closest timestamp to the given timestamp. Removes all older values
+   * including the returned one from the buffer.
+   *
+   * @param timestamp The timestamp value.
+   * @param onlyOlder Whether this method can return a new value in case its timestamp value is
+   *     closest to {@code timestamp}.
+   * @return The value with the closest timestamp or null if the buffer is empty or there is no
+   *     older value and {@code onlyOlder} is true.
+   */
+  private @Nullable V poll(long timestamp, boolean onlyOlder) {
+    V value = null;
+    long previousTimeDiff = Long.MAX_VALUE;
+    while (size > 0) {
+      long timeDiff = timestamp - timestamps[first];
+      if (timeDiff < 0 && (onlyOlder || -timeDiff >= previousTimeDiff)) {
+        break;
+      }
+      previousTimeDiff = timeDiff;
+      value = values[first];
+      values[first] = null;
+      first = (first + 1) % values.length;
+      size--;
+    }
+    return value;
+  }
+
+  private void clearBufferOnTimeDiscontinuity(long timestamp) {
+    if (size > 0) {
+      int last = (first + size - 1) % values.length;
+      if (timestamp <= timestamps[last]) {
+        clear();
+      }
+    }
+  }
+
+  private void doubleCapacityIfFull() {
+    int capacity = values.length;
+    if (size < capacity) {
+      return;
+    }
+    int newCapacity = capacity * 2;
+    long[] newTimestamps = new long[newCapacity];
+    V[] newValues = newArray(newCapacity);
+    // Reset the loop starting index to 0 while coping to the new buffer.
+    // First copy the values from 'first' index to the end of original array.
+    int length = capacity - first;
+    System.arraycopy(timestamps, first, newTimestamps, 0, length);
+    System.arraycopy(values, first, newValues, 0, length);
+    // Then the values from index 0 to 'first' index.
+    if (first > 0) {
+      System.arraycopy(timestamps, 0, newTimestamps, length, first);
+      System.arraycopy(values, 0, newValues, length, first);
+    }
+    timestamps = newTimestamps;
+    values = newValues;
+    first = 0;
+  }
+
+  private void addUnchecked(long timestamp, V value) {
+    int next = (first + size) % values.length;
+    timestamps[next] = timestamp;
+    values[next] = value;
+    size++;
+  }
+
+  @SuppressWarnings("unchecked")
+  private static  V[] newArray(int length) {
+    return (V[]) new Object[length];
+  }
+}
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 58a4f64816..2f30612081 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
@@ -16,6 +16,7 @@
 package com.google.android.exoplayer2.util;
 
 import android.Manifest.permission;
+import android.annotation.SuppressLint;
 import android.annotation.TargetApi;
 import android.app.Activity;
 import android.content.ComponentName;
@@ -34,7 +35,6 @@ import android.os.Handler;
 import android.os.Looper;
 import android.os.Parcel;
 import android.security.NetworkSecurityPolicy;
-import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 import android.telephony.TelephonyManager;
 import android.text.TextUtils;
@@ -67,7 +67,6 @@ import java.util.TimeZone;
 import java.util.UUID;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
-import java.util.concurrent.ThreadFactory;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import java.util.zip.DataFormatException;
@@ -86,9 +85,7 @@ public final class Util {
    * Like {@link android.os.Build.VERSION#SDK_INT}, but in a place where it can be conveniently
    * overridden for local testing.
    */
-  public static final int SDK_INT =
-      (Build.VERSION.SDK_INT == 25 && Build.VERSION.CODENAME.charAt(0) == 'O') ? 26
-      : Build.VERSION.SDK_INT;
+  public static final int SDK_INT = Build.VERSION.SDK_INT;
 
   /**
    * Like {@link Build#DEVICE}, but in a place where it can be conveniently overridden for local
@@ -341,12 +338,7 @@ public final class Util {
    * @return The executor.
    */
   public static ExecutorService newSingleThreadExecutor(final String threadName) {
-    return Executors.newSingleThreadExecutor(new ThreadFactory() {
-      @Override
-      public Thread newThread(@NonNull Runnable r) {
-        return new Thread(r, threadName);
-      }
-    });
+    return Executors.newSingleThreadExecutor(runnable -> new Thread(runnable, threadName));
   }
 
   /**
@@ -1252,6 +1244,8 @@ public final class Util {
       case C.ENCODING_PCM_32BIT:
       case C.ENCODING_PCM_FLOAT:
         return channelCount * 4;
+      case C.ENCODING_PCM_A_LAW:
+      case C.ENCODING_PCM_MU_LAW:
       case C.ENCODING_INVALID:
       case Format.NO_VALUE:
       default:
@@ -1332,6 +1326,7 @@ public final class Util {
       case C.USAGE_NOTIFICATION_EVENT:
         return C.STREAM_TYPE_NOTIFICATION;
       case C.USAGE_ASSISTANCE_ACCESSIBILITY:
+      case C.USAGE_ASSISTANT:
       case C.USAGE_UNKNOWN:
       default:
         return C.STREAM_TYPE_DEFAULT;
@@ -1448,6 +1443,8 @@ public final class Util {
         return C.DEFAULT_TEXT_BUFFER_SIZE;
       case C.TRACK_TYPE_METADATA:
         return C.DEFAULT_METADATA_BUFFER_SIZE;
+      case C.TRACK_TYPE_CAMERA_MOTION:
+        return C.DEFAULT_CAMERA_MOTION_BUFFER_SIZE;
       default:
         throw new IllegalStateException();
     }
@@ -1557,7 +1554,7 @@ public final class Util {
    * and is not declared to be thrown.
    */
   public static void sneakyThrow(Throwable t) {
-    Util.sneakyThrowInternal(t);
+    Util.sneakyThrowInternal(t);
   }
 
   @SuppressWarnings("unchecked")
@@ -1753,6 +1750,7 @@ public final class Util {
         // Attempt to read sys.display-size.
         String sysDisplaySize = null;
         try {
+          @SuppressLint("PrivateApi")
           Class systemProperties = Class.forName("android.os.SystemProperties");
           Method getMethod = systemProperties.getMethod("get", String.class);
           sysDisplaySize = (String) getMethod.invoke(systemProperties, "sys.display-size");
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 181232b7b2..d1cda57ce0 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
@@ -136,6 +136,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
   private long lastInputTimeUs;
   private long outputStreamOffsetUs;
   private int pendingOutputStreamOffsetCount;
+  private @Nullable VideoFrameMetadataListener frameMetadataListener;
 
   /**
    * @param context A context.
@@ -386,6 +387,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
       if (codec != null) {
         codec.setVideoScalingMode(scalingMode);
       }
+    } else if (messageType == C.MSG_SET_VIDEO_FRAME_METADATA_LISTENER) {
+      frameMetadataListener = (VideoFrameMetadataListener) message;
     } else {
       super.handleMessage(messageType, message);
     }
@@ -444,6 +447,12 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
     return surface != null || shouldUseDummySurface(codecInfo);
   }
 
+  @Override
+  protected boolean getCodecNeedsEosPropagation() {
+    // In tunneling mode we can't dequeue an end-of-stream buffer, so propagate it in the renderer.
+    return tunneling;
+  }
+
   @Override
   protected void configureCodec(
       MediaCodecInfo codecInfo,
@@ -587,9 +596,17 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
   }
 
   @Override
-  protected boolean processOutputBuffer(long positionUs, long elapsedRealtimeUs, MediaCodec codec,
-      ByteBuffer buffer, int bufferIndex, int bufferFlags, long bufferPresentationTimeUs,
-      boolean shouldSkip) throws ExoPlaybackException {
+  protected boolean processOutputBuffer(
+      long positionUs,
+      long elapsedRealtimeUs,
+      MediaCodec codec,
+      ByteBuffer buffer,
+      int bufferIndex,
+      int bufferFlags,
+      long bufferPresentationTimeUs,
+      boolean shouldSkip,
+      Format format)
+      throws ExoPlaybackException {
     if (initialPositionUs == C.TIME_UNSET) {
       initialPositionUs = positionUs;
     }
@@ -616,8 +633,10 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
     if (!renderedFirstFrame
         || (isStarted
             && shouldForceRenderOutputBuffer(earlyUs, elapsedRealtimeNowUs - lastRenderTimeUs))) {
+      long releaseTimeNs = System.nanoTime();
+      notifyFrameMetadataListener(presentationTimeUs, releaseTimeNs, format);
       if (Util.SDK_INT >= 21) {
-        renderOutputBufferV21(codec, bufferIndex, presentationTimeUs, System.nanoTime());
+        renderOutputBufferV21(codec, bufferIndex, presentationTimeUs, releaseTimeNs);
       } else {
         renderOutputBuffer(codec, bufferIndex, presentationTimeUs);
       }
@@ -653,6 +672,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
     if (Util.SDK_INT >= 21) {
       // Let the underlying framework time the release.
       if (earlyUs < 50000) {
+        notifyFrameMetadataListener(presentationTimeUs, adjustedReleaseTimeNs, format);
         renderOutputBufferV21(codec, bufferIndex, presentationTimeUs, adjustedReleaseTimeNs);
         return true;
       }
@@ -670,6 +690,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
             return false;
           }
         }
+        notifyFrameMetadataListener(presentationTimeUs, adjustedReleaseTimeNs, format);
         renderOutputBuffer(codec, bufferIndex, presentationTimeUs);
         return true;
       }
@@ -679,10 +700,18 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
     return false;
   }
 
+  private void notifyFrameMetadataListener(
+      long presentationTimeUs, long releaseTimeNs, Format format) {
+    if (frameMetadataListener != null) {
+      frameMetadataListener.onVideoFrameAboutToBeRendered(
+          presentationTimeUs, releaseTimeNs, format);
+    }
+  }
+
   /**
    * Returns the offset that should be subtracted from {@code bufferPresentationTimeUs} in {@link
-   * #processOutputBuffer(long, long, MediaCodec, ByteBuffer, int, int, long, boolean)} to get the
-   * playback position with respect to the media.
+   * #processOutputBuffer(long, long, MediaCodec, ByteBuffer, int, int, long, boolean, Format)} to
+   * get the playback position with respect to the media.
    */
   protected long getOutputStreamOffsetUs() {
     return outputStreamOffsetUs;
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/VideoFrameMetadataListener.java b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoFrameMetadataListener.java
new file mode 100644
index 0000000000..b467d0f421
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoFrameMetadataListener.java
@@ -0,0 +1,31 @@
+/*
+ * 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.video;
+
+import com.google.android.exoplayer2.Format;
+
+/** A listener for metadata corresponding to video frame being rendered. */
+public interface VideoFrameMetadataListener {
+  /**
+   * Called when the video frame about to be rendered. This method is called on the playback thread.
+   *
+   * @param presentationTimeUs The presentation time of the output buffer, in microseconds.
+   * @param releaseTimeNs The wallclock time at which the frame should be displayed, in nanoseconds.
+   *     If the platform API version of the device is less than 21, then this is the best effort.
+   * @param format The format associated with the frame.
+   */
+  void onVideoFrameAboutToBeRendered(long presentationTimeUs, long releaseTimeNs, Format format);
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/VideoRendererEventListener.java b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoRendererEventListener.java
index d6ea0ebae2..f96aae77b9 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/video/VideoRendererEventListener.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoRendererEventListener.java
@@ -129,12 +129,7 @@ public interface VideoRendererEventListener {
      */
     public void enabled(final DecoderCounters decoderCounters) {
       if (listener != null) {
-        handler.post(new Runnable() {
-          @Override
-          public void run() {
-            listener.onVideoEnabled(decoderCounters);
-          }
-        });
+        handler.post(() -> listener.onVideoEnabled(decoderCounters));
       }
     }
 
@@ -144,13 +139,10 @@ public interface VideoRendererEventListener {
     public void decoderInitialized(final String decoderName,
         final long initializedTimestampMs, final long initializationDurationMs) {
       if (listener != null) {
-        handler.post(new Runnable() {
-          @Override
-          public void run() {
-            listener.onVideoDecoderInitialized(decoderName, initializedTimestampMs,
-                initializationDurationMs);
-          }
-        });
+        handler.post(
+            () ->
+                listener.onVideoDecoderInitialized(
+                    decoderName, initializedTimestampMs, initializationDurationMs));
       }
     }
 
@@ -159,12 +151,7 @@ public interface VideoRendererEventListener {
      */
     public void inputFormatChanged(final Format format) {
       if (listener != null) {
-        handler.post(new Runnable() {
-          @Override
-          public void run() {
-            listener.onVideoInputFormatChanged(format);
-          }
-        });
+        handler.post(() -> listener.onVideoInputFormatChanged(format));
       }
     }
 
@@ -173,12 +160,7 @@ public interface VideoRendererEventListener {
      */
     public void droppedFrames(final int droppedFrameCount, final long elapsedMs) {
       if (listener != null) {
-        handler.post(new Runnable()  {
-          @Override
-          public void run() {
-            listener.onDroppedFrames(droppedFrameCount, elapsedMs);
-          }
-        });
+        handler.post(() -> listener.onDroppedFrames(droppedFrameCount, elapsedMs));
       }
     }
 
@@ -188,13 +170,10 @@ public interface VideoRendererEventListener {
     public void videoSizeChanged(final int width, final int height,
         final int unappliedRotationDegrees, final float pixelWidthHeightRatio) {
       if (listener != null) {
-        handler.post(new Runnable()  {
-          @Override
-          public void run() {
-            listener.onVideoSizeChanged(width, height, unappliedRotationDegrees,
-                pixelWidthHeightRatio);
-          }
-        });
+        handler.post(
+            () ->
+                listener.onVideoSizeChanged(
+                    width, height, unappliedRotationDegrees, pixelWidthHeightRatio));
       }
     }
 
@@ -203,12 +182,7 @@ public interface VideoRendererEventListener {
      */
     public void renderedFirstFrame(final Surface surface) {
       if (listener != null) {
-        handler.post(new Runnable()  {
-          @Override
-          public void run() {
-            listener.onRenderedFirstFrame(surface);
-          }
-        });
+        handler.post(() -> listener.onRenderedFirstFrame(surface));
       }
     }
 
@@ -217,13 +191,11 @@ public interface VideoRendererEventListener {
      */
     public void disabled(final DecoderCounters counters) {
       if (listener != null) {
-        handler.post(new Runnable() {
-          @Override
-          public void run() {
-            counters.ensureUpdated();
-            listener.onVideoDisabled(counters);
-          }
-        });
+        handler.post(
+            () -> {
+              counters.ensureUpdated();
+              listener.onVideoDisabled(counters);
+            });
       }
     }
 
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/spherical/CameraMotionListener.java b/library/core/src/main/java/com/google/android/exoplayer2/video/spherical/CameraMotionListener.java
new file mode 100644
index 0000000000..33fc639412
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/video/spherical/CameraMotionListener.java
@@ -0,0 +1,32 @@
+/*
+ * 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.video.spherical;
+
+/** Listens camera motion. */
+public interface CameraMotionListener {
+
+  /**
+   * Called when a new camera motion is read. This method is called on the playback thread.
+   *
+   * @param timeUs The presentation time of the data.
+   * @param rotation Angle axis orientation in radians representing the rotation from camera
+   *     coordinate system to world coordinate system.
+   */
+  void onCameraMotion(long timeUs, float[] rotation);
+
+  /** Called when the camera motion track position is reset. */
+  void onCameraMotionReset();
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/spherical/CameraMotionRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/spherical/CameraMotionRenderer.java
new file mode 100644
index 0000000000..5fab84ed8d
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/video/spherical/CameraMotionRenderer.java
@@ -0,0 +1,129 @@
+/*
+ * 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.video.spherical;
+
+import android.support.annotation.Nullable;
+import com.google.android.exoplayer2.BaseRenderer;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ExoPlaybackException;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.FormatHolder;
+import com.google.android.exoplayer2.Renderer;
+import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
+import com.google.android.exoplayer2.util.MimeTypes;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import com.google.android.exoplayer2.util.Util;
+import java.nio.ByteBuffer;
+
+/** A {@link Renderer} that parses the camera motion track. */
+public class CameraMotionRenderer extends BaseRenderer {
+
+  // The amount of time to read samples ahead of the current time.
+  private static final int SAMPLE_WINDOW_DURATION_US = 100000;
+
+  private final FormatHolder formatHolder;
+  private final DecoderInputBuffer buffer;
+  private final ParsableByteArray scratch;
+
+  private long offsetUs;
+  private @Nullable CameraMotionListener listener;
+  private long lastTimestampUs;
+
+  public CameraMotionRenderer() {
+    super(C.TRACK_TYPE_CAMERA_MOTION);
+    formatHolder = new FormatHolder();
+    buffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL);
+    scratch = new ParsableByteArray();
+  }
+
+  @Override
+  public int supportsFormat(Format format) {
+    return MimeTypes.APPLICATION_CAMERA_MOTION.equals(format.sampleMimeType)
+        ? FORMAT_HANDLED
+        : FORMAT_UNSUPPORTED_TYPE;
+  }
+
+  @Override
+  public void handleMessage(int messageType, Object message) throws ExoPlaybackException {
+    if (messageType == C.MSG_SET_CAMERA_MOTION_LISTENER) {
+      listener = (CameraMotionListener) message;
+    } else {
+      super.handleMessage(messageType, message);
+    }
+  }
+
+  @Override
+  protected void onStreamChanged(Format[] formats, long offsetUs) throws ExoPlaybackException {
+    this.offsetUs = offsetUs;
+  }
+
+  @Override
+  protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException {
+    lastTimestampUs = 0;
+    if (listener != null) {
+      listener.onCameraMotionReset();
+    }
+  }
+
+  @Override
+  protected void onDisabled() {
+    lastTimestampUs = 0;
+  }
+
+  @Override
+  public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException {
+    // Keep reading available samples as long as the sample time is not too far into the future.
+    while (!hasReadStreamToEnd() && lastTimestampUs < positionUs + SAMPLE_WINDOW_DURATION_US) {
+      buffer.clear();
+      int result = readSource(formatHolder, buffer, /* formatRequired= */ false);
+      if (result != C.RESULT_BUFFER_READ || buffer.isEndOfStream()) {
+        return;
+      }
+
+      buffer.flip();
+      lastTimestampUs = buffer.timeUs;
+      if (listener != null) {
+        float[] rotation = parseMetadata(buffer.data);
+        if (rotation != null) {
+          Util.castNonNull(listener).onCameraMotion(lastTimestampUs - offsetUs, rotation);
+        }
+      }
+    }
+  }
+
+  @Override
+  public boolean isEnded() {
+    return hasReadStreamToEnd();
+  }
+
+  @Override
+  public boolean isReady() {
+    return true;
+  }
+
+  private @Nullable float[] parseMetadata(ByteBuffer data) {
+    if (data.remaining() != 16) {
+      return null;
+    }
+    scratch.reset(data.array(), data.limit());
+    scratch.setPosition(data.arrayOffset() + 4); // skip reserved bytes too.
+    float[] result = new float[3];
+    for (int i = 0; i < 3; i++) {
+      result[i] = Float.intBitsToFloat(scratch.readLittleEndianInt());
+    }
+    return result;
+  }
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/spherical/FrameRotationQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/video/spherical/FrameRotationQueue.java
new file mode 100644
index 0000000000..d7404cbce4
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/video/spherical/FrameRotationQueue.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.video.spherical;
+
+import android.opengl.Matrix;
+import com.google.android.exoplayer2.util.TimedValueQueue;
+
+/**
+ * This class serves multiple purposes:
+ *
+ * 
    + *
  • Queues the rotation metadata extracted from camera motion track. + *
  • Converts the metadata to rotation matrices in OpenGl coordinate system. + *
  • Recenters the rotations to componsate the yaw of the initial rotation. + *
+ */ +public final class FrameRotationQueue { + private final float[] recenterMatrix; + private final float[] rotationMatrix; + private final TimedValueQueue rotations; + private boolean recenterMatrixComputed; + + public FrameRotationQueue() { + recenterMatrix = new float[16]; + rotationMatrix = new float[16]; + rotations = new TimedValueQueue<>(); + } + + /** + * Sets a rotation for a given timestamp. + * + * @param timestampUs Timestamp of the rotation. + * @param angleAxis Angle axis orientation in radians representing the rotation from camera + * coordinate system to world coordinate system. + */ + public void setRotation(long timestampUs, float[] angleAxis) { + rotations.add(timestampUs, angleAxis); + } + + /** Removes all of the rotations and forces rotations to be recentered. */ + public void reset() { + rotations.clear(); + recenterMatrixComputed = false; + } + + /** + * Copies the rotation matrix with the greatest timestamp which is less than or equal to the given + * timestamp to {@code matrix}. Removes all older rotations and the returned one from the queue. + * Does nothing if there is no such rotation. + * + * @param matrix A float array to hold the rotation matrix. + * @param timestampUs The time in microseconds to query the rotation. + * @return Whether a rotation matrix is copied to {@code matrix}. + */ + public boolean pollRotationMatrix(float[] matrix, long timestampUs) { + float[] rotation = rotations.pollFloor(timestampUs); + if (rotation == null) { + return false; + } + // TODO [Internal: b/113315546]: Slerp between the floor and ceil rotation. + getRotationMatrixFromAngleAxis(rotationMatrix, rotation); + if (!recenterMatrixComputed) { + computeRecenterMatrix(recenterMatrix, rotationMatrix); + recenterMatrixComputed = true; + } + Matrix.multiplyMM(matrix, 0, recenterMatrix, 0, rotationMatrix, 0); + return true; + } + + /** + * Computes a recentering matrix from the given angle-axis rotation only accounting for yaw. Roll + * and tilt will not be compensated. + */ + private static void computeRecenterMatrix(float[] recenterMatrix, float[] rotationMatrix) { + // The re-centering matrix is computed as follows: + // recenter.row(2) = temp.col(2).transpose(); + // recenter.row(0) = recenter.row(1).cross(recenter.row(2)).normalized(); + // recenter.row(2) = recenter.row(0).cross(recenter.row(1)).normalized(); + // | temp[10] 0 -temp[8] 0| + // | 0 1 0 0| + // recenter = | temp[8] 0 temp[10] 0| + // | 0 0 0 1| + Matrix.setIdentityM(recenterMatrix, 0); + float normRowSqr = + rotationMatrix[10] * rotationMatrix[10] + rotationMatrix[8] * rotationMatrix[8]; + float normRow = (float) Math.sqrt(normRowSqr); + recenterMatrix[0] = rotationMatrix[10] / normRow; + recenterMatrix[2] = rotationMatrix[8] / normRow; + recenterMatrix[8] = -rotationMatrix[8] / normRow; + recenterMatrix[10] = rotationMatrix[10] / normRow; + } + + private static void getRotationMatrixFromAngleAxis(float[] matrix, float[] angleAxis) { + // Convert coordinates to OpenGL coordinates. + // CAMM motion metadata: +x right, +y down, and +z forward. + // OpenGL: +x right, +y up, -z forwards + float x = angleAxis[0]; + float y = -angleAxis[1]; + float z = -angleAxis[2]; + float angleRad = Matrix.length(x, y, z); + if (angleRad != 0) { + float angleDeg = (float) Math.toDegrees(angleRad); + Matrix.setRotateM(matrix, 0, angleDeg, x / angleRad, y / angleRad, z / angleRad); + } else { + Matrix.setIdentityM(matrix, 0); + } + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/spherical/Projection.java b/library/core/src/main/java/com/google/android/exoplayer2/video/spherical/Projection.java new file mode 100644 index 0000000000..3a585ccd64 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/spherical/Projection.java @@ -0,0 +1,234 @@ +/* + * 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.video.spherical; + +import android.support.annotation.IntDef; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.C.StereoMode; +import com.google.android.exoplayer2.util.Assertions; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** The projection mesh used with 360/VR videos. */ +public final class Projection { + + /** Enforces allowed (sub) mesh draw modes. */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({DRAW_MODE_TRIANGLES, DRAW_MODE_TRIANGLES_STRIP, DRAW_MODE_TRIANGLES_FAN}) + public @interface DrawMode {} + /** Triangle draw mode. */ + public static final int DRAW_MODE_TRIANGLES = 0; + /** Triangle strip draw mode. */ + public static final int DRAW_MODE_TRIANGLES_STRIP = 1; + /** Triangle fan draw mode. */ + public static final int DRAW_MODE_TRIANGLES_FAN = 2; + + /** Number of position coordinates per vertex. */ + public static final int TEXTURE_COORDS_PER_VERTEX = 2; + /** Number of texture coordinates per vertex. */ + public static final int POSITION_COORDS_PER_VERTEX = 3; + + /** + * Generates a complete sphere equirectangular projection. + * + * @param stereoMode A {@link C.StereoMode} value. + */ + public static Projection createEquirectangular(@C.StereoMode int stereoMode) { + return createEquirectangular( + /* radius= */ 50, // Should be large enough that there are no stereo artifacts. + /* latitudes= */ 36, // Should be large enough to prevent videos looking wavy. + /* longitudes= */ 72, // Should be large enough to prevent videos looking wavy. + /* verticalFovDegrees= */ 180, + /* horizontalFovDegrees= */ 360, + stereoMode); + } + + /** + * Generates an equirectangular projection. + * + * @param radius Size of the sphere. Must be > 0. + * @param latitudes Number of rows that make up the sphere. Must be >= 1. + * @param longitudes Number of columns that make up the sphere. Must be >= 1. + * @param verticalFovDegrees Total latitudinal degrees that are covered by the sphere. Must be in + * (0, 180]. + * @param horizontalFovDegrees Total longitudinal degrees that are covered by the sphere.Must be + * in (0, 360]. + * @param stereoMode A {@link C.StereoMode} value. + * @return an equirectangular projection. + */ + public static Projection createEquirectangular( + float radius, + int latitudes, + int longitudes, + float verticalFovDegrees, + float horizontalFovDegrees, + @C.StereoMode int stereoMode) { + Assertions.checkArgument(radius > 0); + Assertions.checkArgument(latitudes >= 1); + Assertions.checkArgument(longitudes >= 1); + Assertions.checkArgument(verticalFovDegrees > 0 && verticalFovDegrees <= 180); + Assertions.checkArgument(horizontalFovDegrees > 0 && horizontalFovDegrees <= 360); + + // Compute angular size in radians of each UV quad. + float verticalFovRads = (float) Math.toRadians(verticalFovDegrees); + float horizontalFovRads = (float) Math.toRadians(horizontalFovDegrees); + float quadHeightRads = verticalFovRads / latitudes; + float quadWidthRads = horizontalFovRads / longitudes; + + // Each latitude strip has 2 * (longitudes quads + extra edge) vertices + 2 degenerate vertices. + int vertexCount = (2 * (longitudes + 1) + 2) * latitudes; + // Buffer to return. + float[] vertexData = new float[vertexCount * POSITION_COORDS_PER_VERTEX]; + float[] textureData = new float[vertexCount * TEXTURE_COORDS_PER_VERTEX]; + + // Generate the data for the sphere which is a set of triangle strips representing each + // latitude band. + int vOffset = 0; // Offset into the vertexData array. + int tOffset = 0; // Offset into the textureData array. + // (i, j) represents a quad in the equirectangular sphere. + for (int j = 0; j < latitudes; ++j) { // For each horizontal triangle strip. + // Each latitude band lies between the two phi values. Each vertical edge on a band lies on + // a theta value. + float phiLow = quadHeightRads * j - verticalFovRads / 2; + float phiHigh = quadHeightRads * (j + 1) - verticalFovRads / 2; + + for (int i = 0; i < longitudes + 1; ++i) { // For each vertical edge in the band. + for (int k = 0; k < 2; ++k) { // For low and high points on an edge. + // For each point, determine it's position in polar coordinates. + float phi = k == 0 ? phiLow : phiHigh; + float theta = quadWidthRads * i + (float) Math.PI - horizontalFovRads / 2; + + // Set vertex position data as Cartesian coordinates. + vertexData[vOffset++] = -(float) (radius * Math.sin(theta) * Math.cos(phi)); + vertexData[vOffset++] = (float) (radius * Math.sin(phi)); + vertexData[vOffset++] = (float) (radius * Math.cos(theta) * Math.cos(phi)); + + textureData[tOffset++] = i * quadWidthRads / horizontalFovRads; + textureData[tOffset++] = (j + k) * quadHeightRads / verticalFovRads; + + // Break up the triangle strip with degenerate vertices by copying first and last points. + if ((i == 0 && k == 0) || (i == longitudes && k == 1)) { + System.arraycopy( + vertexData, + vOffset - POSITION_COORDS_PER_VERTEX, + vertexData, + vOffset, + POSITION_COORDS_PER_VERTEX); + vOffset += POSITION_COORDS_PER_VERTEX; + System.arraycopy( + textureData, + tOffset - TEXTURE_COORDS_PER_VERTEX, + textureData, + tOffset, + TEXTURE_COORDS_PER_VERTEX); + tOffset += TEXTURE_COORDS_PER_VERTEX; + } + } + // Move on to the next vertical edge in the triangle strip. + } + // Move on to the next triangle strip. + } + SubMesh subMesh = + new SubMesh(SubMesh.VIDEO_TEXTURE_ID, vertexData, textureData, DRAW_MODE_TRIANGLES_STRIP); + return new Projection(new Mesh(subMesh), stereoMode); + } + + /** The Mesh corresponding to the left eye. */ + public final Mesh leftMesh; + /** + * The Mesh corresponding to the right eye. If {@code singleMesh} is true then this mesh is + * identical to {@link #leftMesh}. + */ + public final Mesh rightMesh; + /** The stereo mode. */ + public final @StereoMode int stereoMode; + /** Whether the left and right mesh are identical. */ + public final boolean singleMesh; + + /** + * Creates a Projection with single mesh. + * + * @param mesh the Mesh for both eyes. + * @param stereoMode A {@link StereoMode} value. + */ + public Projection(Mesh mesh, int stereoMode) { + this(mesh, mesh, stereoMode); + } + + /** + * Creates a Projection with dual mesh. Use {@link #Projection(Mesh, int)} if there is single mesh + * for both eyes. + * + * @param leftMesh the Mesh corresponding to the left eye. + * @param rightMesh the Mesh corresponding to the right eye. + * @param stereoMode A {@link C.StereoMode} value. + */ + public Projection(Mesh leftMesh, Mesh rightMesh, int stereoMode) { + this.leftMesh = leftMesh; + this.rightMesh = rightMesh; + this.stereoMode = stereoMode; + this.singleMesh = leftMesh == rightMesh; + } + + /** The sub mesh associated with the {@link Mesh}. */ + public static final class SubMesh { + /** Texture ID for video frames. */ + public static final int VIDEO_TEXTURE_ID = 0; + + /** Texture ID. */ + public final int textureId; + /** The drawing mode. One of {@link DrawMode}. */ + public final @DrawMode int mode; + /** The SubMesh vertices. */ + public final float[] vertices; + /** The SubMesh texture coordinates. */ + public final float[] textureCoords; + + public SubMesh(int textureId, float[] vertices, float[] textureCoords, @DrawMode int mode) { + this.textureId = textureId; + Assertions.checkArgument( + vertices.length * (long) TEXTURE_COORDS_PER_VERTEX + == textureCoords.length * (long) POSITION_COORDS_PER_VERTEX); + this.vertices = vertices; + this.textureCoords = textureCoords; + this.mode = mode; + } + + /** Returns the SubMesh vertex count. */ + public int getVertexCount() { + return vertices.length / POSITION_COORDS_PER_VERTEX; + } + } + + /** A Mesh associated with the projection scene. */ + public static final class Mesh { + private final SubMesh[] subMeshes; + + public Mesh(SubMesh... subMeshes) { + this.subMeshes = subMeshes; + } + + /** Returns the number of sub meshes. */ + public int getSubMeshCount() { + return subMeshes.length; + } + + /** Returns the SubMesh for the given index. */ + public SubMesh getSubMesh(int index) { + return subMeshes[index]; + } + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/spherical/ProjectionDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/video/spherical/ProjectionDecoder.java new file mode 100644 index 0000000000..7a3c4998b3 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/spherical/ProjectionDecoder.java @@ -0,0 +1,238 @@ +/* + * 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.video.spherical; + +import android.support.annotation.Nullable; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.util.ParsableBitArray; +import com.google.android.exoplayer2.util.ParsableByteArray; +import com.google.android.exoplayer2.util.Util; +import com.google.android.exoplayer2.video.spherical.Projection.Mesh; +import com.google.android.exoplayer2.video.spherical.Projection.SubMesh; +import java.util.ArrayList; +import java.util.zip.Inflater; + +/** + * A decoder for the projection mesh. + * + *

The mesh boxes parsed are described at + * Spherical Video V2 RFC. + * + *

The decoder does not perform CRC checks at the moment. + */ +public final class ProjectionDecoder { + + private static final int TYPE_YTMP = Util.getIntegerCodeForString("ytmp"); + private static final int TYPE_MSHP = Util.getIntegerCodeForString("mshp"); + private static final int TYPE_RAW = Util.getIntegerCodeForString("raw "); + private static final int TYPE_DFL8 = Util.getIntegerCodeForString("dfl8"); + private static final int TYPE_MESH = Util.getIntegerCodeForString("mesh"); + private static final int TYPE_PROJ = Util.getIntegerCodeForString("proj"); + + // Sanity limits to prevent a bad file from creating an OOM situation. We don't expect a mesh to + // exceed these limits. + private static final int MAX_COORDINATE_COUNT = 10000; + private static final int MAX_VERTEX_COUNT = 32 * 1000; + private static final int MAX_TRIANGLE_INDICES = 128 * 1000; + + private ProjectionDecoder() {} + + /* + * Decodes the projection data. + * + * @param projectionData The projection data. + * @param stereoMode A {@link C.StereoMode} value. + * @return The projection or null if the data can't be decoded. + */ + public static @Nullable Projection decode(byte[] projectionData, @C.StereoMode int stereoMode) { + ParsableByteArray input = new ParsableByteArray(projectionData); + // MP4 containers include the proj box but webm containers do not. + // Both containers use mshp. + ArrayList meshes = null; + try { + meshes = isProj(input) ? parseProj(input) : parseMshp(input); + } catch (ArrayIndexOutOfBoundsException ignored) { + // Do nothing. + } + if (meshes == null) { + return null; + } else { + switch (meshes.size()) { + case 1: + return new Projection(meshes.get(0), stereoMode); + case 2: + return new Projection(meshes.get(0), meshes.get(1), stereoMode); + case 0: + default: + return null; + } + } + } + + /** Returns true if the input contains a proj box. Indicates MP4 container. */ + private static boolean isProj(ParsableByteArray input) { + input.skipBytes(4); // size + int type = input.readInt(); + input.setPosition(0); + return type == TYPE_PROJ; + } + + private static @Nullable ArrayList parseProj(ParsableByteArray input) { + input.skipBytes(8); // size and type. + int position = input.getPosition(); + int limit = input.limit(); + while (position < limit) { + int childEnd = position + input.readInt(); + if (childEnd <= position || childEnd > limit) { + return null; + } + int childAtomType = input.readInt(); + // Some early files named the atom ytmp rather than mshp. + if (childAtomType == TYPE_YTMP || childAtomType == TYPE_MSHP) { + input.setLimit(childEnd); + return parseMshp(input); + } + position = childEnd; + input.setPosition(position); + } + return null; + } + + private static @Nullable ArrayList parseMshp(ParsableByteArray input) { + int version = input.readUnsignedByte(); + if (version != 0) { + return null; + } + input.skipBytes(7); // flags + crc. + int encoding = input.readInt(); + if (encoding == TYPE_DFL8) { + ParsableByteArray output = new ParsableByteArray(); + Inflater inflater = new Inflater(true); + try { + if (!Util.inflate(input, output, inflater)) { + return null; + } + } finally { + inflater.end(); + } + input = output; + } else if (encoding != TYPE_RAW) { + return null; + } + return parseRawMshpData(input); + } + + /** Parses MSHP data after the encoding_four_cc field. */ + private static @Nullable ArrayList parseRawMshpData(ParsableByteArray input) { + ArrayList meshes = new ArrayList<>(); + int position = input.getPosition(); + int limit = input.limit(); + while (position < limit) { + int childEnd = position + input.readInt(); + if (childEnd <= position || childEnd > limit) { + return null; + } + int childAtomType = input.readInt(); + if (childAtomType == TYPE_MESH) { + Mesh mesh = parseMesh(input); + if (mesh == null) { + return null; + } + meshes.add(mesh); + } + position = childEnd; + input.setPosition(position); + } + return meshes; + } + + private static @Nullable Mesh parseMesh(ParsableByteArray input) { + // Read the coordinates. + int coordinateCount = input.readInt(); + if (coordinateCount > MAX_COORDINATE_COUNT) { + return null; + } + float[] coordinates = new float[coordinateCount]; + for (int coordinate = 0; coordinate < coordinateCount; coordinate++) { + coordinates[coordinate] = input.readFloat(); + } + // Read the vertices. + int vertexCount = input.readInt(); + if (vertexCount > MAX_VERTEX_COUNT) { + return null; + } + + final double log2 = Math.log(2.0); + int coordinateCountSizeBits = (int) Math.ceil(Math.log(2.0 * coordinateCount) / log2); + + ParsableBitArray bitInput = new ParsableBitArray(input.data); + bitInput.setPosition(input.getPosition() * 8); + float[] vertices = new float[vertexCount * 5]; + int[] coordinateIndices = new int[5]; + int vertexIndex = 0; + for (int vertex = 0; vertex < vertexCount; vertex++) { + for (int i = 0; i < 5; i++) { + int coordinateIndex = + coordinateIndices[i] + decodeZigZag(bitInput.readBits(coordinateCountSizeBits)); + if (coordinateIndex >= coordinateCount || coordinateIndex < 0) { + return null; + } + vertices[vertexIndex++] = coordinates[coordinateIndex]; + coordinateIndices[i] = coordinateIndex; + } + } + + // Pad to next byte boundary + bitInput.setPosition(((bitInput.getPosition() + 7) & ~7)); + + int subMeshCount = bitInput.readBits(32); + SubMesh[] subMeshes = new SubMesh[subMeshCount]; + for (int i = 0; i < subMeshCount; i++) { + int textureId = bitInput.readBits(8); + int drawMode = bitInput.readBits(8); + int triangleIndexCount = bitInput.readBits(32); + if (triangleIndexCount > MAX_TRIANGLE_INDICES) { + return null; + } + int vertexCountSizeBits = (int) Math.ceil(Math.log(2.0 * vertexCount) / log2); + int index = 0; + float[] triangleVertices = new float[triangleIndexCount * 3]; + float[] textureCoords = new float[triangleIndexCount * 2]; + for (int counter = 0; counter < triangleIndexCount; counter++) { + index += decodeZigZag(bitInput.readBits(vertexCountSizeBits)); + if (index < 0 || index >= vertexCount) { + return null; + } + triangleVertices[counter * 3] = vertices[index * 5]; + triangleVertices[counter * 3 + 1] = vertices[index * 5 + 1]; + triangleVertices[counter * 3 + 2] = vertices[index * 5 + 2]; + textureCoords[counter * 2] = vertices[index * 5 + 3]; + textureCoords[counter * 2 + 1] = vertices[index * 5 + 4]; + } + subMeshes[i] = new SubMesh(textureId, triangleVertices, textureCoords, drawMode); + } + return new Mesh(subMeshes); + } + + /** + * Decodes Zigzag encoding as described in + * https://developers.google.com/protocol-buffers/docs/encoding#signed-integers + */ + private static int decodeZigZag(int n) { + return (n >> 1) ^ -(n & 1); + } +} 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 1e676f2123..8846e31917 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 @@ -29,7 +29,6 @@ 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.ShuffleOrder; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.ads.AdPlaybackState; @@ -187,10 +186,28 @@ public final class ExoPlayerTest { @Test public void testReadAheadToEndDoesNotResetRenderer() throws Exception { // Use sufficiently short periods to ensure the player attempts to read all at once. - TimelineWindowDefinition windowDefinition = + TimelineWindowDefinition windowDefinition0 = new TimelineWindowDefinition( - /* isSeekable= */ false, /* isDynamic= */ false, /* durationUs= */ 100_000); - Timeline timeline = new FakeTimeline(windowDefinition, windowDefinition, windowDefinition); + /* periodCount= */ 1, + /* id= */ 0, + /* isSeekable= */ false, + /* isDynamic= */ false, + /* durationUs= */ 100_000); + TimelineWindowDefinition windowDefinition1 = + new TimelineWindowDefinition( + /* periodCount= */ 1, + /* id= */ 1, + /* isSeekable= */ false, + /* isDynamic= */ false, + /* durationUs= */ 100_000); + TimelineWindowDefinition windowDefinition2 = + new TimelineWindowDefinition( + /* periodCount= */ 1, + /* id= */ 2, + /* isSeekable= */ false, + /* isDynamic= */ false, + /* durationUs= */ 100_000); + Timeline timeline = new FakeTimeline(windowDefinition0, windowDefinition1, windowDefinition2); final FakeRenderer videoRenderer = new FakeRenderer(Builder.VIDEO_FORMAT); FakeMediaClockRenderer audioRenderer = new FakeMediaClockRenderer(Builder.AUDIO_FORMAT) { @@ -277,24 +294,15 @@ public final class ExoPlayerTest { .waitForTimelineChanged(timeline) .prepareSource(secondSource) .executeRunnable( - new Runnable() { - @Override - public void run() { - try { - queuedSourceInfoCountDownLatch.await(); - } catch (InterruptedException e) { - // Ignore. - } + () -> { + try { + queuedSourceInfoCountDownLatch.await(); + } catch (InterruptedException e) { + // Ignore. } }) .prepareSource(thirdSource) - .executeRunnable( - new Runnable() { - @Override - public void run() { - completePreparationCountDownLatch.countDown(); - } - }) + .executeRunnable(completePreparationCountDownLatch::countDown) .build(); ExoPlayerTestRunner testRunner = new Builder() @@ -436,13 +444,7 @@ public final class ExoPlayerTest { new ActionSchedule.Builder("testAdGroupWithLoadErrorIsSkipped") .pause() .waitForPlaybackState(Player.STATE_READY) - .executeRunnable( - new Runnable() { - @Override - public void run() { - fakeMediaSource.setNewSourceInfo(adErrorTimeline, null); - } - }) + .executeRunnable(() -> fakeMediaSource.setNewSourceInfo(adErrorTimeline, null)) .waitForTimelineChanged(adErrorTimeline) .play() .build(); @@ -823,13 +825,7 @@ public final class ExoPlayerTest { new ActionSchedule.Builder("testDynamicTimelineChangeReason") .pause() .waitForTimelineChanged(timeline1) - .executeRunnable( - new Runnable() { - @Override - public void run() { - mediaSource.setNewSourceInfo(timeline2, null); - } - }) + .executeRunnable(() -> mediaSource.setNewSourceInfo(timeline2, null)) .waitForTimelineChanged(timeline2) .play() .build(); @@ -911,26 +907,17 @@ public final class ExoPlayerTest { .waitForPlaybackState(Player.STATE_BUFFERING) // Block until createPeriod has been called on the fake media source. .executeRunnable( - new Runnable() { - @Override - public void run() { - try { - createPeriodCalledCountDownLatch.await(); - } catch (InterruptedException e) { - throw new IllegalStateException(e); - } + () -> { + try { + createPeriodCalledCountDownLatch.await(); + } catch (InterruptedException e) { + throw new IllegalStateException(e); } }) // Set playback parameters (while the fake media period is not yet prepared). .setPlaybackParameters(new PlaybackParameters(/* speed= */ 2f, /* pitch= */ 2f)) // Complete preparation of the fake media period. - .executeRunnable( - new Runnable() { - @Override - public void run() { - fakeMediaPeriodHolder[0].setPreparationComplete(); - } - }) + .executeRunnable(() -> fakeMediaPeriodHolder[0].setPreparationComplete()) .build(); new ExoPlayerTestRunner.Builder() .setMediaSource(mediaSource) @@ -1280,13 +1267,7 @@ public final class ExoPlayerTest { // is still being prepared. The error will be thrown while the player handles the new // source info. .seek(/* windowIndex= */ 100, /* positionMs= */ 0) - .executeRunnable( - new Runnable() { - @Override - public void run() { - mediaSource.setNewSourceInfo(timeline, /* newManifest= */ null); - } - }) + .executeRunnable(() -> mediaSource.setNewSourceInfo(timeline, /* newManifest= */ null)) .waitForPlaybackState(Player.STATE_IDLE) .build(); ExoPlayerTestRunner testRunner = @@ -1308,12 +1289,12 @@ public final class ExoPlayerTest { @Test public void testPlaybackErrorDuringSourceInfoRefreshWithShuffleModeEnabledUsesCorrectFirstPeriod() throws Exception { - Timeline timeline = new FakeTimeline(/* windowCount= */ 1); - FakeMediaSource mediaSource = new FakeMediaSource(/* timeline= */ null, /* manifest= */ null); + FakeMediaSource mediaSource = + new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1), /* manifest= */ null); ConcatenatingMediaSource concatenatingMediaSource = new ConcatenatingMediaSource( /* isAtomic= */ false, new FakeShuffleOrder(0), mediaSource, mediaSource); - AtomicInteger windowIndexAfterReprepare = new AtomicInteger(); + AtomicInteger windowIndexAfterError = new AtomicInteger(); ActionSchedule actionSchedule = new ActionSchedule.Builder("testPlaybackErrorDuringSourceInfoRefreshUsesCorrectFirstPeriod") .setShuffleModeEnabled(true) @@ -1322,17 +1303,12 @@ public final class ExoPlayerTest { // is still being prepared. The error will be thrown while the player handles the new // source info. .seek(/* windowIndex= */ 100, /* positionMs= */ 0) - .executeRunnable(() -> mediaSource.setNewSourceInfo(timeline, /* newManifest= */ null)) .waitForPlaybackState(Player.STATE_IDLE) - // Re-prepare to play the source in its default shuffled order. - .prepareSource( - concatenatingMediaSource, /* resetPosition= */ false, /* resetState= */ false) - .waitForTimelineChanged(null) .executeRunnable( new PlayerRunnable() { @Override public void run(SimpleExoPlayer player) { - windowIndexAfterReprepare.set(player.getCurrentWindowIndex()); + windowIndexAfterError.set(player.getCurrentWindowIndex()); } }) .build(); @@ -1348,7 +1324,7 @@ public final class ExoPlayerTest { // Expected exception. assertThat(e.getUnexpectedException()).isInstanceOf(IllegalSeekPositionException.class); } - assertThat(windowIndexAfterReprepare.get()).isEqualTo(1); + assertThat(windowIndexAfterError.get()).isEqualTo(1); } @Test @@ -1789,13 +1765,7 @@ public final class ExoPlayerTest { .pause() .waitForTimelineChanged(timeline) .sendMessage(target, /* positionMs= */ 50) - .executeRunnable( - new Runnable() { - @Override - public void run() { - mediaSource.setNewSourceInfo(secondTimeline, null); - } - }) + .executeRunnable(() -> mediaSource.setNewSourceInfo(secondTimeline, null)) .waitForTimelineChanged(secondTimeline) .play() .build(); @@ -1868,13 +1838,7 @@ public final class ExoPlayerTest { .pause() .waitForTimelineChanged(timeline) .sendMessage(target, /* windowIndex = */ 1, /* positionMs= */ 50) - .executeRunnable( - new Runnable() { - @Override - public void run() { - mediaSource.setNewSourceInfo(secondTimeline, null); - } - }) + .executeRunnable(() -> mediaSource.setNewSourceInfo(secondTimeline, null)) .waitForTimelineChanged(secondTimeline) .seek(/* windowIndex= */ 0, /* positionMs= */ 0) .play() @@ -1943,13 +1907,7 @@ public final class ExoPlayerTest { }) // Play a bit to ensure message arrived in internal player. .playUntilPosition(/* windowIndex= */ 0, /* positionMs= */ 30) - .executeRunnable( - new Runnable() { - @Override - public void run() { - message.get().cancel(); - } - }) + .executeRunnable(() -> message.get().cancel()) .play() .build(); new Builder() @@ -1987,13 +1945,7 @@ public final class ExoPlayerTest { .playUntilPosition(/* windowIndex= */ 0, /* positionMs= */ 51) // Seek back, cancel the message, and play past the same position again. .seek(/* positionMs= */ 0) - .executeRunnable( - new Runnable() { - @Override - public void run() { - message.get().cancel(); - } - }) + .executeRunnable(() -> message.get().cancel()) .play() .build(); new Builder() @@ -2064,13 +2016,7 @@ public final class ExoPlayerTest { .playUntilPosition( /* windowIndex= */ 0, /* positionMs= */ C.usToMs(TimelineWindowDefinition.DEFAULT_WINDOW_DURATION_US)) - .executeRunnable( - new Runnable() { - @Override - public void run() { - mediaSource.setNewSourceInfo(timeline2, /* newManifest= */ null); - } - }) + .executeRunnable(() -> mediaSource.setNewSourceInfo(timeline2, /* newManifest= */ null)) .waitForTimelineChanged(timeline2) .play() .build(); @@ -2085,9 +2031,12 @@ public final class ExoPlayerTest { // Assert that the second period was re-created from the new timeline. assertThat(mediaSource.getCreatedMediaPeriods()) .containsExactly( - new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 0), - new MediaPeriodId(/* periodIndex= */ 1, /* windowSequenceNumber= */ 1), - new MediaPeriodId(/* periodIndex= */ 1, /* windowSequenceNumber= */ 2)) + new MediaPeriodId( + timeline1.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 0), + new MediaPeriodId( + timeline1.getUidOfPeriod(/* periodIndex= */ 1), /* windowSequenceNumber= */ 1), + new MediaPeriodId( + timeline2.getUidOfPeriod(/* periodIndex= */ 1), /* windowSequenceNumber= */ 2)) .inOrder(); } @@ -2123,10 +2072,14 @@ public final class ExoPlayerTest { testRunner.assertPlayedPeriodIndices(0, 1, 0, 1); assertThat(mediaSource.getCreatedMediaPeriods()) .containsAllOf( - new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 0), - new MediaPeriodId(/* periodIndex= */ 1, /* windowSequenceNumber= */ 0)); + new MediaPeriodId( + timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 0), + new MediaPeriodId( + timeline.getUidOfPeriod(/* periodIndex= */ 1), /* windowSequenceNumber= */ 0)); assertThat(mediaSource.getCreatedMediaPeriods()) - .doesNotContain(new MediaPeriodId(/* periodIndex= */ 1, /* windowSequenceNumber= */ 1)); + .doesNotContain( + new MediaPeriodId( + timeline.getUidOfPeriod(/* periodIndex= */ 1), /* windowSequenceNumber= */ 1)); } @Test @@ -2294,21 +2247,20 @@ public final class ExoPlayerTest { @Test public void testUpdateTrackSelectorThenSeekToUnpreparedPeriod_returnsEmptyTrackGroups() throws Exception { - Timeline fakeTimeline = new FakeTimeline(/* windowCount= */ 1); + // Use unset duration to prevent pre-loading of the second window. + Timeline fakeTimeline = + new FakeTimeline( + new TimelineWindowDefinition( + /* isSeekable= */ true, /* isDynamic= */ false, /* durationUs= */ C.TIME_UNSET)); MediaSource[] fakeMediaSources = { new FakeMediaSource(fakeTimeline, null, Builder.VIDEO_FORMAT), new FakeMediaSource(fakeTimeline, null, Builder.AUDIO_FORMAT) }; - MediaSource mediaSource = - new ConcatenatingMediaSource( - /* isAtomic= */ false, - /* useLazyPreparation= */ true, - new ShuffleOrder.DefaultShuffleOrder(0), - fakeMediaSources); + MediaSource mediaSource = new ConcatenatingMediaSource(fakeMediaSources); FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT); DefaultTrackSelector trackSelector = new DefaultTrackSelector(); ActionSchedule actionSchedule = - new ActionSchedule.Builder("testSendMessages") + new ActionSchedule.Builder("testUpdateTrackSelectorThenSeekToUnpreparedPeriod") .pause() .waitForPlaybackState(Player.STATE_READY) .seek(/* windowIndex= */ 1, /* positionMs= */ 0) diff --git a/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java index 3216087169..9ec22c0d51 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java @@ -17,9 +17,6 @@ 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; @@ -35,10 +32,7 @@ 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; @@ -53,7 +47,6 @@ 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; @@ -115,28 +108,16 @@ public final class AnalyticsCollectorTest { 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)); + + private EventWindowAndPeriodId period0; + private EventWindowAndPeriodId period1; + private EventWindowAndPeriodId period0Seq0; + private EventWindowAndPeriodId period1Seq1; + private EventWindowAndPeriodId period0Seq1; + private EventWindowAndPeriodId period1Seq0; + private EventWindowAndPeriodId period1Seq2; + private EventWindowAndPeriodId window0Period1Seq0; + private EventWindowAndPeriodId window1Period0Seq1; @Test public void testEmptyTimeline() throws Exception { @@ -162,34 +143,35 @@ public final class AnalyticsCollectorTest { Builder.AUDIO_FORMAT); TestAnalyticsListener listener = runAnalyticsTest(mediaSource); + populateEventIds(SINGLE_PERIOD_TIMELINE); assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED)) .containsExactly( WINDOW_0 /* setPlayWhenReady */, WINDOW_0 /* BUFFERING */, - PERIOD_0 /* READY */, - PERIOD_0 /* ENDED */); + period0 /* READY */, + period0 /* 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); + .containsExactly(period0 /* started */, period0 /* stopped */); + assertThat(listener.getEvents(EVENT_TRACKS_CHANGED)).containsExactly(period0); assertThat(listener.getEvents(EVENT_LOAD_STARTED)) - .containsExactly(WINDOW_0 /* manifest */, PERIOD_0 /* media */); + .containsExactly(WINDOW_0 /* manifest */, period0 /* media */); assertThat(listener.getEvents(EVENT_LOAD_COMPLETED)) - .containsExactly(WINDOW_0 /* manifest */, PERIOD_0 /* media */); + .containsExactly(WINDOW_0 /* manifest */, period0 /* 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); + .containsExactly(period0 /* audio */, period0 /* video */); + assertThat(listener.getEvents(EVENT_MEDIA_PERIOD_CREATED)).containsExactly(period0); + assertThat(listener.getEvents(EVENT_READING_STARTED)).containsExactly(period0); assertThat(listener.getEvents(EVENT_DECODER_ENABLED)) - .containsExactly(PERIOD_0 /* audio */, PERIOD_0 /* video */); + .containsExactly(period0 /* audio */, period0 /* video */); assertThat(listener.getEvents(EVENT_DECODER_INIT)) - .containsExactly(PERIOD_0 /* audio */, PERIOD_0 /* video */); + .containsExactly(period0 /* audio */, period0 /* 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); + .containsExactly(period0 /* audio */, period0 /* video */); + assertThat(listener.getEvents(EVENT_AUDIO_SESSION_ID)).containsExactly(period0); + assertThat(listener.getEvents(EVENT_DROPPED_VIDEO_FRAMES)).containsExactly(period0); + assertThat(listener.getEvents(EVENT_VIDEO_SIZE_CHANGED)).containsExactly(period0); + assertThat(listener.getEvents(EVENT_RENDERED_FIRST_FRAME)).containsExactly(period0); listener.assertNoMoreEvents(); } @@ -209,47 +191,48 @@ public final class AnalyticsCollectorTest { Builder.AUDIO_FORMAT)); TestAnalyticsListener listener = runAnalyticsTest(mediaSource); + populateEventIds(listener.lastReportedTimeline); assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED)) .containsExactly( WINDOW_0 /* setPlayWhenReady */, WINDOW_0 /* BUFFERING */, - PERIOD_0 /* READY */, - PERIOD_1 /* ENDED */); + period0 /* READY */, + period1 /* ENDED */); assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)).containsExactly(WINDOW_0); - assertThat(listener.getEvents(EVENT_POSITION_DISCONTINUITY)).containsExactly(PERIOD_1); + assertThat(listener.getEvents(EVENT_POSITION_DISCONTINUITY)).containsExactly(period1); 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); + .containsExactly(period0, period0, period0, period0); + assertThat(listener.getEvents(EVENT_TRACKS_CHANGED)).containsExactly(period0, period1); assertThat(listener.getEvents(EVENT_LOAD_STARTED)) .containsExactly( WINDOW_0 /* manifest */, - PERIOD_0 /* media */, + period0 /* media */, WINDOW_1 /* manifest */, - PERIOD_1 /* media */); + period1 /* media */); assertThat(listener.getEvents(EVENT_LOAD_COMPLETED)) .containsExactly( WINDOW_0 /* manifest */, - PERIOD_0 /* media */, + period0 /* media */, WINDOW_1 /* manifest */, - PERIOD_1 /* media */); + period1 /* 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); + period0 /* audio */, period0 /* video */, period1 /* audio */, period1 /* video */); + assertThat(listener.getEvents(EVENT_MEDIA_PERIOD_CREATED)).containsExactly(period0, period1); + assertThat(listener.getEvents(EVENT_MEDIA_PERIOD_RELEASED)).containsExactly(period0); + assertThat(listener.getEvents(EVENT_READING_STARTED)).containsExactly(period0, period1); assertThat(listener.getEvents(EVENT_DECODER_ENABLED)) - .containsExactly(PERIOD_0 /* audio */, PERIOD_0 /* video */); + .containsExactly(period0 /* audio */, period0 /* video */); assertThat(listener.getEvents(EVENT_DECODER_INIT)) .containsExactly( - PERIOD_0 /* audio */, PERIOD_0 /* video */, PERIOD_1 /* audio */, PERIOD_1 /* video */); + period0 /* audio */, period0 /* video */, period1 /* audio */, period1 /* 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); + period0 /* audio */, period0 /* video */, period1 /* audio */, period1 /* video */); + assertThat(listener.getEvents(EVENT_AUDIO_SESSION_ID)).containsExactly(period0); + assertThat(listener.getEvents(EVENT_DROPPED_VIDEO_FRAMES)).containsExactly(period1); + assertThat(listener.getEvents(EVENT_VIDEO_SIZE_CHANGED)).containsExactly(period0); + assertThat(listener.getEvents(EVENT_RENDERED_FIRST_FRAME)).containsExactly(period0); listener.assertNoMoreEvents(); } @@ -262,47 +245,48 @@ public final class AnalyticsCollectorTest { SINGLE_PERIOD_TIMELINE, /* manifest= */ null, Builder.AUDIO_FORMAT)); TestAnalyticsListener listener = runAnalyticsTest(mediaSource); + populateEventIds(listener.lastReportedTimeline); 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 */); + period0 /* READY */, + period1 /* BUFFERING */, + period1 /* READY */, + period1 /* ENDED */); assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)).containsExactly(WINDOW_0); - assertThat(listener.getEvents(EVENT_POSITION_DISCONTINUITY)).containsExactly(PERIOD_1); + assertThat(listener.getEvents(EVENT_POSITION_DISCONTINUITY)).containsExactly(period1); 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); + .containsExactly(period0, period0, period0, period0); + assertThat(listener.getEvents(EVENT_TRACKS_CHANGED)).containsExactly(period0, period1); assertThat(listener.getEvents(EVENT_LOAD_STARTED)) .containsExactly( WINDOW_0 /* manifest */, - PERIOD_0 /* media */, + period0 /* media */, WINDOW_1 /* manifest */, - PERIOD_1 /* media */); + period1 /* media */); assertThat(listener.getEvents(EVENT_LOAD_COMPLETED)) .containsExactly( WINDOW_0 /* manifest */, - PERIOD_0 /* media */, + period0 /* media */, WINDOW_1 /* manifest */, - PERIOD_1 /* media */); + period1 /* 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); + .containsExactly(period0 /* video */, period1 /* audio */); + assertThat(listener.getEvents(EVENT_MEDIA_PERIOD_CREATED)).containsExactly(period0, period1); + assertThat(listener.getEvents(EVENT_MEDIA_PERIOD_RELEASED)).containsExactly(period0); + assertThat(listener.getEvents(EVENT_READING_STARTED)).containsExactly(period0, period1); assertThat(listener.getEvents(EVENT_DECODER_ENABLED)) - .containsExactly(PERIOD_0 /* video */, PERIOD_1 /* audio */); + .containsExactly(period0 /* video */, period1 /* audio */); assertThat(listener.getEvents(EVENT_DECODER_INIT)) - .containsExactly(PERIOD_0 /* video */, PERIOD_1 /* audio */); + .containsExactly(period0 /* video */, period1 /* 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); + .containsExactly(period0 /* video */, period1 /* audio */); + assertThat(listener.getEvents(EVENT_DECODER_DISABLED)).containsExactly(period0); + assertThat(listener.getEvents(EVENT_AUDIO_SESSION_ID)).containsExactly(period1); + assertThat(listener.getEvents(EVENT_DROPPED_VIDEO_FRAMES)).containsExactly(period0); + assertThat(listener.getEvents(EVENT_VIDEO_SIZE_CHANGED)).containsExactly(period0); + assertThat(listener.getEvents(EVENT_RENDERED_FIRST_FRAME)).containsExactly(period0); listener.assertNoMoreEvents(); } @@ -322,51 +306,52 @@ public final class AnalyticsCollectorTest { .build(); TestAnalyticsListener listener = runAnalyticsTest(mediaSource, actionSchedule); + populateEventIds(listener.lastReportedTimeline); 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 */); + period0 /* READY */, + period1 /* BUFFERING */, + period1 /* READY */, + period1 /* setPlayWhenReady=true */, + period1 /* 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); + assertThat(listener.getEvents(EVENT_POSITION_DISCONTINUITY)).containsExactly(period1); + assertThat(listener.getEvents(EVENT_SEEK_STARTED)).containsExactly(period0); + assertThat(listener.getEvents(EVENT_SEEK_PROCESSED)).containsExactly(period1); 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(loadingEvents).containsAllOf(period0, period0); + assertThat(listener.getEvents(EVENT_TRACKS_CHANGED)).containsExactly(period0, period1); assertThat(listener.getEvents(EVENT_LOAD_STARTED)) .containsExactly( WINDOW_0 /* manifest */, - PERIOD_0 /* media */, + period0 /* media */, WINDOW_1 /* manifest */, - PERIOD_1 /* media */); + period1 /* media */); assertThat(listener.getEvents(EVENT_LOAD_COMPLETED)) .containsExactly( WINDOW_0 /* manifest */, - PERIOD_0 /* media */, + period0 /* media */, WINDOW_1 /* manifest */, - PERIOD_1 /* media */); + period1 /* 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); + .containsExactly(period0 /* video */, period1 /* audio */); + assertThat(listener.getEvents(EVENT_MEDIA_PERIOD_CREATED)).containsExactly(period0, period1); + assertThat(listener.getEvents(EVENT_MEDIA_PERIOD_RELEASED)).containsExactly(period0); + assertThat(listener.getEvents(EVENT_READING_STARTED)).containsExactly(period0, period1); assertThat(listener.getEvents(EVENT_DECODER_ENABLED)) - .containsExactly(PERIOD_0 /* video */, PERIOD_1 /* audio */); + .containsExactly(period0 /* video */, period1 /* audio */); assertThat(listener.getEvents(EVENT_DECODER_INIT)) - .containsExactly(PERIOD_0 /* video */, PERIOD_1 /* audio */); + .containsExactly(period0 /* video */, period1 /* 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); + .containsExactly(period0 /* video */, period1 /* audio */); + assertThat(listener.getEvents(EVENT_DECODER_DISABLED)).containsExactly(period0); + assertThat(listener.getEvents(EVENT_AUDIO_SESSION_ID)).containsExactly(period1); + assertThat(listener.getEvents(EVENT_VIDEO_SIZE_CHANGED)).containsExactly(period0); + assertThat(listener.getEvents(EVENT_RENDERED_FIRST_FRAME)).containsExactly(period0); listener.assertNoMoreEvents(); } @@ -393,64 +378,64 @@ public final class AnalyticsCollectorTest { .build(); TestAnalyticsListener listener = runAnalyticsTest(mediaSource, actionSchedule); + populateEventIds(listener.lastReportedTimeline); 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 */); + period0 /* READY */, + period0 /* setPlayWhenReady=true */, + period0 /* setPlayWhenReady=false */, + period0 /* BUFFERING */, + period0 /* READY */, + period0 /* setPlayWhenReady=true */, + period1Seq2 /* BUFFERING */, + period1Seq2 /* READY */, + period1Seq2 /* 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); + .containsExactly(period0, period1Seq2); + assertThat(listener.getEvents(EVENT_SEEK_STARTED)).containsExactly(period0); + assertThat(listener.getEvents(EVENT_SEEK_PROCESSED)).containsExactly(period0); 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); + .containsExactly(period0, period0, period0, period0, period0, period0); + assertThat(listener.getEvents(EVENT_TRACKS_CHANGED)).containsExactly(period0, period1Seq2); assertThat(listener.getEvents(EVENT_LOAD_STARTED)) .containsExactly( WINDOW_0 /* manifest */, - PERIOD_0 /* media */, + period0 /* media */, WINDOW_1 /* manifest */, - PERIOD_1_SEQ_1 /* media */, - PERIOD_1_SEQ_2 /* media */); + period1Seq1 /* media */, + period1Seq2 /* media */); assertThat(listener.getEvents(EVENT_LOAD_COMPLETED)) .containsExactly( WINDOW_0 /* manifest */, - PERIOD_0 /* media */, + period0 /* media */, WINDOW_1 /* manifest */, - PERIOD_1_SEQ_1 /* media */, - PERIOD_1_SEQ_2 /* media */); + period1Seq1 /* media */, + period1Seq2 /* media */); assertThat(listener.getEvents(EVENT_DOWNSTREAM_FORMAT_CHANGED)) - .containsExactly(PERIOD_0, PERIOD_1_SEQ_1, PERIOD_1_SEQ_2, PERIOD_1_SEQ_2); + .containsExactly(period0, period1Seq1, period1Seq2, period1Seq2); assertThat(listener.getEvents(EVENT_MEDIA_PERIOD_CREATED)) - .containsExactly(PERIOD_0, PERIOD_1_SEQ_1, PERIOD_1_SEQ_2); + .containsExactly(period0, period1Seq1, period1Seq2); assertThat(listener.getEvents(EVENT_MEDIA_PERIOD_RELEASED)) - .containsExactly(PERIOD_0, PERIOD_1_SEQ_1); + .containsExactly(period0, period1Seq1); assertThat(listener.getEvents(EVENT_READING_STARTED)) - .containsExactly(PERIOD_0, PERIOD_1_SEQ_1, PERIOD_1_SEQ_2); + .containsExactly(period0, period1Seq1, period1Seq2); assertThat(listener.getEvents(EVENT_DECODER_ENABLED)) - .containsExactly(PERIOD_0, PERIOD_0, PERIOD_1_SEQ_2); + .containsExactly(period0, period0, period1Seq2); assertThat(listener.getEvents(EVENT_DECODER_INIT)) - .containsExactly(PERIOD_0, PERIOD_1_SEQ_1, PERIOD_1_SEQ_2, PERIOD_1_SEQ_2); + .containsExactly(period0, period1Seq1, period1Seq2, period1Seq2); 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); + .containsExactly(period0, period1Seq1, period1Seq2, period1Seq2); + assertThat(listener.getEvents(EVENT_DECODER_DISABLED)).containsExactly(period0); + assertThat(listener.getEvents(EVENT_AUDIO_SESSION_ID)).containsExactly(period1Seq2); 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); + .containsExactly(period0, period0, period1Seq2); + assertThat(listener.getEvents(EVENT_VIDEO_SIZE_CHANGED)).containsExactly(period0, period1Seq2); assertThat(listener.getEvents(EVENT_RENDERED_FIRST_FRAME)) - .containsExactly(PERIOD_0, PERIOD_1_SEQ_2); + .containsExactly(period0, period1Seq2); listener.assertNoMoreEvents(); } @@ -469,54 +454,52 @@ public final class AnalyticsCollectorTest { .build(); TestAnalyticsListener listener = runAnalyticsTest(mediaSource1, actionSchedule); + populateEventIds(SINGLE_PERIOD_TIMELINE); assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED)) .containsExactly( WINDOW_0 /* setPlayWhenReady=true */, WINDOW_0 /* BUFFERING */, WINDOW_0 /* setPlayWhenReady=false */, - PERIOD_0_SEQ_0 /* READY */, + period0Seq0 /* READY */, WINDOW_0 /* BUFFERING */, WINDOW_0 /* setPlayWhenReady=true */, - PERIOD_0_SEQ_1 /* READY */, - PERIOD_0_SEQ_1 /* ENDED */); + period0Seq1 /* READY */, + period0Seq1 /* 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); + .containsExactly(period0Seq0, period0Seq0, period0Seq1, period0Seq1); assertThat(listener.getEvents(EVENT_TRACKS_CHANGED)) .containsExactly( - PERIOD_0_SEQ_0 /* prepared */, WINDOW_0 /* reset */, PERIOD_0_SEQ_1 /* prepared */); + period0Seq0 /* prepared */, WINDOW_0 /* reset */, period0Seq1 /* prepared */); assertThat(listener.getEvents(EVENT_LOAD_STARTED)) .containsExactly( WINDOW_0 /* manifest */, - PERIOD_0_SEQ_0 /* media */, + period0Seq0 /* media */, WINDOW_0 /* manifest */, - PERIOD_0_SEQ_1 /* media */); + period0Seq1 /* media */); assertThat(listener.getEvents(EVENT_LOAD_COMPLETED)) .containsExactly( WINDOW_0 /* manifest */, - PERIOD_0_SEQ_0 /* media */, + period0Seq0 /* media */, WINDOW_0 /* manifest */, - PERIOD_0_SEQ_1 /* media */); + period0Seq1 /* media */); assertThat(listener.getEvents(EVENT_DOWNSTREAM_FORMAT_CHANGED)) - .containsExactly(PERIOD_0_SEQ_0, PERIOD_0_SEQ_1); + .containsExactly(period0Seq0, period0Seq1); 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); + .containsExactly(period0Seq0, period0Seq1); + assertThat(listener.getEvents(EVENT_MEDIA_PERIOD_RELEASED)).containsExactly(period0Seq0); + assertThat(listener.getEvents(EVENT_READING_STARTED)).containsExactly(period0Seq0, period0Seq1); + assertThat(listener.getEvents(EVENT_DECODER_ENABLED)).containsExactly(period0Seq0, period0Seq1); + assertThat(listener.getEvents(EVENT_DECODER_INIT)).containsExactly(period0Seq0, period0Seq1); 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); + .containsExactly(period0Seq0, period0Seq1); + assertThat(listener.getEvents(EVENT_DECODER_DISABLED)).containsExactly(period0Seq0); + assertThat(listener.getEvents(EVENT_DROPPED_VIDEO_FRAMES)).containsExactly(period0Seq1); assertThat(listener.getEvents(EVENT_VIDEO_SIZE_CHANGED)) - .containsExactly(PERIOD_0_SEQ_0, PERIOD_0_SEQ_1); + .containsExactly(period0Seq0, period0Seq1); assertThat(listener.getEvents(EVENT_RENDERED_FIRST_FRAME)) - .containsExactly(PERIOD_0_SEQ_0, PERIOD_0_SEQ_1); + .containsExactly(period0Seq0, period0Seq1); listener.assertNoMoreEvents(); } @@ -534,55 +517,51 @@ public final class AnalyticsCollectorTest { .build(); TestAnalyticsListener listener = runAnalyticsTest(mediaSource, actionSchedule); + populateEventIds(SINGLE_PERIOD_TIMELINE); assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED)) .containsExactly( WINDOW_0 /* setPlayWhenReady=true */, WINDOW_0 /* BUFFERING */, - PERIOD_0_SEQ_0 /* READY */, + period0Seq0 /* 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); + period0Seq0 /* READY */, + period0Seq0 /* ENDED */); 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); + .containsExactly(period0Seq0, period0Seq0, period0Seq0, period0Seq0); 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_TRACKS_CHANGED)).containsExactly(period0Seq0, period0Seq0); assertThat(listener.getEvents(EVENT_LOAD_STARTED)) .containsExactly( WINDOW_0 /* manifest */, - PERIOD_0_SEQ_0 /* media */, + period0Seq0 /* media */, WINDOW_0 /* manifest */, - PERIOD_0_SEQ_0 /* media */); + period0Seq0 /* media */); assertThat(listener.getEvents(EVENT_LOAD_COMPLETED)) .containsExactly( WINDOW_0 /* manifest */, - PERIOD_0_SEQ_0 /* media */, + period0Seq0 /* media */, WINDOW_0 /* manifest */, - PERIOD_0_SEQ_0 /* media */); + period0Seq0 /* media */); assertThat(listener.getEvents(EVENT_DOWNSTREAM_FORMAT_CHANGED)) - .containsExactly(PERIOD_0_SEQ_0, PERIOD_0_SEQ_0); + .containsExactly(period0Seq0, period0Seq0); 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); + .containsExactly(period0Seq0, period0Seq0); + assertThat(listener.getEvents(EVENT_MEDIA_PERIOD_RELEASED)).containsExactly(period0Seq0); + assertThat(listener.getEvents(EVENT_READING_STARTED)).containsExactly(period0Seq0, period0Seq0); + assertThat(listener.getEvents(EVENT_DECODER_ENABLED)).containsExactly(period0Seq0, period0Seq0); + assertThat(listener.getEvents(EVENT_DECODER_INIT)).containsExactly(period0Seq0, period0Seq0); 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); + .containsExactly(period0Seq0, period0Seq0); + assertThat(listener.getEvents(EVENT_DECODER_DISABLED)).containsExactly(period0Seq0); assertThat(listener.getEvents(EVENT_DROPPED_VIDEO_FRAMES)) - .containsExactly(PERIOD_0_SEQ_0, PERIOD_0_SEQ_0); + .containsExactly(period0Seq0, period0Seq0); assertThat(listener.getEvents(EVENT_VIDEO_SIZE_CHANGED)) - .containsExactly(PERIOD_0_SEQ_0, PERIOD_0_SEQ_0); + .containsExactly(period0Seq0, period0Seq0); assertThat(listener.getEvents(EVENT_RENDERED_FIRST_FRAME)) - .containsExactly(PERIOD_0_SEQ_0, PERIOD_0_SEQ_0); + .containsExactly(period0Seq0, period0Seq0); listener.assertNoMoreEvents(); } @@ -601,67 +580,64 @@ public final class AnalyticsCollectorTest { // 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); - } - }) + /* currentIndex= */ 0, /* newIndex= */ 1)) .waitForTimelineChanged(/* expectedTimeline= */ null) .play() .build(); TestAnalyticsListener listener = runAnalyticsTest(concatenatedMediaSource, actionSchedule); + populateEventIds(listener.lastReportedTimeline); 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); + window0Period1Seq0 /* READY */, + window0Period1Seq0 /* setPlayWhenReady=true */, + window0Period1Seq0 /* setPlayWhenReady=false */, + period1Seq0 /* setPlayWhenReady=true */, + period1Seq0 /* BUFFERING */, + period1Seq0 /* ENDED */); + assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)).containsExactly(WINDOW_0, period1Seq0); 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); + .containsExactly( + window0Period1Seq0, window0Period1Seq0, window0Period1Seq0, window0Period1Seq0); + assertThat(listener.getEvents(EVENT_TRACKS_CHANGED)).containsExactly(window0Period1Seq0); assertThat(listener.getEvents(EVENT_LOAD_STARTED)) .containsExactly( - WINDOW_0 /* manifest */, PERIOD_0_SEQ_0 /* media */, PERIOD_1_SEQ_1 /* media */); + WINDOW_0 /* manifest */, + window0Period1Seq0 /* media */, + window1Period0Seq1 /* media */); assertThat(listener.getEvents(EVENT_LOAD_COMPLETED)) .containsExactly( - WINDOW_0 /* manifest */, PERIOD_0_SEQ_0 /* media */, PERIOD_1_SEQ_1 /* media */); + WINDOW_0 /* manifest */, + window0Period1Seq0 /* media */, + window1Period0Seq1 /* media */); assertThat(listener.getEvents(EVENT_DOWNSTREAM_FORMAT_CHANGED)) - .containsExactly(PERIOD_0_SEQ_0, PERIOD_1_SEQ_1); + .containsExactly(window0Period1Seq0, window1Period0Seq1); 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); + .containsExactly(window0Period1Seq0, window1Period0Seq1); + assertThat(listener.getEvents(EVENT_MEDIA_PERIOD_RELEASED)).containsExactly(period0Seq1); assertThat(listener.getEvents(EVENT_READING_STARTED)) - .containsExactly(PERIOD_0_SEQ_0, PERIOD_1_SEQ_1); + .containsExactly(window0Period1Seq0, window1Period0Seq1); assertThat(listener.getEvents(EVENT_DECODER_ENABLED)) - .containsExactly(PERIOD_0_SEQ_0, PERIOD_0_SEQ_0); + .containsExactly(window0Period1Seq0, window0Period1Seq0); assertThat(listener.getEvents(EVENT_DECODER_INIT)) - .containsExactly(PERIOD_0_SEQ_0, PERIOD_1_SEQ_1); + .containsExactly(window0Period1Seq0, window1Period0Seq1); 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); + .containsExactly(window0Period1Seq0, window1Period0Seq1); + assertThat(listener.getEvents(EVENT_DECODER_DISABLED)).containsExactly(window0Period1Seq0); + assertThat(listener.getEvents(EVENT_DROPPED_VIDEO_FRAMES)).containsExactly(window0Period1Seq0); + assertThat(listener.getEvents(EVENT_VIDEO_SIZE_CHANGED)).containsExactly(window0Period1Seq0); + assertThat(listener.getEvents(EVENT_RENDERED_FIRST_FRAME)).containsExactly(window0Period1Seq0); 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() @@ -678,8 +654,51 @@ public final class AnalyticsCollectorTest { .build(); TestAnalyticsListener listener = runAnalyticsTest(mediaSource, actionSchedule); - assertThat(listener.getEvents(EVENT_SEEK_STARTED)).containsExactly(PERIOD_0); - assertThat(listener.getEvents(EVENT_SEEK_PROCESSED)).containsExactly(PERIOD_0); + populateEventIds(SINGLE_PERIOD_TIMELINE); + assertThat(listener.getEvents(EVENT_SEEK_STARTED)).containsExactly(period0); + assertThat(listener.getEvents(EVENT_SEEK_PROCESSED)).containsExactly(period0); + } + + private void populateEventIds(Timeline timeline) { + period0 = + new EventWindowAndPeriodId( + /* windowIndex= */ 0, + new MediaPeriodId( + timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 0)); + period0Seq0 = period0; + period0Seq1 = + new EventWindowAndPeriodId( + /* windowIndex= */ 0, + new MediaPeriodId( + timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 1)); + window1Period0Seq1 = + new EventWindowAndPeriodId( + /* windowIndex= */ 1, + new MediaPeriodId( + timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 1)); + if (timeline.getPeriodCount() > 1) { + period1 = + new EventWindowAndPeriodId( + /* windowIndex= */ 1, + new MediaPeriodId( + timeline.getUidOfPeriod(/* periodIndex= */ 1), /* windowSequenceNumber= */ 1)); + period1Seq1 = period1; + period1Seq0 = + new EventWindowAndPeriodId( + /* windowIndex= */ 1, + new MediaPeriodId( + timeline.getUidOfPeriod(/* periodIndex= */ 1), /* windowSequenceNumber= */ 0)); + period1Seq2 = + new EventWindowAndPeriodId( + /* windowIndex= */ 1, + new MediaPeriodId( + timeline.getUidOfPeriod(/* periodIndex= */ 1), /* windowSequenceNumber= */ 2)); + window0Period1Seq0 = + new EventWindowAndPeriodId( + /* windowIndex= */ 0, + new MediaPeriodId( + timeline.getUidOfPeriod(/* periodIndex= */ 1), /* windowSequenceNumber= */ 0)); + } } private static TestAnalyticsListener runAnalyticsTest(MediaSource mediaSource) throws Exception { @@ -689,21 +708,16 @@ public final class AnalyticsCollectorTest { 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[] { + (eventHandler, + videoRendererEventListener, + audioRendererEventListener, + textRendererOutput, + metadataRendererOutput, + drmSessionManager) -> + new Renderer[] { new FakeVideoRenderer(eventHandler, videoRendererEventListener), new FakeAudioRenderer(eventHandler, audioRendererEventListener) }; - } - }; TestAnalyticsListener listener = new TestAnalyticsListener(); try { new ExoPlayerTestRunner.Builder() @@ -854,7 +868,7 @@ public final class AnalyticsCollectorTest { + "window=" + windowIndex + ", period=" - + mediaPeriodId.periodIndex + + mediaPeriodId.periodUid + ", sequence=" + mediaPeriodId.windowSequenceNumber + '}' @@ -869,10 +883,13 @@ public final class AnalyticsCollectorTest { private static final class TestAnalyticsListener implements AnalyticsListener { + public Timeline lastReportedTimeline; + private final ArrayList reportedEvents; public TestAnalyticsListener() { reportedEvents = new ArrayList<>(); + lastReportedTimeline = Timeline.EMPTY; } public List getEvents(int eventType) { @@ -900,6 +917,7 @@ public final class AnalyticsCollectorTest { @Override public void onTimelineChanged(EventTime eventTime, int reason) { + lastReportedTimeline = eventTime.timeline; reportedEvents.add(new ReportedEvent(EVENT_TIMELINE_CHANGED, eventTime)); } 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 index 04de9a76f4..bd559218c6 100644 --- 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 @@ -401,8 +401,8 @@ public final class SilenceSkippingAudioProcessorTest { 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]); + for (short channelLevel : channelLevels) { + buffer.put(channelLevel); } } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/drm/DrmInitDataTest.java b/library/core/src/test/java/com/google/android/exoplayer2/drm/DrmInitDataTest.java index 2b3bdd6a2f..2b740de113 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/drm/DrmInitDataTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/drm/DrmInitDataTest.java @@ -96,7 +96,7 @@ public class DrmInitDataTest { } @Test - @Deprecated + @SuppressWarnings("deprecation") public void testGetByUuid() { // Basic matching. DrmInitData testInitData = new DrmInitData(DATA_1, DATA_2); @@ -130,6 +130,7 @@ public class DrmInitDataTest { } @Test + @SuppressWarnings("deprecation") public void testSchemeDatasWithSameUuid() { DrmInitData testInitData = new DrmInitData(DATA_1, DATA_1B); assertThat(testInitData.schemeDataCount).isEqualTo(2); 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 index a397f70886..f43f356482 100644 --- 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 @@ -21,7 +21,6 @@ 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; @@ -95,12 +94,8 @@ public final class Id3PeekerTest { 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'; - } - }); + (majorVersion, id0, id1, id2, id3) -> + id0 == 'C' && id1 == 'O' && id2 == 'M' && id3 == 'M'); assertThat(metadata.length()).isEqualTo(1); CommentFrame commentFrame = (CommentFrame) metadata.get(0); 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 index 39c1bfe05b..c3c33e3350 100644 --- 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 @@ -250,14 +250,11 @@ public final class AmrExtractorTest { @NonNull private static ExtractorAsserts.ExtractorFactory createAmrExtractorFactory(boolean withSeeking) { - return new ExtractorAsserts.ExtractorFactory() { - @Override - public Extractor create() { - if (!withSeeking) { - return new AmrExtractor(); - } else { - return new AmrExtractor(AmrExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING); - } + return () -> { + if (!withSeeking) { + return new AmrExtractor(); + } else { + return new AmrExtractor(AmrExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING); } }; } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/flv/FlvExtractorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/flv/FlvExtractorTest.java index 5a093988dd..316148d9b9 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/flv/FlvExtractorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/flv/FlvExtractorTest.java @@ -15,9 +15,7 @@ */ package com.google.android.exoplayer2.extractor.flv; -import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.testutil.ExtractorAsserts; -import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; @@ -28,13 +26,6 @@ public final class FlvExtractorTest { @Test public void testSample() throws Exception { - ExtractorAsserts.assertBehavior( - new ExtractorFactory() { - @Override - public Extractor create() { - return new FlvExtractor(); - } - }, - "flv/sample.flv"); + ExtractorAsserts.assertBehavior(FlvExtractor::new, "flv/sample.flv"); } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractorTest.java index 4a0f87a80a..2e673037d0 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractorTest.java @@ -15,9 +15,7 @@ */ package com.google.android.exoplayer2.extractor.mkv; -import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.testutil.ExtractorAsserts; -import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; @@ -28,37 +26,17 @@ public final class MatroskaExtractorTest { @Test public void testMkvSample() throws Exception { - ExtractorAsserts.assertBehavior( - new ExtractorFactory() { - @Override - public Extractor create() { - return new MatroskaExtractor(); - } - }, - "mkv/sample.mkv"); + ExtractorAsserts.assertBehavior(MatroskaExtractor::new, "mkv/sample.mkv"); } @Test public void testWebmSubsampleEncryption() throws Exception { ExtractorAsserts.assertBehavior( - new ExtractorFactory() { - @Override - public Extractor create() { - return new MatroskaExtractor(); - } - }, - "mkv/subsample_encrypted_noaltref.webm"); + MatroskaExtractor::new, "mkv/subsample_encrypted_noaltref.webm"); } @Test public void testWebmSubsampleEncryptionWithAltrefFrames() throws Exception { - ExtractorAsserts.assertBehavior( - new ExtractorFactory() { - @Override - public Extractor create() { - return new MatroskaExtractor(); - } - }, - "mkv/subsample_encrypted_altref.webm"); + ExtractorAsserts.assertBehavior(MatroskaExtractor::new, "mkv/subsample_encrypted_altref.webm"); } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp3/Mp3ExtractorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp3/Mp3ExtractorTest.java index b977766a1c..62a4f1a193 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp3/Mp3ExtractorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp3/Mp3ExtractorTest.java @@ -15,9 +15,7 @@ */ package com.google.android.exoplayer2.extractor.mp3; -import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.testutil.ExtractorAsserts; -import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; @@ -28,25 +26,11 @@ public final class Mp3ExtractorTest { @Test public void testMp3Sample() throws Exception { - ExtractorAsserts.assertBehavior( - new ExtractorFactory() { - @Override - public Extractor create() { - return new Mp3Extractor(); - } - }, - "mp3/bear.mp3"); + ExtractorAsserts.assertBehavior(Mp3Extractor::new, "mp3/bear.mp3"); } @Test public void testTrimmedMp3Sample() throws Exception { - ExtractorAsserts.assertBehavior( - new ExtractorFactory() { - @Override - public Extractor create() { - return new Mp3Extractor(); - } - }, - "mp3/play-trimmed.mp3"); + ExtractorAsserts.assertBehavior(Mp3Extractor::new, "mp3/play-trimmed.mp3"); } } 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 8662434f81..f9362f9cda 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 @@ -16,7 +16,6 @@ package com.google.android.exoplayer2.extractor.mp4; import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.testutil.ExtractorAsserts; import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory; import com.google.android.exoplayer2.util.MimeTypes; @@ -53,11 +52,6 @@ public final class FragmentedMp4ExtractorTest { } private static ExtractorFactory getExtractorFactory(final List closedCaptionFormats) { - return new ExtractorFactory() { - @Override - public Extractor create() { - return new FragmentedMp4Extractor(0, null, null, null, closedCaptionFormats); - } - }; + return () -> new FragmentedMp4Extractor(0, null, null, null, closedCaptionFormats); } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/Mp4ExtractorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/Mp4ExtractorTest.java index f1812a69c4..8850a755be 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/Mp4ExtractorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/Mp4ExtractorTest.java @@ -16,9 +16,7 @@ package com.google.android.exoplayer2.extractor.mp4; import android.annotation.TargetApi; -import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.testutil.ExtractorAsserts; -import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; @@ -30,13 +28,6 @@ public final class Mp4ExtractorTest { @Test public void testMp4Sample() throws Exception { - ExtractorAsserts.assertBehavior( - new ExtractorFactory() { - @Override - public Extractor create() { - return new Mp4Extractor(); - } - }, - "mp4/sample.mp4"); + ExtractorAsserts.assertBehavior(Mp4Extractor::new, "mp4/sample.mp4"); } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggExtractorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggExtractorTest.java index 20808f73f2..289c168725 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggExtractorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggExtractorTest.java @@ -17,7 +17,6 @@ package com.google.android.exoplayer2.extractor.ogg; import static com.google.common.truth.Truth.assertThat; -import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.testutil.ExtractorAsserts; import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory; import com.google.android.exoplayer2.testutil.FakeExtractorInput; @@ -32,13 +31,7 @@ import org.robolectric.RobolectricTestRunner; @RunWith(RobolectricTestRunner.class) public final class OggExtractorTest { - private static final ExtractorFactory OGG_EXTRACTOR_FACTORY = - new ExtractorFactory() { - @Override - public Extractor create() { - return new OggExtractor(); - } - }; + private static final ExtractorFactory OGG_EXTRACTOR_FACTORY = OggExtractor::new; @Test public void testOpus() throws Exception { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractorTest.java index 565d609842..62ad774fd3 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractorTest.java @@ -17,9 +17,7 @@ package com.google.android.exoplayer2.extractor.rawcc; import android.annotation.TargetApi; import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.testutil.ExtractorAsserts; -import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory; import com.google.android.exoplayer2.util.MimeTypes; import org.junit.Test; import org.junit.runner.RunWith; @@ -33,10 +31,8 @@ public final class RawCcExtractorTest { @Test public void testRawCcSample() throws Exception { ExtractorAsserts.assertBehavior( - new ExtractorFactory() { - @Override - public Extractor create() { - return new RawCcExtractor( + () -> + new RawCcExtractor( Format.createTextContainerFormat( /* id= */ null, /* label= */ null, @@ -46,9 +42,7 @@ public final class RawCcExtractorTest { /* bitrate= */ Format.NO_VALUE, /* selectionFlags= */ 0, /* language= */ null, - /* accessibilityChannel= */ 1)); - } - }, + /* accessibilityChannel= */ 1)), "rawcc/sample.rawcc"); } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/Ac3ExtractorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/Ac3ExtractorTest.java index ec7afeeeab..4afd6979dc 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/Ac3ExtractorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/Ac3ExtractorTest.java @@ -15,9 +15,7 @@ */ package com.google.android.exoplayer2.extractor.ts; -import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.testutil.ExtractorAsserts; -import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; @@ -28,13 +26,6 @@ public final class Ac3ExtractorTest { @Test public void testSample() throws Exception { - ExtractorAsserts.assertBehavior( - new ExtractorFactory() { - @Override - public Extractor create() { - return new Ac3Extractor(); - } - }, - "ts/sample.ac3"); + ExtractorAsserts.assertBehavior(Ac3Extractor::new, "ts/sample.ac3"); } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorTest.java index fe2046cbe4..7f0db67133 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorTest.java @@ -15,9 +15,7 @@ */ package com.google.android.exoplayer2.extractor.ts; -import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.testutil.ExtractorAsserts; -import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; @@ -28,27 +26,16 @@ public final class AdtsExtractorTest { @Test public void testSample() throws Exception { - ExtractorAsserts.assertBehavior( - new ExtractorFactory() { - @Override - public Extractor create() { - return new AdtsExtractor(); - } - }, - "ts/sample.adts"); + ExtractorAsserts.assertBehavior(AdtsExtractor::new, "ts/sample.adts"); } @Test public void testSample_withSeeking() throws Exception { ExtractorAsserts.assertBehavior( - new ExtractorFactory() { - @Override - public Extractor create() { - return new AdtsExtractor( + () -> + new AdtsExtractor( /* firstStreamSampleTimestampUs= */ 0, - /* flags= */ AdtsExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING); - } - }, + /* flags= */ AdtsExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING), "ts/sample_cbs.adts"); } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/PsExtractorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/PsExtractorTest.java index 798f1ce5e3..0e0fd52175 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/PsExtractorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/PsExtractorTest.java @@ -15,9 +15,7 @@ */ package com.google.android.exoplayer2.extractor.ts; -import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.testutil.ExtractorAsserts; -import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; @@ -28,13 +26,6 @@ public final class PsExtractorTest { @Test public void testSample() throws Exception { - ExtractorAsserts.assertBehavior( - new ExtractorFactory() { - @Override - public Extractor create() { - return new PsExtractor(); - } - }, - "ts/sample.ps"); + ExtractorAsserts.assertBehavior(PsExtractor::new, "ts/sample.ps"); } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java index 2f3813e9e3..332fbe384a 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java @@ -27,7 +27,6 @@ import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.EsInfo; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; import com.google.android.exoplayer2.testutil.ExtractorAsserts; -import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory; import com.google.android.exoplayer2.testutil.FakeExtractorInput; import com.google.android.exoplayer2.testutil.FakeExtractorOutput; import com.google.android.exoplayer2.testutil.FakeTrackOutput; @@ -50,14 +49,7 @@ public final class TsExtractorTest { @Test public void testSample() throws Exception { - ExtractorAsserts.assertBehavior( - new ExtractorFactory() { - @Override - public Extractor create() { - return new TsExtractor(); - } - }, - "ts/sample.ts"); + ExtractorAsserts.assertBehavior(TsExtractor::new, "ts/sample.ts"); } @Test @@ -82,15 +74,7 @@ public final class TsExtractorTest { fileData = out.toByteArray(); ExtractorAsserts.assertOutput( - new ExtractorFactory() { - @Override - public Extractor create() { - return new TsExtractor(); - } - }, - "ts/sample.ts", - fileData, - RuntimeEnvironment.application); + TsExtractor::new, "ts/sample.ts", fileData, RuntimeEnvironment.application); } @Test diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/wav/WavExtractorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/wav/WavExtractorTest.java index e75525bb1e..f4df4036f6 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/wav/WavExtractorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/wav/WavExtractorTest.java @@ -15,9 +15,7 @@ */ package com.google.android.exoplayer2.extractor.wav; -import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.testutil.ExtractorAsserts; -import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; @@ -28,13 +26,6 @@ public final class WavExtractorTest { @Test public void testSample() throws Exception { - ExtractorAsserts.assertBehavior( - new ExtractorFactory() { - @Override - public Extractor create() { - return new WavExtractor(); - } - }, - "wav/sample.wav"); + ExtractorAsserts.assertBehavior(WavExtractor::new, "wav/sample.wav"); } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java index 4a1876f69c..234377895f 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java @@ -331,13 +331,7 @@ public class DownloadManagerTest { remove2Action.post().assertStarted(); download2Action.post().assertDoesNotStart(); - runOnMainThread( - new Runnable() { - @Override - public void run() { - downloadManager.stopDownloads(); - } - }); + runOnMainThread(() -> downloadManager.stopDownloads()); download1Action.assertStopped(); @@ -354,13 +348,7 @@ public class DownloadManagerTest { // New download actions can be added but they don't start. download3Action.post().assertDoesNotStart(); - runOnMainThread( - new Runnable() { - @Override - public void run() { - downloadManager.startDownloads(); - } - }); + runOnMainThread(() -> downloadManager.startDownloads()); download2Action.assertStarted().unblock().assertCompleted(); download3Action.assertStarted().unblock().assertCompleted(); @@ -380,24 +368,12 @@ public class DownloadManagerTest { // 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(); - } - }); + runOnMainThread(() -> downloadManager.stopDownloads()); // download1Action doesn't stop yet as it ignores interrupts. download2Action.assertStopped(); - runOnMainThread( - new Runnable() { - @Override - public void run() { - downloadManager.startDownloads(); - } - }); + runOnMainThread(() -> downloadManager.startDownloads()); // download2Action starts immediately. download2Action.assertStarted(); @@ -421,22 +397,19 @@ public class DownloadManagerTest { } 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(); - } + () -> { + 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); @@ -445,13 +418,7 @@ public class DownloadManagerTest { private void releaseDownloadManager() throws Exception { try { - runOnMainThread( - new Runnable() { - @Override - public void run() { - downloadManager.release(); - } - }); + runOnMainThread(() -> downloadManager.release()); } catch (Throwable throwable) { throw new Exception(throwable); } @@ -519,13 +486,7 @@ public class DownloadManagerTest { } private FakeDownloadAction post() { - runOnMainThread( - new Runnable() { - @Override - public void run() { - downloadManager.handleAction(FakeDownloadAction.this); - } - }); + runOnMainThread(() -> downloadManager.handleAction(FakeDownloadAction.this)); return this; } 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 0209ff86a2..ee8cdf4887 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 @@ -469,11 +469,11 @@ public final class ClippingMediaSourceTest { private static MediaLoadData getClippingMediaSourceMediaLoadData( long clippingStartUs, long clippingEndUs, final long eventStartUs, final long eventEndUs) throws IOException { + Timeline timeline = + new SinglePeriodTimeline( + TEST_PERIOD_DURATION_US, /* isSeekable= */ true, /* isDynamic= */ false); FakeMediaSource fakeMediaSource = - new FakeMediaSource( - new SinglePeriodTimeline( - TEST_PERIOD_DURATION_US, /* isSeekable= */ true, /* isDynamic= */ false), - /* manifest= */ null) { + new FakeMediaSource(timeline, /* manifest= */ null) { @Override protected FakeMediaPeriod createFakeMediaPeriod( MediaPeriodId id, @@ -501,9 +501,7 @@ public final class ClippingMediaSourceTest { final MediaLoadData[] reportedMediaLoadData = new MediaLoadData[1]; try { testRunner.runOnPlaybackThread( - new Runnable() { - @Override - public void run() { + () -> clippingMediaSource.addEventListener( new Handler(), new DefaultMediaSourceEventListener() { @@ -514,13 +512,12 @@ public final class ClippingMediaSourceTest { MediaLoadData mediaLoadData) { reportedMediaLoadData[0] = mediaLoadData; } - }); - } - }); + })); testRunner.prepareSource(); // Create period to send the test event configured above. testRunner.createPeriod( - new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 0)); + new MediaPeriodId( + timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 0)); assertThat(reportedMediaLoadData[0]).isNotNull(); } finally { testRunner.release(); @@ -584,7 +581,9 @@ public final class ClippingMediaSourceTest { clippedTimelines[0] = testRunner.prepareSource(); MediaPeriod mediaPeriod = testRunner.createPeriod( - new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 0)); + new MediaPeriodId( + clippedTimelines[0].getUidOfPeriod(/* periodIndex= */ 0), + /* windowSequenceNumber= */ 0)); for (int i = 0; i < additionalTimelines.length; i++) { fakeMediaSource.setNewSourceInfo(additionalTimelines[i], /* newManifest= */ null); clippedTimelines[i + 1] = testRunner.assertTimelineChangeBlocking(); 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 f9c327ed2b..507b718e8f 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 @@ -97,7 +97,7 @@ public final class ConcatenatingMediaSourceTest { // Add bulk. mediaSource.addMediaSources( - 3, Arrays.asList(childSources[4], childSources[5], childSources[6])); + 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); @@ -247,12 +247,7 @@ public final class ConcatenatingMediaSourceTest { // 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); - } - }); + () -> lazySources[1].setNewSourceInfo(createFakeTimeline(8), null)); timeline = testRunner.assertTimelineChangeBlocking(); TimelineAsserts.assertPeriodCounts(timeline, 1, 9); TimelineAsserts.assertWindowTags(timeline, 111, 999); @@ -279,25 +274,22 @@ public final class ConcatenatingMediaSourceTest { // called yet. MediaPeriod lazyPeriod = testRunner.createPeriod( - new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 0)); + new MediaPeriodId( + timeline.getUidOfPeriod(/* 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)); + new MediaPeriodId( + timeline.getUidOfPeriod(/* 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); - } - }); + () -> lazySources[3].setNewSourceInfo(createFakeTimeline(7), null)); timeline = testRunner.assertTimelineChangeBlocking(); TimelineAsserts.assertPeriodCounts(timeline, 8, 1, 2, 9); TimelineAsserts.assertWindowTags(timeline, 888, 111, 222, 999); @@ -383,7 +375,7 @@ public final class ConcatenatingMediaSourceTest { }; Timeline nonEmptyTimeline = new FakeTimeline(/* windowCount = */ 1); - mediaSource.addMediaSources(Arrays.asList(childSources)); + mediaSource.addMediaSources(Arrays.asList(childSources)); Timeline timeline = testRunner.prepareSource(); TimelineAsserts.assertEmpty(timeline); @@ -484,12 +476,7 @@ public final class ConcatenatingMediaSourceTest { testRunner.prepareSource(); final TimelineGrabber timelineGrabber = new TimelineGrabber(testRunner); dummyMainThread.runOnMainThread( - new Runnable() { - @Override - public void run() { - mediaSource.addMediaSource(createFakeMediaSource(), timelineGrabber); - } - }); + () -> mediaSource.addMediaSource(createFakeMediaSource(), timelineGrabber)); Timeline timeline = timelineGrabber.assertTimelineChangeBlocking(); assertThat(timeline.getWindowCount()).isEqualTo(1); } finally { @@ -504,15 +491,11 @@ public final class ConcatenatingMediaSourceTest { 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); - } - }); + timelineGrabber)); Timeline timeline = timelineGrabber.assertTimelineChangeBlocking(); assertThat(timeline.getWindowCount()).isEqualTo(2); } finally { @@ -527,12 +510,8 @@ public final class ConcatenatingMediaSourceTest { testRunner.prepareSource(); final TimelineGrabber timelineGrabber = new TimelineGrabber(testRunner); dummyMainThread.runOnMainThread( - new Runnable() { - @Override - public void run() { - mediaSource.addMediaSource(/* index */ 0, createFakeMediaSource(), timelineGrabber); - } - }); + () -> + mediaSource.addMediaSource(/* index */ 0, createFakeMediaSource(), timelineGrabber)); Timeline timeline = timelineGrabber.assertTimelineChangeBlocking(); assertThat(timeline.getWindowCount()).isEqualTo(1); } finally { @@ -547,16 +526,12 @@ public final class ConcatenatingMediaSourceTest { 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); - } - }); + timelineGrabber)); Timeline timeline = timelineGrabber.assertTimelineChangeBlocking(); assertThat(timeline.getWindowCount()).isEqualTo(2); } finally { @@ -569,23 +544,12 @@ public final class ConcatenatingMediaSourceTest { DummyMainThread dummyMainThread = new DummyMainThread(); try { testRunner.prepareSource(); - dummyMainThread.runOnMainThread( - new Runnable() { - @Override - public void run() { - mediaSource.addMediaSource(createFakeMediaSource()); - } - }); + dummyMainThread.runOnMainThread(() -> mediaSource.addMediaSource(createFakeMediaSource())); testRunner.assertTimelineChangeBlocking(); final TimelineGrabber timelineGrabber = new TimelineGrabber(testRunner); dummyMainThread.runOnMainThread( - new Runnable() { - @Override - public void run() { - mediaSource.removeMediaSource(/* index */ 0, timelineGrabber); - } - }); + () -> mediaSource.removeMediaSource(/* index */ 0, timelineGrabber)); Timeline timeline = timelineGrabber.assertTimelineChangeBlocking(); assertThat(timeline.getWindowCount()).isEqualTo(0); } finally { @@ -599,24 +563,15 @@ public final class ConcatenatingMediaSourceTest { try { testRunner.prepareSource(); dummyMainThread.runOnMainThread( - new Runnable() { - @Override - public void run() { + () -> mediaSource.addMediaSources( Arrays.asList( - new MediaSource[] {createFakeMediaSource(), createFakeMediaSource()})); - } - }); + 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); - } - }); + () -> mediaSource.moveMediaSource(/* fromIndex */ 1, /* toIndex */ 0, timelineGrabber)); Timeline timeline = timelineGrabber.assertTimelineChangeBlocking(); assertThat(timeline.getWindowCount()).isEqualTo(2); } finally { @@ -652,23 +607,27 @@ public final class ConcatenatingMediaSourceTest { // Create all periods and assert period creation of child media sources has been called. testRunner.assertPrepareAndReleaseAllPeriods(); + Object timelineContentOnlyPeriodUid0 = timelineContentOnly.getUidOfPeriod(/* periodIndex= */ 0); + Object timelineContentOnlyPeriodUid1 = timelineContentOnly.getUidOfPeriod(/* periodIndex= */ 1); + Object timelineWithAdsPeriodUid0 = timelineWithAds.getUidOfPeriod(/* periodIndex= */ 0); + Object timelineWithAdsPeriodUid1 = timelineWithAds.getUidOfPeriod(/* periodIndex= */ 1); mediaSourceContentOnly.assertMediaPeriodCreated( - new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 0)); + new MediaPeriodId(timelineContentOnlyPeriodUid0, /* windowSequenceNumber= */ 0)); mediaSourceContentOnly.assertMediaPeriodCreated( - new MediaPeriodId(/* periodIndex= */ 1, /* windowSequenceNumber= */ 0)); + new MediaPeriodId(timelineContentOnlyPeriodUid1, /* windowSequenceNumber= */ 0)); mediaSourceWithAds.assertMediaPeriodCreated( - new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 1)); + new MediaPeriodId(timelineWithAdsPeriodUid0, /* windowSequenceNumber= */ 1)); mediaSourceWithAds.assertMediaPeriodCreated( - new MediaPeriodId(/* periodIndex= */ 1, /* windowSequenceNumber= */ 1)); + new MediaPeriodId(timelineWithAdsPeriodUid1, /* windowSequenceNumber= */ 1)); mediaSourceWithAds.assertMediaPeriodCreated( new MediaPeriodId( - /* periodIndex= */ 0, + timelineWithAdsPeriodUid0, /* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, /* windowSequenceNumber= */ 1)); mediaSourceWithAds.assertMediaPeriodCreated( new MediaPeriodId( - /* periodIndex= */ 1, + timelineWithAdsPeriodUid1, /* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, /* windowSequenceNumber= */ 1)); @@ -683,7 +642,7 @@ public final class ConcatenatingMediaSourceTest { ConcatenatingMediaSource mediaSource = new ConcatenatingMediaSource(/* isAtomic= */ true, new FakeShuffleOrder(0)); testRunner = new MediaSourceTestRunner(mediaSource, null); - mediaSource.addMediaSources(Arrays.asList(createMediaSources(3))); + mediaSource.addMediaSources(Arrays.asList(createMediaSources(3))); Timeline timeline = testRunner.prepareSource(); TimelineAsserts.assertWindowTags(timeline, 111, 222, 333); TimelineAsserts.assertPeriodCounts(timeline, 1, 2, 3); @@ -768,10 +727,11 @@ public final class ConcatenatingMediaSourceTest { public void testRemoveChildSourceWithActiveMediaPeriod() throws IOException { FakeMediaSource childSource = createFakeMediaSource(); mediaSource.addMediaSource(childSource); - testRunner.prepareSource(); + Timeline timeline = testRunner.prepareSource(); MediaPeriod mediaPeriod = testRunner.createPeriod( - new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 0)); + new MediaPeriodId( + timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 0)); mediaSource.removeMediaSource(/* index= */ 0); testRunner.assertTimelineChangeBlocking(); testRunner.releasePeriod(mediaPeriod); @@ -781,27 +741,29 @@ public final class ConcatenatingMediaSourceTest { @Test public void testDuplicateMediaSources() throws IOException, InterruptedException { - FakeMediaSource childSource = - new FakeMediaSource(new FakeTimeline(/* windowCount= */ 2), /* manifest= */ null); + Timeline childTimeline = new FakeTimeline(/* windowCount= */ 2); + FakeMediaSource childSource = new FakeMediaSource(childTimeline, /* manifest= */ null); mediaSource.addMediaSource(childSource); mediaSource.addMediaSource(childSource); testRunner.prepareSource(); - mediaSource.addMediaSources(Arrays.asList(childSource, childSource)); + mediaSource.addMediaSources(Arrays.asList(childSource, childSource)); Timeline timeline = testRunner.assertTimelineChangeBlocking(); TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1, 1, 1, 1, 1, 1); testRunner.assertPrepareAndReleaseAllPeriods(); + Object childPeriodUid0 = childTimeline.getUidOfPeriod(/* periodIndex= */ 0); + Object childPeriodUid1 = childTimeline.getUidOfPeriod(/* periodIndex= */ 1); 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)); + new MediaPeriodId(childPeriodUid0, /* windowSequenceNumber= */ 0), + new MediaPeriodId(childPeriodUid0, /* windowSequenceNumber= */ 2), + new MediaPeriodId(childPeriodUid0, /* windowSequenceNumber= */ 4), + new MediaPeriodId(childPeriodUid0, /* windowSequenceNumber= */ 6), + new MediaPeriodId(childPeriodUid1, /* windowSequenceNumber= */ 1), + new MediaPeriodId(childPeriodUid1, /* windowSequenceNumber= */ 3), + new MediaPeriodId(childPeriodUid1, /* windowSequenceNumber= */ 5), + new MediaPeriodId(childPeriodUid1, /* windowSequenceNumber= */ 7)); // Assert that only one manifest load is reported because the source is reused. testRunner.assertCompletedManifestLoads(/* windowIndices= */ 0); assertCompletedAllMediaPeriodLoads(timeline); @@ -812,13 +774,13 @@ public final class ConcatenatingMediaSourceTest { @Test public void testDuplicateNestedMediaSources() throws IOException, InterruptedException { - FakeMediaSource childSource = - new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1), /* manifest= */ null); + Timeline childTimeline = new FakeTimeline(/* windowCount= */ 1); + FakeMediaSource childSource = new FakeMediaSource(childTimeline, /* manifest= */ null); ConcatenatingMediaSource nestedConcatenation = new ConcatenatingMediaSource(); testRunner.prepareSource(); mediaSource.addMediaSources( - Arrays.asList(childSource, nestedConcatenation, nestedConcatenation)); + Arrays.asList(childSource, nestedConcatenation, nestedConcatenation)); testRunner.assertTimelineChangeBlocking(); nestedConcatenation.addMediaSource(childSource); testRunner.assertTimelineChangeBlocking(); @@ -827,13 +789,14 @@ public final class ConcatenatingMediaSourceTest { TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1, 1, 1); testRunner.assertPrepareAndReleaseAllPeriods(); + Object childPeriodUid = childTimeline.getUidOfPeriod(/* periodIndex= */ 0); 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)); + new MediaPeriodId(childPeriodUid, /* windowSequenceNumber= */ 0), + new MediaPeriodId(childPeriodUid, /* windowSequenceNumber= */ 1), + new MediaPeriodId(childPeriodUid, /* windowSequenceNumber= */ 2), + new MediaPeriodId(childPeriodUid, /* windowSequenceNumber= */ 3), + new MediaPeriodId(childPeriodUid, /* windowSequenceNumber= */ 4)); // Assert that only one manifest load is needed because the source is reused. testRunner.assertCompletedManifestLoads(/* windowIndices= */ 0); assertCompletedAllMediaPeriodLoads(timeline); @@ -849,23 +812,14 @@ public final class ConcatenatingMediaSourceTest { final FakeMediaSource unpreparedChildSource = new FakeMediaSource(/* timeline= */ null, /* manifest= */ null); dummyMainThread.runOnMainThread( - new Runnable() { - @Override - public void run() { - mediaSource.addMediaSource(preparedChildSource); - mediaSource.addMediaSource(unpreparedChildSource); - } + () -> { + 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); - } - }); + dummyMainThread.runOnMainThread(() -> mediaSource.clear(timelineGrabber)); Timeline timeline = timelineGrabber.assertTimelineChangeBlocking(); assertThat(timeline.isEmpty()).isTrue(); @@ -900,16 +854,18 @@ public final class ConcatenatingMediaSourceTest { ConcatenatingMediaSource childSource = new ConcatenatingMediaSource(nestedChildSources); mediaSource.addMediaSource(childSource); - testRunner.prepareSource(); + Timeline timeline = testRunner.prepareSource(); MediaPeriod mediaPeriod = testRunner.createPeriod( - new MediaPeriodId(/* periodIndex= */ 1, /* windowSequenceNumber= */ 0)); + new MediaPeriodId( + timeline.getUidOfPeriod(/* periodIndex= */ 1), /* windowSequenceNumber= */ 0)); childSource.moveMediaSource(/* currentIndex= */ 0, /* newIndex= */ 1); - testRunner.assertTimelineChangeBlocking(); + timeline = testRunner.assertTimelineChangeBlocking(); testRunner.preparePeriod(mediaPeriod, /* positionUs= */ 0); testRunner.assertCompletedMediaPeriodLoads( - new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 0)); + new MediaPeriodId( + timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 0)); } @Test @@ -938,8 +894,10 @@ public final class ConcatenatingMediaSourceTest { new DefaultShuffleOrder(0), childSources); testRunner = new MediaSourceTestRunner(mediaSource, /* allocator= */ null); - testRunner.prepareSource(); - testRunner.createPeriod(new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 0)); + Timeline timeline = testRunner.prepareSource(); + testRunner.createPeriod( + new MediaPeriodId( + timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 0)); assertThat(childSources[0].isPrepared()).isTrue(); assertThat(childSources[1].isPrepared()).isFalse(); @@ -955,13 +913,16 @@ public final class ConcatenatingMediaSourceTest { new DefaultShuffleOrder(0), childSources); testRunner = new MediaSourceTestRunner(mediaSource, /* allocator= */ null); - testRunner.prepareSource(); + Timeline timeline = testRunner.prepareSource(); // The lazy preparation must only be triggered once, even if we create multiple periods from // the media source. FakeMediaSource.prepareSource asserts that it's not called twice, so // creating two periods shouldn't throw. - testRunner.createPeriod(new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 0)); - testRunner.createPeriod(new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 0)); + MediaPeriodId mediaPeriodId = + new MediaPeriodId( + timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 0); + testRunner.createPeriod(mediaPeriodId); + testRunner.createPeriod(mediaPeriodId); } private void assertCompletedAllMediaPeriodLoads(Timeline timeline) { @@ -974,11 +935,12 @@ public final class ConcatenatingMediaSourceTest { periodIndex <= window.lastPeriodIndex; periodIndex++) { timeline.getPeriod(periodIndex, period); - expectedMediaPeriodIds.add(new MediaPeriodId(periodIndex, windowIndex)); + Object periodUid = timeline.getUidOfPeriod(periodIndex); + expectedMediaPeriodIds.add(new MediaPeriodId(periodUid, 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)); + new MediaPeriodId(periodUid, adGroupIndex, adIndex, windowIndex)); } } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/MergingMediaSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/MergingMediaSourceTest.java index e74347d2f4..3318f5a42f 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/MergingMediaSourceTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/MergingMediaSourceTest.java @@ -95,15 +95,15 @@ public class MergingMediaSourceTest { for (int i = 0; i < timelines.length; i++) { mediaSources[i] = new FakeMediaSource(timelines[i], null); } - MergingMediaSource mediaSource = new MergingMediaSource(mediaSources); - MediaSourceTestRunner testRunner = new MediaSourceTestRunner(mediaSource, null); + MergingMediaSource mergingMediaSource = new MergingMediaSource(mediaSources); + MediaSourceTestRunner testRunner = new MediaSourceTestRunner(mergingMediaSource, null); try { Timeline timeline = testRunner.prepareSource(); // The merged timeline should always be the one from the first child. assertThat(timeline).isEqualTo(timelines[0]); testRunner.releaseSource(); - for (int i = 0; i < mediaSources.length; i++) { - mediaSources[i].assertReleased(); + for (FakeMediaSource mediaSource : mediaSources) { + mediaSource.assertReleased(); } } finally { testRunner.release(); 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 2587b78d99..1b57341d33 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 @@ -45,11 +45,11 @@ public final class SinglePeriodTimelineTest { public void testGetPeriodPositionDynamicWindowUnknownDuration() { SinglePeriodTimeline timeline = new SinglePeriodTimeline(C.TIME_UNSET, false, true); // Should return null with any positive position projection. - Pair position = timeline.getPeriodPosition(window, period, 0, C.TIME_UNSET, 1); + Pair position = timeline.getPeriodPosition(window, period, 0, C.TIME_UNSET, 1); assertThat(position).isNull(); // Should return (0, 0) without a position projection. position = timeline.getPeriodPosition(window, period, 0, C.TIME_UNSET, 0); - assertThat(position.first).isEqualTo(0); + assertThat(position.first).isEqualTo(timeline.getUidOfPeriod(0)); assertThat(position.second).isEqualTo(0); } @@ -66,16 +66,16 @@ public final class SinglePeriodTimelineTest { /* 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); + Pair position = + timeline.getPeriodPosition(window, period, 0, C.TIME_UNSET, windowDurationUs + 1); assertThat(position).isNull(); // Should return (0, duration) with a projection equal to window duration. position = timeline.getPeriodPosition(window, period, 0, C.TIME_UNSET, windowDurationUs); - assertThat(position.first).isEqualTo(0); + assertThat(position.first).isEqualTo(timeline.getUidOfPeriod(0)); assertThat(position.second).isEqualTo(windowDurationUs); // Should return (0, 0) without a position projection. position = timeline.getPeriodPosition(window, period, 0, C.TIME_UNSET, 0); - assertThat(position.first).isEqualTo(0); + assertThat(position.first).isEqualTo(timeline.getUidOfPeriod(0)); assertThat(position.second).isEqualTo(0); } 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 730572bbd8..348352eb30 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 @@ -63,6 +63,7 @@ public final class AdaptiveTrackSelectionTest { BandwidthMeter initialBandwidthMeter = mock(BandwidthMeter.class); BandwidthMeter injectedBandwidthMeter = mock(BandwidthMeter.class); Format format = videoFormat(/* bitrate= */ 500, /* width= */ 320, /* height= */ 240); + @SuppressWarnings("deprecation") AdaptiveTrackSelection adaptiveTrackSelection = new AdaptiveTrackSelection.Factory(initialBandwidthMeter) .createTrackSelection(new TrackGroup(format), injectedBandwidthMeter, /* tracks= */ 0); 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 13314dccf0..86d810989f 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 @@ -147,6 +147,7 @@ public final class DefaultTrackSelectorTest { /* selectUndeterminedTextLanguage= */ false, /* disabledTextTrackSelectionFlags= */ 0, /* forceLowestBitrate= */ true, + /* forceHighestSupportedBitrate= */ true, /* allowMixedMimeAdaptiveness= */ false, /* allowNonSeamlessAdaptiveness= */ true, /* maxVideoWidth= */ 1, 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 6b48cffdd5..55d05eb7d4 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 @@ -60,13 +60,7 @@ public final class CacheDataSourceTest { testDataUri = Uri.parse("test_data"); fixedCacheKey = CacheUtil.generateKey(testDataUri); expectedCacheKey = fixedCacheKey; - cacheKeyFactory = - new CacheKeyFactory() { - @Override - public String buildCacheKey(DataSpec dataSpec) { - return CACHE_KEY_PREFIX + "." + CacheUtil.generateKey(dataSpec.uri); - } - }; + cacheKeyFactory = dataSpec -> CACHE_KEY_PREFIX + "." + CacheUtil.generateKey(dataSpec.uri); tempFolder = Util.createTempDirectory(RuntimeEnvironment.application, "ExoPlayerTest"); cache = new SimpleCache(tempFolder, new NoOpCacheEvictor()); } @@ -366,13 +360,7 @@ public final class CacheDataSourceTest { // Insert an action just before the end of the data to fail the test if reading from upstream // reaches end of the data. fakeData - .appendReadAction( - new Runnable() { - @Override - public void run() { - fail("Read from upstream shouldn't reach to the end of the data."); - } - }) + .appendReadAction(() -> fail("Read from upstream shouldn't reach to the end of the data.")) .appendReadData(1); // Create cache read-only CacheDataSource. CacheDataSource cacheDataSource = @@ -408,13 +396,7 @@ public final class CacheDataSourceTest { // Insert an action just before the end of the data to fail the test if reading from upstream // reaches end of the data. fakeData - .appendReadAction( - new Runnable() { - @Override - public void run() { - fail("Read from upstream shouldn't reach to the end of the data."); - } - }) + .appendReadAction(() -> fail("Read from upstream shouldn't reach to the end of the data.")) .appendReadData(1); // Lock the content on the cache. 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 e3917b58d0..36fb78894f 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 @@ -292,22 +292,15 @@ public final class CacheUtilTest { @Test public void testCachePolling() throws Exception { final CachingCounters counters = new CachingCounters(); - FakeDataSet fakeDataSet = new FakeDataSet().newData("test_data") - .appendReadData(TestUtil.buildTestData(100)) - .appendReadAction(new Runnable() { - @Override - public void run() { - assertCounters(counters, 0, 100, 300); - } - }) - .appendReadData(TestUtil.buildTestData(100)) - .appendReadAction(new Runnable() { - @Override - public void run() { - assertCounters(counters, 0, 200, 300); - } - }) - .appendReadData(TestUtil.buildTestData(100)).endData(); + FakeDataSet fakeDataSet = + new FakeDataSet() + .newData("test_data") + .appendReadData(TestUtil.buildTestData(100)) + .appendReadAction(() -> assertCounters(counters, 0, 100, 300)) + .appendReadData(TestUtil.buildTestData(100)) + .appendReadAction(() -> assertCounters(counters, 0, 200, 300)) + .appendReadData(TestUtil.buildTestData(100)) + .endData(); FakeDataSource dataSource = new FakeDataSource(fakeDataSet); CacheUtil.cache( diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTrackerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTrackerTest.java index 50f9cd2ae8..f8da2b1085 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTrackerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTrackerTest.java @@ -61,8 +61,7 @@ public final class CachedRegionTrackerTest { @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); - when(cache.addListener(anyString(), any(Cache.Listener.class))) - .thenReturn(new TreeSet()); + when(cache.addListener(anyString(), any(Cache.Listener.class))).thenReturn(new TreeSet<>()); tracker = new CachedRegionTracker(cache, CACHE_KEY, CHUNK_INDEX); cacheDir = Util.createTempDirectory(RuntimeEnvironment.application, "ExoPlayerTest"); index = new CachedContentIndex(cacheDir); 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 15e2b80f59..a4e444386a 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 @@ -37,8 +37,6 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; @@ -259,12 +257,12 @@ public class SimpleCacheTest { addCache(simpleCache, KEY_1, 0, 15); // Make index.store() throw exception from now on. - doAnswer(new Answer() { - @Override - public Object answer(InvocationOnMock invocation) throws Throwable { - throw new Cache.CacheException("SimpleCacheTest"); - } - }).when(index).store(); + doAnswer( + invocation -> { + throw new CacheException("SimpleCacheTest"); + }) + .when(index) + .store(); // Adding more content will make LeastRecentlyUsedCacheEvictor evict previous content. try { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/util/TimedValueQueueTest.java b/library/core/src/test/java/com/google/android/exoplayer2/util/TimedValueQueueTest.java new file mode 100644 index 0000000000..ca34bc3216 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/util/TimedValueQueueTest.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.util; + +import static com.google.common.truth.Truth.assertThat; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +/** Unit test for {@link TimedValueQueue}. */ +@RunWith(RobolectricTestRunner.class) +public class TimedValueQueueTest { + + private TimedValueQueue queue; + + @Before + public void setUp() throws Exception { + queue = new TimedValueQueue<>(); + } + + @Test + public void testAddAndPollValues() { + queue.add(0, "a"); + queue.add(1, "b"); + queue.add(2, "c"); + assertThat(queue.poll(0)).isEqualTo("a"); + assertThat(queue.poll(1)).isEqualTo("b"); + assertThat(queue.poll(2)).isEqualTo("c"); + } + + @Test + public void testBufferCapacityIncreasesAutomatically() { + queue = new TimedValueQueue<>(1); + for (int i = 0; i < 20; i++) { + queue.add(i, "" + i); + if ((i & 1) == 1) { + assertThat(queue.poll(0)).isEqualTo("" + (i / 2)); + } + } + assertThat(queue.size()).isEqualTo(10); + } + + @Test + public void testTimeDiscontinuityClearsValues() { + queue.add(1, "b"); + queue.add(2, "c"); + queue.add(0, "a"); + assertThat(queue.size()).isEqualTo(1); + assertThat(queue.poll(0)).isEqualTo("a"); + } + + @Test + public void testTimeDiscontinuityOnFullBufferClearsValues() { + queue = new TimedValueQueue<>(2); + queue.add(1, "b"); + queue.add(3, "c"); + queue.add(2, "a"); + assertThat(queue.size()).isEqualTo(1); + assertThat(queue.poll(2)).isEqualTo("a"); + } + + @Test + public void testPollReturnsClosestValue() { + queue.add(0, "a"); + queue.add(3, "b"); + assertThat(queue.poll(2)).isEqualTo("b"); + assertThat(queue.size()).isEqualTo(0); + } + + @Test + public void testPollRemovesPreviousValues() { + queue.add(0, "a"); + queue.add(1, "b"); + queue.add(2, "c"); + assertThat(queue.poll(1)).isEqualTo("b"); + assertThat(queue.size()).isEqualTo(1); + } + + @Test + public void testPollFloorReturnsClosestPreviousValue() { + queue.add(0, "a"); + queue.add(3, "b"); + assertThat(queue.pollFloor(2)).isEqualTo("a"); + assertThat(queue.pollFloor(2)).isEqualTo(null); + assertThat(queue.pollFloor(3)).isEqualTo("b"); + assertThat(queue.size()).isEqualTo(0); + } + + @Test + public void testPollFloorRemovesPreviousValues() { + queue.add(0, "a"); + queue.add(1, "b"); + queue.add(2, "c"); + assertThat(queue.pollFloor(1)).isEqualTo("b"); + assertThat(queue.size()).isEqualTo(1); + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/video/spherical/FrameRotationQueueTest.java b/library/core/src/test/java/com/google/android/exoplayer2/video/spherical/FrameRotationQueueTest.java new file mode 100644 index 0000000000..071cd582d5 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/video/spherical/FrameRotationQueueTest.java @@ -0,0 +1,133 @@ +/* + * 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.video.spherical; + +import static com.google.common.truth.Truth.assertThat; + +import android.opengl.Matrix; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +/** Tests {@link FrameRotationQueue}. */ +@RunWith(RobolectricTestRunner.class) +public class FrameRotationQueueTest { + + private FrameRotationQueue frameRotationQueue; + private float[] rotationMatrix; + + @Before + public void setUp() throws Exception { + frameRotationQueue = new FrameRotationQueue(); + rotationMatrix = new float[16]; + } + + @Test + public void testGetRotationMatrixReturnsNull_whenEmpty() throws Exception { + assertThat(frameRotationQueue.pollRotationMatrix(rotationMatrix, 0)).isFalse(); + } + + @Test + public void testGetRotationMatrixReturnsNotNull_whenNotEmpty() throws Exception { + frameRotationQueue.setRotation(0, new float[] {1, 2, 3}); + assertThat(frameRotationQueue.pollRotationMatrix(rotationMatrix, 0)).isTrue(); + assertThat(rotationMatrix).hasLength(16); + } + + @Test + public void testConvertsAngleAxisToRotationMatrix() throws Exception { + doTestAngleAxisToRotationMatrix(/* angleRadian= */ 0, /* x= */ 1, /* y= */ 0, /* z= */ 0); + frameRotationQueue.reset(); + doTestAngleAxisToRotationMatrix(/* angleRadian= */ 1, /* x= */ 1, /* y= */ 0, /* z= */ 0); + frameRotationQueue.reset(); + doTestAngleAxisToRotationMatrix(/* angleRadian= */ 1, /* x= */ 0, /* y= */ 0, /* z= */ 1); + // Don't reset frameRotationQueue as we use recenter matrix from previous calls. + doTestAngleAxisToRotationMatrix(/* angleRadian= */ -1, /* x= */ 0, /* y= */ 1, /* z= */ 0); + doTestAngleAxisToRotationMatrix(/* angleRadian= */ 1, /* x= */ 1, /* y= */ 1, /* z= */ 1); + } + + @Test + public void testRecentering_justYaw() throws Exception { + float[] actualMatrix = + getRotationMatrixFromAngleAxis( + /* angleRadian= */ (float) Math.PI, /* x= */ 0, /* y= */ 1, /* z= */ 0); + float[] expectedMatrix = new float[16]; + Matrix.setIdentityM(expectedMatrix, 0); + assertEquals(actualMatrix, expectedMatrix); + } + + @Test + public void testRecentering_yawAndPitch() throws Exception { + float[] matrix = + getRotationMatrixFromAngleAxis( + /* angleRadian= */ (float) Math.PI, /* x= */ 1, /* y= */ 1, /* z= */ 0); + assertMultiplication( + /* xr= */ 0, /* yr= */ 0, /* zr= */ 1, matrix, /* x= */ 0, /* y= */ 0, /* z= */ 1); + } + + @Test + public void testRecentering_yawAndPitch2() throws Exception { + float[] matrix = + getRotationMatrixFromAngleAxis( + /* angleRadian= */ (float) Math.PI / 2, /* x= */ 1, /* y= */ 1, /* z= */ 0); + float sqrt2 = (float) Math.sqrt(2); + assertMultiplication( + /* xr= */ sqrt2, /* yr= */ 0, /* zr= */ 0, matrix, /* x= */ 1, /* y= */ -1, /* z= */ 0); + } + + @Test + public void testRecentering_yawAndPitchAndRoll() throws Exception { + float[] matrix = + getRotationMatrixFromAngleAxis( + /* angleRadian= */ (float) Math.PI * 2 / 3, /* x= */ 1, /* y= */ 1, /* z= */ 1); + assertMultiplication( + /* xr= */ 0, /* yr= */ 0, /* zr= */ 1, matrix, /* x= */ 0, /* y= */ 0, /* z= */ 1); + } + + private void doTestAngleAxisToRotationMatrix(float angleRadian, int x, int y, int z) { + float[] actualMatrix = getRotationMatrixFromAngleAxis(angleRadian, x, y, z); + float[] expectedMatrix = createRotationMatrix(angleRadian, x, y, z); + assertEquals(actualMatrix, expectedMatrix); + } + + private float[] getRotationMatrixFromAngleAxis(float angleRadian, int x, int y, int z) { + float length = Matrix.length(x, y, z); + float factor = angleRadian / length; + // Negate y and z to revert OpenGL coordinate system conversion. + frameRotationQueue.setRotation(0, new float[] {x * factor, -y * factor, -z * factor}); + frameRotationQueue.pollRotationMatrix(rotationMatrix, 0); + return rotationMatrix; + } + + private static void assertMultiplication( + float xr, float yr, float zr, float[] actualMatrix, float x, float y, float z) { + float[] vector = new float[] {x, y, z, 0}; + float[] resultVec = new float[4]; + Matrix.multiplyMV(resultVec, 0, actualMatrix, 0, vector, 0); + assertEquals(resultVec, new float[] {xr, yr, zr, 0}); + } + + private static float[] createRotationMatrix(float angleRadian, int x, int y, int z) { + float[] expectedMatrix = new float[16]; + Matrix.setRotateM(expectedMatrix, 0, (float) Math.toDegrees(angleRadian), x, y, z); + return expectedMatrix; + } + + private static void assertEquals(float[] actual, float[] expected) { + assertThat(actual).usingTolerance(1.0e-5).containsExactly(expected).inOrder(); + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/video/spherical/ProjectionDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/video/spherical/ProjectionDecoderTest.java new file mode 100644 index 0000000000..af1a8421b4 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/video/spherical/ProjectionDecoderTest.java @@ -0,0 +1,95 @@ +/* + * 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.video.spherical; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.util.Util; +import java.util.Arrays; +import junit.framework.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +/** Tests for {@link ProjectionDecoder}. */ +@RunWith(RobolectricTestRunner.class) +public final class ProjectionDecoderTest { + + private static final byte[] PROJ_DATA = + Util.getBytesFromHexString( + "0000008D70726F6A0000008579746D7000000000ABA158D672617720000000716D65736800000006BF800000" + + "3F8000003F0000003F2AAAAB000000003EAAAAAB000000100024200104022430010421034020400123" + + "1020401013020010102222001001003100200010320010000000010000000000240084009066080420" + + "9020108421002410860214C1200660"); + + private static final int MSHP_OFFSET = 16; + private static final int VERTEX_COUNT = 36; + private static final float[] FIRST_VERTEX = {-1.0f, -1.0f, 1.0f}; + private static final float[] LAST_VERTEX = {1.0f, -1.0f, -1.0f}; + private static final float[] FIRST_UV = {0.5f, 1.0f}; + private static final float[] LAST_UV = {1.0f, 1.0f}; + + @Test + public void testDecodeProj() { + testDecoding(PROJ_DATA); + } + + @Test + public void testDecodeMshp() { + testDecoding(Arrays.copyOfRange(PROJ_DATA, MSHP_OFFSET, PROJ_DATA.length)); + } + + private static void testDecoding(byte[] data) { + Projection projection = ProjectionDecoder.decode(data, C.STEREO_MODE_MONO); + assertThat(projection).isNotNull(); + assertThat(projection.stereoMode).isEqualTo(C.STEREO_MODE_MONO); + assertThat(projection.leftMesh).isNotNull(); + assertThat(projection.rightMesh).isNotNull(); + assertThat(projection.singleMesh).isTrue(); + testSubMesh(projection.leftMesh); + } + + /** Tests the that SubMesh (mesh with the video) contains expected data. */ + private static void testSubMesh(Projection.Mesh leftMesh) { + assertThat(leftMesh.getSubMeshCount()).isEqualTo(1); + + Projection.SubMesh subMesh = leftMesh.getSubMesh(0); + assertThat(subMesh.mode).isEqualTo(Projection.DRAW_MODE_TRIANGLES); + + float[] vertices = subMesh.vertices; + float[] uv = subMesh.textureCoords; + assertThat(vertices.length).isEqualTo(VERTEX_COUNT * 3); + assertThat(subMesh.textureCoords.length).isEqualTo(VERTEX_COUNT * 2); + + // Test first vertex + testCoordinate(FIRST_VERTEX, vertices, 0, 3); + // Test last vertex + testCoordinate(LAST_VERTEX, vertices, VERTEX_COUNT * 3 - 3, 3); + + // Test first uv + testCoordinate(FIRST_UV, uv, 0, 2); + // Test last uv + testCoordinate(LAST_UV, uv, VERTEX_COUNT * 2 - 2, 2); + } + + /** Tests that the output coordinates match the expected. */ + private static void testCoordinate(float[] expected, float[] output, int offset, int count) { + for (int i = 0; i < count; i++) { + Assert.assertEquals(expected[i], output[i + offset]); + } + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/video/spherical/ProjectionTest.java b/library/core/src/test/java/com/google/android/exoplayer2/video/spherical/ProjectionTest.java new file mode 100644 index 0000000000..0e2d0999fb --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/video/spherical/ProjectionTest.java @@ -0,0 +1,93 @@ +/* + * 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.video.spherical; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import com.google.android.exoplayer2.C; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +/** Tests for {@link Projection}. */ +@RunWith(RobolectricTestRunner.class) +public class ProjectionTest { + private static final float EPSILON = .00001f; + + // Default 360 sphere. + private static final float RADIUS = 1; + private static final int LATITUDES = 12; + private static final int LONGITUDES = 24; + private static final float VERTICAL_FOV_DEGREES = 180; + private static final float HORIZONTAL_FOV_DEGREES = 360; + + @Test + public void testSphericalMesh() throws Exception { + // Only the first param is important in this test. + Projection projection = + Projection.createEquirectangular( + RADIUS, + LATITUDES, + LONGITUDES, + VERTICAL_FOV_DEGREES, + HORIZONTAL_FOV_DEGREES, + C.STEREO_MODE_MONO); + + Projection.SubMesh subMesh = projection.leftMesh.getSubMesh(0); + assertThat(subMesh.getVertexCount()).isGreaterThan(LATITUDES * LONGITUDES); + + float[] data = subMesh.vertices; + for (int i = 0; i < data.length; ) { + float x = data[i++]; + float y = data[i++]; + float z = data[i++]; + assertEquals(RADIUS, Math.sqrt(x * x + y * y + z * z), EPSILON); + } + } + + @Test + public void testArgumentValidation() { + checkIllegalArgumentException(0, 1, 1, 1, 1); + checkIllegalArgumentException(1, 0, 1, 1, 1); + checkIllegalArgumentException(1, 1, 0, 1, 1); + checkIllegalArgumentException(1, 1, 1, 0, 1); + checkIllegalArgumentException(1, 1, 1, 181, 1); + checkIllegalArgumentException(1, 1, 1, 1, 0); + checkIllegalArgumentException(1, 1, 1, 1, 361); + } + + private void checkIllegalArgumentException( + float radius, + int latitudes, + int longitudes, + float verticalFovDegrees, + float horizontalFovDegrees) { + try { + Projection.createEquirectangular( + radius, + latitudes, + longitudes, + verticalFovDegrees, + horizontalFovDegrees, + C.STEREO_MODE_MONO); + fail(); + } catch (IllegalArgumentException e) { + // Do nothing. Expected. + } + } +} 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 d52049931f..a501435262 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 @@ -73,8 +73,8 @@ import java.util.List; private final PlayerEmsgHandler playerEmsgHandler; private final IdentityHashMap, PlayerTrackEmsgHandler> trackEmsgHandlerBySampleStream; + private final EventDispatcher eventDispatcher; - private EventDispatcher eventDispatcher; private @Nullable Callback callback; private ChunkSampleStream[] sampleStreams; private EventSampleStream[] eventSampleStreams; @@ -131,13 +131,6 @@ import java.util.List; */ public void updateManifest(DashManifest manifest, int periodIndex) { this.manifest = manifest; - if (this.periodIndex != periodIndex) { - eventDispatcher = - eventDispatcher.withParameters( - /* windowIndex= */ 0, - eventDispatcher.mediaPeriodId.copyWithPeriodIndex(periodIndex), - manifest.getPeriod(periodIndex).startMs); - } this.periodIndex = periodIndex; playerEmsgHandler.updateManifest(manifest); if (sampleStreams != null) { 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 ede757aed0..6546863f66 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 @@ -589,18 +589,8 @@ public final class DashMediaSource extends BaseMediaSource { } else { manifestCallback = new ManifestCallback(); manifestLoadErrorThrower = new ManifestLoadErrorThrower(); - refreshManifestRunnable = new Runnable() { - @Override - public void run() { - startLoadingManifest(); - } - }; - simulateManifestRefreshRunnable = new Runnable() { - @Override - public void run() { - processManifest(false); - } - }; + refreshManifestRunnable = this::startLoadingManifest; + simulateManifestRefreshRunnable = () -> processManifest(false); } } @@ -641,7 +631,7 @@ public final class DashMediaSource extends BaseMediaSource { @Override public MediaPeriod createPeriod(MediaPeriodId periodId, Allocator allocator) { - int periodIndex = periodId.periodIndex; + int periodIndex = (Integer) periodId.periodUid - firstPeriodId; EventDispatcher periodEventDispatcher = createEventDispatcher(periodId, manifest.getPeriod(periodIndex).startMs); DashMediaPeriod mediaPeriod = @@ -719,6 +709,7 @@ public final class DashMediaSource extends BaseMediaSource { manifestEventDispatcher.loadCompleted( loadable.dataSpec, loadable.getUri(), + loadable.getResponseHeaders(), loadable.type, elapsedRealtimeMs, loadDurationMs, @@ -810,6 +801,7 @@ public final class DashMediaSource extends BaseMediaSource { manifestEventDispatcher.loadError( loadable.dataSpec, loadable.getUri(), + loadable.getResponseHeaders(), loadable.type, elapsedRealtimeMs, loadDurationMs, @@ -824,6 +816,7 @@ public final class DashMediaSource extends BaseMediaSource { manifestEventDispatcher.loadCompleted( loadable.dataSpec, loadable.getUri(), + loadable.getResponseHeaders(), loadable.type, elapsedRealtimeMs, loadDurationMs, @@ -839,6 +832,7 @@ public final class DashMediaSource extends BaseMediaSource { manifestEventDispatcher.loadError( loadable.dataSpec, loadable.getUri(), + loadable.getResponseHeaders(), loadable.type, elapsedRealtimeMs, loadDurationMs, @@ -854,6 +848,7 @@ public final class DashMediaSource extends BaseMediaSource { manifestEventDispatcher.loadCanceled( loadable.dataSpec, loadable.getUri(), + loadable.getResponseHeaders(), loadable.type, elapsedRealtimeMs, loadDurationMs, @@ -1041,8 +1036,7 @@ public final class DashMediaSource extends BaseMediaSource { private void startLoading(ParsingLoadable loadable, Loader.Callback> callback, int minRetryCount) { long elapsedRealtimeMs = loader.startLoading(loadable, callback, minRetryCount); - manifestEventDispatcher.loadStarted( - loadable.dataSpec, loadable.dataSpec.uri, loadable.type, elapsedRealtimeMs); + manifestEventDispatcher.loadStarted(loadable.dataSpec, loadable.type, elapsedRealtimeMs); } private long getNowUnixTimeUs() { 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 841da07114..a597d780e0 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 @@ -298,7 +298,7 @@ public class DashDownloaderTest { } private DashDownloader getDashDownloader(FakeDataSet fakeDataSet, StreamKey... keys) { - return getDashDownloader(new Factory(null).setFakeDataSet(fakeDataSet), keys); + return getDashDownloader(new Factory().setFakeDataSet(fakeDataSet), keys); } private DashDownloader getDashDownloader(Factory factory, StreamKey... keys) { diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadManagerDashTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadManagerDashTest.java index d2ba826c66..88d4ed6a9d 100644 --- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadManagerDashTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadManagerDashTest.java @@ -110,18 +110,14 @@ public class DownloadManagerDashTest { 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(); + () -> { + try { + // Wait until interrupted. + while (true) { + Thread.sleep(100000); } + } catch (InterruptedException ignored) { + Thread.currentThread().interrupt(); } }) .appendReadData(TEST_MPD) @@ -130,13 +126,10 @@ public class DownloadManagerDashTest { // 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(fakeStreamKey1, fakeStreamKey2); - downloadManager.release(); - } + () -> { + // Setup an Action and immediately release the DM. + handleDownloadAction(fakeStreamKey1, fakeStreamKey2); + downloadManager.release(); }); assertThat(actionFile.exists()).isTrue(); @@ -146,13 +139,7 @@ public class DownloadManagerDashTest { // Revert fakeDataSet to normal. fakeDataSet.setData(TEST_MPD_URI, TEST_MPD); - dummyMainThread.runOnMainThread( - new Runnable() { - @Override - public void run() { - createDownloadManager(); - } - }); + dummyMainThread.runOnMainThread(this::createDownloadManager); // Block on the test thread. blockUntilTasksCompleteAndThrowAnyDownloadError(); @@ -178,13 +165,7 @@ public class DownloadManagerDashTest { public void testHandleInterferingDownloadAction() throws Throwable { fakeDataSet .newData("audio_segment_2") - .appendReadAction( - new Runnable() { - @Override - public void run() { - handleDownloadAction(fakeStreamKey2); - } - }) + .appendReadAction(() -> handleDownloadAction(fakeStreamKey2)) .appendReadData(TestUtil.buildTestData(5)) .endData(); @@ -224,13 +205,7 @@ public class DownloadManagerDashTest { final ConditionVariable downloadInProgressCondition = new ConditionVariable(); fakeDataSet .newData("audio_segment_2") - .appendReadAction( - new Runnable() { - @Override - public void run() { - downloadInProgressCondition.open(); - } - }) + .appendReadAction(downloadInProgressCondition::open) .appendReadData(TestUtil.buildTestData(5)) .endData(); @@ -259,24 +234,20 @@ public class DownloadManagerDashTest { 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); + () -> { + Factory fakeDataSourceFactory = new FakeDataSource.Factory().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(); - } + downloadManagerListener = + new TestDownloadManagerListener(downloadManager, dummyMainThread); + downloadManager.addListener(downloadManagerListener); + downloadManager.startDownloads(); }); } diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadServiceDashTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadServiceDashTest.java index c0f48857c2..70a64f5524 100644 --- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadServiceDashTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadServiceDashTest.java @@ -78,15 +78,12 @@ public class DownloadServiceDashTest { 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(); - } + () -> { + if (pauseDownloadCondition != null) { + try { + pauseDownloadCondition.block(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); } } }; @@ -104,60 +101,51 @@ public class DownloadServiceDashTest { .setRandomData("text_segment_2", 2) .setRandomData("text_segment_3", 3); final DataSource.Factory fakeDataSourceFactory = - new FakeDataSource.Factory(null).setFakeDataSet(fakeDataSet); + new FakeDataSource.Factory().setFakeDataSet(fakeDataSet); fakeStreamKey1 = new StreamKey(0, 0, 0); fakeStreamKey2 = new StreamKey(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(DownloadService.FOREGROUND_NOTIFICATION_ID_NONE) { - @Override - protected DownloadManager getDownloadManager() { - return dashDownloadManager; - } - - @Nullable - @Override - protected Scheduler getScheduler() { - return null; - } - }; - dashDownloadService.onCreate(); + () -> { + 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(DownloadService.FOREGROUND_NOTIFICATION_ID_NONE) { + @Override + protected DownloadManager getDownloadManager() { + return dashDownloadManager; + } + + @Nullable + @Override + protected Scheduler getScheduler() { + return null; + } + }; + dashDownloadService.onCreate(); }); } @After public void tearDown() { - dummyMainThread.runOnMainThread( - new Runnable() { - @Override - public void run() { - dashDownloadService.onDestroy(); - } - }); + dummyMainThread.runOnMainThread(() -> dashDownloadService.onDestroy()); Util.recursiveDelete(tempFolder); dummyMainThread.release(); } @@ -210,13 +198,10 @@ public class DownloadServiceDashTest { private void callDownloadServiceOnStart(final DownloadAction action) { dummyMainThread.runOnMainThread( - new Runnable() { - @Override - public void run() { - Intent startIntent = - DownloadService.buildAddActionIntent(context, DownloadService.class, action, false); - dashDownloadService.onStartCommand(startIntent, 0, 0); - } + () -> { + Intent startIntent = + DownloadService.buildAddActionIntent(context, DownloadService.class, action, false); + dashDownloadService.onStartCommand(startIntent, 0, 0); }); } 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 ae50c93b83..9b8473ee56 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 @@ -22,8 +22,10 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.source.BehindLiveWindowException; import com.google.android.exoplayer2.source.TrackGroup; +import com.google.android.exoplayer2.source.chunk.BaseMediaChunkIterator; import com.google.android.exoplayer2.source.chunk.Chunk; import com.google.android.exoplayer2.source.chunk.DataChunk; +import com.google.android.exoplayer2.source.chunk.MediaChunkIterator; import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.HlsUrl; import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist; import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Segment; @@ -554,4 +556,49 @@ import java.util.List; } + /** {@link MediaChunkIterator} wrapping a {@link HlsMediaPlaylist}. */ + private static final class HlsMediaPlaylistSegmentIterator extends BaseMediaChunkIterator { + + private final HlsMediaPlaylist playlist; + private final long startOfPlaylistInPeriodUs; + + /** + * Creates iterator. + * + * @param playlist The {@link HlsMediaPlaylist} to wrap. + * @param startOfPlaylistInPeriodUs The start time of the playlist in the period, in + * microseconds. + * @param chunkIndex The chunk index in the playlist at which the iterator will start. + */ + public HlsMediaPlaylistSegmentIterator( + HlsMediaPlaylist playlist, long startOfPlaylistInPeriodUs, int chunkIndex) { + super(/* fromIndex= */ chunkIndex, /* toIndex= */ playlist.segments.size() - 1); + this.playlist = playlist; + this.startOfPlaylistInPeriodUs = startOfPlaylistInPeriodUs; + } + + @Override + public DataSpec getDataSpec() { + checkInBounds(); + Segment segment = playlist.segments.get((int) getCurrentIndex()); + Uri chunkUri = UriUtil.resolveToUri(playlist.baseUri, segment.url); + return new DataSpec( + chunkUri, segment.byterangeOffset, segment.byterangeLength, /* key= */ null); + } + + @Override + public long getChunkStartTimeUs() { + checkInBounds(); + Segment segment = playlist.segments.get((int) getCurrentIndex()); + return startOfPlaylistInPeriodUs + segment.relativeStartTimeUs; + } + + @Override + public long getChunkEndTimeUs() { + checkInBounds(); + Segment segment = playlist.segments.get((int) getCurrentIndex()); + long segmentStartTimeInPeriodUs = startOfPlaylistInPeriodUs + segment.relativeStartTimeUs; + return segmentStartTimeInPeriodUs + segment.durationUs; + } + } } 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 1000a38820..79b030a0ee 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 @@ -32,6 +32,7 @@ import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispat import com.google.android.exoplayer2.source.SequenceableLoader; import com.google.android.exoplayer2.source.SinglePeriodTimeline; import com.google.android.exoplayer2.source.ads.AdsMediaSource; +import com.google.android.exoplayer2.source.hls.playlist.DefaultHlsPlaylistParserFactory; import com.google.android.exoplayer2.source.hls.playlist.DefaultHlsPlaylistTracker; import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist; import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylist; @@ -62,7 +63,7 @@ public final class HlsMediaSource extends BaseMediaSource private final HlsDataSourceFactory hlsDataSourceFactory; private HlsExtractorFactory extractorFactory; - private @Nullable ParsingLoadable.Parser playlistParser; + private @Nullable HlsPlaylistParserFactory playlistParserFactory; private @Nullable HlsPlaylistTracker playlistTracker; private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; private LoadErrorHandlingPolicy loadErrorHandlingPolicy; @@ -164,23 +165,19 @@ public final class HlsMediaSource extends BaseMediaSource } /** - * Sets the parser to parse HLS playlists. The default is an instance of {@link - * HlsPlaylistParser}. + * Sets the factory from which playlist parsers will be obtained. The default value is created + * by calling {@link DefaultHlsPlaylistParserFactory#DefaultHlsPlaylistParserFactory()}. * *

Must not be called after calling {@link #setPlaylistTracker} on the same builder. * - * @param playlistParser A {@link ParsingLoadable.Parser} for HLS playlists. + * @param playlistParserFactory An {@link HlsPlaylistParserFactory}. * @return This factory, for convenience. * @throws IllegalStateException If one of the {@code create} methods has already been called. - * @deprecated Use {@link #setPlaylistTracker(HlsPlaylistTracker)} instead. Using this method - * prevents support for attributes that are carried over from the master playlist to the - * media playlists. */ - @Deprecated - public Factory setPlaylistParser(ParsingLoadable.Parser playlistParser) { + public Factory setPlaylistParserFactory(HlsPlaylistParserFactory playlistParserFactory) { Assertions.checkState(!isCreateCalled); Assertions.checkState(playlistTracker == null, "A playlist tracker has already been set."); - this.playlistParser = Assertions.checkNotNull(playlistParser); + this.playlistParserFactory = Assertions.checkNotNull(playlistParserFactory); return this; } @@ -189,7 +186,7 @@ public final class HlsMediaSource extends BaseMediaSource * DefaultHlsPlaylistTracker}. Playlist trackers must not be shared by {@link HlsMediaSource} * instances. * - *

Must not be called after calling {@link #setPlaylistParser} on the same builder. + *

Must not be called after calling {@link #setPlaylistParserFactory} on the same builder. * * @param playlistTracker A tracker for HLS playlists. * @return This factory, for convenience. @@ -197,7 +194,8 @@ public final class HlsMediaSource extends BaseMediaSource */ public Factory setPlaylistTracker(HlsPlaylistTracker playlistTracker) { Assertions.checkState(!isCreateCalled); - Assertions.checkState(playlistParser == null, "A playlist parser has already been set."); + Assertions.checkState( + playlistParserFactory == null, "A playlist parser factory has already been set."); this.playlistTracker = Assertions.checkNotNull(playlistTracker); return this; } @@ -244,14 +242,16 @@ public final class HlsMediaSource extends BaseMediaSource public HlsMediaSource createMediaSource(Uri playlistUri) { isCreateCalled = true; if (playlistTracker == null) { - if (playlistParser == null) { + if (playlistParserFactory == null) { playlistTracker = new DefaultHlsPlaylistTracker( - hlsDataSourceFactory, loadErrorHandlingPolicy, HlsPlaylistParserFactory.DEFAULT); + hlsDataSourceFactory, + loadErrorHandlingPolicy, + new DefaultHlsPlaylistParserFactory()); } else { playlistTracker = new DefaultHlsPlaylistTracker( - hlsDataSourceFactory, loadErrorHandlingPolicy, playlistParser); + hlsDataSourceFactory, loadErrorHandlingPolicy, playlistParserFactory); } } return new HlsMediaSource( @@ -309,6 +309,7 @@ public final class HlsMediaSource extends BaseMediaSource * @deprecated Use {@link Factory} instead. */ @Deprecated + @SuppressWarnings("deprecation") public HlsMediaSource( Uri manifestUri, DataSource.Factory dataSourceFactory, @@ -334,6 +335,7 @@ public final class HlsMediaSource extends BaseMediaSource * @deprecated Use {@link Factory} instead. */ @Deprecated + @SuppressWarnings("deprecation") public HlsMediaSource( Uri manifestUri, DataSource.Factory dataSourceFactory, @@ -364,6 +366,7 @@ public final class HlsMediaSource extends BaseMediaSource * @deprecated Use {@link Factory} instead. */ @Deprecated + @SuppressWarnings("deprecation") public HlsMediaSource( Uri manifestUri, HlsDataSourceFactory dataSourceFactory, @@ -425,7 +428,6 @@ public final class HlsMediaSource extends BaseMediaSource @Override public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { - Assertions.checkArgument(id.periodIndex == 0); EventDispatcher eventDispatcher = createEventDispatcher(id); return new HlsMediaPeriod( extractorFactory, 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 5c63e19f28..9bfdae1cf4 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 @@ -571,7 +571,6 @@ import java.util.List; loadable, this, loadErrorHandlingPolicy.getMinimumLoadableRetryCount(loadable.type)); eventDispatcher.loadStarted( loadable.dataSpec, - loadable.dataSpec.uri, loadable.type, trackType, loadable.trackFormat, @@ -596,6 +595,7 @@ import java.util.List; eventDispatcher.loadCompleted( loadable.dataSpec, loadable.getUri(), + loadable.getResponseHeaders(), loadable.type, trackType, loadable.trackFormat, @@ -619,6 +619,7 @@ import java.util.List; eventDispatcher.loadCanceled( loadable.dataSpec, loadable.getUri(), + loadable.getResponseHeaders(), loadable.type, trackType, loadable.trackFormat, @@ -680,6 +681,7 @@ import java.util.List; eventDispatcher.loadError( loadable.dataSpec, loadable.getUri(), + loadable.getResponseHeaders(), loadable.type, trackType, loadable.trackFormat, diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistParserFactory.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistParserFactory.java new file mode 100644 index 0000000000..9058980c73 --- /dev/null +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistParserFactory.java @@ -0,0 +1,54 @@ +/* + * 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 com.google.android.exoplayer2.offline.FilteringManifestParser; +import com.google.android.exoplayer2.offline.StreamKey; +import com.google.android.exoplayer2.upstream.ParsingLoadable; +import java.util.Collections; +import java.util.List; + +/** Default implementation for {@link HlsPlaylistParserFactory}. */ +public final class DefaultHlsPlaylistParserFactory implements HlsPlaylistParserFactory { + + private final List streamKeys; + + /** Creates an instance that does not filter any parsing results. */ + public DefaultHlsPlaylistParserFactory() { + this(Collections.emptyList()); + } + + /** + * Creates an instance that filters the parsing results using the given {@code streamKeys}. + * + * @param streamKeys See {@link + * FilteringManifestParser#FilteringManifestParser(ParsingLoadable.Parser, List)}. + */ + public DefaultHlsPlaylistParserFactory(List streamKeys) { + this.streamKeys = streamKeys; + } + + @Override + public ParsingLoadable.Parser createPlaylistParser() { + return new FilteringManifestParser<>(new HlsPlaylistParser(), streamKeys); + } + + @Override + public ParsingLoadable.Parser createPlaylistParser( + HlsMasterPlaylist masterPlaylist) { + return new FilteringManifestParser<>(new HlsPlaylistParser(masterPlaylist), streamKeys); + } +} diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistTracker.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistTracker.java index a61c8116ac..ac94d7307e 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistTracker.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistTracker.java @@ -123,7 +123,6 @@ public final class DefaultHlsPlaylistTracker loadErrorHandlingPolicy.getMinimumLoadableRetryCount(masterPlaylistLoadable.type)); eventDispatcher.loadStarted( masterPlaylistLoadable.dataSpec, - masterPlaylistLoadable.dataSpec.uri, masterPlaylistLoadable.type, elapsedRealtime); } @@ -155,7 +154,7 @@ public final class DefaultHlsPlaylistTracker } @Override - public HlsMasterPlaylist getMasterPlaylist() { + public @Nullable HlsMasterPlaylist getMasterPlaylist() { return masterPlaylist; } @@ -234,6 +233,7 @@ public final class DefaultHlsPlaylistTracker eventDispatcher.loadCompleted( loadable.dataSpec, loadable.getUri(), + loadable.getResponseHeaders(), C.DATA_TYPE_MANIFEST, elapsedRealtimeMs, loadDurationMs, @@ -249,6 +249,7 @@ public final class DefaultHlsPlaylistTracker eventDispatcher.loadCanceled( loadable.dataSpec, loadable.getUri(), + loadable.getResponseHeaders(), C.DATA_TYPE_MANIFEST, elapsedRealtimeMs, loadDurationMs, @@ -269,6 +270,7 @@ public final class DefaultHlsPlaylistTracker eventDispatcher.loadError( loadable.dataSpec, loadable.getUri(), + loadable.getResponseHeaders(), C.DATA_TYPE_MANIFEST, elapsedRealtimeMs, loadDurationMs, @@ -496,6 +498,7 @@ public final class DefaultHlsPlaylistTracker eventDispatcher.loadCompleted( loadable.dataSpec, loadable.getUri(), + loadable.getResponseHeaders(), C.DATA_TYPE_MANIFEST, elapsedRealtimeMs, loadDurationMs, @@ -514,6 +517,7 @@ public final class DefaultHlsPlaylistTracker eventDispatcher.loadCanceled( loadable.dataSpec, loadable.getUri(), + loadable.getResponseHeaders(), C.DATA_TYPE_MANIFEST, elapsedRealtimeMs, loadDurationMs, @@ -555,6 +559,7 @@ public final class DefaultHlsPlaylistTracker eventDispatcher.loadError( loadable.dataSpec, loadable.getUri(), + loadable.getResponseHeaders(), C.DATA_TYPE_MANIFEST, elapsedRealtimeMs, loadDurationMs, @@ -583,7 +588,6 @@ public final class DefaultHlsPlaylistTracker loadErrorHandlingPolicy.getMinimumLoadableRetryCount(mediaPlaylistLoadable.type)); eventDispatcher.loadStarted( mediaPlaylistLoadable.dataSpec, - mediaPlaylistLoadable.dataSpec.uri, mediaPlaylistLoadable.type, elapsedRealtime); } 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 c45c2dd547..bb01ade28d 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,6 +21,7 @@ import com.google.android.exoplayer2.util.MimeTypes; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Map; /** Represents an HLS master playlist. */ public final class HlsMasterPlaylist extends HlsPlaylist { @@ -35,7 +36,8 @@ public final class HlsMasterPlaylist extends HlsPlaylist { /* subtitles= */ Collections.emptyList(), /* muxedAudioFormat= */ null, /* muxedCaptionFormats= */ Collections.emptyList(), - /* hasIndependentSegments= */ false); + /* hasIndependentSegments= */ false, + /* variableDefinitions= */ Collections.emptyMap()); public static final int GROUP_INDEX_VARIANT = 0; public static final int GROUP_INDEX_AUDIO = 1; @@ -110,6 +112,8 @@ public final class HlsMasterPlaylist extends HlsPlaylist { * captions information. */ public final List muxedCaptionFormats; + /** Contains variable definitions, as defined by the #EXT-X-DEFINE tag. */ + public final Map variableDefinitions; /** * @param baseUri See {@link #baseUri}. @@ -120,6 +124,7 @@ public final class HlsMasterPlaylist extends HlsPlaylist { * @param muxedAudioFormat See {@link #muxedAudioFormat}. * @param muxedCaptionFormats See {@link #muxedCaptionFormats}. * @param hasIndependentSegments See {@link #hasIndependentSegments}. + * @param variableDefinitions See {@link #variableDefinitions}. */ public HlsMasterPlaylist( String baseUri, @@ -129,7 +134,8 @@ public final class HlsMasterPlaylist extends HlsPlaylist { List subtitles, Format muxedAudioFormat, List muxedCaptionFormats, - boolean hasIndependentSegments) { + boolean hasIndependentSegments, + Map variableDefinitions) { super(baseUri, tags, hasIndependentSegments); this.variants = Collections.unmodifiableList(variants); this.audios = Collections.unmodifiableList(audios); @@ -137,6 +143,7 @@ public final class HlsMasterPlaylist extends HlsPlaylist { this.muxedAudioFormat = muxedAudioFormat; this.muxedCaptionFormats = muxedCaptionFormats != null ? Collections.unmodifiableList(muxedCaptionFormats) : null; + this.variableDefinitions = Collections.unmodifiableMap(variableDefinitions); } @Override @@ -149,7 +156,8 @@ public final class HlsMasterPlaylist extends HlsPlaylist { copyRenditionsList(subtitles, GROUP_INDEX_SUBTITLE, streamKeys), muxedAudioFormat, muxedCaptionFormats, - hasIndependentSegments); + hasIndependentSegments, + variableDefinitions); } /** @@ -169,7 +177,8 @@ public final class HlsMasterPlaylist extends HlsPlaylist { emptyList, /* muxedAudioFormat= */ null, /* muxedCaptionFormats= */ null, - /* hasIndependentSegments= */ false); + /* hasIndependentSegments= */ false, + /* variableDefinitions= */ Collections.emptyMap()); } private static List copyRenditionsList( 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 841c13f953..a29808933b 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 @@ -154,11 +154,13 @@ public final class HlsMediaPlaylist extends HlsPlaylist { } /** - * Type of the playlist, as defined by #EXT-X-PLAYLIST-TYPE. + * Type of the playlist, as defined by #EXT-X-PLAYLIST-TYPE. One of {@link + * #PLAYLIST_TYPE_UNKNOWN}, {@link #PLAYLIST_TYPE_VOD} or {@link #PLAYLIST_TYPE_EVENT}. */ @Retention(RetentionPolicy.SOURCE) @IntDef({PLAYLIST_TYPE_UNKNOWN, PLAYLIST_TYPE_VOD, PLAYLIST_TYPE_EVENT}) public @interface PlaylistType {} + public static final int PLAYLIST_TYPE_UNKNOWN = 0; public static final int PLAYLIST_TYPE_VOD = 1; public static final int PLAYLIST_TYPE_EVENT = 2; diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistSegmentIterator.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistSegmentIterator.java deleted file mode 100644 index 4c654dc572..0000000000 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistSegmentIterator.java +++ /dev/null @@ -1,67 +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.source.chunk.BaseMediaChunkIterator; -import com.google.android.exoplayer2.source.chunk.MediaChunkIterator; -import com.google.android.exoplayer2.upstream.DataSpec; -import com.google.android.exoplayer2.util.UriUtil; - -/** {@link MediaChunkIterator} wrapping a {@link HlsMediaPlaylist}. */ -public final class HlsMediaPlaylistSegmentIterator extends BaseMediaChunkIterator { - - private final HlsMediaPlaylist playlist; - private final long startOfPlaylistInPeriodUs; - - /** - * Creates iterator. - * - * @param playlist The {@link HlsMediaPlaylist} to wrap. - * @param startOfPlaylistInPeriodUs The start time of the playlist in the period, in microseconds. - * @param chunkIndex The chunk index in the playlist at which the iterator will start. - */ - public HlsMediaPlaylistSegmentIterator( - HlsMediaPlaylist playlist, long startOfPlaylistInPeriodUs, int chunkIndex) { - super(/* fromIndex= */ chunkIndex, /* toIndex= */ playlist.segments.size() - 1); - this.playlist = playlist; - this.startOfPlaylistInPeriodUs = startOfPlaylistInPeriodUs; - } - - @Override - public DataSpec getDataSpec() { - checkInBounds(); - HlsMediaPlaylist.Segment segment = playlist.segments.get((int) getCurrentIndex()); - Uri chunkUri = UriUtil.resolveToUri(playlist.baseUri, segment.url); - return new DataSpec( - chunkUri, segment.byterangeOffset, segment.byterangeLength, /* key= */ null); - } - - @Override - public long getChunkStartTimeUs() { - checkInBounds(); - HlsMediaPlaylist.Segment segment = playlist.segments.get((int) getCurrentIndex()); - return startOfPlaylistInPeriodUs + segment.relativeStartTimeUs; - } - - @Override - public long getChunkEndTimeUs() { - checkInBounds(); - HlsMediaPlaylist.Segment segment = playlist.segments.get((int) getCurrentIndex()); - long segmentStartTimeInPeriodUs = startOfPlaylistInPeriodUs + segment.relativeStartTimeUs; - return segmentStartTimeInPeriodUs + segment.durationUs; - } -} 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 e287b5220e..49826902cd 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 @@ -40,6 +40,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Queue; import java.util.TreeMap; import java.util.regex.Matcher; @@ -57,6 +58,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser variantUrls = new HashSet<>(); HashMap audioGroupIdToCodecs = new HashMap<>(); + HashMap variableDefinitions = new HashMap<>(); ArrayList variants = new ArrayList<>(); ArrayList audios = new ArrayList<>(); ArrayList subtitles = new ArrayList<>(); @@ -258,7 +265,11 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser variableDefinitions = new HashMap<>(); List segments = new ArrayList<>(); List tags = new ArrayList<>(); @@ -465,7 +483,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser 1) { @@ -587,7 +620,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser variableDefinitions) throws ParserException { + String keyFormatVersions = + parseOptionalStringAttr(line, REGEX_KEYFORMATVERSIONS, "1", variableDefinitions); if (!"1".equals(keyFormatVersions)) { // Not supported. return null; } - String uriString = parseStringAttr(line, REGEX_URI); + String uriString = parseStringAttr(line, REGEX_URI, variableDefinitions); byte[] data = Base64.decode(uriString.substring(uriString.indexOf(',')), Base64.DEFAULT); byte[] psshData = PsshAtomUtil.buildPsshAtom(C.PLAYREADY_UUID, data); return new SchemeData(C.PLAYREADY_UUID, MimeTypes.VIDEO_MP4, psshData); } - private static @Nullable SchemeData parseWidevineSchemeData(String line, String keyFormat) + private static @Nullable SchemeData parseWidevineSchemeData( + String line, String keyFormat, Map variableDefinitions) throws ParserException { if (KEYFORMAT_WIDEVINE_PSSH_BINARY.equals(keyFormat)) { - String uriString = parseStringAttr(line, REGEX_URI); - return new SchemeData(C.WIDEVINE_UUID, MimeTypes.VIDEO_MP4, - Base64.decode(uriString.substring(uriString.indexOf(',')), Base64.DEFAULT)); + String uriString = parseStringAttr(line, REGEX_URI, variableDefinitions); + return new SchemeData( + C.WIDEVINE_UUID, + MimeTypes.VIDEO_MP4, + Base64.decode(uriString.substring(uriString.indexOf(',')), Base64.DEFAULT)); } if (KEYFORMAT_WIDEVINE_PSSH_JSON.equals(keyFormat)) { try { @@ -657,19 +695,21 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser variableDefinitions) + throws ParserException { + String value = parseOptionalStringAttr(line, pattern, variableDefinitions); if (value != null) { return value; } else { @@ -677,14 +717,39 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser variableDefinitions) { + return parseOptionalStringAttr(line, pattern, null, variableDefinitions); } private static @PolyNull String parseOptionalStringAttr( - String line, Pattern pattern, @PolyNull String defaultValue) { + String line, + Pattern pattern, + @PolyNull String defaultValue, + Map variableDefinitions) { Matcher matcher = pattern.matcher(line); - return matcher.find() ? matcher.group(1) : defaultValue; + String value = matcher.find() ? matcher.group(1) : defaultValue; + return variableDefinitions.isEmpty() || value == null + ? value + : replaceVariableReferences(value, variableDefinitions); + } + + private static String replaceVariableReferences( + String string, Map variableDefinitions) { + Matcher matcher = REGEX_VARIABLE_REFERENCE.matcher(string); + // TODO: Replace StringBuffer with StringBuilder once Java 9 is available. + StringBuffer stringWithReplacements = new StringBuffer(); + while (matcher.find()) { + String groupName = matcher.group(1); + if (variableDefinitions.containsKey(groupName)) { + matcher.appendReplacement( + stringWithReplacements, Matcher.quoteReplacement(variableDefinitions.get(groupName))); + } else { + // The variable is not defined. The value is ignored. + } + } + matcher.appendTail(stringWithReplacements); + return stringWithReplacements.toString(); } private static boolean parseOptionalBooleanAttribute( diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParserFactory.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParserFactory.java index 717825c168..814060bf7d 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParserFactory.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParserFactory.java @@ -20,20 +20,6 @@ import com.google.android.exoplayer2.upstream.ParsingLoadable; /** Factory for {@link HlsPlaylist} parsers. */ public interface HlsPlaylistParserFactory { - HlsPlaylistParserFactory DEFAULT = - new HlsPlaylistParserFactory() { - @Override - public ParsingLoadable.Parser createPlaylistParser() { - return new HlsPlaylistParser(); - } - - @Override - public ParsingLoadable.Parser createPlaylistParser( - HlsMasterPlaylist masterPlaylist) { - return new HlsPlaylistParser(masterPlaylist); - } - }; - /** * Returns a stand-alone playlist parser. Playlists parsed by the returned parser do not inherit * any attributes from other playlists. 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 acc5236311..825988994e 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 @@ -183,7 +183,7 @@ public class HlsDownloaderTest { } private HlsDownloader getHlsDownloader(String mediaPlaylistUri, List keys) { - Factory factory = new Factory(null).setFakeDataSet(fakeDataSet); + Factory factory = new Factory().setFakeDataSet(fakeDataSet); return new HlsDownloader( Uri.parse(mediaPlaylistUri), keys, new DownloaderConstructorHelper(cache, factory)); } @@ -191,8 +191,7 @@ public class HlsDownloaderTest { private static ArrayList getKeys(int... variantIndices) { ArrayList streamKeys = new ArrayList<>(); for (int variantIndex : variantIndices) { - final int trackIndex = variantIndex; - streamKeys.add(new StreamKey(HlsMasterPlaylist.GROUP_INDEX_VARIANT, trackIndex)); + streamKeys.add(new StreamKey(HlsMasterPlaylist.GROUP_INDEX_VARIANT, variantIndex)); } return streamKeys; } diff --git a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java index 11fef3c844..d818111eec 100644 --- a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java +++ b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java @@ -117,6 +117,15 @@ public class HlsMasterPlaylistParserTest { + "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2 , avc1.66.30 \"\n" + "http://example.com/spaces_in_codecs.m3u8\n"; + private static final String PLAYLIST_WITH_VARIABLE_SUBSTITUTION = + " #EXTM3U \n" + + "\n" + + "#EXT-X-DEFINE:NAME=\"codecs\",VALUE=\"mp4a.40.5\"\n" + + "#EXT-X-DEFINE:NAME=\"tricky\",VALUE=\"This/{$nested}/reference/shouldnt/work\"\n" + + "#EXT-X-DEFINE:NAME=\"nested\",VALUE=\"This should not be inserted\"\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS=\"{$codecs}\"\n" + + "http://example.com/{$tricky}\n"; + @Test public void testParseMasterPlaylist() throws IOException { HlsMasterPlaylist masterPlaylist = parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_SIMPLE); @@ -218,6 +227,15 @@ public class HlsMasterPlaylistParserTest { assertThat(playlistWithoutIndependentSegments.hasIndependentSegments).isFalse(); } + @Test + public void testVariableSubstitution() throws IOException { + HlsMasterPlaylist playlistWithSubstitutions = + parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_VARIABLE_SUBSTITUTION); + HlsMasterPlaylist.HlsUrl variant = playlistWithSubstitutions.variants.get(0); + assertThat(variant.format.codecs).isEqualTo("mp4a.40.5"); + assertThat(variant.url).isEqualTo("http://example.com/This/{$nested}/reference/shouldnt/work"); + } + private static HlsMasterPlaylist parseMasterPlaylist(String uri, String playlistString) throws IOException { Uri playlistUri = Uri.parse(uri); 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 6e71aebb74..e7bf3c6324 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 @@ -25,6 +25,7 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.util.Collections; +import java.util.HashMap; import java.util.List; import org.junit.Test; import org.junit.runner.RunWith; @@ -397,9 +398,71 @@ public class HlsMediaPlaylistParserTest { /* subtitles= */ Collections.emptyList(), /* muxedAudioFormat= */ null, /* muxedCaptionFormats= */ null, - /* hasIndependentSegments= */ true); + /* hasIndependentSegments= */ true, + /* variableDefinitions */ Collections.emptyMap()); HlsMediaPlaylist playlistWithInheritance = (HlsMediaPlaylist) new HlsPlaylistParser(masterPlaylist).parse(playlistUri, inputStream); assertThat(playlistWithInheritance.hasIndependentSegments).isTrue(); } + + @Test + public void testVariableSubstitution() throws IOException { + Uri playlistUri = Uri.parse("https://example.com/substitution.m3u8"); + String playlistString = + "#EXTM3U\n" + + "#EXT-X-VERSION:8\n" + + "#EXT-X-DEFINE:NAME=\"underscore_1\",VALUE=\"{\"\n" + + "#EXT-X-DEFINE:NAME=\"dash-1\",VALUE=\"replaced_value.ts\"\n" + + "#EXT-X-TARGETDURATION:5\n" + + "#EXT-X-MEDIA-SEQUENCE:10\n" + + "#EXTINF:5.005,\n" + + "segment1.ts\n" + + "#EXT-X-MAP:URI=\"{$dash-1}\"" + + "#EXTINF:5.005,\n" + + "segment{$underscore_1}$name_1}\n"; + InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString)); + HlsMediaPlaylist playlist = + (HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream); + Segment segment = playlist.segments.get(1); + assertThat(segment.initializationSegment.url).isEqualTo("replaced_value.ts"); + assertThat(segment.url).isEqualTo("segment{$name_1}"); + } + + @Test + public void testInheritedVariableSubstitution() throws IOException { + Uri playlistUri = Uri.parse("https://example.com/test3.m3u8"); + String playlistString = + "#EXTM3U\n" + + "#EXT-X-VERSION:8\n" + + "#EXT-X-TARGETDURATION:5\n" + + "#EXT-X-MEDIA-SEQUENCE:10\n" + + "#EXT-X-DEFINE:IMPORT=\"imported_base\"\n" + + "#EXTINF:5.005,\n" + + "{$imported_base}1.ts\n" + + "#EXTINF:5.005,\n" + + "{$imported_base}2.ts\n" + + "#EXTINF:5.005,\n" + + "{$imported_base}3.ts\n" + + "#EXTINF:5.005,\n" + + "{$imported_base}4.ts\n"; + InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString)); + HashMap variableDefinitions = new HashMap<>(); + variableDefinitions.put("imported_base", "long_path"); + HlsMasterPlaylist masterPlaylist = + new HlsMasterPlaylist( + /* baseUri= */ "", + /* tags= */ Collections.emptyList(), + /* variants= */ Collections.emptyList(), + /* audios= */ Collections.emptyList(), + /* subtitles= */ Collections.emptyList(), + /* muxedAudioFormat= */ null, + /* muxedCaptionFormats= */ Collections.emptyList(), + /* hasIndependentSegments= */ false, + variableDefinitions); + HlsMediaPlaylist playlist = + (HlsMediaPlaylist) new HlsPlaylistParser(masterPlaylist).parse(playlistUri, inputStream); + for (int i = 1; i <= 4; i++) { + assertThat(playlist.segments.get(i - 1).url).isEqualTo("long_path" + i + ".ts"); + } + } } 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 831d21eeb7..9491298368 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 @@ -24,11 +24,13 @@ import com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor; import com.google.android.exoplayer2.extractor.mp4.Track; import com.google.android.exoplayer2.extractor.mp4.TrackEncryptionBox; import com.google.android.exoplayer2.source.BehindLiveWindowException; +import com.google.android.exoplayer2.source.chunk.BaseMediaChunkIterator; import com.google.android.exoplayer2.source.chunk.Chunk; import com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper; import com.google.android.exoplayer2.source.chunk.ChunkHolder; import com.google.android.exoplayer2.source.chunk.ContainerMediaChunk; import com.google.android.exoplayer2.source.chunk.MediaChunk; +import com.google.android.exoplayer2.source.chunk.MediaChunkIterator; import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest; import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest.StreamElement; import com.google.android.exoplayer2.trackselection.TrackSelection; @@ -297,4 +299,42 @@ public class DefaultSsChunkSource implements SsChunkSource { return lastChunkEndTimeUs - playbackPositionUs; } + /** {@link MediaChunkIterator} wrapping a track of a {@link StreamElement}. */ + private static final class StreamElementIterator extends BaseMediaChunkIterator { + + private final StreamElement streamElement; + private final int trackIndex; + + /** + * Creates iterator. + * + * @param streamElement The {@link StreamElement} to wrap. + * @param trackIndex The track index in the stream element. + * @param chunkIndex The chunk index at which the iterator will start. + */ + public StreamElementIterator(StreamElement streamElement, int trackIndex, int chunkIndex) { + super(/* fromIndex= */ chunkIndex, /* toIndex= */ streamElement.chunkCount - 1); + this.streamElement = streamElement; + this.trackIndex = trackIndex; + } + + @Override + public DataSpec getDataSpec() { + checkInBounds(); + Uri uri = streamElement.buildRequestUri(trackIndex, (int) getCurrentIndex()); + return new DataSpec(uri); + } + + @Override + public long getChunkStartTimeUs() { + checkInBounds(); + return streamElement.getStartTimeUs((int) getCurrentIndex()); + } + + @Override + public long getChunkEndTimeUs() { + long chunkStartTimeUs = getChunkStartTimeUs(); + return chunkStartTimeUs + streamElement.getChunkDurationUs((int) getCurrentIndex()); + } + } } 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 1a80ade01d..a756b7f4f1 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 @@ -330,6 +330,7 @@ public final class SsMediaSource extends BaseMediaSource * @deprecated Use {@link Factory} instead. */ @Deprecated + @SuppressWarnings("deprecation") public SsMediaSource( SsManifest manifest, SsChunkSource.Factory chunkSourceFactory, @@ -388,6 +389,7 @@ public final class SsMediaSource extends BaseMediaSource * @deprecated Use {@link Factory} instead. */ @Deprecated + @SuppressWarnings("deprecation") public SsMediaSource( Uri manifestUri, DataSource.Factory manifestDataSourceFactory, @@ -420,6 +422,7 @@ public final class SsMediaSource extends BaseMediaSource * @deprecated Use {@link Factory} instead. */ @Deprecated + @SuppressWarnings("deprecation") public SsMediaSource( Uri manifestUri, DataSource.Factory manifestDataSourceFactory, @@ -525,7 +528,6 @@ public final class SsMediaSource extends BaseMediaSource @Override public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { - Assertions.checkArgument(id.periodIndex == 0); EventDispatcher eventDispatcher = createEventDispatcher(id); SsMediaPeriod period = new SsMediaPeriod( @@ -570,6 +572,7 @@ public final class SsMediaSource extends BaseMediaSource manifestEventDispatcher.loadCompleted( loadable.dataSpec, loadable.getUri(), + loadable.getResponseHeaders(), loadable.type, elapsedRealtimeMs, loadDurationMs, @@ -586,6 +589,7 @@ public final class SsMediaSource extends BaseMediaSource manifestEventDispatcher.loadCanceled( loadable.dataSpec, loadable.getUri(), + loadable.getResponseHeaders(), loadable.type, elapsedRealtimeMs, loadDurationMs, @@ -603,6 +607,7 @@ public final class SsMediaSource extends BaseMediaSource manifestEventDispatcher.loadError( loadable.dataSpec, loadable.getUri(), + loadable.getResponseHeaders(), loadable.type, elapsedRealtimeMs, loadDurationMs, @@ -684,12 +689,7 @@ public final class SsMediaSource extends BaseMediaSource } long nextLoadTimestamp = manifestLoadStartTimestamp + MINIMUM_MANIFEST_REFRESH_PERIOD_MS; long delayUntilNextLoad = Math.max(0, nextLoadTimestamp - SystemClock.elapsedRealtime()); - manifestRefreshHandler.postDelayed(new Runnable() { - @Override - public void run() { - startLoadingManifest(); - } - }, delayUntilNextLoad); + manifestRefreshHandler.postDelayed(this::startLoadingManifest, delayUntilNextLoad); } private void startLoadingManifest() { @@ -698,8 +698,7 @@ public final class SsMediaSource extends BaseMediaSource long elapsedRealtimeMs = manifestLoader.startLoading( loadable, this, loadErrorHandlingPolicy.getMinimumLoadableRetryCount(loadable.type)); - manifestEventDispatcher.loadStarted( - loadable.dataSpec, loadable.dataSpec.uri, 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/SsManifest.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifest.java index 51284f06c4..2c508f0fde 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 @@ -20,9 +20,6 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.offline.FilterableManifest; import com.google.android.exoplayer2.offline.StreamKey; -import com.google.android.exoplayer2.source.chunk.BaseMediaChunkIterator; -import com.google.android.exoplayer2.source.chunk.MediaChunkIterator; -import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.UriUtil; import com.google.android.exoplayer2.util.Util; @@ -51,45 +48,6 @@ public class SsManifest implements FilterableManifest { } } - /** {@link MediaChunkIterator} wrapping a track of a {@link StreamElement}. */ - public static final class StreamElementIterator extends BaseMediaChunkIterator { - - private final StreamElement streamElement; - private final int trackIndex; - - /** - * Creates iterator. - * - * @param streamElement The {@link StreamElement} to wrap. - * @param trackIndex The track index in the stream element. - * @param chunkIndex The chunk index at which the iterator will start. - */ - public StreamElementIterator(StreamElement streamElement, int trackIndex, int chunkIndex) { - super(/* fromIndex= */ chunkIndex, /* toIndex= */ streamElement.chunkCount - 1); - this.streamElement = streamElement; - this.trackIndex = trackIndex; - } - - @Override - public DataSpec getDataSpec() { - checkInBounds(); - Uri uri = streamElement.buildRequestUri(trackIndex, (int) getCurrentIndex()); - return new DataSpec(uri); - } - - @Override - public long getChunkStartTimeUs() { - checkInBounds(); - return streamElement.getStartTimeUs((int) getCurrentIndex()); - } - - @Override - public long getChunkEndTimeUs() { - long chunkStartTimeUs = getChunkStartTimeUs(); - return chunkStartTimeUs + streamElement.getChunkDurationUs((int) getCurrentIndex()); - } - } - /** * Represents a StreamIndex element. */ diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestParser.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestParser.java index c2437db189..36eb6665f3 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestParser.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestParser.java @@ -213,7 +213,7 @@ public class SsManifestParser implements ParsingLoadable.Parser { /** * @param xmlParser The underlying {@link XmlPullParser} - * @throws ParserException + * @throws ParserException If a parsing error occurs. */ protected void parseStartTag(XmlPullParser xmlParser) throws ParserException { // Do nothing. 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 05f2582f0d..dc8d6754f5 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 @@ -64,9 +64,7 @@ public class SsManifestTest { SsManifest sourceManifest = newSsManifest(newStreamElement("1", formats[0]), newStreamElement("2", formats[1])); - List keys = Arrays.asList(new StreamKey(1, 0)); - // Keys don't need to be in any particular order - Collections.shuffle(keys, new Random(0)); + List keys = Collections.singletonList(new StreamKey(1, 0)); SsManifest copyManifest = sourceManifest.copy(keys); 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 227eb52e79..0158a55f66 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 @@ -45,7 +45,11 @@ public final class AspectRatioFrameLayout extends FrameLayout { } // LINT.IfChange - /** Resize modes for {@link AspectRatioFrameLayout}. */ + /** + * Resize modes for {@link AspectRatioFrameLayout}. One of {@link #RESIZE_MODE_FIT}, {@link + * #RESIZE_MODE_FIXED_WIDTH}, {@link #RESIZE_MODE_FIXED_HEIGHT}, {@link #RESIZE_MODE_FILL} or + * {@link #RESIZE_MODE_ZOOM}. + */ @Retention(RetentionPolicy.SOURCE) @IntDef({ RESIZE_MODE_FIT, diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java index da03d28cba..5467538c0f 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java @@ -34,10 +34,13 @@ public class PlaybackControlView extends PlayerControlView { public interface VisibilityListener extends com.google.android.exoplayer2.ui.PlayerControlView.VisibilityListener {} + @Deprecated + @SuppressWarnings("deprecation") private static final class DefaultControlDispatcher extends com.google.android.exoplayer2.DefaultControlDispatcher implements ControlDispatcher {} /** @deprecated Use {@link com.google.android.exoplayer2.DefaultControlDispatcher}. */ @Deprecated + @SuppressWarnings("deprecation") public static final ControlDispatcher DEFAULT_CONTROL_DISPATCHER = new DefaultControlDispatcher(); /** The default fast forward increment, in milliseconds. */ diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerControlView.java index abe884ce53..af20c09756 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerControlView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerControlView.java @@ -208,6 +208,8 @@ public class PlayerControlView extends FrameLayout { private final Formatter formatter; private final Timeline.Period period; private final Timeline.Window window; + private final Runnable updateProgressAction; + private final Runnable hideAction; private final Drawable repeatOffButtonDrawable; private final Drawable repeatOneButtonDrawable; @@ -236,22 +238,6 @@ public class PlayerControlView extends FrameLayout { private long[] extraAdGroupTimesMs; private boolean[] extraPlayedAdGroups; - private final Runnable updateProgressAction = - new Runnable() { - @Override - public void run() { - updateProgress(); - } - }; - - private final Runnable hideAction = - new Runnable() { - @Override - public void run() { - hide(); - } - }; - public PlayerControlView(Context context) { this(context, null); } @@ -303,6 +289,8 @@ public class PlayerControlView extends FrameLayout { extraPlayedAdGroups = new boolean[0]; componentListener = new ComponentListener(); controlDispatcher = new com.google.android.exoplayer2.DefaultControlDispatcher(); + updateProgressAction = this::updateProgress; + hideAction = this::hide; LayoutInflater.from(context).inflate(controllerLayoutId, this); setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java index f3edacaebc..805ae0fa67 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java @@ -15,8 +15,6 @@ */ 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; @@ -45,6 +43,7 @@ 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.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -203,14 +202,11 @@ public class PlayerNotificationManager { public void onBitmap(final Bitmap bitmap) { if (bitmap != null) { mainHandler.post( - new Runnable() { - @Override - public void run() { - if (player != null - && notificationTag == currentNotificationTag - && isNotificationStarted) { - updateNotification(bitmap); - } + () -> { + if (player != null + && notificationTag == currentNotificationTag + && isNotificationStarted) { + updateNotification(bitmap); } }); } @@ -232,8 +228,12 @@ public class PlayerNotificationManager { /** 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) + /** + * Visibility of notification on the lock screen. One of {@link + * NotificationCompat#VISIBILITY_PRIVATE}, {@link NotificationCompat#VISIBILITY_PUBLIC} or {@link + * NotificationCompat#VISIBILITY_SECRET}. + */ + @Retention(RetentionPolicy.SOURCE) @IntDef({ NotificationCompat.VISIBILITY_PRIVATE, NotificationCompat.VISIBILITY_PUBLIC, @@ -241,8 +241,13 @@ public class PlayerNotificationManager { }) public @interface Visibility {} - /** Priority of the notification (required for API 25 and lower). */ - @Retention(SOURCE) + /** + * Priority of the notification (required for API 25 and lower). One of {@link + * NotificationCompat#PRIORITY_DEFAULT}, {@link NotificationCompat#PRIORITY_MAX}, {@link + * NotificationCompat#PRIORITY_HIGH}, {@link NotificationCompat#PRIORITY_LOW }or {@link + * NotificationCompat#PRIORITY_MIN}. + */ + @Retention(RetentionPolicy.SOURCE) @IntDef({ NotificationCompat.PRIORITY_DEFAULT, NotificationCompat.PRIORITY_MAX, 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 99f38b4c40..142b251a61 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 @@ -59,6 +59,7 @@ import com.google.android.exoplayer2.text.TextOutput; 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.ui.spherical.SingleTapListener; import com.google.android.exoplayer2.ui.spherical.SphericalSurfaceView; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.ErrorMessageProvider; @@ -243,9 +244,12 @@ public class PlayerView extends FrameLayout { private static final int SURFACE_TYPE_TEXTURE_VIEW = 2; private static final int SURFACE_TYPE_MONO360_VIEW = 3; - /** Determines when the buffering view is shown. */ - @IntDef({SHOW_BUFFERING_NEVER, SHOW_BUFFERING_WHEN_PLAYING, SHOW_BUFFERING_ALWAYS}) + /** + * Determines when the buffering view is shown. One of {@link #SHOW_BUFFERING_NEVER}, {@link + * #SHOW_BUFFERING_WHEN_PLAYING} or {@link #SHOW_BUFFERING_ALWAYS}. + */ @Retention(RetentionPolicy.SOURCE) + @IntDef({SHOW_BUFFERING_NEVER, SHOW_BUFFERING_WHEN_PLAYING, SHOW_BUFFERING_ALWAYS}) public @interface ShowBuffering {} /** The buffering view is never shown. */ public static final int SHOW_BUFFERING_NEVER = 0; @@ -387,6 +391,7 @@ public class PlayerView extends FrameLayout { Assertions.checkState(Util.SDK_INT >= 15); SphericalSurfaceView sphericalSurfaceView = new SphericalSurfaceView(context); sphericalSurfaceView.setSurfaceListener(componentListener); + sphericalSurfaceView.setSingleTapListener(componentListener); surfaceView = sphericalSurfaceView; break; default: @@ -509,6 +514,8 @@ public class PlayerView extends FrameLayout { oldVideoComponent.clearVideoTextureView((TextureView) surfaceView); } else if (surfaceView instanceof SphericalSurfaceView) { oldVideoComponent.clearVideoSurface(((SphericalSurfaceView) surfaceView).getSurface()); + oldVideoComponent.clearVideoFrameMetadataListener(((SphericalSurfaceView) surfaceView)); + oldVideoComponent.clearCameraMotionListener(((SphericalSurfaceView) surfaceView)); } else if (surfaceView instanceof SurfaceView) { oldVideoComponent.clearVideoSurfaceView((SurfaceView) surfaceView); } @@ -534,6 +541,8 @@ public class PlayerView extends FrameLayout { if (surfaceView instanceof TextureView) { newVideoComponent.setVideoTextureView((TextureView) surfaceView); } else if (surfaceView instanceof SphericalSurfaceView) { + newVideoComponent.setVideoFrameMetadataListener(((SphericalSurfaceView) surfaceView)); + newVideoComponent.setCameraMotionListener(((SphericalSurfaceView) surfaceView)); newVideoComponent.setVideoSurface(((SphericalSurfaceView) surfaceView).getSurface()); } else if (surfaceView instanceof SurfaceView) { newVideoComponent.setVideoSurfaceView((SurfaceView) surfaceView); @@ -595,7 +604,7 @@ public class PlayerView extends FrameLayout { } /** Returns the default artwork to display. */ - public Drawable getDefaultArtwork() { + public @Nullable Drawable getDefaultArtwork() { return defaultArtwork; } @@ -1016,15 +1025,10 @@ public class PlayerView extends FrameLayout { @Override public boolean onTouchEvent(MotionEvent ev) { - if (!useController || player == null || ev.getActionMasked() != MotionEvent.ACTION_DOWN) { + if (ev.getActionMasked() != MotionEvent.ACTION_DOWN) { return false; } - if (!controller.isVisible()) { - maybeShowController(true); - } else if (controllerHideOnTouch) { - controller.hide(); - } - return true; + return toggleControllerVisibility(); } @Override @@ -1062,6 +1066,18 @@ public class PlayerView extends FrameLayout { } } + private boolean toggleControllerVisibility() { + if (!useController || player == null) { + return false; + } + if (!controller.isVisible()) { + maybeShowController(true); + } else if (controllerHideOnTouch) { + controller.hide(); + } + return true; + } + /** Shows the playback controls, but only if forced or shown indefinitely. */ private void maybeShowController(boolean isForced) { if (isPlayingAd() && controllerHideDuringAds) { @@ -1281,7 +1297,8 @@ public class PlayerView extends FrameLayout { TextOutput, VideoListener, OnLayoutChangeListener, - SphericalSurfaceView.SurfaceListener { + SphericalSurfaceView.SurfaceListener, + SingleTapListener { // TextOutput implementation @@ -1386,5 +1403,12 @@ public class PlayerView extends FrameLayout { } } } + + // SingleTapListener implementation + + @Override + public boolean onSingleTapUp(MotionEvent e) { + return toggleControllerVisibility(); + } } } diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java index b8098b6fa7..55745a7cb5 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java @@ -20,6 +20,7 @@ import android.content.Context; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.AttributeSet; +import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.SimpleExoPlayer; /** @deprecated Use {@link PlayerView}. */ @@ -45,7 +46,10 @@ public final class SimpleExoPlayerView extends PlayerView { * @param player The player whose target view is being switched. * @param oldPlayerView The old view to detach from the player. * @param newPlayerView The new view to attach to the player. + * @deprecated Use {@link PlayerView#switchTargetView(Player, PlayerView, PlayerView)} instead. */ + @Deprecated + @SuppressWarnings("deprecation") public static void switchTargetView( @NonNull SimpleExoPlayer player, @Nullable SimpleExoPlayerView oldPlayerView, diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java index db46ee4912..b041b0e03d 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java @@ -431,6 +431,7 @@ import com.google.android.exoplayer2.util.Util; * latter only checks the text of each sequence, and does not check for equality of styling that * may be embedded within the {@link CharSequence}s. */ + @SuppressWarnings("UndefinedEquals") private static boolean areCharSequencesEqual(CharSequence first, CharSequence second) { // Some CharSequence implementations don't perform a cheap referential equality check in their // equals methods, so we perform one explicitly here. 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 index fe5d5cbbc5..3f09ac2427 100644 --- 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 @@ -19,7 +19,6 @@ 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.support.annotation.AttrRes; import android.support.annotation.Nullable; @@ -80,13 +79,7 @@ public class TrackSelectionView extends LinearLayout { 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(); - } - }; + Dialog.OnClickListener okClickListener = (dialog, which) -> selectionView.applySelection(); AlertDialog dialog = builder diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/Mesh.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/Mesh.java deleted file mode 100644 index d3d7d854ae..0000000000 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/Mesh.java +++ /dev/null @@ -1,290 +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.ui.spherical; - -import static com.google.android.exoplayer2.ui.spherical.GlUtil.checkGlError; - -import android.annotation.TargetApi; -import android.opengl.GLES11Ext; -import android.opengl.GLES20; -import com.google.android.exoplayer2.C; -import java.nio.FloatBuffer; - -/** - * Utility class to generate & render spherical meshes for video or images. Use the static creation - * methods to construct the Mesh's data. Then call the Mesh constructor on the GL thread when ready. - * Use glDraw method to render it. - */ -@TargetApi(15) -/*package*/ final class Mesh { - - /** Defines the constants identifying the current eye type. */ - /*package*/ interface EyeType { - /** Single eye in monocular rendering. */ - int MONOCULAR = 0; - - /** The left eye in stereo rendering. */ - int LEFT = 1; - - /** The right eye in stereo rendering. */ - int RIGHT = 2; - } - - // Basic vertex & fragment shaders to render a mesh with 3D position & 2D texture data. - private static final String[] VERTEX_SHADER_CODE = - new String[] { - "uniform mat4 uMvpMatrix;", - "attribute vec4 aPosition;", - "attribute vec2 aTexCoords;", - "varying vec2 vTexCoords;", - - // Standard transformation. - "void main() {", - " gl_Position = uMvpMatrix * aPosition;", - " vTexCoords = aTexCoords;", - "}" - }; - private static final String[] FRAGMENT_SHADER_CODE = - new String[] { - // This is required since the texture data is GL_TEXTURE_EXTERNAL_OES. - "#extension GL_OES_EGL_image_external : require", - "precision mediump float;", - - // Standard texture rendering shader. - "uniform samplerExternalOES uTexture;", - "varying vec2 vTexCoords;", - "void main() {", - " gl_FragColor = texture2D(uTexture, vTexCoords);", - "}" - }; - - // Constants related to vertex data. - private static final int POSITION_COORDS_PER_VERTEX = 3; // X, Y, Z. - // The vertex contains texture coordinates for both the left & right eyes. If the scene is - // rendered in VR, the appropriate part of the vertex will be selected at runtime. For a mono - // scene, only the left eye's UV coordinates are used. - // For mono media, the UV coordinates are duplicated in each. For stereo media, the UV coords - // point to the appropriate part of the source media. - private static final int TEXTURE_COORDS_PER_VERTEX = 2 * 2; - private static final int COORDS_PER_VERTEX = - POSITION_COORDS_PER_VERTEX + TEXTURE_COORDS_PER_VERTEX; - // Data is tightly packed. Each vertex is [x, y, z, u_left, v_left, u_right, v_right]. - private static final int VERTEX_STRIDE_BYTES = COORDS_PER_VERTEX * C.BYTES_PER_FLOAT; - - // Vertices for the mesh with 3D position + left 2D texture UV + right 2D texture UV. - private final int vertixCount; - private final FloatBuffer vertexBuffer; - - // Program related GL items. These are only valid if program != 0. - private int program; - private int mvpMatrixHandle; - private int positionHandle; - private int texCoordsHandle; - private int textureHandle; - - /** - * Generates a 3D UV sphere for rendering monoscopic or stereoscopic video. - * - *

This can be called on any thread. The returned {@link Mesh} isn't valid until {@link - * #init()} is called. - * - * @param radius Size of the sphere. Must be > 0. - * @param latitudes Number of rows that make up the sphere. Must be >= 1. - * @param longitudes Number of columns that make up the sphere. Must be >= 1. - * @param verticalFovDegrees Total latitudinal degrees that are covered by the sphere. Must be in - * (0, 180]. - * @param horizontalFovDegrees Total longitudinal degrees that are covered by the sphere.Must be - * in (0, 360]. - * @param stereoMode A {@link C.StereoMode} value. - * @return Unintialized Mesh. - */ - public static Mesh createUvSphere( - float radius, - int latitudes, - int longitudes, - float verticalFovDegrees, - float horizontalFovDegrees, - @C.StereoMode int stereoMode) { - return new Mesh( - createUvSphereVertexData( - radius, latitudes, longitudes, verticalFovDegrees, horizontalFovDegrees, stereoMode)); - } - - /** Used by static constructors. */ - private Mesh(float[] vertexData) { - vertixCount = vertexData.length / COORDS_PER_VERTEX; - vertexBuffer = GlUtil.createBuffer(vertexData); - } - - /** Initializes of the GL components. */ - /* package */ void init() { - program = GlUtil.compileProgram(VERTEX_SHADER_CODE, FRAGMENT_SHADER_CODE); - mvpMatrixHandle = GLES20.glGetUniformLocation(program, "uMvpMatrix"); - positionHandle = GLES20.glGetAttribLocation(program, "aPosition"); - texCoordsHandle = GLES20.glGetAttribLocation(program, "aTexCoords"); - textureHandle = GLES20.glGetUniformLocation(program, "uTexture"); - } - - /** - * Renders the mesh. This must be called on the GL thread. - * - * @param textureId GL_TEXTURE_EXTERNAL_OES used for this mesh. - * @param mvpMatrix The Model View Projection matrix. - * @param eyeType An {@link EyeType} value. - */ - /* package */ void draw(int textureId, float[] mvpMatrix, int eyeType) { - // Configure shader. - GLES20.glUseProgram(program); - checkGlError(); - - GLES20.glEnableVertexAttribArray(positionHandle); - GLES20.glEnableVertexAttribArray(texCoordsHandle); - checkGlError(); - - GLES20.glUniformMatrix4fv(mvpMatrixHandle, 1, false, mvpMatrix, 0); - GLES20.glActiveTexture(GLES20.GL_TEXTURE0); - GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, textureId); - GLES20.glUniform1i(textureHandle, 0); - checkGlError(); - - // Load position data. - vertexBuffer.position(0); - GLES20.glVertexAttribPointer( - positionHandle, - POSITION_COORDS_PER_VERTEX, - GLES20.GL_FLOAT, - false, - VERTEX_STRIDE_BYTES, - vertexBuffer); - checkGlError(); - - // Load texture data. Eye.Type.RIGHT uses the left eye's data. - int textureOffset = - (eyeType == EyeType.RIGHT) ? POSITION_COORDS_PER_VERTEX + 2 : POSITION_COORDS_PER_VERTEX; - vertexBuffer.position(textureOffset); - GLES20.glVertexAttribPointer( - texCoordsHandle, - TEXTURE_COORDS_PER_VERTEX, - GLES20.GL_FLOAT, - false, - VERTEX_STRIDE_BYTES, - vertexBuffer); - checkGlError(); - - // Render. - GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, vertixCount); - checkGlError(); - - GLES20.glDisableVertexAttribArray(positionHandle); - GLES20.glDisableVertexAttribArray(texCoordsHandle); - } - - /** Cleans up the GL resources. */ - /* package */ void shutdown() { - if (program != 0) { - GLES20.glDeleteProgram(program); - } - } - - // @VisibleForTesting - /*package*/ static float[] createUvSphereVertexData( - float radius, - int latitudes, - int longitudes, - float verticalFovDegrees, - float horizontalFovDegrees, - @C.StereoMode int stereoMode) { - if (radius <= 0 - || latitudes < 1 - || longitudes < 1 - || verticalFovDegrees <= 0 - || verticalFovDegrees > 180 - || horizontalFovDegrees <= 0 - || horizontalFovDegrees > 360) { - throw new IllegalArgumentException("Invalid parameters for sphere."); - } - - // Compute angular size in radians of each UV quad. - float verticalFovRads = (float) Math.toRadians(verticalFovDegrees); - float horizontalFovRads = (float) Math.toRadians(horizontalFovDegrees); - float quadHeightRads = verticalFovRads / latitudes; - float quadWidthRads = horizontalFovRads / longitudes; - - // Each latitude strip has 2 * (longitudes quads + extra edge) vertices + 2 degenerate vertices. - int vertexCount = (2 * (longitudes + 1) + 2) * latitudes; - // Buffer to return. - float[] vertexData = new float[vertexCount * COORDS_PER_VERTEX]; - - // Generate the data for the sphere which is a set of triangle strips representing each - // latitude band. - int offset = 0; // Offset into the vertexData array. - // (i, j) represents a quad in the equirectangular sphere. - for (int j = 0; j < latitudes; ++j) { // For each horizontal triangle strip. - // Each latitude band lies between the two phi values. Each vertical edge on a band lies on - // a theta value. - float phiLow = (quadHeightRads * j - verticalFovRads / 2); - float phiHigh = (quadHeightRads * (j + 1) - verticalFovRads / 2); - - for (int i = 0; i < longitudes + 1; ++i) { // For each vertical edge in the band. - for (int k = 0; k < 2; ++k) { // For low and high points on an edge. - // For each point, determine it's position in polar coordinates. - float phi = (k == 0) ? phiLow : phiHigh; - float theta = quadWidthRads * i + (float) Math.PI - horizontalFovRads / 2; - - // Set vertex position data as Cartesian coordinates. - vertexData[offset] = -(float) (radius * Math.sin(theta) * Math.cos(phi)); - vertexData[offset + 1] = (float) (radius * Math.sin(phi)); - vertexData[offset + 2] = (float) (radius * Math.cos(theta) * Math.cos(phi)); - - // Set vertex texture.x data. - if (stereoMode == C.STEREO_MODE_LEFT_RIGHT) { - // For left-right media, each eye's x coordinate points to the left or right half of the - // texture. - vertexData[offset + 3] = (i * quadWidthRads / horizontalFovRads) / 2; - vertexData[offset + 5] = (i * quadWidthRads / horizontalFovRads) / 2 + .5f; - } else { - // For top-bottom or monoscopic media, the eye's x spans the full width of the texture. - vertexData[offset + 3] = i * quadWidthRads / horizontalFovRads; - vertexData[offset + 5] = i * quadWidthRads / horizontalFovRads; - } - - // Set vertex texture.y data. The "1 - ..." is due to Canvas vs GL coords. - if (stereoMode == C.STEREO_MODE_TOP_BOTTOM) { - // For top-bottom media, each eye's y coordinate points to the top or bottom half of the - // texture. - vertexData[offset + 4] = 1 - (((j + k) * quadHeightRads / verticalFovRads) / 2 + .5f); - vertexData[offset + 6] = 1 - ((j + k) * quadHeightRads / verticalFovRads) / 2; - } else { - // For left-right or monoscopic media, the eye's y spans the full height of the texture. - vertexData[offset + 4] = 1 - (j + k) * quadHeightRads / verticalFovRads; - vertexData[offset + 6] = 1 - (j + k) * quadHeightRads / verticalFovRads; - } - offset += COORDS_PER_VERTEX; - - // Break up the triangle strip with degenerate vertices by copying first and last points. - if ((i == 0 && k == 0) || (i == longitudes && k == 1)) { - System.arraycopy( - vertexData, offset - COORDS_PER_VERTEX, vertexData, offset, COORDS_PER_VERTEX); - offset += COORDS_PER_VERTEX; - } - } - // Move on to the next vertical edge in the triangle strip. - } - // Move on to the next triangle strip. - } - return vertexData; - } -} diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/ProjectionRenderer.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/ProjectionRenderer.java new file mode 100644 index 0000000000..3b3e921253 --- /dev/null +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/ProjectionRenderer.java @@ -0,0 +1,239 @@ +/* + * 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.spherical; + +import static com.google.android.exoplayer2.ui.spherical.GlUtil.checkGlError; + +import android.annotation.TargetApi; +import android.opengl.GLES11Ext; +import android.opengl.GLES20; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.video.spherical.Projection; +import java.nio.FloatBuffer; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * Utility class to render spherical meshes for video or images. Call {@link #init()} on the GL + * thread when ready. + */ +@TargetApi(15) +/*package*/ final class ProjectionRenderer { + + /** Defines the constants identifying the current eye type. */ + /*package*/ interface EyeType { + /** Single eye in monocular rendering. */ + int MONOCULAR = 0; + + /** The left eye in stereo rendering. */ + int LEFT = 1; + + /** The right eye in stereo rendering. */ + int RIGHT = 2; + } + + /** + * Returns whether {@code projection} is supported. At least it should have left mesh and there + * should be only one sub mesh per mesh. + */ + public static boolean isSupported(Projection projection) { + Projection.Mesh leftMesh = projection.leftMesh; + Projection.Mesh rightMesh = projection.rightMesh; + return leftMesh.getSubMeshCount() == 1 + && leftMesh.getSubMesh(0).textureId == Projection.SubMesh.VIDEO_TEXTURE_ID + && rightMesh.getSubMeshCount() == 1 + && rightMesh.getSubMesh(0).textureId == Projection.SubMesh.VIDEO_TEXTURE_ID; + } + + // Basic vertex & fragment shaders to render a mesh with 3D position & 2D texture data. + private static final String[] VERTEX_SHADER_CODE = + new String[] { + "uniform mat4 uMvpMatrix;", + "uniform mat3 uTexMatrix;", + "attribute vec4 aPosition;", + "attribute vec2 aTexCoords;", + "varying vec2 vTexCoords;", + + // Standard transformation. + "void main() {", + " gl_Position = uMvpMatrix * aPosition;", + " vTexCoords = (uTexMatrix * vec3(aTexCoords, 1)).xy;", + "}" + }; + private static final String[] FRAGMENT_SHADER_CODE = + new String[] { + // This is required since the texture data is GL_TEXTURE_EXTERNAL_OES. + "#extension GL_OES_EGL_image_external : require", + "precision mediump float;", + + // Standard texture rendering shader. + "uniform samplerExternalOES uTexture;", + "varying vec2 vTexCoords;", + "void main() {", + " gl_FragColor = texture2D(uTexture, vTexCoords);", + "}" + }; + + // Texture transform matrices. + private static final float[] TEX_MATRIX_WHOLE = { + 1.0f, 0.0f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f, 1.0f, 1.0f + }; + private static final float[] TEX_MATRIX_TOP = { + 1.0f, 0.0f, 0.0f, 0.0f, -0.5f, 0.0f, 0.0f, 0.5f, 1.0f + }; + private static final float[] TEX_MATRIX_BOTTOM = { + 1.0f, 0.0f, 0.0f, 0.0f, -0.5f, 0.0f, 0.0f, 1.0f, 1.0f + }; + private static final float[] TEX_MATRIX_LEFT = { + 0.5f, 0.0f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f, 1.0f, 1.0f + }; + private static final float[] TEX_MATRIX_RIGHT = { + 0.5f, 0.0f, 0.0f, 0.0f, -1.0f, 0.0f, 0.5f, 1.0f, 1.0f + }; + + private int stereoMode; + private @Nullable MeshData leftMeshData; + private @Nullable MeshData rightMeshData; + + // Program related GL items. These are only valid if program != 0. + private int program; + private int mvpMatrixHandle; + private int uTexMatrixHandle; + private int positionHandle; + private int texCoordsHandle; + private int textureHandle; + + /** + * Sets a {@link Projection} to be used. + * + * @param projection Contains the projection data to be rendered. + * @see #isSupported(Projection) + */ + public void setProjection(Projection projection) { + if (!isSupported(projection)) { + return; + } + stereoMode = projection.stereoMode; + leftMeshData = new MeshData(projection.leftMesh.getSubMesh(0)); + rightMeshData = + projection.singleMesh ? leftMeshData : new MeshData(projection.rightMesh.getSubMesh(0)); + } + + /** Initializes of the GL components. */ + /* package */ void init() { + program = GlUtil.compileProgram(VERTEX_SHADER_CODE, FRAGMENT_SHADER_CODE); + mvpMatrixHandle = GLES20.glGetUniformLocation(program, "uMvpMatrix"); + uTexMatrixHandle = GLES20.glGetUniformLocation(program, "uTexMatrix"); + positionHandle = GLES20.glGetAttribLocation(program, "aPosition"); + texCoordsHandle = GLES20.glGetAttribLocation(program, "aTexCoords"); + textureHandle = GLES20.glGetUniformLocation(program, "uTexture"); + } + + /** + * Renders the mesh. This must be called on the GL thread. + * + * @param textureId GL_TEXTURE_EXTERNAL_OES used for this mesh. + * @param mvpMatrix The Model View Projection matrix. + * @param eyeType An {@link EyeType} value. + */ + /* package */ void draw(int textureId, float[] mvpMatrix, int eyeType) { + // Configure shader. + GLES20.glUseProgram(program); + checkGlError(); + + GLES20.glEnableVertexAttribArray(positionHandle); + GLES20.glEnableVertexAttribArray(texCoordsHandle); + checkGlError(); + + float[] texMatrix; + if (stereoMode == C.STEREO_MODE_TOP_BOTTOM) { + texMatrix = eyeType == EyeType.RIGHT ? TEX_MATRIX_BOTTOM : TEX_MATRIX_TOP; + } else if (stereoMode == C.STEREO_MODE_LEFT_RIGHT) { + texMatrix = eyeType == EyeType.RIGHT ? TEX_MATRIX_RIGHT : TEX_MATRIX_LEFT; + } else { + texMatrix = TEX_MATRIX_WHOLE; + } + GLES20.glUniformMatrix3fv(uTexMatrixHandle, 1, false, texMatrix, 0); + + GLES20.glUniformMatrix4fv(mvpMatrixHandle, 1, false, mvpMatrix, 0); + GLES20.glActiveTexture(GLES20.GL_TEXTURE0); + GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, textureId); + GLES20.glUniform1i(textureHandle, 0); + checkGlError(); + + MeshData meshData = + Assertions.checkNotNull(eyeType == EyeType.RIGHT ? rightMeshData : leftMeshData); + + // Load position data. + GLES20.glVertexAttribPointer( + positionHandle, + Projection.POSITION_COORDS_PER_VERTEX, + GLES20.GL_FLOAT, + false, + Projection.POSITION_COORDS_PER_VERTEX * C.BYTES_PER_FLOAT, + meshData.vertexBuffer); + checkGlError(); + + // Load texture data. + GLES20.glVertexAttribPointer( + texCoordsHandle, + Projection.TEXTURE_COORDS_PER_VERTEX, + GLES20.GL_FLOAT, + false, + Projection.TEXTURE_COORDS_PER_VERTEX * C.BYTES_PER_FLOAT, + meshData.textureBuffer); + checkGlError(); + + // Render. + GLES20.glDrawArrays(meshData.drawMode, 0, meshData.vertexCount); + checkGlError(); + + GLES20.glDisableVertexAttribArray(positionHandle); + GLES20.glDisableVertexAttribArray(texCoordsHandle); + } + + /** Cleans up the GL resources. */ + /* package */ void shutdown() { + if (program != 0) { + GLES20.glDeleteProgram(program); + } + } + + private static class MeshData { + private final int vertexCount; + private final FloatBuffer vertexBuffer; + private final FloatBuffer textureBuffer; + @Projection.DrawMode private final int drawMode; + + public MeshData(Projection.SubMesh subMesh) { + vertexCount = subMesh.getVertexCount(); + vertexBuffer = GlUtil.createBuffer(subMesh.vertices); + textureBuffer = GlUtil.createBuffer(subMesh.textureCoords); + switch (subMesh.mode) { + case Projection.DRAW_MODE_TRIANGLES_STRIP: + drawMode = GLES20.GL_TRIANGLE_STRIP; + break; + case Projection.DRAW_MODE_TRIANGLES_FAN: + drawMode = GLES20.GL_TRIANGLE_FAN; + break; + case Projection.DRAW_MODE_TRIANGLES: + default: + drawMode = GLES20.GL_TRIANGLES; + break; + } + } + } +} diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/SceneRenderer.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/SceneRenderer.java index 96788000ca..023d68f988 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/SceneRenderer.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/SceneRenderer.java @@ -19,9 +19,13 @@ import static com.google.android.exoplayer2.ui.spherical.GlUtil.checkGlError; import android.graphics.SurfaceTexture; import android.opengl.GLES20; +import android.opengl.Matrix; import android.support.annotation.Nullable; -import com.google.android.exoplayer2.ui.spherical.Mesh.EyeType; +import com.google.android.exoplayer2.ui.spherical.ProjectionRenderer.EyeType; import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.TimedValueQueue; +import com.google.android.exoplayer2.video.spherical.FrameRotationQueue; +import com.google.android.exoplayer2.video.spherical.Projection; import java.util.concurrent.atomic.AtomicBoolean; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @@ -33,14 +37,31 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /*package*/ final class SceneRenderer { private final AtomicBoolean frameAvailable; + private final ProjectionRenderer projectionRenderer; + private final FrameRotationQueue frameRotationQueue; + private final TimedValueQueue sampleTimestampQueue; + private final float[] rotationMatrix; + private final float[] tempMatrix; private int textureId; - @Nullable private SurfaceTexture surfaceTexture; - @MonotonicNonNull private Mesh mesh; - private boolean meshInitialized; + private @MonotonicNonNull SurfaceTexture surfaceTexture; + private @Nullable Projection pendingProjection; + private long pendingProjectionTimeNs; + private long lastFrameTimestamp; + private boolean resetRotationAtNextFrame; - public SceneRenderer() { + public SceneRenderer( + Projection projection, + FrameRotationQueue frameRotationQueue, + TimedValueQueue sampleTimestampQueue) { + this.frameRotationQueue = frameRotationQueue; + this.sampleTimestampQueue = sampleTimestampQueue; frameAvailable = new AtomicBoolean(); + projectionRenderer = new ProjectionRenderer(); + projectionRenderer.setProjection(projection); + rotationMatrix = new float[16]; + tempMatrix = new float[16]; + resetRotation(); } /** Initializes the renderer. */ @@ -49,19 +70,23 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; GLES20.glClearColor(0.5f, 0.5f, 0.5f, 1.0f); checkGlError(); + projectionRenderer.init(); + checkGlError(); + textureId = GlUtil.createExternalTexture(); surfaceTexture = new SurfaceTexture(textureId); surfaceTexture.setOnFrameAvailableListener(surfaceTexture -> frameAvailable.set(true)); return surfaceTexture; } - /** Sets a {@link Mesh} to be used to display video. */ - public void setMesh(Mesh mesh) { - if (this.mesh != null) { - this.mesh.shutdown(); - } - this.mesh = mesh; - meshInitialized = false; + public void resetRotation() { + resetRotationAtNextFrame = true; + } + + /** Sets a {@link Projection} to be used to display video. */ + public void setProjection(Projection projection, long timeNs) { + pendingProjection = projection; + pendingProjectionTimeNs = timeNs; } /** @@ -71,14 +96,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; * @param eyeType an {@link EyeType} value */ public void drawFrame(float[] viewProjectionMatrix, int eyeType) { - if (mesh == null) { - return; - } - if (!meshInitialized) { - meshInitialized = true; - mesh.init(); - } - // glClear isn't strictly necessary when rendering fully spherical panoramas, but it can improve // performance on tiled renderers by causing the GPU to discard previous data. GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); @@ -87,8 +104,20 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; if (frameAvailable.compareAndSet(true, false)) { Assertions.checkNotNull(surfaceTexture).updateTexImage(); checkGlError(); + if (resetRotationAtNextFrame) { + Matrix.setIdentityM(rotationMatrix, 0); + } + lastFrameTimestamp = surfaceTexture.getTimestamp(); + Long sampleTimestamp = sampleTimestampQueue.poll(lastFrameTimestamp); + if (sampleTimestamp != null) { + frameRotationQueue.pollRotationMatrix(rotationMatrix, sampleTimestamp); + } } - - mesh.draw(textureId, viewProjectionMatrix, eyeType); + if (pendingProjection != null && pendingProjectionTimeNs <= lastFrameTimestamp) { + projectionRenderer.setProjection(pendingProjection); + pendingProjection = null; + } + Matrix.multiplyMM(tempMatrix, 0, viewProjectionMatrix, 0, rotationMatrix, 0); + projectionRenderer.draw(textureId, tempMatrix, eyeType); } } diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/SingleTapListener.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/SingleTapListener.java new file mode 100644 index 0000000000..7328bdfcab --- /dev/null +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/SingleTapListener.java @@ -0,0 +1,29 @@ +/* + * 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.spherical; + +import android.view.MotionEvent; + +/** Listens tap events on a {@link android.view.View}. */ +public interface SingleTapListener { + /** + * Notified when a tap occurs with the up {@link MotionEvent} that triggered it. + * + * @param e The up motion event that completed the first tap. + * @return Whether the event is consumed. + */ + boolean onSingleTapUp(MotionEvent e); +} diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/SphericalSurfaceView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/SphericalSurfaceView.java index f4386a44c9..30995aca5f 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/SphericalSurfaceView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/SphericalSurfaceView.java @@ -37,9 +37,17 @@ import android.view.Display; import android.view.Surface; import android.view.WindowManager; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.ui.spherical.Mesh.EyeType; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.ui.spherical.ProjectionRenderer.EyeType; import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.TimedValueQueue; import com.google.android.exoplayer2.util.Util; +import com.google.android.exoplayer2.video.VideoFrameMetadataListener; +import com.google.android.exoplayer2.video.spherical.CameraMotionListener; +import com.google.android.exoplayer2.video.spherical.FrameRotationQueue; +import com.google.android.exoplayer2.video.spherical.Projection; +import com.google.android.exoplayer2.video.spherical.ProjectionDecoder; +import java.util.Arrays; import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.opengles.GL10; @@ -54,7 +62,8 @@ import javax.microedition.khronos.opengles.GL10; * match what they expect. */ @TargetApi(15) -public final class SphericalSurfaceView extends GLSurfaceView { +public final class SphericalSurfaceView extends GLSurfaceView + implements VideoFrameMetadataListener, CameraMotionListener { /** * This listener can be used to be notified when the {@link Surface} associated with this view is @@ -70,17 +79,6 @@ public final class SphericalSurfaceView extends GLSurfaceView { void surfaceChanged(@Nullable Surface surface); } - // A spherical mesh for video should be large enough that there are no stereo artifacts. - private static final int SPHERE_RADIUS_METERS = 50; - - // TODO These should be configured based on the video type. It's assumed 360 video here. - private static final int DEFAULT_SPHERE_HORIZONTAL_DEGREES = 360; - private static final int DEFAULT_SPHERE_VERTICAL_DEGREES = 180; - - // The 360 x 180 sphere has 5 degree quads. Increase these if lines in videos look wavy. - private static final int DEFAULT_SPHERE_COLUMNS = 72; - private static final int DEFAULT_SPHERE_ROWS = 36; - // Arbitrary vertical field of view. private static final int FIELD_OF_VIEW_DEGREES = 90; private static final float Z_NEAR = .1f; @@ -96,9 +94,15 @@ public final class SphericalSurfaceView extends GLSurfaceView { private final PhoneOrientationListener phoneOrientationListener; private final Renderer renderer; private final Handler mainHandler; + private final TimedValueQueue sampleTimestampQueue; + private final FrameRotationQueue frameRotationQueue; + private final TouchTracker touchTracker; private @Nullable SurfaceListener surfaceListener; private @Nullable SurfaceTexture surfaceTexture; private @Nullable Surface surface; + private @C.StreamType int defaultStereoMode; + private @C.StreamType int currentStereoMode; + private @Nullable byte[] currentProjectionData; public SphericalSurfaceView(Context context) { this(context, null); @@ -106,7 +110,6 @@ public final class SphericalSurfaceView extends GLSurfaceView { public SphericalSurfaceView(Context context, @Nullable AttributeSet attributeSet) { super(context, attributeSet); - mainHandler = new Handler(Looper.getMainLooper()); // Configure sensors and touch. @@ -119,9 +122,15 @@ public final class SphericalSurfaceView extends GLSurfaceView { int type = Util.SDK_INT >= 18 ? Sensor.TYPE_GAME_ROTATION_VECTOR : Sensor.TYPE_ROTATION_VECTOR; orientationSensor = sensorManager.getDefaultSensor(type); - renderer = new Renderer(); + defaultStereoMode = C.STEREO_MODE_MONO; + currentStereoMode = defaultStereoMode; + Projection projection = Projection.createEquirectangular(defaultStereoMode); + frameRotationQueue = new FrameRotationQueue(); + sampleTimestampQueue = new TimedValueQueue<>(); + SceneRenderer scene = new SceneRenderer(projection, frameRotationQueue, sampleTimestampQueue); + renderer = new Renderer(scene); - TouchTracker touchTracker = new TouchTracker(renderer, PX_PER_DEGREES); + touchTracker = new TouchTracker(context, renderer, PX_PER_DEGREES); WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); Display display = Assertions.checkNotNull(windowManager).getDefaultDisplay(); phoneOrientationListener = new PhoneOrientationListener(display, touchTracker, renderer); @@ -129,30 +138,16 @@ public final class SphericalSurfaceView extends GLSurfaceView { setEGLContextClientVersion(2); setRenderer(renderer); setOnTouchListener(touchTracker); - - setStereoMode(C.STEREO_MODE_MONO); } /** - * Sets stereo mode of the media to be played. + * Sets the default stereo mode. If the played video doesn't contain a stereo mode the default one + * is used. * - * @param stereoMode One of {@link C#STEREO_MODE_MONO}, {@link C#STEREO_MODE_TOP_BOTTOM}, {@link - * C#STEREO_MODE_LEFT_RIGHT}. + * @param stereoMode A {@link C.StereoMode} value. */ - public void setStereoMode(@C.StereoMode int stereoMode) { - Assertions.checkState( - stereoMode == C.STEREO_MODE_MONO - || stereoMode == C.STEREO_MODE_TOP_BOTTOM - || stereoMode == C.STEREO_MODE_LEFT_RIGHT); - Mesh mesh = - Mesh.createUvSphere( - SPHERE_RADIUS_METERS, - DEFAULT_SPHERE_ROWS, - DEFAULT_SPHERE_COLUMNS, - DEFAULT_SPHERE_VERTICAL_DEGREES, - DEFAULT_SPHERE_HORIZONTAL_DEGREES, - stereoMode); - queueEvent(() -> renderer.scene.setMesh(mesh)); + public void setDefaultStereoMode(@C.StereoMode int stereoMode) { + defaultStereoMode = stereoMode; } /** Returns the {@link Surface} associated with this view. */ @@ -169,6 +164,34 @@ public final class SphericalSurfaceView extends GLSurfaceView { surfaceListener = listener; } + /** Sets the {@link SingleTapListener} used to listen to single tap events on this view. */ + public void setSingleTapListener(@Nullable SingleTapListener listener) { + touchTracker.setSingleTapListener(listener); + } + + // VideoFrameMetadataListener implementation. + + @Override + public void onVideoFrameAboutToBeRendered( + long presentationTimeUs, long releaseTimeNs, Format format) { + sampleTimestampQueue.add(releaseTimeNs, presentationTimeUs); + setProjection(format.projectionData, format.stereoMode, releaseTimeNs); + } + + // CameraMotionListener implementation. + + @Override + public void onCameraMotion(long timeUs, float[] rotation) { + frameRotationQueue.setRotation(timeUs, rotation); + } + + @Override + public void onCameraMotionReset() { + sampleTimestampQueue.clear(); + frameRotationQueue.reset(); + queueEvent(renderer.scene::resetRotation); + } + @Override public void onResume() { super.onResume(); @@ -230,6 +253,35 @@ public final class SphericalSurfaceView extends GLSurfaceView { } } + /** + * Sets projection data and stereo mode of the media to be played. + * + * @param projectionData Contains the projection data to be rendered. + * @param stereoMode A {@link C.StereoMode} value. + * @param timeNs When then new projection should be used. + */ + private void setProjection( + @Nullable byte[] projectionData, @C.StereoMode int stereoMode, long timeNs) { + byte[] oldProjectionData = currentProjectionData; + int oldStereoMode = currentStereoMode; + currentProjectionData = projectionData; + currentStereoMode = stereoMode == Format.NO_VALUE ? defaultStereoMode : stereoMode; + if (oldStereoMode == currentStereoMode + && Arrays.equals(oldProjectionData, currentProjectionData)) { + return; + } + + Projection projectionFromData = null; + if (currentProjectionData != null) { + projectionFromData = ProjectionDecoder.decode(currentProjectionData, currentStereoMode); + } + Projection projection = + projectionFromData != null && ProjectionRenderer.isSupported(projectionFromData) + ? projectionFromData + : Projection.createEquirectangular(currentStereoMode); + queueEvent(() -> renderer.scene.setProjection(projection, timeNs)); + } + /** Detects sensor events and saves them as a matrix. */ private static class PhoneOrientationListener implements SensorEventListener { private final float[] phoneInWorldSpaceMatrix = new float[16]; @@ -327,8 +379,8 @@ public final class SphericalSurfaceView extends GLSurfaceView { private final float[] viewMatrix = new float[16]; private final float[] tempMatrix = new float[16]; - public Renderer() { - scene = new SceneRenderer(); + public Renderer(SceneRenderer scene) { + this.scene = scene; Matrix.setIdentityM(deviceOrientationMatrix, 0); Matrix.setIdentityM(touchPitchMatrix, 0); Matrix.setIdentityM(touchYawMatrix, 0); diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/TouchTracker.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/TouchTracker.java index ea3a0b4e16..335f611b58 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/TouchTracker.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/TouchTracker.java @@ -15,8 +15,11 @@ */ package com.google.android.exoplayer2.ui.spherical; +import android.content.Context; import android.graphics.PointF; import android.support.annotation.BinderThread; +import android.support.annotation.Nullable; +import android.view.GestureDetector; import android.view.MotionEvent; import android.view.View; @@ -42,7 +45,8 @@ import android.view.View; * Mesh as the user moves their finger. However, that requires quaternion interpolation. */ // @VisibleForTesting -/*package*/ class TouchTracker implements View.OnTouchListener { +/*package*/ class TouchTracker extends GestureDetector.SimpleOnGestureListener + implements View.OnTouchListener { /*package*/ interface Listener { void onScrollChange(PointF scrollOffsetDegrees); @@ -58,16 +62,27 @@ import android.view.View; private final Listener listener; private final float pxPerDegrees; + private final GestureDetector gestureDetector; // The conversion from touch to yaw & pitch requires compensating for device roll. This is set // on the sensor thread and read on the UI thread. private volatile float roll; + private @Nullable SingleTapListener singleTapListener; - public TouchTracker(Listener listener, float pxPerDegrees) { + @SuppressWarnings({ + "nullness:assignment.type.incompatible", + "nullness:argument.type.incompatible" + }) + public TouchTracker(Context context, Listener listener, float pxPerDegrees) { this.listener = listener; this.pxPerDegrees = pxPerDegrees; + gestureDetector = new GestureDetector(context, this); roll = SphericalSurfaceView.UPRIGHT_ROLL; } + public void setSingleTapListener(@Nullable SingleTapListener listener) { + singleTapListener = listener; + } + /** * Converts ACTION_MOVE events to pitch & yaw events while compensating for device roll. * @@ -75,36 +90,46 @@ import android.view.View; */ @Override public boolean onTouch(View v, MotionEvent event) { - switch (event.getAction()) { - case MotionEvent.ACTION_DOWN: - // Initialize drag gesture. - previousTouchPointPx.set(event.getX(), event.getY()); - return true; - case MotionEvent.ACTION_MOVE: - // Calculate the touch delta in screen space. - float touchX = (event.getX() - previousTouchPointPx.x) / pxPerDegrees; - float touchY = (event.getY() - previousTouchPointPx.y) / pxPerDegrees; - previousTouchPointPx.set(event.getX(), event.getY()); + return gestureDetector.onTouchEvent(event); + } - float r = roll; // Copy volatile state. - float cr = (float) Math.cos(r); - float sr = (float) Math.sin(r); - // To convert from screen space to the 3D space, we need to adjust the drag vector based - // on the roll of the phone. This is standard rotationMatrix(roll) * vector math but has - // an inverted y-axis due to the screen-space coordinates vs GL coordinates. - // Handle yaw. - accumulatedTouchOffsetDegrees.x -= cr * touchX - sr * touchY; - // Handle pitch and limit it to 45 degrees. - accumulatedTouchOffsetDegrees.y += sr * touchX + cr * touchY; - accumulatedTouchOffsetDegrees.y = - Math.max( - -MAX_PITCH_DEGREES, Math.min(MAX_PITCH_DEGREES, accumulatedTouchOffsetDegrees.y)); + @Override + public boolean onDown(MotionEvent e) { + // Initialize drag gesture. + previousTouchPointPx.set(e.getX(), e.getY()); + return true; + } - listener.onScrollChange(accumulatedTouchOffsetDegrees); - return true; - default: - return false; + @Override + public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { + // Calculate the touch delta in screen space. + float touchX = (e2.getX() - previousTouchPointPx.x) / pxPerDegrees; + float touchY = (e2.getY() - previousTouchPointPx.y) / pxPerDegrees; + previousTouchPointPx.set(e2.getX(), e2.getY()); + + float r = roll; // Copy volatile state. + float cr = (float) Math.cos(r); + float sr = (float) Math.sin(r); + // To convert from screen space to the 3D space, we need to adjust the drag vector based + // on the roll of the phone. This is standard rotationMatrix(roll) * vector math but has + // an inverted y-axis due to the screen-space coordinates vs GL coordinates. + // Handle yaw. + accumulatedTouchOffsetDegrees.x -= cr * touchX - sr * touchY; + // Handle pitch and limit it to 45 degrees. + accumulatedTouchOffsetDegrees.y += sr * touchX + cr * touchY; + accumulatedTouchOffsetDegrees.y = + Math.max(-MAX_PITCH_DEGREES, Math.min(MAX_PITCH_DEGREES, accumulatedTouchOffsetDegrees.y)); + + listener.onScrollChange(accumulatedTouchOffsetDegrees); + return true; + } + + @Override + public boolean onSingleTapUp(MotionEvent e) { + if (singleTapListener != null) { + return singleTapListener.onSingleTapUp(e); } + return false; } @BinderThread diff --git a/library/ui/src/main/res/layout/exo_playback_control_view.xml b/library/ui/src/main/res/layout/exo_playback_control_view.xml index 159844c234..534655f2f4 100644 --- a/library/ui/src/main/res/layout/exo_playback_control_view.xml +++ b/library/ui/src/main/res/layout/exo_playback_control_view.xml @@ -14,12 +14,14 @@ limitations under the License. --> + android:orientation="vertical" + tools:targetApi="28"> + android:name="androidx.test.runner.AndroidJUnitRunner"/> diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/CommonEncryptionDrmTest.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/CommonEncryptionDrmTest.java index a4cd35911b..1832e16a98 100644 --- a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/CommonEncryptionDrmTest.java +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/CommonEncryptionDrmTest.java @@ -15,26 +15,27 @@ */ package com.google.android.exoplayer2.playbacktests.gts; -import android.test.ActivityInstrumentationTestCase2; +import static androidx.test.InstrumentationRegistry.getInstrumentation; + +import androidx.test.rule.ActivityTestRule; +import androidx.test.runner.AndroidJUnit4; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.testutil.ActionSchedule; import com.google.android.exoplayer2.testutil.HostActivity; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; -/** - * Test playback of encrypted DASH streams using different CENC scheme types. - */ -public final class CommonEncryptionDrmTest extends ActivityInstrumentationTestCase2 { +/** Test playback of encrypted DASH streams using different CENC scheme types. */ +@RunWith(AndroidJUnit4.class) +public final class CommonEncryptionDrmTest { private static final String TAG = "CencDrmTest"; - private static final String URL_cenc = - "https://storage.googleapis.com/exoplayer-test-media-1/gts/tears-cenc.mpd"; - private static final String URL_cbc1 = - "https://storage.googleapis.com/exoplayer-test-media-1/gts/tears-aes-cbc1.mpd"; - private static final String URL_cbcs = - "https://storage.googleapis.com/exoplayer-test-media-1/gts/tears-aes-cbcs.mpd"; private static final String ID_AUDIO = "0"; private static final String[] IDS_VIDEO = new String[] {"1", "2"}; @@ -44,37 +45,38 @@ public final class CommonEncryptionDrmTest extends ActivityInstrumentationTestCa .seekAndWait(270000).delay(10000).seekAndWait(200000).delay(10000).seekAndWait(732000) .build(); + @Rule public ActivityTestRule testRule = new ActivityTestRule<>(HostActivity.class); + private DashTestRunner testRunner; - public CommonEncryptionDrmTest() { - super(HostActivity.class); + @Before + public void setUp() { + testRunner = + new DashTestRunner(TAG, testRule.getActivity(), getInstrumentation()) + .setWidevineInfo(MimeTypes.VIDEO_H264, false) + .setActionSchedule(ACTION_SCHEDULE_WITH_SEEKS) + .setAudioVideoFormats(ID_AUDIO, IDS_VIDEO) + .setCanIncludeAdditionalVideoFormats(true); } - @Override - protected void setUp() throws Exception { - super.setUp(); - - testRunner = new DashTestRunner(TAG, getActivity(), getInstrumentation()) - .setWidevineInfo(MimeTypes.VIDEO_H264, false) - .setActionSchedule(ACTION_SCHEDULE_WITH_SEEKS) - .setAudioVideoFormats(ID_AUDIO, IDS_VIDEO) - .setCanIncludeAdditionalVideoFormats(true); - } - - @Override - protected void tearDown() throws Exception { + @After + public void tearDown() { testRunner = null; - super.tearDown(); } + @Test public void testCencSchemeTypeV18() { if (Util.SDK_INT < 18) { // Pass. return; } - testRunner.setStreamName("test_widevine_h264_scheme_cenc").setManifestUrl(URL_cenc).run(); + testRunner + .setStreamName("test_widevine_h264_scheme_cenc") + .setManifestUrl(DashTestData.WIDEVINE_SCHEME_CENC) + .run(); } + @Test public void testCbc1SchemeTypeV25() { if (Util.SDK_INT < 25) { // cbc1 support was added in API 24, but it is stable from API 25 onwards. @@ -82,9 +84,13 @@ public final class CommonEncryptionDrmTest extends ActivityInstrumentationTestCa // Pass. return; } - testRunner.setStreamName("test_widevine_h264_scheme_cbc1").setManifestUrl(URL_cbc1).run(); + testRunner + .setStreamName("test_widevine_h264_scheme_cbc1") + .setManifestUrl(DashTestData.WIDEVINE_SCHEME_CBC1) + .run(); } + @Test public void testCbcsSchemeTypeV25() { if (Util.SDK_INT < 25) { // cbcs support was added in API 24, but it is stable from API 25 onwards. @@ -92,11 +98,14 @@ public final class CommonEncryptionDrmTest extends ActivityInstrumentationTestCa // Pass. return; } - testRunner.setStreamName("test_widevine_h264_scheme_cbcs").setManifestUrl(URL_cbcs).run(); + testRunner + .setStreamName("test_widevine_h264_scheme_cbcs") + .setManifestUrl(DashTestData.WIDEVINE_SCHEME_CBCS) + .run(); } + @Test public void testCensSchemeTypeV25() { // TODO: Implement once content is available. Track [internal: b/31219813]. } - } 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 79d39096c5..0dd05e7fd3 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,10 +15,12 @@ */ package com.google.android.exoplayer2.playbacktests.gts; +import static androidx.test.InstrumentationRegistry.getInstrumentation; import static com.google.common.truth.Truth.assertWithMessage; import android.net.Uri; -import android.test.ActivityInstrumentationTestCase2; +import androidx.test.rule.ActivityTestRule; +import androidx.test.runner.AndroidJUnit4; import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.source.dash.DashUtil; @@ -37,36 +39,38 @@ 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.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; -/** - * Tests downloaded DASH playbacks. - */ -public final class DashDownloadTest extends ActivityInstrumentationTestCase2 { +/** Tests downloaded DASH playbacks. */ +@RunWith(AndroidJUnit4.class) +public final class DashDownloadTest { private static final String TAG = "DashDownloadTest"; private static final Uri MANIFEST_URI = Uri.parse(DashTestData.H264_MANIFEST); + @Rule public ActivityTestRule testRule = new ActivityTestRule<>(HostActivity.class); + private DashTestRunner testRunner; private File tempFolder; private SimpleCache cache; private DefaultHttpDataSourceFactory httpDataSourceFactory; private CacheDataSourceFactory offlineDataSourceFactory; - public DashDownloadTest() { - super(HostActivity.class); - } - - @Override - protected void setUp() throws Exception { - super.setUp(); - testRunner = new DashTestRunner(TAG, getActivity(), getInstrumentation()) - .setManifestUrl(DashTestData.H264_MANIFEST) - .setFullPlaybackNoSeeking(true) - .setCanIncludeAdditionalVideoFormats(false) - .setAudioVideoFormats(DashTestData.AAC_AUDIO_REPRESENTATION_ID, - DashTestData.H264_CDD_FIXED); - tempFolder = Util.createTempDirectory(getActivity(), "ExoPlayerTest"); + @Before + public void setUp() throws Exception { + testRunner = + new DashTestRunner(TAG, testRule.getActivity(), getInstrumentation()) + .setManifestUrl(DashTestData.H264_MANIFEST) + .setFullPlaybackNoSeeking(true) + .setCanIncludeAdditionalVideoFormats(false) + .setAudioVideoFormats( + DashTestData.AAC_AUDIO_REPRESENTATION_ID, DashTestData.H264_CDD_FIXED); + tempFolder = Util.createTempDirectory(testRule.getActivity(), "ExoPlayerTest"); cache = new SimpleCache(tempFolder, new NoOpCacheEvictor()); httpDataSourceFactory = new DefaultHttpDataSourceFactory("ExoPlayer", null); offlineDataSourceFactory = @@ -74,16 +78,16 @@ public final class DashDownloadTest extends ActivityInstrumentationTestCase2 { +/** Tests DASH playbacks using {@link ExoPlayer}. */ +@RunWith(AndroidJUnit4.class) +public final class DashStreamingTest { private static final String TAG = "DashStreamingTest"; @@ -78,27 +84,24 @@ public final class DashStreamingTest extends ActivityInstrumentationTestCase2 testRule = new ActivityTestRule<>(HostActivity.class); + private DashTestRunner testRunner; - public DashStreamingTest() { - super(HostActivity.class); + @Before + public void setUp() { + testRunner = new DashTestRunner(TAG, testRule.getActivity(), getInstrumentation()); } - @Override - protected void setUp() throws Exception { - super.setUp(); - testRunner = new DashTestRunner(TAG, getActivity(), getInstrumentation()); - } - - @Override - protected void tearDown() throws Exception { + @After + public void tearDown() { testRunner = null; - super.tearDown(); } // H264 CDD. + @Test public void testH264Fixed() { if (Util.SDK_INT < 16) { // Pass. @@ -113,6 +116,7 @@ public final class DashStreamingTest extends ActivityInstrumentationTestCase2 drmSessionManager) { SimpleExoPlayer player = ExoPlayerFactory.newSimpleInstance( - host, new DebugRenderersFactory(host), trackSelector, drmSessionManager); + host, + new DebugRenderersFactory(host), + trackSelector, + new DefaultLoadControl(), + drmSessionManager); player.setVideoSurface(surface); return player; } @Override - protected MediaSource buildSource( - HostActivity host, String userAgent, TransferListener mediaTransferListener) { - DataSource.Factory manifestDataSourceFactory = dataSourceFactory != null - ? dataSourceFactory : new DefaultDataSourceFactory(host, userAgent); - DataSource.Factory mediaDataSourceFactory = dataSourceFactory != null - ? dataSourceFactory - : new DefaultDataSourceFactory(host, userAgent, mediaTransferListener); + protected MediaSource buildSource(HostActivity host, String userAgent) { + DataSource.Factory dataSourceFactory = + this.dataSourceFactory != null + ? this.dataSourceFactory + : new DefaultDataSourceFactory(host, userAgent); Uri manifestUri = Uri.parse(manifestUrl); - DefaultDashChunkSource.Factory chunkSourceFactory = new DefaultDashChunkSource.Factory( - mediaDataSourceFactory); - return new DashMediaSource.Factory(chunkSourceFactory, manifestDataSourceFactory) - .setMinLoadableRetryCount(MIN_LOADABLE_RETRY_COUNT) - .setLivePresentationDelayMs(0) + return new DashMediaSource.Factory(dataSourceFactory) + .setLoadErrorHandlingPolicy(new DefaultLoadErrorHandlingPolicy(MIN_LOADABLE_RETRY_COUNT)) .createMediaSource(manifestUri); } diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashWidevineOfflineTest.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashWidevineOfflineTest.java index 95c9cfbf97..7beaafd143 100644 --- a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashWidevineOfflineTest.java +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashWidevineOfflineTest.java @@ -15,13 +15,16 @@ */ package com.google.android.exoplayer2.playbacktests.gts; +import static androidx.test.InstrumentationRegistry.getInstrumentation; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; +import static org.junit.Assert.fail; import android.media.MediaDrm.MediaDrmStateException; import android.net.Uri; -import android.test.ActivityInstrumentationTestCase2; import android.util.Pair; +import androidx.test.rule.ActivityTestRule; +import androidx.test.runner.AndroidJUnit4; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.drm.DrmInitData; import com.google.android.exoplayer2.drm.DrmSession.DrmSessionException; @@ -36,11 +39,15 @@ import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; import java.io.IOException; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; -/** - * Tests Widevine encrypted DASH playbacks using offline keys. - */ -public final class DashWidevineOfflineTest extends ActivityInstrumentationTestCase2 { +/** Tests Widevine encrypted DASH playbacks using offline keys. */ +@RunWith(AndroidJUnit4.class) +public final class DashWidevineOfflineTest { private static final String TAG = "DashWidevineOfflineTest"; private static final String USER_AGENT = "ExoPlayerPlaybackTests"; @@ -50,21 +57,20 @@ public final class DashWidevineOfflineTest extends ActivityInstrumentationTestCa private OfflineLicenseHelper offlineLicenseHelper; private byte[] offlineLicenseKeySetId; - public DashWidevineOfflineTest() { - super(HostActivity.class); - } + @Rule public ActivityTestRule testRule = new ActivityTestRule<>(HostActivity.class); - @Override - protected void setUp() throws Exception { - super.setUp(); - testRunner = new DashTestRunner(TAG, getActivity(), getInstrumentation()) - .setStreamName("test_widevine_h264_fixed_offline") - .setManifestUrl(DashTestData.WIDEVINE_H264_MANIFEST) - .setWidevineInfo(MimeTypes.VIDEO_H264, true) - .setFullPlaybackNoSeeking(true) - .setCanIncludeAdditionalVideoFormats(false) - .setAudioVideoFormats(DashTestData.WIDEVINE_AAC_AUDIO_REPRESENTATION_ID, - DashTestData.WIDEVINE_H264_CDD_FIXED); + @Before + public void setUp() throws Exception { + testRunner = + new DashTestRunner(TAG, testRule.getActivity(), getInstrumentation()) + .setStreamName("test_widevine_h264_fixed_offline") + .setManifestUrl(DashTestData.WIDEVINE_H264_MANIFEST) + .setWidevineInfo(MimeTypes.VIDEO_H264, true) + .setFullPlaybackNoSeeking(true) + .setCanIncludeAdditionalVideoFormats(false) + .setAudioVideoFormats( + DashTestData.WIDEVINE_AAC_AUDIO_REPRESENTATION_ID, + DashTestData.WIDEVINE_H264_CDD_FIXED); boolean useL1Widevine = DashTestRunner.isL1WidevineAvailable(MimeTypes.VIDEO_H264); String widevineLicenseUrl = DashTestData.getWidevineLicenseUrl(true, useL1Widevine); @@ -75,8 +81,8 @@ public final class DashWidevineOfflineTest extends ActivityInstrumentationTestCa } } - @Override - protected void tearDown() throws Exception { + @After + public void tearDown() throws Exception { testRunner = null; if (offlineLicenseKeySetId != null) { releaseLicense(); @@ -86,11 +92,11 @@ public final class DashWidevineOfflineTest extends ActivityInstrumentationTestCa } offlineLicenseHelper = null; httpDataSourceFactory = null; - super.tearDown(); } // Offline license tests + @Test public void testWidevineOfflineLicenseV22() throws Exception { if (Util.SDK_INT < 22) { return; // Pass. @@ -103,6 +109,7 @@ public final class DashWidevineOfflineTest extends ActivityInstrumentationTestCa assertThat(offlineLicenseKeySetId).isNotNull(); } + @Test public void testWidevineOfflineReleasedLicenseV22() throws Throwable { if (Util.SDK_INT < 22) { return; // Pass. @@ -129,6 +136,7 @@ public final class DashWidevineOfflineTest extends ActivityInstrumentationTestCa } } + @Test public void testWidevineOfflineExpiredLicenseV22() throws Exception { if (Util.SDK_INT < 22) { return; // Pass. @@ -158,6 +166,7 @@ public final class DashWidevineOfflineTest extends ActivityInstrumentationTestCa testRunner.run(); } + @Test public void testWidevineOfflineLicenseExpiresOnPauseV22() throws Exception { if (Util.SDK_INT < 22) { return; // Pass. diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/EnumerateDecodersTest.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/EnumerateDecodersTest.java index 5157ab672c..b9c513fe72 100644 --- a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/EnumerateDecodersTest.java +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/EnumerateDecodersTest.java @@ -15,11 +15,13 @@ */ package com.google.android.exoplayer2.playbacktests.gts; +import static androidx.test.InstrumentationRegistry.getInstrumentation; + import android.media.MediaCodecInfo.AudioCapabilities; import android.media.MediaCodecInfo.CodecCapabilities; import android.media.MediaCodecInfo.CodecProfileLevel; import android.media.MediaCodecInfo.VideoCapabilities; -import android.test.InstrumentationTestCase; +import androidx.test.runner.AndroidJUnit4; import com.google.android.exoplayer2.mediacodec.MediaCodecInfo; import com.google.android.exoplayer2.mediacodec.MediaCodecUtil; import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException; @@ -29,9 +31,13 @@ import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; import java.util.Arrays; import java.util.List; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; /** Tests enumeration of decoders using {@link MediaCodecUtil}. */ -public class EnumerateDecodersTest extends InstrumentationTestCase { +@RunWith(AndroidJUnit4.class) +public class EnumerateDecodersTest { private static final String TAG = "EnumerateDecodersTest"; @@ -40,14 +46,14 @@ public class EnumerateDecodersTest extends InstrumentationTestCase { private MetricsLogger metricsLogger; - @Override - protected void setUp() throws Exception { - super.setUp(); + @Before + public void setUp() { metricsLogger = MetricsLogger.Factory.createDefault( getInstrumentation(), TAG, REPORT_NAME, REPORT_OBJECT_NAME); } + @Test public void testEnumerateDecoders() throws Exception { enumerateDecoders(MimeTypes.VIDEO_H263); enumerateDecoders(MimeTypes.VIDEO_H264); @@ -82,33 +88,44 @@ public class EnumerateDecodersTest extends InstrumentationTestCase { } private void enumerateDecoders(String mimeType) throws DecoderQueryException { - logDecoderInfos(MediaCodecUtil.getDecoderInfos(mimeType, /* secure= */ false)); - logDecoderInfos(MediaCodecUtil.getDecoderInfos(mimeType, /* secure= */ true)); + logDecoderInfos(mimeType, /* secure= */ false); + logDecoderInfos(mimeType, /* secure= */ true); } - private void logDecoderInfos(List mediaCodecInfos) { + private void logDecoderInfos(String mimeType, boolean secure) throws DecoderQueryException { + List mediaCodecInfos = MediaCodecUtil.getDecoderInfos(mimeType, secure); for (MediaCodecInfo mediaCodecInfo : mediaCodecInfos) { CodecCapabilities capabilities = Assertions.checkNotNull(mediaCodecInfo.capabilities); metricsLogger.logMetric( - "capabilities_" + mediaCodecInfo.name, codecCapabilitiesToString(capabilities)); + "capabilities_" + mediaCodecInfo.name, codecCapabilitiesToString(mimeType, capabilities)); } } - private static String codecCapabilitiesToString(CodecCapabilities codecCapabilities) { - String mimeType = codecCapabilities.getMimeType(); - boolean isVideo = MimeTypes.isVideo(mimeType); - boolean isAudio = MimeTypes.isAudio(mimeType); + private static String codecCapabilitiesToString( + String requestedMimeType, CodecCapabilities codecCapabilities) { + boolean isVideo = MimeTypes.isVideo(requestedMimeType); + boolean isAudio = MimeTypes.isAudio(requestedMimeType); StringBuilder result = new StringBuilder(); - result.append("[mimeType=").append(mimeType).append(", profileLevels="); - profileLevelsToString(codecCapabilities.profileLevels, result); - result.append(", maxSupportedInstances=").append(codecCapabilities.getMaxSupportedInstances()); - if (isVideo) { - result.append(", videoCapabilities="); - videoCapabilitiesToString(codecCapabilities.getVideoCapabilities(), result); - result.append(", colorFormats=").append(Arrays.toString(codecCapabilities.colorFormats)); - } else if (isAudio) { - result.append(", audioCapabilities="); - audioCapabilitiesToString(codecCapabilities.getAudioCapabilities(), result); + result.append("[requestedMimeType=").append(requestedMimeType); + if (Util.SDK_INT >= 21) { + result.append(", mimeType=").append(codecCapabilities.getMimeType()); + } + result.append(", profileLevels="); + appendProfileLevels(codecCapabilities.profileLevels, result); + if (Util.SDK_INT >= 23) { + result + .append(", maxSupportedInstances=") + .append(codecCapabilities.getMaxSupportedInstances()); + } + if (Util.SDK_INT >= 21) { + if (isVideo) { + result.append(", videoCapabilities="); + appendVideoCapabilities(codecCapabilities.getVideoCapabilities(), result); + result.append(", colorFormats=").append(Arrays.toString(codecCapabilities.colorFormats)); + } else if (isAudio) { + result.append(", audioCapabilities="); + appendAudioCapabilities(codecCapabilities.getAudioCapabilities(), result); + } } if (Util.SDK_INT >= 19 && isVideo @@ -134,7 +151,7 @@ public class EnumerateDecodersTest extends InstrumentationTestCase { return result.toString(); } - private static void audioCapabilitiesToString( + private static void appendAudioCapabilities( AudioCapabilities audioCapabilities, StringBuilder result) { result .append("[bitrateRange=") @@ -146,7 +163,7 @@ public class EnumerateDecodersTest extends InstrumentationTestCase { .append(']'); } - private static void videoCapabilitiesToString( + private static void appendVideoCapabilities( VideoCapabilities videoCapabilities, StringBuilder result) { result .append("[bitrateRange=") @@ -164,8 +181,7 @@ public class EnumerateDecodersTest extends InstrumentationTestCase { .append(']'); } - private static void profileLevelsToString( - CodecProfileLevel[] profileLevels, StringBuilder result) { + private static void appendProfileLevels(CodecProfileLevel[] profileLevels, StringBuilder result) { result.append('['); int count = profileLevels.length; for (int i = 0; i < count; 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 a6f672e54d..f06fcc3add 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 @@ -34,6 +34,7 @@ import com.google.android.exoplayer2.testutil.ActionSchedule.PlayerRunnable; import com.google.android.exoplayer2.testutil.ActionSchedule.PlayerTarget; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.Parameters; +import com.google.android.exoplayer2.util.ConditionVariable; import com.google.android.exoplayer2.util.HandlerWrapper; /** @@ -469,12 +470,8 @@ public abstract class Action { SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) { player .createMessage( - new Target() { - @Override - public void handleMessage(int messageType, Object payload) - throws ExoPlaybackException { - throw exception; - } + (messageType, payload) -> { + throw exception; }) .send(); } @@ -507,14 +504,22 @@ public abstract class Action { final Surface surface, final HandlerWrapper handler, final ActionNode nextAction) { - // Schedule one message on the playback thread to pause the player immediately. + Handler testThreadHandler = new Handler(); + // Schedule a message on the playback thread to ensure the player is paused immediately. player .createMessage( - new Target() { - @Override - public void handleMessage(int messageType, Object payload) - throws ExoPlaybackException { - player.setPlayWhenReady(/* playWhenReady= */ false); + (messageType, payload) -> { + // Block playback thread until pause command has been sent from test thread. + ConditionVariable blockPlaybackThreadCondition = new ConditionVariable(); + testThreadHandler.post( + () -> { + player.setPlayWhenReady(/* playWhenReady= */ false); + blockPlaybackThreadCondition.open(); + }); + try { + blockPlaybackThreadCondition.block(); + } catch (InterruptedException e) { + // Ignore. } }) .setPosition(windowIndex, positionMs) @@ -522,15 +527,10 @@ public abstract class Action { // Schedule another message on this test thread to continue action schedule. player .createMessage( - new Target() { - @Override - public void handleMessage(int messageType, Object payload) - throws ExoPlaybackException { - nextAction.schedule(player, trackSelector, surface, handler); - } - }) + (messageType, payload) -> + nextAction.schedule(player, trackSelector, surface, handler)) .setPosition(windowIndex, positionMs) - .setHandler(new Handler()) + .setHandler(testThreadHandler) .send(); player.setPlayWhenReady(true); } 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 74fa13ece1..39a95af36d 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 @@ -610,13 +610,7 @@ public final class ActionSchedule { ActionNode nextAction) { Assertions.checkArgument(nextAction == null); if (callback != null) { - handler.post( - new Runnable() { - @Override - public void run() { - callback.onActionScheduleFinished(); - } - }); + handler.post(() -> callback.onActionScheduleFinished()); } } 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 02a8a0597d..627b5b72f3 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 @@ -126,17 +126,33 @@ public class DebugRenderersFactory extends DefaultRenderersFactory { } @Override - protected boolean processOutputBuffer(long positionUs, long elapsedRealtimeUs, MediaCodec codec, - ByteBuffer buffer, int bufferIndex, int bufferFlags, long bufferPresentationTimeUs, - boolean shouldSkip) throws ExoPlaybackException { + protected boolean processOutputBuffer( + long positionUs, + long elapsedRealtimeUs, + MediaCodec codec, + ByteBuffer buffer, + int bufferIndex, + int bufferFlags, + long bufferPresentationTimeUs, + boolean shouldSkip, + Format format) + throws ExoPlaybackException { if (skipToPositionBeforeRenderingFirstFrame && bufferPresentationTimeUs < positionUs) { // After the codec has been initialized, don't render the first frame until we've caught up // to the playback position. Else test runs on devices that do not support dummy surface // will drop frames between rendering the first one and catching up [Internal: b/66494991]. shouldSkip = true; } - return super.processOutputBuffer(positionUs, elapsedRealtimeUs, codec, buffer, bufferIndex, - bufferFlags, bufferPresentationTimeUs, shouldSkip); + return super.processOutputBuffer( + positionUs, + elapsedRealtimeUs, + codec, + buffer, + bufferIndex, + bufferFlags, + bufferPresentationTimeUs, + shouldSkip, + format); } @Override diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/DummyMainThread.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/DummyMainThread.java index 8f65dc876a..858d287196 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/DummyMainThread.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/DummyMainThread.java @@ -60,12 +60,9 @@ public final class DummyMainThread { } else { final ConditionVariable finishedCondition = new ConditionVariable(); handler.post( - new Runnable() { - @Override - public void run() { - runnable.run(); - finishedCondition.open(); - } + () -> { + runnable.run(); + finishedCondition.open(); }); assertThat(finishedCondition.block(timeoutMs)).isTrue(); } 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 00e2943086..efee52a472 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 @@ -22,15 +22,16 @@ import android.os.Looper; import android.os.SystemClock; import android.util.Log; import android.view.Surface; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.DefaultLoadControl; import com.google.android.exoplayer2.DefaultRenderersFactory; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayerFactory; -import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.RenderersFactory; import com.google.android.exoplayer2.SimpleExoPlayer; -import com.google.android.exoplayer2.audio.AudioRendererEventListener; +import com.google.android.exoplayer2.analytics.AnalyticsListener; import com.google.android.exoplayer2.audio.DefaultAudioSink; import com.google.android.exoplayer2.decoder.DecoderCounters; import com.google.android.exoplayer2.drm.DrmSessionManager; @@ -40,20 +41,13 @@ import com.google.android.exoplayer2.testutil.HostActivity.HostedTest; import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.MappingTrackSelector; -import com.google.android.exoplayer2.upstream.BandwidthMeter; -import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; -import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.util.Clock; +import com.google.android.exoplayer2.util.EventLogger; import com.google.android.exoplayer2.util.HandlerWrapper; import com.google.android.exoplayer2.util.Util; -import com.google.android.exoplayer2.video.VideoRendererEventListener; /** A {@link HostedTest} for {@link ExoPlayer} playback tests. */ -public abstract class ExoHostedTest - implements Player.EventListener, - HostedTest, - AudioRendererEventListener, - VideoRendererEventListener { +public abstract class ExoHostedTest implements AnalyticsListener, HostedTest { static { // DefaultAudioSink is able to work around spurious timestamps reported by the platform (by @@ -80,9 +74,7 @@ public abstract class ExoHostedTest private SimpleExoPlayer player; private Surface surface; private ExoPlaybackException playerError; - private Player.EventListener playerEventListener; - private VideoRendererEventListener videoDebugListener; - private AudioRendererEventListener audioDebugListener; + private AnalyticsListener analyticsListener; private boolean playerWasPrepared; private boolean playing; @@ -135,33 +127,11 @@ public abstract class ExoHostedTest } } - /** - * Sets an {@link Player.EventListener} to listen for ExoPlayer events during the test. - */ - public final void setEventListener(Player.EventListener eventListener) { - this.playerEventListener = eventListener; + /** Sets an {@link AnalyticsListener} to listen for events during the test. */ + public final void setAnalyticsListener(AnalyticsListener analyticsListener) { + this.analyticsListener = analyticsListener; if (player != null) { - player.addListener(eventListener); - } - } - - /** - * Sets an {@link VideoRendererEventListener} to listen for video debug events during the test. - */ - public final void setVideoDebugListener(VideoRendererEventListener videoDebugListener) { - this.videoDebugListener = videoDebugListener; - if (player != null) { - player.addVideoDebugListener(videoDebugListener); - } - } - - /** - * Sets an {@link AudioRendererEventListener} to listen for audio debug events during the test. - */ - public final void setAudioDebugListener(AudioRendererEventListener audioDebugListener) { - this.audioDebugListener = audioDebugListener; - if (player != null) { - player.addAudioDebugListener(audioDebugListener); + player.addAnalyticsListener(analyticsListener); } } @@ -171,24 +141,16 @@ public abstract class ExoHostedTest public final void onStart(HostActivity host, Surface surface) { this.surface = surface; // Build the player. - DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(); - trackSelector = buildTrackSelector(host, bandwidthMeter); + trackSelector = buildTrackSelector(host); String userAgent = "ExoPlayerPlaybackTests"; DrmSessionManager drmSessionManager = buildDrmSessionManager(userAgent); player = buildExoPlayer(host, surface, trackSelector, drmSessionManager); - player.prepare(buildSource(host, Util.getUserAgent(host, userAgent), bandwidthMeter)); - if (playerEventListener != null) { - player.addListener(playerEventListener); + player.prepare(buildSource(host, Util.getUserAgent(host, userAgent))); + player.addAnalyticsListener(this); + player.addAnalyticsListener(new EventLogger(trackSelector, tag)); + if (analyticsListener != null) { + player.addAnalyticsListener(analyticsListener); } - if (videoDebugListener != null) { - player.addVideoDebugListener(videoDebugListener); - } - if (audioDebugListener != null) { - player.addAudioDebugListener(audioDebugListener); - } - player.addListener(this); - player.addAudioDebugListener(this); - player.addVideoDebugListener(this); player.setPlayWhenReady(true); actionHandler = Clock.DEFAULT.createHandler(Looper.myLooper(), /* callback= */ null); // Schedule any pending actions. @@ -231,10 +193,11 @@ public abstract class ExoHostedTest assertPassed(audioDecoderCounters, videoDecoderCounters); } - // Player.EventListener + // AnalyticsListener @Override - public final void onPlayerStateChanged(boolean playWhenReady, int playbackState) { + public final void onPlayerStateChanged( + EventTime eventTime, boolean playWhenReady, int playbackState) { Log.d(tag, "state [" + playWhenReady + ", " + playbackState + "]"); playerWasPrepared |= playbackState != Player.STATE_IDLE; if (playbackState == Player.STATE_ENDED @@ -251,85 +214,20 @@ public abstract class ExoHostedTest } @Override - public final void onPlayerError(ExoPlaybackException error) { + public final void onPlayerError(EventTime eventTime, ExoPlaybackException error) { playerWasPrepared = true; playerError = error; onPlayerErrorInternal(error); } - // AudioRendererEventListener - @Override - public void onAudioEnabled(DecoderCounters counters) { - Log.d(tag, "audioEnabled"); - } - - @Override - public void onAudioSessionId(int audioSessionId) { - Log.d(tag, "audioSessionId [" + audioSessionId + "]"); - } - - @Override - public void onAudioDecoderInitialized(String decoderName, long elapsedRealtimeMs, - long initializationDurationMs) { - Log.d(tag, "audioDecoderInitialized [" + decoderName + "]"); - } - - @Override - public void onAudioInputFormatChanged(Format format) { - Log.d(tag, "audioFormatChanged [" + Format.toLogString(format) + "]"); - } - - @Override - public void onAudioDisabled(DecoderCounters counters) { - Log.d(tag, "audioDisabled"); - audioDecoderCounters.merge(counters); - } - - @Override - public void onAudioSinkUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) { - Log.e(tag, "audioTrackUnderrun [" + bufferSize + ", " + bufferSizeMs + ", " - + elapsedSinceLastFeedMs + "]", null); - } - - // VideoRendererEventListener - - @Override - public void onVideoEnabled(DecoderCounters counters) { - Log.d(tag, "videoEnabled"); - } - - @Override - public void onVideoDecoderInitialized(String decoderName, long elapsedRealtimeMs, - long initializationDurationMs) { - Log.d(tag, "videoDecoderInitialized [" + decoderName + "]"); - } - - @Override - public void onVideoInputFormatChanged(Format format) { - Log.d(tag, "videoFormatChanged [" + Format.toLogString(format) + "]"); - } - - @Override - public void onVideoDisabled(DecoderCounters counters) { - Log.d(tag, "videoDisabled"); - videoDecoderCounters.merge(counters); - } - - @Override - public void onDroppedFrames(int count, long elapsed) { - Log.d(tag, "droppedFrames [" + count + "]"); - } - - @Override - public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, - float pixelWidthHeightRatio) { - // Do nothing. - } - - @Override - public void onRenderedFirstFrame(Surface surface) { - // Do nothing. + public void onDecoderDisabled( + EventTime eventTime, int trackType, DecoderCounters decoderCounters) { + if (trackType == C.TRACK_TYPE_AUDIO) { + audioDecoderCounters.merge(decoderCounters); + } else if (trackType == C.TRACK_TYPE_VIDEO) { + videoDecoderCounters.merge(decoderCounters); + } } // Internal logic @@ -344,12 +242,7 @@ public abstract class ExoHostedTest player = null; // We post opening of the finished condition so that any events posted to the main thread as a // result of player.release() are guaranteed to be handled before the test returns. - actionHandler.post(new Runnable() { - @Override - public void run() { - testFinished.open(); - } - }); + actionHandler.post(testFinished::open); return true; } @@ -359,26 +252,30 @@ public abstract class ExoHostedTest } @SuppressWarnings("unused") - protected DefaultTrackSelector buildTrackSelector( - HostActivity host, BandwidthMeter bandwidthMeter) { - return new DefaultTrackSelector(new AdaptiveTrackSelection.Factory(bandwidthMeter)); + protected DefaultTrackSelector buildTrackSelector(HostActivity host) { + return new DefaultTrackSelector(new AdaptiveTrackSelection.Factory()); } @SuppressWarnings("unused") - protected SimpleExoPlayer buildExoPlayer(HostActivity host, Surface surface, + protected SimpleExoPlayer buildExoPlayer( + HostActivity host, + Surface surface, MappingTrackSelector trackSelector, DrmSessionManager drmSessionManager) { - RenderersFactory renderersFactory = new DefaultRenderersFactory(host, drmSessionManager, - DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF, 0); + RenderersFactory renderersFactory = + new DefaultRenderersFactory( + host, + DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF, + /* allowedVideoJoiningTimeMs= */ 0); SimpleExoPlayer player = - ExoPlayerFactory.newSimpleInstance(host, renderersFactory, trackSelector); + ExoPlayerFactory.newSimpleInstance( + host, renderersFactory, trackSelector, new DefaultLoadControl(), drmSessionManager); player.setVideoSurface(surface); return player; } @SuppressWarnings("unused") - protected abstract MediaSource buildSource( - HostActivity host, String userAgent, TransferListener mediaTransferListener); + protected abstract MediaSource buildSource(HostActivity host, String userAgent); @SuppressWarnings("unused") protected void onPlayerErrorInternal(ExoPlaybackException error) { 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 5ac071d9a2..b613f7f364 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 @@ -32,21 +32,16 @@ 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.TrackSelectionArray; import com.google.android.exoplayer2.trackselection.TrackSelector; +import com.google.android.exoplayer2.upstream.BandwidthMeter; import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer2.util.Clock; import com.google.android.exoplayer2.util.HandlerWrapper; import com.google.android.exoplayer2.util.MimeTypes; -import com.google.android.exoplayer2.video.VideoRendererEventListener; import java.util.ArrayList; import java.util.Arrays; import java.util.concurrent.CountDownLatch; @@ -81,13 +76,12 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc private MediaSource mediaSource; private DefaultTrackSelector trackSelector; private LoadControl loadControl; + private BandwidthMeter bandwidthMeter; private Format[] supportedFormats; private Renderer[] renderers; private RenderersFactory renderersFactory; private ActionSchedule actionSchedule; private Player.EventListener eventListener; - private VideoRendererEventListener videoRendererEventListener; - private AudioRendererEventListener audioRendererEventListener; private AnalyticsListener analyticsListener; private Integer expectedPlayerEndedCount; @@ -162,6 +156,18 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc return this; } + /** + * Sets the {@link BandwidthMeter} to be used by the test runner. The default value is a {@link + * DefaultBandwidthMeter} in its default configuration. + * + * @param bandwidthMeter The {@link BandwidthMeter} to be used by the test runner. + * @return This builder. + */ + public Builder setBandwidthMeter(BandwidthMeter bandwidthMeter) { + this.bandwidthMeter = bandwidthMeter; + return this; + } + /** * Sets a list of {@link Format}s to be used by a {@link FakeMediaSource} to create media * periods and for setting up a {@link FakeRenderer}. The default value is a single @@ -242,28 +248,6 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc return this; } - /** - * Sets a {@link VideoRendererEventListener} to be registered. - * - * @param eventListener A {@link VideoRendererEventListener} to be registered. - * @return This builder. - */ - public Builder setVideoRendererEventListener(VideoRendererEventListener eventListener) { - this.videoRendererEventListener = eventListener; - return this; - } - - /** - * Sets an {@link AudioRendererEventListener} to be registered. - * - * @param eventListener An {@link AudioRendererEventListener} to be registered. - * @return This builder. - */ - public Builder setAudioRendererEventListener(AudioRendererEventListener eventListener) { - this.audioRendererEventListener = eventListener; - return this; - } - /** * Sets an {@link AnalyticsListener} to be registered. * @@ -302,23 +286,20 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc if (trackSelector == null) { trackSelector = new DefaultTrackSelector(); } + if (bandwidthMeter == null) { + bandwidthMeter = new DefaultBandwidthMeter.Builder().build(); + } if (renderersFactory == null) { if (renderers == null) { renderers = new Renderer[] {new FakeRenderer(supportedFormats)}; } renderersFactory = - new RenderersFactory() { - @Override - public Renderer[] createRenderers( - android.os.Handler eventHandler, - VideoRendererEventListener videoRendererEventListener, - AudioRendererEventListener audioRendererEventListener, - TextOutput textRendererOutput, - MetadataOutput metadataRendererOutput, - DrmSessionManager drmSessionManager) { - return renderers; - } - }; + (eventHandler, + videoRendererEventListener, + audioRendererEventListener, + textRendererOutput, + metadataRendererOutput, + drmSessionManager) -> renderers; } if (loadControl == null) { loadControl = new DefaultLoadControl(); @@ -342,10 +323,9 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc renderersFactory, trackSelector, loadControl, + bandwidthMeter, actionSchedule, eventListener, - videoRendererEventListener, - audioRendererEventListener, analyticsListener, expectedPlayerEndedCount); } @@ -357,10 +337,9 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc private final RenderersFactory renderersFactory; private final DefaultTrackSelector trackSelector; private final LoadControl loadControl; + private final BandwidthMeter bandwidthMeter; 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; @@ -385,10 +364,9 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc RenderersFactory renderersFactory, DefaultTrackSelector trackSelector, LoadControl loadControl, + BandwidthMeter bandwidthMeter, @Nullable ActionSchedule actionSchedule, @Nullable Player.EventListener eventListener, - @Nullable VideoRendererEventListener videoRendererEventListener, - @Nullable AudioRendererEventListener audioRendererEventListener, @Nullable AnalyticsListener analyticsListener, int expectedPlayerEndedCount) { this.context = context; @@ -397,10 +375,9 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc this.renderersFactory = renderersFactory; this.trackSelector = trackSelector; this.loadControl = loadControl; + this.bandwidthMeter = bandwidthMeter; this.actionSchedule = actionSchedule; this.eventListener = eventListener; - this.videoRendererEventListener = videoRendererEventListener; - this.audioRendererEventListener = audioRendererEventListener; this.analyticsListener = analyticsListener; this.timelines = new ArrayList<>(); this.manifests = new ArrayList<>(); @@ -425,35 +402,25 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc */ public ExoPlayerTestRunner start() { handler.post( - new Runnable() { - @Override - public void run() { - try { - player = - new TestSimpleExoPlayer( - context, renderersFactory, trackSelector, loadControl, clock); - player.addListener(ExoPlayerTestRunner.this); - if (eventListener != null) { - player.addListener(eventListener); - } - if (videoRendererEventListener != null) { - player.addVideoDebugListener(videoRendererEventListener); - } - if (audioRendererEventListener != null) { - player.addAudioDebugListener(audioRendererEventListener); - } - if (analyticsListener != null) { - player.addAnalyticsListener(analyticsListener); - } - player.setPlayWhenReady(true); - if (actionSchedule != null) { - actionSchedule.start( - player, trackSelector, null, handler, ExoPlayerTestRunner.this); - } - player.prepare(mediaSource); - } catch (Exception e) { - handleException(e); + () -> { + try { + player = + new TestSimpleExoPlayer( + context, renderersFactory, trackSelector, loadControl, bandwidthMeter, clock); + player.addListener(ExoPlayerTestRunner.this); + if (eventListener != null) { + player.addListener(eventListener); } + if (analyticsListener != null) { + player.addAnalyticsListener(analyticsListener); + } + player.setPlayWhenReady(true); + if (actionSchedule != null) { + actionSchedule.start(player, trackSelector, null, handler, ExoPlayerTestRunner.this); + } + player.prepare(mediaSource); + } catch (Exception e) { + handleException(e); } }); return this; @@ -579,20 +546,18 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc // Private implementation details. private void release() throws InterruptedException { - handler.post(new Runnable() { - @Override - public void run() { - try { - if (player != null) { - player.release(); + handler.post( + () -> { + try { + if (player != null) { + player.release(); + } + } catch (Exception e) { + handleException(e); + } finally { + playerThread.quit(); } - } catch (Exception e) { - handleException(e); - } finally { - playerThread.quit(); - } - } - }); + }); playerThread.join(); } @@ -664,6 +629,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc RenderersFactory renderersFactory, TrackSelector trackSelector, LoadControl loadControl, + BandwidthMeter bandwidthMeter, Clock clock) { super( context, @@ -671,7 +637,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc trackSelector, loadControl, /* drmSessionManager= */ null, - new DefaultBandwidthMeter.Builder().build(), + bandwidthMeter, new AnalyticsCollector.Factory(), clock, Looper.myLooper()); 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 6218e4624d..f8bf950ef2 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 @@ -25,6 +25,7 @@ import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.chunk.ChunkSampleStream; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.upstream.Allocator; +import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy; import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.util.MimeTypes; import java.util.ArrayList; @@ -76,8 +77,13 @@ public class FakeAdaptiveMediaPeriod extends FakeMediaPeriod } @Override - public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, - SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { + @SuppressWarnings("unchecked") + public long selectTracks( + TrackSelection[] selections, + boolean[] mayRetainStreamFlags, + SampleStream[] streams, + boolean[] streamResetFlags, + long positionUs) { long returnPositionUs = super.selectTracks(selections, mayRetainStreamFlags, streams, streamResetFlags, positionUs); List> validStreams = new ArrayList<>(); @@ -137,13 +143,13 @@ public class FakeAdaptiveMediaPeriod extends FakeMediaPeriod chunkSourceFactory.createChunkSource(trackSelection, durationUs, transferListener); return new ChunkSampleStream<>( MimeTypes.getTrackType(trackSelection.getSelectedFormat().sampleMimeType), - null, - null, + /* embeddedTrackTypes= */ null, + /* embeddedTrackFormats= */ null, chunkSource, - this, + /* callback= */ this, allocator, - 0, - 3, + /* positionUs= */ 0, + new DefaultLoadErrorHandlingPolicy(/* minimumLoadableRetryCount= */ 3), eventDispatcher); } 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 6510c8b425..089528bfde 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 @@ -15,12 +15,10 @@ */ package com.google.android.exoplayer2.testutil; -import android.os.Handler; import android.support.annotation.Nullable; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline.Period; import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.source.MediaSourceEventListener; import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.upstream.Allocator; @@ -38,12 +36,9 @@ public class FakeAdaptiveMediaSource extends FakeMediaSource { Timeline timeline, Object manifest, TrackGroupArray trackGroupArray, - Handler eventHandler, - MediaSourceEventListener eventListener, FakeChunkSource.Factory chunkSourceFactory) { super(timeline, manifest, trackGroupArray); this.chunkSourceFactory = chunkSourceFactory; - addEventListener(eventHandler, eventListener); } @Override @@ -53,7 +48,7 @@ public class FakeAdaptiveMediaSource extends FakeMediaSource { Allocator allocator, EventDispatcher eventDispatcher, @Nullable TransferListener transferListener) { - Period period = timeline.getPeriod(id.periodIndex, new Period()); + Period period = timeline.getPeriodByUid(id.periodUid, new Period()); return new FakeAdaptiveMediaPeriod( trackGroupArray, eventDispatcher, diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSource.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSource.java index d222b4f22f..9f6fdc9d49 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSource.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSource.java @@ -16,7 +16,6 @@ package com.google.android.exoplayer2.testutil; import android.net.Uri; -import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.testutil.FakeDataSet.FakeData; import com.google.android.exoplayer2.testutil.FakeDataSet.FakeData.Segment; @@ -24,7 +23,6 @@ import com.google.android.exoplayer2.upstream.BaseDataSource; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSourceException; import com.google.android.exoplayer2.upstream.DataSpec; -import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; import java.util.ArrayList; @@ -40,14 +38,9 @@ public class FakeDataSource extends BaseDataSource { */ public static class Factory implements DataSource.Factory { - protected final TransferListener transferListener; protected FakeDataSet fakeDataSet; protected boolean isNetwork; - public Factory(@Nullable TransferListener transferListener) { - this.transferListener = transferListener; - } - public final Factory setFakeDataSet(FakeDataSet fakeDataSet) { this.fakeDataSet = fakeDataSet; return this; @@ -60,11 +53,7 @@ public class FakeDataSource extends BaseDataSource { @Override public DataSource createDataSource() { - FakeDataSource dataSource = new FakeDataSource(fakeDataSet, isNetwork); - if (transferListener != null) { - dataSource.addTransferListener(transferListener); - } - return dataSource; + return new FakeDataSource(fakeDataSet, isNetwork); } } 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 8cef80766b..f2739f2b4d 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 @@ -31,6 +31,7 @@ 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; +import java.util.Collections; /** * Fake {@link MediaPeriod} that provides tracks from the given {@link TrackGroupArray}. Selecting @@ -93,13 +94,7 @@ public class FakeMediaPeriod implements MediaPeriod { public synchronized void setPreparationComplete() { deferOnPrepared = false; if (playerHandler != null && prepareCallback != null) { - playerHandler.post( - new Runnable() { - @Override - public void run() { - finishPreparation(); - } - }); + playerHandler.post(this::finishPreparation); } } @@ -121,7 +116,6 @@ public class FakeMediaPeriod implements MediaPeriod { public synchronized void prepare(Callback callback, long positionUs) { eventDispatcher.loadStarted( FAKE_DATA_SPEC, - FAKE_DATA_SPEC.uri, C.DATA_TYPE_MEDIA, C.TRACK_TYPE_UNKNOWN, /* trackFormat= */ null, @@ -234,6 +228,7 @@ public class FakeMediaPeriod implements MediaPeriod { eventDispatcher.loadCompleted( FAKE_DATA_SPEC, FAKE_DATA_SPEC.uri, + /* responseHeaders= */ Collections.emptyMap(), C.DATA_TYPE_MEDIA, C.TRACK_TYPE_UNKNOWN, /* trackFormat= */ null, 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 2dfc45d71f..2fca4f42c7 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 @@ -40,6 +40,7 @@ import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; import java.util.ArrayList; +import java.util.Collections; import java.util.List; /** @@ -111,8 +112,9 @@ public class FakeMediaSource extends BaseMediaSource { public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { assertThat(preparedSource).isTrue(); assertThat(releasedSource).isFalse(); - Assertions.checkIndex(id.periodIndex, 0, timeline.getPeriodCount()); - Period period = timeline.getPeriod(id.periodIndex, new Period()); + int periodIndex = timeline.getIndexOfPeriod(id.periodUid); + Assertions.checkArgument(periodIndex != C.INDEX_UNSET); + Period period = timeline.getPeriod(periodIndex, new Period()); EventDispatcher eventDispatcher = createEventDispatcher(period.windowIndex, id, period.getPositionInWindowMs()); FakeMediaPeriod mediaPeriod = @@ -149,15 +151,12 @@ public class FakeMediaSource extends BaseMediaSource { public synchronized void setNewSourceInfo(final Timeline newTimeline, final Object newManifest) { if (sourceInfoRefreshHandler != null) { sourceInfoRefreshHandler.post( - new Runnable() { - @Override - public void run() { - assertThat(releasedSource).isFalse(); - assertThat(preparedSource).isTrue(); - timeline = newTimeline; - manifest = newManifest; - finishSourcePreparation(); - } + () -> { + assertThat(releasedSource).isFalse(); + assertThat(preparedSource).isTrue(); + timeline = newTimeline; + manifest = newManifest; + finishSourcePreparation(); }); } else { timeline = newTimeline; @@ -227,6 +226,7 @@ public class FakeMediaSource extends BaseMediaSource { new LoadEventInfo( FAKE_DATA_SPEC, FAKE_DATA_SPEC.uri, + /* responseHeaders= */ Collections.emptyMap(), elapsedRealTimeMs, /* loadDurationMs= */ 0, /* bytesLoaded= */ 0), @@ -235,6 +235,7 @@ public class FakeMediaSource extends BaseMediaSource { new LoadEventInfo( FAKE_DATA_SPEC, FAKE_DATA_SPEC.uri, + /* responseHeaders= */ Collections.emptyMap(), elapsedRealTimeMs, /* loadDurationMs= */ 0, /* bytesLoaded= */ MANIFEST_LOAD_BYTES), diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/HostActivity.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/HostActivity.java index fde92d690d..f36859d1ab 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/HostActivity.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/HostActivity.java @@ -118,13 +118,11 @@ public final class HostActivity extends Activity implements SurfaceHolder.Callba forcedStopped = false; hostedTestStarted = false; - runOnUiThread(new Runnable() { - @Override - public void run() { - HostActivity.this.hostedTest = hostedTest; - maybeStartHostedTest(); - } - }); + runOnUiThread( + () -> { + HostActivity.this.hostedTest = hostedTest; + maybeStartHostedTest(); + }); if (!hostedTestStartedCondition.block(START_TIMEOUT_MS)) { String message = @@ -145,12 +143,7 @@ public final class HostActivity extends Activity implements SurfaceHolder.Callba fail(message); } } else { - runOnUiThread(new Runnable() { - @Override - public void run() { - hostedTest.forceStop(); - } - }); + runOnUiThread(hostedTest::forceStop); String message = "Test timed out after " + timeoutMs + " ms."; Log.e(TAG, message); if (failOnTimeout) { diff --git a/testutils/src/test/java/com/google/android/exoplayer2/testutil/FakeClockTest.java b/testutils/src/test/java/com/google/android/exoplayer2/testutil/FakeClockTest.java index 725753ce46..90e70e4538 100644 --- a/testutils/src/test/java/com/google/android/exoplayer2/testutil/FakeClockTest.java +++ b/testutils/src/test/java/com/google/android/exoplayer2/testutil/FakeClockTest.java @@ -131,13 +131,7 @@ public final class FakeClockTest { private static void waitForHandler(HandlerWrapper handler) { final ConditionVariable handlerFinished = new ConditionVariable(); - handler.post( - new Runnable() { - @Override - public void run() { - handlerFinished.open(); - } - }); + handler.post(handlerFinished::open); handlerFinished.block(); } diff --git a/testutils/src/test/java/com/google/android/exoplayer2/testutil/FakeDataSetTest.java b/testutils/src/test/java/com/google/android/exoplayer2/testutil/FakeDataSetTest.java index 75c6f886c2..99469295bb 100644 --- a/testutils/src/test/java/com/google/android/exoplayer2/testutil/FakeDataSetTest.java +++ b/testutils/src/test/java/com/google/android/exoplayer2/testutil/FakeDataSetTest.java @@ -65,11 +65,8 @@ public final class FakeDataSetTest { public void testSegmentTypes() { byte[] testData = TestUtil.buildTestData(3); Runnable runnable = - new Runnable() { - @Override - public void run() { - // Do nothing. - } + () -> { + // Do nothing. }; IOException exception = new IOException(); FakeDataSet fakeDataSet = 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 4d4a53bcdd..d3eca63461 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 @@ -51,8 +51,9 @@ public class FakeTrackSelector extends DefaultTrackSelector { int[] rendererMixedMimeTypeAdaptationSupports, Parameters params) throws ExoPlaybackException { - TrackSelection[] selections = new TrackSelection[mappedTrackInfo.length]; - for (int i = 0; i < mappedTrackInfo.length; i++) { + int rendererCount = mappedTrackInfo.getRendererCount(); + TrackSelection[] selections = new TrackSelection[rendererCount]; + for (int i = 0; i < rendererCount; i++) { TrackGroupArray trackGroupArray = mappedTrackInfo.getTrackGroups(i); boolean hasTracks = trackGroupArray.length > 0; selections[i] = hasTracks ? reuseOrCreateTrackSelection(trackGroupArray.get(0)) : null; 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 3fffdb2696..70e7669dfb 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 @@ -97,16 +97,13 @@ public class MediaSourceTestRunner { final Throwable[] throwable = new Throwable[1]; final ConditionVariable finishedCondition = new ConditionVariable(); playbackHandler.post( - new Runnable() { - @Override - public void run() { - try { - runnable.run(); - } catch (Throwable e) { - throwable[0] = e; - } finally { - finishedCondition.open(); - } + () -> { + try { + runnable.run(); + } catch (Throwable e) { + throwable[0] = e; + } finally { + finishedCondition.open(); } }); assertThat(finishedCondition.block(TIMEOUT_MS)).isTrue(); @@ -123,22 +120,19 @@ public class MediaSourceTestRunner { public Timeline prepareSource() throws IOException { final IOException[] prepareError = new IOException[1]; runOnPlaybackThread( - new Runnable() { - @Override - public void run() { - mediaSource.prepareSource( - player, - /* isTopLevelSource= */ true, - mediaSourceListener, - /* mediaTransferListener= */ null); - try { - // TODO: This only catches errors that are set synchronously in prepareSource. To - // capture async errors we'll need to poll maybeThrowSourceInfoRefreshError until the - // first call to onSourceInfoRefreshed. - mediaSource.maybeThrowSourceInfoRefreshError(); - } catch (IOException e) { - prepareError[0] = e; - } + () -> { + mediaSource.prepareSource( + player, + /* isTopLevelSource= */ true, + mediaSourceListener, + /* mediaTransferListener= */ null); + try { + // TODO: This only catches errors that are set synchronously in prepareSource. To + // capture async errors we'll need to poll maybeThrowSourceInfoRefreshError until the + // first call to onSourceInfoRefreshed. + mediaSource.maybeThrowSourceInfoRefreshError(); + } catch (IOException e) { + prepareError[0] = e; } }); if (prepareError[0] != null) { @@ -156,13 +150,7 @@ public class MediaSourceTestRunner { */ public MediaPeriod createPeriod(final MediaPeriodId periodId) { final MediaPeriod[] holder = new MediaPeriod[1]; - runOnPlaybackThread( - new Runnable() { - @Override - public void run() { - holder[0] = mediaSource.createPeriod(periodId, allocator); - } - }); + runOnPlaybackThread(() -> holder[0] = mediaSource.createPeriod(periodId, allocator)); assertThat(holder[0]).isNotNull(); return holder[0]; } @@ -179,24 +167,21 @@ public class MediaSourceTestRunner { final ConditionVariable prepareCalled = new ConditionVariable(); final CountDownLatch preparedCountDown = new CountDownLatch(1); runOnPlaybackThread( - new Runnable() { - @Override - public void run() { - mediaPeriod.prepare( - new MediaPeriod.Callback() { - @Override - public void onPrepared(MediaPeriod mediaPeriod) { - preparedCountDown.countDown(); - } + () -> { + mediaPeriod.prepare( + new MediaPeriod.Callback() { + @Override + public void onPrepared(MediaPeriod mediaPeriod1) { + preparedCountDown.countDown(); + } - @Override - public void onContinueLoadingRequested(MediaPeriod source) { - // Do nothing. - } - }, - positionUs); - prepareCalled.open(); - } + @Override + public void onContinueLoadingRequested(MediaPeriod source) { + // Do nothing. + } + }, + positionUs); + prepareCalled.open(); }); prepareCalled.block(); return preparedCountDown; @@ -208,13 +193,7 @@ public class MediaSourceTestRunner { * @param mediaPeriod The {@link MediaPeriod} to release. */ public void releasePeriod(final MediaPeriod mediaPeriod) { - runOnPlaybackThread( - new Runnable() { - @Override - public void run() { - mediaSource.releasePeriod(mediaPeriod); - } - }); + runOnPlaybackThread(() -> mediaSource.releasePeriod(mediaPeriod)); } /** @@ -222,13 +201,7 @@ public class MediaSourceTestRunner { * thread. */ public void releaseSource() { - runOnPlaybackThread( - new Runnable() { - @Override - public void run() { - mediaSource.releaseSource(mediaSourceListener); - } - }); + runOnPlaybackThread(() -> mediaSource.releaseSource(mediaSourceListener)); } /** @@ -278,12 +251,12 @@ public class MediaSourceTestRunner { public void assertPrepareAndReleaseAllPeriods() throws InterruptedException { Timeline.Period period = new Timeline.Period(); for (int i = 0; i < timeline.getPeriodCount(); i++) { - timeline.getPeriod(i, period); - assertPrepareAndReleasePeriod(new MediaPeriodId(i, period.windowIndex)); + timeline.getPeriod(i, period, /* setIds= */ true); + assertPrepareAndReleasePeriod(new MediaPeriodId(period.uid, period.windowIndex)); for (int adGroupIndex = 0; adGroupIndex < period.getAdGroupCount(); adGroupIndex++) { for (int adIndex = 0; adIndex < period.getAdCountInAdGroup(adGroupIndex); adIndex++) { assertPrepareAndReleasePeriod( - new MediaPeriodId(i, adGroupIndex, adIndex, period.windowIndex)); + new MediaPeriodId(period.uid, adGroupIndex, adIndex, period.windowIndex)); } } } @@ -299,7 +272,7 @@ public class MediaSourceTestRunner { // to releasePeriod. MediaPeriodId secondMediaPeriodId = new MediaPeriodId( - mediaPeriodId.periodIndex, + mediaPeriodId.periodUid, mediaPeriodId.adGroupIndex, mediaPeriodId.adIndexInAdGroup, mediaPeriodId.windowSequenceNumber + 1000); @@ -349,8 +322,8 @@ public class MediaSourceTestRunner { int windowIndex = windowIndexAndMediaPeriodId.first; MediaPeriodId mediaPeriodId = windowIndexAndMediaPeriodId.second; if (expectedLoads.remove(mediaPeriodId)) { - assertThat(windowIndex) - .isEqualTo(timeline.getPeriod(mediaPeriodId.periodIndex, period).windowIndex); + int periodIndex = timeline.getIndexOfPeriod(mediaPeriodId.periodUid); + assertThat(windowIndex).isEqualTo(timeline.getPeriod(periodIndex, period).windowIndex); } } assertWithMessage("Not all expected media source loads have been completed.") 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 246e2918c4..be41aa3eeb 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 @@ -190,11 +190,15 @@ public abstract class StubExoPlayer implements ExoPlayer { } @Override + @Deprecated + @SuppressWarnings("deprecation") public void sendMessages(ExoPlayerMessage... messages) { throw new UnsupportedOperationException(); } @Override + @Deprecated + @SuppressWarnings("deprecation") public void blockingSendMessages(ExoPlayerMessage... messages) { throw new UnsupportedOperationException(); } @@ -299,6 +303,11 @@ public abstract class StubExoPlayer implements ExoPlayer { throw new UnsupportedOperationException(); } + @Override + public long getContentDuration() { + throw new UnsupportedOperationException(); + } + @Override public long getContentPosition() { 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 index b624c49350..7e0ffc1772 100644 --- 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 @@ -81,12 +81,9 @@ public final class TestDownloadManagerListener implements DownloadManager.Listen downloadFinishedCondition = new CountDownLatch(1); } dummyMainThread.runOnMainThread( - new Runnable() { - @Override - public void run() { - if (downloadManager.isIdle()) { - downloadFinishedCondition.countDown(); - } + () -> { + if (downloadManager.isIdle()) { + downloadFinishedCondition.countDown(); } }); assertThat(downloadFinishedCondition.await(TIMEOUT, TimeUnit.MILLISECONDS)).isTrue(); @@ -98,7 +95,7 @@ public final class TestDownloadManagerListener implements DownloadManager.Listen private ArrayBlockingQueue getStateQueue(DownloadAction action) { synchronized (actionStates) { if (!actionStates.containsKey(action)) { - actionStates.put(action, new ArrayBlockingQueue(10)); + actionStates.put(action, new ArrayBlockingQueue<>(10)); } return actionStates.get(action); }