From cdae9ac5d26b18f5a84c4dbd79568ee1e6ae239b Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 26 Jan 2016 05:05:41 -0800 Subject: [PATCH] ExoPlayer V2 Refactor - Steps 1/2. GitHub note - Apologies for the cryptic change descriptions, they relate to a design doc that's not externally visible. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=113043764 --- demo/src/main/.classpath | 10 - demo/src/main/.project | 53 -- .../main/.settings/org.eclipse.jdt.core.prefs | 4 - .../exoplayer/demo/PlayerActivity.java | 22 +- ...rerBuilder.java => DashSourceBuilder.java} | 67 +- .../exoplayer/demo/player/DemoPlayer.java | 131 ++-- .../demo/player/ExtractorRendererBuilder.java | 88 --- .../demo/player/ExtractorSourceBuilder.java | 62 ++ ...ererBuilder.java => HlsSourceBuilder.java} | 48 +- ...java => SmoothStreamingSourceBuilder.java} | 54 +- demo_misc/README.md | 7 - demo_misc/vp9_opus_sw/README.md | 5 - demo_misc/vp9_opus_sw/build.gradle | 40 -- demo_misc/vp9_opus_sw/src/main/.classpath | 9 - demo_misc/vp9_opus_sw/src/main/.project | 33 - .../vp9_opus_sw/src/main/AndroidManifest.xml | 54 -- .../demo/vp9opus/DashRendererBuilder.java | 150 ----- .../demo/vp9opus/PlayerActivity.java | 314 ---------- .../demo/vp9opus/SampleChooserActivity.java | 140 ----- .../vp9_opus_sw/src/main/project.properties | 17 - .../main/res/drawable-hdpi/ic_launcher.png | Bin 2929 -> 0 bytes .../main/res/drawable-mdpi/ic_launcher.png | Bin 1798 -> 0 bytes .../main/res/drawable-xhdpi/ic_launcher.png | Bin 4177 -> 0 bytes .../main/res/drawable-xxhdpi/ic_launcher.png | Bin 6846 -> 0 bytes .../main/res/drawable-xxxhdpi/ic_launcher.png | Bin 9809 -> 0 bytes .../main/res/layout/activity_video_player.xml | 55 -- .../vp9_opus_sw/src/main/res/layout/rows.xml | 20 - .../res/layout/sample_chooser_activity.xml | 25 - .../layout/sample_chooser_inline_header.xml | 25 - .../src/main/res/values-v11/styles.xml | 21 - .../src/main/res/values/strings.xml | 33 - .../src/main/res/values/styles.xml | 26 - extensions/README.md | 3 - extensions/okhttp/README.md | 9 - extensions/okhttp/build.gradle | 42 -- extensions/okhttp/src/main/.classpath | 10 - extensions/okhttp/src/main/.project | 33 - .../okhttp/src/main/AndroidManifest.xml | 22 - .../ext/okhttp/OkHttpDataSource.java | 372 ----------- extensions/okhttp/src/main/project.properties | 16 - extensions/opus/README.md | 131 ---- extensions/opus/build.gradle | 45 -- extensions/opus/src/main/.classpath | 10 - extensions/opus/src/main/.cproject | 57 -- extensions/opus/src/main/.project | 97 --- .../main/.settings/org.eclipse.jdt.core.prefs | 12 - extensions/opus/src/main/AndroidManifest.xml | 22 - .../ext/opus/LibopusAudioTrackRenderer.java | 417 ------------- .../exoplayer/ext/opus/OpusDecoder.java | 110 ---- .../ext/opus/OpusDecoderException.java | 27 - .../ext/opus/OpusDecoderWrapper.java | 400 ------------ extensions/opus/src/main/jni/Android.mk | 33 - extensions/opus/src/main/jni/Application.mk | 20 - .../opus/src/main/jni/convert_android_asm.sh | 47 -- extensions/opus/src/main/jni/libopus.mk | 50 -- extensions/opus/src/main/jni/opus_jni.cc | 100 --- extensions/opus/src/main/proguard.cfg | 6 - extensions/opus/src/main/project.properties | 16 - extensions/opus/src/main/res/.README.txt | 2 - extensions/vp9/README.md | 137 ----- extensions/vp9/build.gradle | 45 -- extensions/vp9/src/androidTest/.classpath | 10 - extensions/vp9/src/androidTest/.project | 45 -- .../vp9/src/androidTest/AndroidManifest.xml | 34 -- .../assets/bear-vp9-odd-dimensions.webm | Bin 31548 -> 0 bytes .../vp9/src/androidTest/assets/bear-vp9.webm | Bin 67504 -> 0 bytes .../androidTest/assets/invalid-bitstream.webm | Bin 47667 -> 0 bytes .../exoplayer/ext/vp9/VpxPlaybackTest.java | 130 ---- .../vp9/src/androidTest/project.properties | 14 - .../vp9/src/androidTest/res/.README.txt | 2 - extensions/vp9/src/main/.classpath | 10 - extensions/vp9/src/main/.cproject | 57 -- extensions/vp9/src/main/.project | 97 --- .../main/.settings/org.eclipse.jdt.core.prefs | 12 - extensions/vp9/src/main/AndroidManifest.xml | 23 - .../ext/vp9/LibvpxVideoTrackRenderer.java | 493 --------------- .../android/exoplayer/ext/vp9/VpxDecoder.java | 98 --- .../ext/vp9/VpxDecoderException.java | 27 - .../exoplayer/ext/vp9/VpxDecoderWrapper.java | 264 -------- .../exoplayer/ext/vp9/VpxOutputBuffer.java | 109 ---- .../ext/vp9/VpxOutputBufferRenderer.java | 28 - .../exoplayer/ext/vp9/VpxRenderer.java | 244 -------- .../ext/vp9/VpxVideoSurfaceView.java | 50 -- extensions/vp9/src/main/jni/Android.mk | 42 -- extensions/vp9/src/main/jni/Application.mk | 20 - .../jni/generate_libvpx_android_configs.sh | 124 ---- extensions/vp9/src/main/jni/libvpx.mk | 51 -- extensions/vp9/src/main/jni/vpx_jni.cc | 176 ------ extensions/vp9/src/main/proguard.cfg | 11 - extensions/vp9/src/main/project.properties | 16 - extensions/vp9/src/main/res/.README.txt | 2 - library/src/androidTest/.classpath | 9 - library/src/androidTest/.project | 62 -- .../.settings/org.eclipse.jdt.core.prefs | 4 - library/src/androidTest/libs/.README.txt | 1 - library/src/androidTest/project.properties | 2 +- library/src/main/.classpath | 9 - library/src/main/.project | 33 - .../main/.settings/org.eclipse.jdt.core.prefs | 4 - .../java/com/google/android/exoplayer/C.java | 7 +- .../android/exoplayer/DummyTrackRenderer.java | 19 +- .../google/android/exoplayer/ExoPlayer.java | 84 ++- .../android/exoplayer/ExoPlayerImpl.java | 21 +- .../exoplayer/ExoPlayerImplInternal.java | 220 +++---- .../exoplayer/FrameworkSampleSource.java | 181 +++--- .../MediaCodecAudioTrackRenderer.java | 31 +- .../exoplayer/MediaCodecTrackRenderer.java | 27 +- .../MediaCodecVideoTrackRenderer.java | 39 +- .../google/android/exoplayer/MediaFormat.java | 4 +- .../android/exoplayer/MultiSampleSource.java | 132 ++++ .../android/exoplayer/SampleSource.java | 311 +++++----- .../exoplayer/SampleSourceTrackRenderer.java | 211 ++----- .../android/exoplayer/SingleSampleSource.java | 49 +- .../android/exoplayer/TrackRenderer.java | 106 +--- .../BaseChunkSampleSourceEventListener.java | 4 +- .../exoplayer/chunk/ChunkSampleSource.java | 100 +-- .../chunk/SingleSampleChunkSource.java | 3 +- .../extractor/ExtractorSampleSource.java | 157 +++-- .../exoplayer/hls/HlsSampleSource.java | 145 +++-- .../metadata/MetadataTrackRenderer.java | 19 +- .../exoplayer/text/TextTrackRenderer.java | 40 +- .../text/eia608/Eia608TrackRenderer.java | 24 +- playbacktests/build.gradle | 38 -- playbacktests/src/main/.classpath | 10 - playbacktests/src/main/.project | 53 -- playbacktests/src/main/AndroidManifest.xml | 43 -- .../exoplayer/playbacktests/gts/DashTest.java | 578 ------------------ .../exoplayer/playbacktests/util/Action.java | 147 ----- .../playbacktests/util/ActionSchedule.java | 226 ------- .../playbacktests/util/CodecCountersUtil.java | 88 --- .../playbacktests/util/ExoHostedTest.java | 182 ------ .../playbacktests/util/HostActivity.java | 249 -------- .../playbacktests/util/LogcatLogger.java | 175 ------ .../util/LogcatMetricsLogger.java | 41 -- .../playbacktests/util/MetricsLogger.java | 57 -- .../playbacktests/util/TestUtil.java | 110 ---- .../src/main/res/layout/host_activity.xml | 28 - settings.gradle | 10 - 138 files changed, 1125 insertions(+), 8961 deletions(-) delete mode 100644 demo/src/main/.classpath delete mode 100644 demo/src/main/.project delete mode 100644 demo/src/main/.settings/org.eclipse.jdt.core.prefs rename demo/src/main/java/com/google/android/exoplayer/demo/player/{DashRendererBuilder.java => DashSourceBuilder.java} (78%) delete mode 100644 demo/src/main/java/com/google/android/exoplayer/demo/player/ExtractorRendererBuilder.java create mode 100644 demo/src/main/java/com/google/android/exoplayer/demo/player/ExtractorSourceBuilder.java rename demo/src/main/java/com/google/android/exoplayer/demo/player/{HlsRendererBuilder.java => HlsSourceBuilder.java} (70%) rename demo/src/main/java/com/google/android/exoplayer/demo/player/{SmoothStreamingRendererBuilder.java => SmoothStreamingSourceBuilder.java} (75%) delete mode 100644 demo_misc/README.md delete mode 100644 demo_misc/vp9_opus_sw/README.md delete mode 100644 demo_misc/vp9_opus_sw/build.gradle delete mode 100644 demo_misc/vp9_opus_sw/src/main/.classpath delete mode 100644 demo_misc/vp9_opus_sw/src/main/.project delete mode 100644 demo_misc/vp9_opus_sw/src/main/AndroidManifest.xml delete mode 100644 demo_misc/vp9_opus_sw/src/main/java/com/google/android/exoplayer/demo/vp9opus/DashRendererBuilder.java delete mode 100644 demo_misc/vp9_opus_sw/src/main/java/com/google/android/exoplayer/demo/vp9opus/PlayerActivity.java delete mode 100644 demo_misc/vp9_opus_sw/src/main/java/com/google/android/exoplayer/demo/vp9opus/SampleChooserActivity.java delete mode 100644 demo_misc/vp9_opus_sw/src/main/project.properties delete mode 100644 demo_misc/vp9_opus_sw/src/main/res/drawable-hdpi/ic_launcher.png delete mode 100644 demo_misc/vp9_opus_sw/src/main/res/drawable-mdpi/ic_launcher.png delete mode 100644 demo_misc/vp9_opus_sw/src/main/res/drawable-xhdpi/ic_launcher.png delete mode 100644 demo_misc/vp9_opus_sw/src/main/res/drawable-xxhdpi/ic_launcher.png delete mode 100644 demo_misc/vp9_opus_sw/src/main/res/drawable-xxxhdpi/ic_launcher.png delete mode 100644 demo_misc/vp9_opus_sw/src/main/res/layout/activity_video_player.xml delete mode 100644 demo_misc/vp9_opus_sw/src/main/res/layout/rows.xml delete mode 100644 demo_misc/vp9_opus_sw/src/main/res/layout/sample_chooser_activity.xml delete mode 100644 demo_misc/vp9_opus_sw/src/main/res/layout/sample_chooser_inline_header.xml delete mode 100644 demo_misc/vp9_opus_sw/src/main/res/values-v11/styles.xml delete mode 100644 demo_misc/vp9_opus_sw/src/main/res/values/strings.xml delete mode 100644 demo_misc/vp9_opus_sw/src/main/res/values/styles.xml delete mode 100644 extensions/README.md delete mode 100644 extensions/okhttp/README.md delete mode 100644 extensions/okhttp/build.gradle delete mode 100644 extensions/okhttp/src/main/.classpath delete mode 100644 extensions/okhttp/src/main/.project delete mode 100644 extensions/okhttp/src/main/AndroidManifest.xml delete mode 100644 extensions/okhttp/src/main/java/com/google/android/exoplayer/ext/okhttp/OkHttpDataSource.java delete mode 100644 extensions/okhttp/src/main/project.properties delete mode 100644 extensions/opus/README.md delete mode 100644 extensions/opus/build.gradle delete mode 100644 extensions/opus/src/main/.classpath delete mode 100644 extensions/opus/src/main/.cproject delete mode 100644 extensions/opus/src/main/.project delete mode 100644 extensions/opus/src/main/.settings/org.eclipse.jdt.core.prefs delete mode 100644 extensions/opus/src/main/AndroidManifest.xml delete mode 100644 extensions/opus/src/main/java/com/google/android/exoplayer/ext/opus/LibopusAudioTrackRenderer.java delete mode 100644 extensions/opus/src/main/java/com/google/android/exoplayer/ext/opus/OpusDecoder.java delete mode 100644 extensions/opus/src/main/java/com/google/android/exoplayer/ext/opus/OpusDecoderException.java delete mode 100644 extensions/opus/src/main/java/com/google/android/exoplayer/ext/opus/OpusDecoderWrapper.java delete mode 100644 extensions/opus/src/main/jni/Android.mk delete mode 100644 extensions/opus/src/main/jni/Application.mk delete mode 100755 extensions/opus/src/main/jni/convert_android_asm.sh delete mode 100644 extensions/opus/src/main/jni/libopus.mk delete mode 100644 extensions/opus/src/main/jni/opus_jni.cc delete mode 100644 extensions/opus/src/main/proguard.cfg delete mode 100644 extensions/opus/src/main/project.properties delete mode 100644 extensions/opus/src/main/res/.README.txt delete mode 100644 extensions/vp9/README.md delete mode 100644 extensions/vp9/build.gradle delete mode 100644 extensions/vp9/src/androidTest/.classpath delete mode 100644 extensions/vp9/src/androidTest/.project delete mode 100644 extensions/vp9/src/androidTest/AndroidManifest.xml delete mode 100644 extensions/vp9/src/androidTest/assets/bear-vp9-odd-dimensions.webm delete mode 100644 extensions/vp9/src/androidTest/assets/bear-vp9.webm delete mode 100644 extensions/vp9/src/androidTest/assets/invalid-bitstream.webm delete mode 100644 extensions/vp9/src/androidTest/java/com/google/android/exoplayer/ext/vp9/VpxPlaybackTest.java delete mode 100644 extensions/vp9/src/androidTest/project.properties delete mode 100644 extensions/vp9/src/androidTest/res/.README.txt delete mode 100644 extensions/vp9/src/main/.classpath delete mode 100644 extensions/vp9/src/main/.cproject delete mode 100644 extensions/vp9/src/main/.project delete mode 100644 extensions/vp9/src/main/.settings/org.eclipse.jdt.core.prefs delete mode 100644 extensions/vp9/src/main/AndroidManifest.xml delete mode 100644 extensions/vp9/src/main/java/com/google/android/exoplayer/ext/vp9/LibvpxVideoTrackRenderer.java delete mode 100644 extensions/vp9/src/main/java/com/google/android/exoplayer/ext/vp9/VpxDecoder.java delete mode 100644 extensions/vp9/src/main/java/com/google/android/exoplayer/ext/vp9/VpxDecoderException.java delete mode 100644 extensions/vp9/src/main/java/com/google/android/exoplayer/ext/vp9/VpxDecoderWrapper.java delete mode 100644 extensions/vp9/src/main/java/com/google/android/exoplayer/ext/vp9/VpxOutputBuffer.java delete mode 100644 extensions/vp9/src/main/java/com/google/android/exoplayer/ext/vp9/VpxOutputBufferRenderer.java delete mode 100644 extensions/vp9/src/main/java/com/google/android/exoplayer/ext/vp9/VpxRenderer.java delete mode 100644 extensions/vp9/src/main/java/com/google/android/exoplayer/ext/vp9/VpxVideoSurfaceView.java delete mode 100644 extensions/vp9/src/main/jni/Android.mk delete mode 100644 extensions/vp9/src/main/jni/Application.mk delete mode 100755 extensions/vp9/src/main/jni/generate_libvpx_android_configs.sh delete mode 100644 extensions/vp9/src/main/jni/libvpx.mk delete mode 100644 extensions/vp9/src/main/jni/vpx_jni.cc delete mode 100644 extensions/vp9/src/main/proguard.cfg delete mode 100644 extensions/vp9/src/main/project.properties delete mode 100644 extensions/vp9/src/main/res/.README.txt delete mode 100644 library/src/androidTest/.classpath delete mode 100644 library/src/androidTest/.project delete mode 100644 library/src/androidTest/.settings/org.eclipse.jdt.core.prefs delete mode 100644 library/src/androidTest/libs/.README.txt delete mode 100644 library/src/main/.classpath delete mode 100644 library/src/main/.project delete mode 100644 library/src/main/.settings/org.eclipse.jdt.core.prefs create mode 100644 library/src/main/java/com/google/android/exoplayer/MultiSampleSource.java delete mode 100644 playbacktests/build.gradle delete mode 100644 playbacktests/src/main/.classpath delete mode 100644 playbacktests/src/main/.project delete mode 100644 playbacktests/src/main/AndroidManifest.xml delete mode 100644 playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/gts/DashTest.java delete mode 100644 playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/util/Action.java delete mode 100644 playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/util/ActionSchedule.java delete mode 100644 playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/util/CodecCountersUtil.java delete mode 100644 playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/util/ExoHostedTest.java delete mode 100644 playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/util/HostActivity.java delete mode 100644 playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/util/LogcatLogger.java delete mode 100644 playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/util/LogcatMetricsLogger.java delete mode 100644 playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/util/MetricsLogger.java delete mode 100644 playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/util/TestUtil.java delete mode 100644 playbacktests/src/main/res/layout/host_activity.xml diff --git a/demo/src/main/.classpath b/demo/src/main/.classpath deleted file mode 100644 index 3ae82311ba..0000000000 --- a/demo/src/main/.classpath +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/demo/src/main/.project b/demo/src/main/.project deleted file mode 100644 index ff9802eb2f..0000000000 --- a/demo/src/main/.project +++ /dev/null @@ -1,53 +0,0 @@ - - - ExoPlayerDemo - - - - - - com.android.ide.eclipse.adt.ResourceManagerBuilder - - - - - com.android.ide.eclipse.adt.PreCompilerBuilder - - - - - org.eclipse.jdt.core.javabuilder - - - - - com.android.ide.eclipse.adt.ApkBuilder - - - - - - com.android.ide.eclipse.adt.AndroidNature - org.eclipse.jdt.core.javanature - - - - 1363908154650 - - 22 - - org.eclipse.ui.ide.multiFilter - 1.0-name-matches-false-false-BUILD - - - - 1363908154652 - - 10 - - org.eclipse.ui.ide.multiFilter - 1.0-name-matches-true-false-build - - - - diff --git a/demo/src/main/.settings/org.eclipse.jdt.core.prefs b/demo/src/main/.settings/org.eclipse.jdt.core.prefs deleted file mode 100644 index 64cef5023a..0000000000 --- a/demo/src/main/.settings/org.eclipse.jdt.core.prefs +++ /dev/null @@ -1,4 +0,0 @@ -eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7 -org.eclipse.jdt.core.compiler.compliance=1.7 -org.eclipse.jdt.core.compiler.source=1.7 diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/PlayerActivity.java b/demo/src/main/java/com/google/android/exoplayer/demo/PlayerActivity.java index e41398b734..06bef7e36e 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/PlayerActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/PlayerActivity.java @@ -23,12 +23,12 @@ import com.google.android.exoplayer.MediaCodecUtil.DecoderQueryException; import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.audio.AudioCapabilities; import com.google.android.exoplayer.audio.AudioCapabilitiesReceiver; -import com.google.android.exoplayer.demo.player.DashRendererBuilder; +import com.google.android.exoplayer.demo.player.DashSourceBuilder; import com.google.android.exoplayer.demo.player.DemoPlayer; -import com.google.android.exoplayer.demo.player.DemoPlayer.RendererBuilder; -import com.google.android.exoplayer.demo.player.ExtractorRendererBuilder; -import com.google.android.exoplayer.demo.player.HlsRendererBuilder; -import com.google.android.exoplayer.demo.player.SmoothStreamingRendererBuilder; +import com.google.android.exoplayer.demo.player.DemoPlayer.SourceBuilder; +import com.google.android.exoplayer.demo.player.ExtractorSourceBuilder; +import com.google.android.exoplayer.demo.player.HlsSourceBuilder; +import com.google.android.exoplayer.demo.player.SmoothStreamingSourceBuilder; import com.google.android.exoplayer.drm.UnsupportedDrmException; import com.google.android.exoplayer.metadata.GeobMetadata; import com.google.android.exoplayer.metadata.PrivMetadata; @@ -296,19 +296,19 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback, // Internal methods - private RendererBuilder getRendererBuilder() { + private SourceBuilder getSourceBuilder() { String userAgent = Util.getUserAgent(this, "ExoPlayerDemo"); switch (contentType) { case Util.TYPE_SS: - return new SmoothStreamingRendererBuilder(this, userAgent, contentUri.toString(), + return new SmoothStreamingSourceBuilder(this, userAgent, contentUri.toString(), new SmoothStreamingTestMediaDrmCallback()); case Util.TYPE_DASH: - return new DashRendererBuilder(this, userAgent, contentUri.toString(), + return new DashSourceBuilder(this, userAgent, contentUri.toString(), new WidevineTestMediaDrmCallback(contentId, provider)); case Util.TYPE_HLS: - return new HlsRendererBuilder(this, userAgent, contentUri.toString()); + return new HlsSourceBuilder(this, userAgent, contentUri.toString()); case Util.TYPE_OTHER: - return new ExtractorRendererBuilder(this, userAgent, contentUri); + return new ExtractorSourceBuilder(this, userAgent, contentUri); default: throw new IllegalStateException("Unsupported type: " + contentType); } @@ -316,7 +316,7 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback, private void preparePlayer(boolean playWhenReady) { if (player == null) { - player = new DemoPlayer(getRendererBuilder()); + player = new DemoPlayer(this, getSourceBuilder()); player.addListener(this); player.setCaptionListener(this); player.setMetadataListener(this); diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/player/DashRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/player/DashSourceBuilder.java similarity index 78% rename from demo/src/main/java/com/google/android/exoplayer/demo/player/DashRendererBuilder.java rename to demo/src/main/java/com/google/android/exoplayer/demo/player/DashSourceBuilder.java index 458f1d4d70..2dc489529e 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/player/DashRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/player/DashSourceBuilder.java @@ -17,50 +17,38 @@ package com.google.android.exoplayer.demo.player; import com.google.android.exoplayer.DefaultLoadControl; import com.google.android.exoplayer.LoadControl; -import com.google.android.exoplayer.MediaCodecAudioTrackRenderer; -import com.google.android.exoplayer.MediaCodecSelector; -import com.google.android.exoplayer.MediaCodecVideoTrackRenderer; -import com.google.android.exoplayer.TrackRenderer; -import com.google.android.exoplayer.audio.AudioCapabilities; +import com.google.android.exoplayer.MultiSampleSource; import com.google.android.exoplayer.chunk.ChunkSampleSource; import com.google.android.exoplayer.chunk.ChunkSource; import com.google.android.exoplayer.chunk.FormatEvaluator.AdaptiveEvaluator; import com.google.android.exoplayer.dash.DashChunkSource; import com.google.android.exoplayer.dash.DefaultDashTrackSelector; -import com.google.android.exoplayer.dash.mpd.AdaptationSet; import com.google.android.exoplayer.dash.mpd.MediaPresentationDescription; import com.google.android.exoplayer.dash.mpd.MediaPresentationDescriptionParser; -import com.google.android.exoplayer.dash.mpd.Period; import com.google.android.exoplayer.dash.mpd.UtcTimingElement; import com.google.android.exoplayer.dash.mpd.UtcTimingElementResolver; import com.google.android.exoplayer.dash.mpd.UtcTimingElementResolver.UtcTimingCallback; -import com.google.android.exoplayer.demo.player.DemoPlayer.RendererBuilder; +import com.google.android.exoplayer.demo.player.DemoPlayer.SourceBuilder; import com.google.android.exoplayer.drm.MediaDrmCallback; -import com.google.android.exoplayer.drm.StreamingDrmSessionManager; -import com.google.android.exoplayer.drm.UnsupportedDrmException; -import com.google.android.exoplayer.text.TextTrackRenderer; +import com.google.android.exoplayer.upstream.BandwidthMeter; import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DefaultAllocator; -import com.google.android.exoplayer.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer.upstream.DefaultUriDataSource; import com.google.android.exoplayer.upstream.UriDataSource; import com.google.android.exoplayer.util.ManifestFetcher; -import com.google.android.exoplayer.util.Util; import android.content.Context; -import android.media.AudioManager; -import android.media.MediaCodec; import android.os.Handler; import android.util.Log; import java.io.IOException; /** - * A {@link RendererBuilder} for DASH. + * A {@link SourceBuilder} for DASH. */ -public class DashRendererBuilder implements RendererBuilder { +public class DashSourceBuilder implements SourceBuilder { - private static final String TAG = "DashRendererBuilder"; + private static final String TAG = "DashSourceBuilder"; private static final int BUFFER_SEGMENT_SIZE = 64 * 1024; private static final int VIDEO_BUFFER_SEGMENTS = 200; @@ -68,10 +56,6 @@ public class DashRendererBuilder implements RendererBuilder { private static final int TEXT_BUFFER_SEGMENTS = 2; private static final int LIVE_EDGE_LATENCY_MS = 30000; - private static final int SECURITY_LEVEL_UNKNOWN = -1; - private static final int SECURITY_LEVEL_1 = 1; - private static final int SECURITY_LEVEL_3 = 3; - private final Context context; private final String userAgent; private final String url; @@ -79,7 +63,7 @@ public class DashRendererBuilder implements RendererBuilder { private AsyncRendererBuilder currentAsyncBuilder; - public DashRendererBuilder(Context context, String userAgent, String url, + public DashSourceBuilder(Context context, String userAgent, String url, MediaDrmCallback drmCallback) { this.context = context; this.userAgent = userAgent; @@ -155,7 +139,7 @@ public class DashRendererBuilder implements RendererBuilder { return; } - player.onRenderersError(e); + player.onSourceBuilderError(e); } @Override @@ -180,11 +164,9 @@ public class DashRendererBuilder implements RendererBuilder { } private void buildRenderers() { + // TODO[REFACTOR]: Bring back DRM support. + /* Period period = manifest.getPeriod(0); - Handler mainHandler = player.getMainHandler(); - LoadControl loadControl = new DefaultLoadControl(new DefaultAllocator(BUFFER_SEGMENT_SIZE)); - DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(mainHandler, player); - boolean hasContentProtection = false; for (int i = 0; i < period.adaptationSets.size(); i++) { AdaptationSet adaptationSet = period.adaptationSets.get(i); @@ -198,7 +180,7 @@ public class DashRendererBuilder implements RendererBuilder { StreamingDrmSessionManager drmSessionManager = null; if (hasContentProtection) { if (Util.SDK_INT < 18) { - player.onRenderersError( + player.onSourceBuilderError( new UnsupportedDrmException(UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME)); return; } @@ -207,23 +189,25 @@ public class DashRendererBuilder implements RendererBuilder { player.getPlaybackLooper(), drmCallback, null, player.getMainHandler(), player); filterHdContent = getWidevineSecurityLevel(drmSessionManager) != SECURITY_LEVEL_1; } catch (UnsupportedDrmException e) { - player.onRenderersError(e); + player.onSourceBuilderError(e); return; } } + */ + + Handler mainHandler = player.getMainHandler(); + BandwidthMeter bandwidthMeter = player.getBandwidthMeter(); + LoadControl loadControl = new DefaultLoadControl(new DefaultAllocator(BUFFER_SEGMENT_SIZE)); // Build the video renderer. DataSource videoDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent); ChunkSource videoChunkSource = new DashChunkSource(manifestFetcher, - DefaultDashTrackSelector.newVideoInstance(context, true, filterHdContent), + DefaultDashTrackSelector.newVideoInstance(context, true, false), videoDataSource, new AdaptiveEvaluator(bandwidthMeter), LIVE_EDGE_LATENCY_MS, elapsedRealtimeOffset, mainHandler, player, DemoPlayer.TYPE_VIDEO); ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl, VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player, DemoPlayer.TYPE_VIDEO); - TrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(context, videoSampleSource, - MediaCodecSelector.DEFAULT, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 5000, - drmSessionManager, true, mainHandler, player, 50); // Build the audio renderer. DataSource audioDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent); @@ -233,9 +217,6 @@ public class DashRendererBuilder implements RendererBuilder { ChunkSampleSource audioSampleSource = new ChunkSampleSource(audioChunkSource, loadControl, AUDIO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player, DemoPlayer.TYPE_AUDIO); - TrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer(audioSampleSource, - MediaCodecSelector.DEFAULT, drmSessionManager, true, mainHandler, player, - AudioCapabilities.getCapabilities(context), AudioManager.STREAM_MUSIC); // Build the text renderer. DataSource textDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent); @@ -245,23 +226,19 @@ public class DashRendererBuilder implements RendererBuilder { ChunkSampleSource textSampleSource = new ChunkSampleSource(textChunkSource, loadControl, TEXT_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player, DemoPlayer.TYPE_TEXT); - TrackRenderer textRenderer = new TextTrackRenderer(textSampleSource, player, - mainHandler.getLooper()); // Invoke the callback. - TrackRenderer[] renderers = new TrackRenderer[DemoPlayer.RENDERER_COUNT]; - renderers[DemoPlayer.TYPE_VIDEO] = videoRenderer; - renderers[DemoPlayer.TYPE_AUDIO] = audioRenderer; - renderers[DemoPlayer.TYPE_TEXT] = textRenderer; - player.onRenderers(renderers, bandwidthMeter); + player.onSource( + new MultiSampleSource(videoSampleSource, audioSampleSource, textSampleSource)); } + /* private static int getWidevineSecurityLevel(StreamingDrmSessionManager sessionManager) { String securityLevelProperty = sessionManager.getPropertyString("securityLevel"); return securityLevelProperty.equals("L1") ? SECURITY_LEVEL_1 : securityLevelProperty .equals("L3") ? SECURITY_LEVEL_3 : SECURITY_LEVEL_UNKNOWN; } - + */ } } diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/player/DemoPlayer.java b/demo/src/main/java/com/google/android/exoplayer/demo/player/DemoPlayer.java index b22e204f3f..26d7487b84 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/player/DemoPlayer.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/player/DemoPlayer.java @@ -16,33 +16,39 @@ package com.google.android.exoplayer.demo.player; import com.google.android.exoplayer.CodecCounters; -import com.google.android.exoplayer.DummyTrackRenderer; import com.google.android.exoplayer.ExoPlaybackException; import com.google.android.exoplayer.ExoPlayer; import com.google.android.exoplayer.MediaCodecAudioTrackRenderer; -import com.google.android.exoplayer.MediaCodecTrackRenderer; +import com.google.android.exoplayer.MediaCodecSelector; import com.google.android.exoplayer.MediaCodecTrackRenderer.DecoderInitializationException; import com.google.android.exoplayer.MediaCodecVideoTrackRenderer; import com.google.android.exoplayer.MediaFormat; +import com.google.android.exoplayer.SampleSource; import com.google.android.exoplayer.TimeRange; import com.google.android.exoplayer.TrackRenderer; +import com.google.android.exoplayer.audio.AudioCapabilities; import com.google.android.exoplayer.audio.AudioTrack; import com.google.android.exoplayer.chunk.ChunkSampleSource; import com.google.android.exoplayer.chunk.Format; import com.google.android.exoplayer.dash.DashChunkSource; import com.google.android.exoplayer.drm.StreamingDrmSessionManager; import com.google.android.exoplayer.hls.HlsSampleSource; +import com.google.android.exoplayer.metadata.Id3Parser; +import com.google.android.exoplayer.metadata.MetadataTrackRenderer; import com.google.android.exoplayer.metadata.MetadataTrackRenderer.MetadataRenderer; import com.google.android.exoplayer.text.Cue; import com.google.android.exoplayer.text.TextRenderer; +import com.google.android.exoplayer.text.TextTrackRenderer; import com.google.android.exoplayer.upstream.BandwidthMeter; import com.google.android.exoplayer.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer.util.DebugTextViewHelper; import com.google.android.exoplayer.util.PlayerControl; +import android.content.Context; +import android.media.AudioManager; +import android.media.MediaCodec; import android.media.MediaCodec.CryptoException; import android.os.Handler; -import android.os.Looper; import android.view.Surface; import java.io.IOException; @@ -53,7 +59,7 @@ import java.util.concurrent.CopyOnWriteArrayList; /** * A wrapper around {@link ExoPlayer} that provides a higher level interface. It can be prepared - * with one of a number of {@link RendererBuilder} classes to suit different use cases (e.g. DASH, + * with one of a number of {@link SourceBuilder} classes to suit different use cases (e.g. DASH, * SmoothStreaming and so on). */ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventListener, @@ -65,20 +71,20 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi /** * Builds renderers for the player. */ - public interface RendererBuilder { + public interface SourceBuilder { /** * Builds renderers for playback. * - * @param player The player for which renderers are being built. {@link DemoPlayer#onRenderers} + * @param player The player for which renderers are being built. {@link DemoPlayer#onSource} * should be invoked once the renderers have been built. If building fails, - * {@link DemoPlayer#onRenderersError} should be invoked. + * {@link DemoPlayer#onSourceBuilderError} should be invoked. */ void buildRenderers(DemoPlayer player); /** * Cancels the current build operation, if there is one. Else does nothing. *

- * A canceled build operation must not invoke {@link DemoPlayer#onRenderers} or - * {@link DemoPlayer#onRenderersError} on the player, which may have been released. + * A canceled build operation must not invoke {@link DemoPlayer#onSource} or + * {@link DemoPlayer#onSourceBuilderError} on the player, which may have been released. */ void cancel(); } @@ -158,27 +164,26 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi public static final int TYPE_TEXT = 2; public static final int TYPE_METADATA = 3; - private static final int RENDERER_BUILDING_STATE_IDLE = 1; - private static final int RENDERER_BUILDING_STATE_BUILDING = 2; - private static final int RENDERER_BUILDING_STATE_BUILT = 3; + private static final int SOURCE_BUILDING_STATE_IDLE = 1; + private static final int SOURCE_BUILDING_STATE_BUILDING = 2; + private static final int SOURCE_BUILDING_STATE_BUILT = 3; - private final RendererBuilder rendererBuilder; private final ExoPlayer player; + private final SourceBuilder sourceBuilder; + private final BandwidthMeter bandwidthMeter; + private final MediaCodecVideoTrackRenderer videoRenderer; private final PlayerControl playerControl; private final Handler mainHandler; private final CopyOnWriteArrayList listeners; - private int rendererBuildingState; + private int sourceBuildingState; private int lastReportedPlaybackState; private boolean lastReportedPlayWhenReady; private Surface surface; - private TrackRenderer videoRenderer; - private CodecCounters codecCounters; private Format videoFormat; private int videoTrackToRestore; - private BandwidthMeter bandwidthMeter; private boolean backgrounded; private CaptionListener captionListener; @@ -186,16 +191,32 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi private InternalErrorListener internalErrorListener; private InfoListener infoListener; - public DemoPlayer(RendererBuilder rendererBuilder) { - this.rendererBuilder = rendererBuilder; - player = ExoPlayer.Factory.newInstance(RENDERER_COUNT, 1000, 5000); + public DemoPlayer(Context context, SourceBuilder sourceBuilder) { + this.sourceBuilder = sourceBuilder; + mainHandler = new Handler(); + bandwidthMeter = new DefaultBandwidthMeter(); + listeners = new CopyOnWriteArrayList<>(); + + // Build the renderers. + videoRenderer = new MediaCodecVideoTrackRenderer(context, MediaCodecSelector.DEFAULT, + MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 5000, mainHandler, this, 50); + TrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer(MediaCodecSelector.DEFAULT, null, + true, mainHandler, this, AudioCapabilities.getCapabilities(context), + AudioManager.STREAM_MUSIC); + TrackRenderer textRenderer = new TextTrackRenderer(this, mainHandler.getLooper()); + MetadataTrackRenderer> id3Renderer = new MetadataTrackRenderer<>( + new Id3Parser(), this, mainHandler.getLooper()); + TrackRenderer[] renderers = new TrackRenderer[] {videoRenderer, audioRenderer, textRenderer, + id3Renderer}; + + // Build the player and associated objects. + player = ExoPlayer.Factory.newInstance(renderers, 1000, 5000); player.addListener(this); playerControl = new PlayerControl(player); - mainHandler = new Handler(); - listeners = new CopyOnWriteArrayList<>(); + + // Set initial state, with the text renderer initially disabled. lastReportedPlaybackState = STATE_IDLE; - rendererBuildingState = RENDERER_BUILDING_STATE_IDLE; - // Disable text initially. + sourceBuildingState = SOURCE_BUILDING_STATE_IDLE; player.setSelectedTrack(TYPE_TEXT, TRACK_DISABLED); } @@ -279,56 +300,38 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi } public void prepare() { - if (rendererBuildingState == RENDERER_BUILDING_STATE_BUILT) { + if (sourceBuildingState == SOURCE_BUILDING_STATE_BUILT) { player.stop(); } - rendererBuilder.cancel(); - videoFormat = null; - videoRenderer = null; - rendererBuildingState = RENDERER_BUILDING_STATE_BUILDING; + sourceBuilder.cancel(); + sourceBuildingState = SOURCE_BUILDING_STATE_BUILDING; maybeReportPlayerState(); - rendererBuilder.buildRenderers(this); + sourceBuilder.buildRenderers(this); } /** - * Invoked with the results from a {@link RendererBuilder}. + * Invoked with the results from a {@link SourceBuilder}. * - * @param renderers Renderers indexed by {@link DemoPlayer} TYPE_* constants. An individual - * element may be null if there do not exist tracks of the corresponding type. - * @param bandwidthMeter Provides an estimate of the currently available bandwidth. May be null. + * @param source The {@link SampleSource} to play. */ - /* package */ void onRenderers(TrackRenderer[] renderers, BandwidthMeter bandwidthMeter) { - for (int i = 0; i < RENDERER_COUNT; i++) { - if (renderers[i] == null) { - // Convert a null renderer to a dummy renderer. - renderers[i] = new DummyTrackRenderer(); - } - } - // Complete preparation. - this.videoRenderer = renderers[TYPE_VIDEO]; - this.codecCounters = videoRenderer instanceof MediaCodecTrackRenderer - ? ((MediaCodecTrackRenderer) videoRenderer).codecCounters - : renderers[TYPE_AUDIO] instanceof MediaCodecTrackRenderer - ? ((MediaCodecTrackRenderer) renderers[TYPE_AUDIO]).codecCounters : null; - this.bandwidthMeter = bandwidthMeter; - pushSurface(false); - player.prepare(renderers); - rendererBuildingState = RENDERER_BUILDING_STATE_BUILT; + /* package */ void onSource(SampleSource source) { + player.prepare(source); + sourceBuildingState = SOURCE_BUILDING_STATE_BUILT; } /** - * Invoked if a {@link RendererBuilder} encounters an error. + * Invoked if a {@link SourceBuilder} encounters an error. * * @param e Describes the error. */ - /* package */ void onRenderersError(Exception e) { + /* package */ void onSourceBuilderError(Exception e) { if (internalErrorListener != null) { internalErrorListener.onRendererInitializationError(e); } for (Listener listener : listeners) { listener.onError(e); } - rendererBuildingState = RENDERER_BUILDING_STATE_IDLE; + sourceBuildingState = SOURCE_BUILDING_STATE_IDLE; maybeReportPlayerState(); } @@ -341,18 +344,18 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi } public void release() { - rendererBuilder.cancel(); - rendererBuildingState = RENDERER_BUILDING_STATE_IDLE; + sourceBuilder.cancel(); + sourceBuildingState = SOURCE_BUILDING_STATE_IDLE; surface = null; player.release(); } public int getPlaybackState() { - if (rendererBuildingState == RENDERER_BUILDING_STATE_BUILDING) { + if (sourceBuildingState == SOURCE_BUILDING_STATE_BUILDING) { return STATE_PREPARING; } int playerState = player.getPlaybackState(); - if (rendererBuildingState == RENDERER_BUILDING_STATE_BUILT && playerState == STATE_IDLE) { + if (sourceBuildingState == SOURCE_BUILDING_STATE_BUILT && playerState == STATE_IDLE) { // This is an edge case where the renderers are built, but are still being passed to the // player's playback thread. return STATE_PREPARING; @@ -372,7 +375,7 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi @Override public CodecCounters getCodecCounters() { - return codecCounters; + return videoRenderer.codecCounters; } @Override @@ -392,10 +395,6 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi return player.getPlayWhenReady(); } - /* package */ Looper getPlaybackLooper() { - return player.getPlaybackLooper(); - } - /* package */ Handler getMainHandler() { return mainHandler; } @@ -407,7 +406,7 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi @Override public void onPlayerError(ExoPlaybackException exception) { - rendererBuildingState = RENDERER_BUILDING_STATE_IDLE; + sourceBuildingState = SOURCE_BUILDING_STATE_IDLE; for (Listener listener : listeners) { listener.onError(exception); } @@ -583,10 +582,6 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi } private void pushSurface(boolean blockForSurfacePush) { - if (videoRenderer == null) { - return; - } - if (blockForSurfacePush) { player.blockingSendMessage( videoRenderer, MediaCodecVideoTrackRenderer.MSG_SET_SURFACE, surface); diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/player/ExtractorRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/player/ExtractorRendererBuilder.java deleted file mode 100644 index 70cdf54717..0000000000 --- a/demo/src/main/java/com/google/android/exoplayer/demo/player/ExtractorRendererBuilder.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright (C) 2014 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.exoplayer.demo.player; - -import com.google.android.exoplayer.MediaCodecAudioTrackRenderer; -import com.google.android.exoplayer.MediaCodecSelector; -import com.google.android.exoplayer.MediaCodecVideoTrackRenderer; -import com.google.android.exoplayer.TrackRenderer; -import com.google.android.exoplayer.audio.AudioCapabilities; -import com.google.android.exoplayer.demo.player.DemoPlayer.RendererBuilder; -import com.google.android.exoplayer.extractor.Extractor; -import com.google.android.exoplayer.extractor.ExtractorSampleSource; -import com.google.android.exoplayer.text.TextTrackRenderer; -import com.google.android.exoplayer.upstream.Allocator; -import com.google.android.exoplayer.upstream.DataSource; -import com.google.android.exoplayer.upstream.DefaultAllocator; -import com.google.android.exoplayer.upstream.DefaultBandwidthMeter; -import com.google.android.exoplayer.upstream.DefaultUriDataSource; - -import android.content.Context; -import android.media.AudioManager; -import android.media.MediaCodec; -import android.net.Uri; - -/** - * A {@link RendererBuilder} for streams that can be read using an {@link Extractor}. - */ -public class ExtractorRendererBuilder implements RendererBuilder { - - private static final int BUFFER_SEGMENT_SIZE = 64 * 1024; - private static final int BUFFER_SEGMENT_COUNT = 256; - - private final Context context; - private final String userAgent; - private final Uri uri; - - public ExtractorRendererBuilder(Context context, String userAgent, Uri uri) { - this.context = context; - this.userAgent = userAgent; - this.uri = uri; - } - - @Override - public void buildRenderers(DemoPlayer player) { - Allocator allocator = new DefaultAllocator(BUFFER_SEGMENT_SIZE); - - // Build the video and audio renderers. - DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(player.getMainHandler(), - null); - DataSource dataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent); - ExtractorSampleSource sampleSource = new ExtractorSampleSource(uri, dataSource, allocator, - BUFFER_SEGMENT_COUNT * BUFFER_SEGMENT_SIZE); - MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(context, - sampleSource, MediaCodecSelector.DEFAULT, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 5000, - player.getMainHandler(), player, 50); - MediaCodecAudioTrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer(sampleSource, - MediaCodecSelector.DEFAULT, null, true, player.getMainHandler(), player, - AudioCapabilities.getCapabilities(context), AudioManager.STREAM_MUSIC); - TrackRenderer textRenderer = new TextTrackRenderer(sampleSource, player, - player.getMainHandler().getLooper()); - - // Invoke the callback. - TrackRenderer[] renderers = new TrackRenderer[DemoPlayer.RENDERER_COUNT]; - renderers[DemoPlayer.TYPE_VIDEO] = videoRenderer; - renderers[DemoPlayer.TYPE_AUDIO] = audioRenderer; - renderers[DemoPlayer.TYPE_TEXT] = textRenderer; - player.onRenderers(renderers, bandwidthMeter); - } - - @Override - public void cancel() { - // Do nothing. - } - -} diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/player/ExtractorSourceBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/player/ExtractorSourceBuilder.java new file mode 100644 index 0000000000..a037c7f562 --- /dev/null +++ b/demo/src/main/java/com/google/android/exoplayer/demo/player/ExtractorSourceBuilder.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2014 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.exoplayer.demo.player; + +import com.google.android.exoplayer.demo.player.DemoPlayer.SourceBuilder; +import com.google.android.exoplayer.extractor.Extractor; +import com.google.android.exoplayer.extractor.ExtractorSampleSource; +import com.google.android.exoplayer.upstream.Allocator; +import com.google.android.exoplayer.upstream.DataSource; +import com.google.android.exoplayer.upstream.DefaultAllocator; +import com.google.android.exoplayer.upstream.DefaultUriDataSource; + +import android.content.Context; +import android.net.Uri; + +/** + * A {@link SourceBuilder} for streams that can be read using an {@link Extractor}. + */ +public class ExtractorSourceBuilder implements SourceBuilder { + + private static final int BUFFER_SEGMENT_SIZE = 64 * 1024; + private static final int BUFFER_SEGMENT_COUNT = 256; + + private final Context context; + private final String userAgent; + private final Uri uri; + + public ExtractorSourceBuilder(Context context, String userAgent, Uri uri) { + this.context = context; + this.userAgent = userAgent; + this.uri = uri; + } + + @Override + public void buildRenderers(DemoPlayer player) { + Allocator allocator = new DefaultAllocator(BUFFER_SEGMENT_SIZE); + DataSource dataSource = new DefaultUriDataSource(context, player.getBandwidthMeter(), + userAgent); + ExtractorSampleSource source = new ExtractorSampleSource(uri, dataSource, allocator, + BUFFER_SEGMENT_COUNT * BUFFER_SEGMENT_SIZE); + player.onSource(source); + } + + @Override + public void cancel() { + // Do nothing. + } + +} diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/player/HlsRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/player/HlsSourceBuilder.java similarity index 70% rename from demo/src/main/java/com/google/android/exoplayer/demo/player/HlsRendererBuilder.java rename to demo/src/main/java/com/google/android/exoplayer/demo/player/HlsSourceBuilder.java index 242faeace4..04707005f4 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/player/HlsRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/player/HlsSourceBuilder.java @@ -17,46 +17,32 @@ package com.google.android.exoplayer.demo.player; import com.google.android.exoplayer.DefaultLoadControl; import com.google.android.exoplayer.LoadControl; -import com.google.android.exoplayer.MediaCodecAudioTrackRenderer; -import com.google.android.exoplayer.MediaCodecSelector; -import com.google.android.exoplayer.MediaCodecVideoTrackRenderer; -import com.google.android.exoplayer.TrackRenderer; -import com.google.android.exoplayer.audio.AudioCapabilities; -import com.google.android.exoplayer.demo.player.DemoPlayer.RendererBuilder; +import com.google.android.exoplayer.demo.player.DemoPlayer.SourceBuilder; import com.google.android.exoplayer.hls.DefaultHlsTrackSelector; import com.google.android.exoplayer.hls.HlsChunkSource; -import com.google.android.exoplayer.hls.HlsMasterPlaylist; import com.google.android.exoplayer.hls.HlsPlaylist; import com.google.android.exoplayer.hls.HlsPlaylistParser; import com.google.android.exoplayer.hls.HlsSampleSource; import com.google.android.exoplayer.hls.PtsTimestampAdjusterProvider; -import com.google.android.exoplayer.metadata.Id3Parser; -import com.google.android.exoplayer.metadata.MetadataTrackRenderer; -import com.google.android.exoplayer.text.TextTrackRenderer; -import com.google.android.exoplayer.text.eia608.Eia608TrackRenderer; +import com.google.android.exoplayer.upstream.BandwidthMeter; import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DefaultAllocator; -import com.google.android.exoplayer.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer.upstream.DefaultUriDataSource; import com.google.android.exoplayer.util.ManifestFetcher; import com.google.android.exoplayer.util.ManifestFetcher.ManifestCallback; import android.content.Context; -import android.media.AudioManager; -import android.media.MediaCodec; import android.os.Handler; import java.io.IOException; -import java.util.Map; /** - * A {@link RendererBuilder} for HLS. + * A {@link SourceBuilder} for HLS. */ -public class HlsRendererBuilder implements RendererBuilder { +public class HlsSourceBuilder implements SourceBuilder { private static final int BUFFER_SEGMENT_SIZE = 64 * 1024; private static final int MAIN_BUFFER_SEGMENTS = 256; - private static final int TEXT_BUFFER_SEGMENTS = 2; private final Context context; private final String userAgent; @@ -64,7 +50,7 @@ public class HlsRendererBuilder implements RendererBuilder { private AsyncRendererBuilder currentAsyncBuilder; - public HlsRendererBuilder(Context context, String userAgent, String url) { + public HlsSourceBuilder(Context context, String userAgent, String url) { this.context = context; this.userAgent = userAgent; this.url = url; @@ -118,7 +104,7 @@ public class HlsRendererBuilder implements RendererBuilder { return; } - player.onRenderersError(e); + player.onSourceBuilderError(e); } @Override @@ -128,8 +114,8 @@ public class HlsRendererBuilder implements RendererBuilder { } Handler mainHandler = player.getMainHandler(); + BandwidthMeter bandwidthMeter = player.getBandwidthMeter(); LoadControl loadControl = new DefaultLoadControl(new DefaultAllocator(BUFFER_SEGMENT_SIZE)); - DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(); PtsTimestampAdjusterProvider timestampAdjusterProvider = new PtsTimestampAdjusterProvider(); // Build the video/audio/metadata renderers. @@ -139,16 +125,10 @@ public class HlsRendererBuilder implements RendererBuilder { timestampAdjusterProvider, HlsChunkSource.ADAPTIVE_MODE_SPLICE); HlsSampleSource sampleSource = new HlsSampleSource(chunkSource, loadControl, MAIN_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player, DemoPlayer.TYPE_VIDEO); - MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(context, - sampleSource, MediaCodecSelector.DEFAULT, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, - 5000, mainHandler, player, 50); - MediaCodecAudioTrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer(sampleSource, - MediaCodecSelector.DEFAULT, null, true, player.getMainHandler(), player, - AudioCapabilities.getCapabilities(context), AudioManager.STREAM_MUSIC); - MetadataTrackRenderer> id3Renderer = new MetadataTrackRenderer<>( - sampleSource, new Id3Parser(), player, mainHandler.getLooper()); + // TODO[REFACTOR]: Bring back caption support. // Build the text renderer, preferring Webvtt where available. + /* boolean preferWebvtt = false; if (manifest instanceof HlsMasterPlaylist) { preferWebvtt = !((HlsMasterPlaylist) manifest).subtitles.isEmpty(); @@ -156,7 +136,7 @@ public class HlsRendererBuilder implements RendererBuilder { TrackRenderer textRenderer; if (preferWebvtt) { DataSource textDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent); - HlsChunkSource textChunkSource = new HlsChunkSource(false /* isMaster */, textDataSource, + HlsChunkSource textChunkSource = new HlsChunkSource(false, textDataSource, url, manifest, DefaultHlsTrackSelector.newVttInstance(), bandwidthMeter, timestampAdjusterProvider, HlsChunkSource.ADAPTIVE_MODE_SPLICE); HlsSampleSource textSampleSource = new HlsSampleSource(textChunkSource, loadControl, @@ -165,13 +145,9 @@ public class HlsRendererBuilder implements RendererBuilder { } else { textRenderer = new Eia608TrackRenderer(sampleSource, player, mainHandler.getLooper()); } + */ - TrackRenderer[] renderers = new TrackRenderer[DemoPlayer.RENDERER_COUNT]; - renderers[DemoPlayer.TYPE_VIDEO] = videoRenderer; - renderers[DemoPlayer.TYPE_AUDIO] = audioRenderer; - renderers[DemoPlayer.TYPE_METADATA] = id3Renderer; - renderers[DemoPlayer.TYPE_TEXT] = textRenderer; - player.onRenderers(renderers, bandwidthMeter); + player.onSource(sampleSource); } } diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/player/SmoothStreamingRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/player/SmoothStreamingSourceBuilder.java similarity index 75% rename from demo/src/main/java/com/google/android/exoplayer/demo/player/SmoothStreamingRendererBuilder.java rename to demo/src/main/java/com/google/android/exoplayer/demo/player/SmoothStreamingSourceBuilder.java index bb769a833d..598af9545c 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/player/SmoothStreamingRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/player/SmoothStreamingSourceBuilder.java @@ -17,43 +17,33 @@ package com.google.android.exoplayer.demo.player; import com.google.android.exoplayer.DefaultLoadControl; import com.google.android.exoplayer.LoadControl; -import com.google.android.exoplayer.MediaCodecAudioTrackRenderer; -import com.google.android.exoplayer.MediaCodecSelector; -import com.google.android.exoplayer.MediaCodecVideoTrackRenderer; -import com.google.android.exoplayer.TrackRenderer; -import com.google.android.exoplayer.audio.AudioCapabilities; +import com.google.android.exoplayer.MultiSampleSource; import com.google.android.exoplayer.chunk.ChunkSampleSource; import com.google.android.exoplayer.chunk.ChunkSource; import com.google.android.exoplayer.chunk.FormatEvaluator.AdaptiveEvaluator; -import com.google.android.exoplayer.demo.player.DemoPlayer.RendererBuilder; -import com.google.android.exoplayer.drm.DrmSessionManager; +import com.google.android.exoplayer.demo.player.DemoPlayer.SourceBuilder; import com.google.android.exoplayer.drm.MediaDrmCallback; -import com.google.android.exoplayer.drm.StreamingDrmSessionManager; -import com.google.android.exoplayer.drm.UnsupportedDrmException; import com.google.android.exoplayer.smoothstreaming.DefaultSmoothStreamingTrackSelector; import com.google.android.exoplayer.smoothstreaming.SmoothStreamingChunkSource; import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest; import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifestParser; -import com.google.android.exoplayer.text.TextTrackRenderer; +import com.google.android.exoplayer.upstream.BandwidthMeter; import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DefaultAllocator; -import com.google.android.exoplayer.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer.upstream.DefaultHttpDataSource; import com.google.android.exoplayer.upstream.DefaultUriDataSource; import com.google.android.exoplayer.util.ManifestFetcher; import com.google.android.exoplayer.util.Util; import android.content.Context; -import android.media.AudioManager; -import android.media.MediaCodec; import android.os.Handler; import java.io.IOException; /** - * A {@link RendererBuilder} for SmoothStreaming. + * A {@link SourceBuilder} for SmoothStreaming. */ -public class SmoothStreamingRendererBuilder implements RendererBuilder { +public class SmoothStreamingSourceBuilder implements SourceBuilder { private static final int BUFFER_SEGMENT_SIZE = 64 * 1024; private static final int VIDEO_BUFFER_SEGMENTS = 200; @@ -68,7 +58,7 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder { private AsyncRendererBuilder currentAsyncBuilder; - public SmoothStreamingRendererBuilder(Context context, String userAgent, String url, + public SmoothStreamingSourceBuilder(Context context, String userAgent, String url, MediaDrmCallback drmCallback) { this.context = context; this.userAgent = userAgent; @@ -126,7 +116,7 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder { return; } - player.onRenderersError(exception); + player.onSourceBuilderError(exception); } @Override @@ -135,15 +125,13 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder { return; } - Handler mainHandler = player.getMainHandler(); - LoadControl loadControl = new DefaultLoadControl(new DefaultAllocator(BUFFER_SEGMENT_SIZE)); - DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(mainHandler, player); - + // TODO[REFACTOR]: Bring back DRM support. + /* // Check drm support if necessary. DrmSessionManager drmSessionManager = null; if (manifest.protectionElement != null) { if (Util.SDK_INT < 18) { - player.onRenderersError( + player.onSourceBuilderError( new UnsupportedDrmException(UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME)); return; } @@ -151,10 +139,15 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder { drmSessionManager = new StreamingDrmSessionManager(manifest.protectionElement.uuid, player.getPlaybackLooper(), drmCallback, null, player.getMainHandler(), player); } catch (UnsupportedDrmException e) { - player.onRenderersError(e); + player.onSourceBuilderError(e); return; } } + */ + + Handler mainHandler = player.getMainHandler(); + BandwidthMeter bandwidthMeter = player.getBandwidthMeter(); + LoadControl loadControl = new DefaultLoadControl(new DefaultAllocator(BUFFER_SEGMENT_SIZE)); // Build the video renderer. DataSource videoDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent); @@ -164,9 +157,6 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder { ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl, VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player, DemoPlayer.TYPE_VIDEO); - TrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(context, videoSampleSource, - MediaCodecSelector.DEFAULT, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 5000, - drmSessionManager, true, mainHandler, player, 50); // Build the audio renderer. DataSource audioDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent); @@ -176,9 +166,6 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder { ChunkSampleSource audioSampleSource = new ChunkSampleSource(audioChunkSource, loadControl, AUDIO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player, DemoPlayer.TYPE_AUDIO); - TrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer(audioSampleSource, - MediaCodecSelector.DEFAULT, drmSessionManager, true, mainHandler, player, - AudioCapabilities.getCapabilities(context), AudioManager.STREAM_MUSIC); // Build the text renderer. DataSource textDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent); @@ -188,15 +175,10 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder { ChunkSampleSource textSampleSource = new ChunkSampleSource(textChunkSource, loadControl, TEXT_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player, DemoPlayer.TYPE_TEXT); - TrackRenderer textRenderer = new TextTrackRenderer(textSampleSource, player, - mainHandler.getLooper()); // Invoke the callback. - TrackRenderer[] renderers = new TrackRenderer[DemoPlayer.RENDERER_COUNT]; - renderers[DemoPlayer.TYPE_VIDEO] = videoRenderer; - renderers[DemoPlayer.TYPE_AUDIO] = audioRenderer; - renderers[DemoPlayer.TYPE_TEXT] = textRenderer; - player.onRenderers(renderers, bandwidthMeter); + player.onSource( + new MultiSampleSource(videoSampleSource, audioSampleSource, textSampleSource)); } } diff --git a/demo_misc/README.md b/demo_misc/README.md deleted file mode 100644 index f7d7af6ac4..0000000000 --- a/demo_misc/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# Miscellaneous demos # - -This folder contains miscellaneous demo applications. For example applications -that demonstrate use of optional extensions, or more advanced features. - -A general purpose ExoPlayer demo application can be found in the [demo](../demo) -folder. diff --git a/demo_misc/vp9_opus_sw/README.md b/demo_misc/vp9_opus_sw/README.md deleted file mode 100644 index 4714d34084..0000000000 --- a/demo_misc/vp9_opus_sw/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# WebM (VP9/Opus) Software Decoder Demo # - -A demo app that shows how to use the ExoPlayer [VP9](../../extensions/vp9) and [Opus](../../extensions/opus) Extensions to enable VP9 and Opus playback in your app by bundling native libraries along with it. - -The demo app depends on the VP9 and Opus Extensions being configured built correctly. diff --git a/demo_misc/vp9_opus_sw/build.gradle b/demo_misc/vp9_opus_sw/build.gradle deleted file mode 100644 index 5b6eac3ab3..0000000000 --- a/demo_misc/vp9_opus_sw/build.gradle +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright (C) 2014 The Android Open Source Project -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -apply plugin: 'com.android.application' - -android { - compileSdkVersion 23 - buildToolsVersion "23.0.1" - - defaultConfig { - minSdkVersion 16 - targetSdkVersion 23 - } - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' - } - } - - lintOptions { - abortOnError false - } -} - -dependencies { - compile project(':library') - compile project(':extension-opus') - compile project(':extension-vp9') -} diff --git a/demo_misc/vp9_opus_sw/src/main/.classpath b/demo_misc/vp9_opus_sw/src/main/.classpath deleted file mode 100644 index be2dd156ff..0000000000 --- a/demo_misc/vp9_opus_sw/src/main/.classpath +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/demo_misc/vp9_opus_sw/src/main/.project b/demo_misc/vp9_opus_sw/src/main/.project deleted file mode 100644 index 1b6d3fd5ba..0000000000 --- a/demo_misc/vp9_opus_sw/src/main/.project +++ /dev/null @@ -1,33 +0,0 @@ - - - ExoPlayerDemoMisc-Vp9OpusSw - - - - - - com.android.ide.eclipse.adt.ResourceManagerBuilder - - - - - com.android.ide.eclipse.adt.PreCompilerBuilder - - - - - org.eclipse.jdt.core.javabuilder - - - - - com.android.ide.eclipse.adt.ApkBuilder - - - - - - com.android.ide.eclipse.adt.AndroidNature - org.eclipse.jdt.core.javanature - - diff --git a/demo_misc/vp9_opus_sw/src/main/AndroidManifest.xml b/demo_misc/vp9_opus_sw/src/main/AndroidManifest.xml deleted file mode 100644 index d71f92db6c..0000000000 --- a/demo_misc/vp9_opus_sw/src/main/AndroidManifest.xml +++ /dev/null @@ -1,54 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/demo_misc/vp9_opus_sw/src/main/java/com/google/android/exoplayer/demo/vp9opus/DashRendererBuilder.java b/demo_misc/vp9_opus_sw/src/main/java/com/google/android/exoplayer/demo/vp9opus/DashRendererBuilder.java deleted file mode 100644 index 06d05070f9..0000000000 --- a/demo_misc/vp9_opus_sw/src/main/java/com/google/android/exoplayer/demo/vp9opus/DashRendererBuilder.java +++ /dev/null @@ -1,150 +0,0 @@ -/* - * Copyright (C) 2014 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.exoplayer.demo.vp9opus; - -import com.google.android.exoplayer.DefaultLoadControl; -import com.google.android.exoplayer.LoadControl; -import com.google.android.exoplayer.MediaCodecAudioTrackRenderer; -import com.google.android.exoplayer.MediaCodecSelector; -import com.google.android.exoplayer.SampleSource; -import com.google.android.exoplayer.TrackRenderer; -import com.google.android.exoplayer.chunk.ChunkSampleSource; -import com.google.android.exoplayer.chunk.ChunkSource; -import com.google.android.exoplayer.chunk.FormatEvaluator.AdaptiveEvaluator; -import com.google.android.exoplayer.dash.DashChunkSource; -import com.google.android.exoplayer.dash.DefaultDashTrackSelector; -import com.google.android.exoplayer.dash.mpd.AdaptationSet; -import com.google.android.exoplayer.dash.mpd.MediaPresentationDescription; -import com.google.android.exoplayer.dash.mpd.MediaPresentationDescriptionParser; -import com.google.android.exoplayer.dash.mpd.Period; -import com.google.android.exoplayer.dash.mpd.Representation; -import com.google.android.exoplayer.ext.opus.LibopusAudioTrackRenderer; -import com.google.android.exoplayer.ext.vp9.LibvpxVideoTrackRenderer; -import com.google.android.exoplayer.upstream.DataSource; -import com.google.android.exoplayer.upstream.DefaultAllocator; -import com.google.android.exoplayer.upstream.DefaultBandwidthMeter; -import com.google.android.exoplayer.upstream.DefaultHttpDataSource; -import com.google.android.exoplayer.upstream.DefaultUriDataSource; -import com.google.android.exoplayer.util.ManifestFetcher; -import com.google.android.exoplayer.util.ManifestFetcher.ManifestCallback; - -import android.text.TextUtils; - -import java.io.IOException; -import java.util.ArrayList; - -/** - * Helper class that parses the manifest and builds the track renderers. - */ -public class DashRendererBuilder implements ManifestCallback { - - private static final int BUFFER_SEGMENT_SIZE = 64 * 1024; - private static final int VIDEO_BUFFER_SEGMENTS = 200; - private static final int AUDIO_BUFFER_SEGMENTS = 60; - - private final String manifestUrl; - private final String userAgent; - private final PlayerActivity player; - - public DashRendererBuilder(String manifestUrl, String userAgent, PlayerActivity player) { - this.manifestUrl = manifestUrl; - this.userAgent = userAgent; - this.player = player; - } - - public void build() { - MediaPresentationDescriptionParser parser = new MediaPresentationDescriptionParser(); - ManifestFetcher manifestFetcher = - new ManifestFetcher<>(manifestUrl, new DefaultHttpDataSource(userAgent, null), parser); - manifestFetcher.singleLoad(player.getMainHandler().getLooper(), this); - } - - @Override - public void onSingleManifestError(IOException e) { - // TODO: do something meaningful here. - e.printStackTrace(); - } - - @Override - public void onSingleManifest(MediaPresentationDescription manifest) { - LoadControl loadControl = new DefaultLoadControl(new DefaultAllocator(BUFFER_SEGMENT_SIZE)); - DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(null, null); - - // Obtain Representations for playback. - Representation audioRepresentation = null; - boolean audioRepresentationIsOpus = false; - ArrayList videoRepresentationsList = new ArrayList<>(); - Period period = manifest.getPeriod(0); - for (int i = 0; i < period.adaptationSets.size(); i++) { - AdaptationSet adaptationSet = period.adaptationSets.get(i); - int adaptationSetType = adaptationSet.type; - for (int j = 0; j < adaptationSet.representations.size(); j++) { - Representation representation = adaptationSet.representations.get(j); - String codecs = representation.format.codecs; - if (adaptationSetType == AdaptationSet.TYPE_AUDIO && audioRepresentation == null) { - audioRepresentation = representation; - audioRepresentationIsOpus = !TextUtils.isEmpty(codecs) && codecs.startsWith("opus"); - } else if (adaptationSetType == AdaptationSet.TYPE_VIDEO && !TextUtils.isEmpty(codecs) - && codecs.startsWith("vp9")) { - videoRepresentationsList.add(representation); - } - } - } - Representation[] videoRepresentations = new Representation[videoRepresentationsList.size()]; - videoRepresentationsList.toArray(videoRepresentations); - - // Build the video renderer. - LibvpxVideoTrackRenderer videoRenderer = null; - if (!videoRepresentationsList.isEmpty()) { - DataSource videoDataSource = new DefaultUriDataSource(player, bandwidthMeter, userAgent); - ChunkSource videoChunkSource = new DashChunkSource( - DefaultDashTrackSelector.newVideoInstance(null, false, false), videoDataSource, - new AdaptiveEvaluator(bandwidthMeter), manifest.getPeriodDuration(0), - AdaptationSet.TYPE_VIDEO, videoRepresentations); - ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl, - VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE); - videoRenderer = new LibvpxVideoTrackRenderer(videoSampleSource, - true, player.getMainHandler(), player, 50); - } - - // Build the audio renderer. - TrackRenderer audioRenderer; - if (audioRepresentation == null) { - audioRenderer = null; - } else { - DataSource audioDataSource = new DefaultUriDataSource(player, bandwidthMeter, userAgent); - DashChunkSource audioChunkSource = new DashChunkSource( - DefaultDashTrackSelector.newAudioInstance(), audioDataSource, null, - manifest.getPeriodDuration(0), AdaptationSet.TYPE_AUDIO, audioRepresentation); - SampleSource audioSampleSource = new ChunkSampleSource(audioChunkSource, loadControl, - AUDIO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE); - if (audioRepresentationIsOpus) { - audioRenderer = new LibopusAudioTrackRenderer(audioSampleSource); - } else { - audioRenderer = new MediaCodecAudioTrackRenderer(audioSampleSource, - MediaCodecSelector.DEFAULT); - } - } - - TrackRenderer[] renderers = new TrackRenderer[(audioRenderer == null) ? 1 : 2]; - renderers[0] = videoRenderer; - if (audioRenderer != null) { - renderers[1] = audioRenderer; - } - player.onRenderersBuilt(renderers); - } - -} diff --git a/demo_misc/vp9_opus_sw/src/main/java/com/google/android/exoplayer/demo/vp9opus/PlayerActivity.java b/demo_misc/vp9_opus_sw/src/main/java/com/google/android/exoplayer/demo/vp9opus/PlayerActivity.java deleted file mode 100644 index 51e070e401..0000000000 --- a/demo_misc/vp9_opus_sw/src/main/java/com/google/android/exoplayer/demo/vp9opus/PlayerActivity.java +++ /dev/null @@ -1,314 +0,0 @@ -/* - * Copyright (C) 2014 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.exoplayer.demo.vp9opus; - -import com.google.android.exoplayer.AspectRatioFrameLayout; -import com.google.android.exoplayer.ExoPlaybackException; -import com.google.android.exoplayer.ExoPlayer; -import com.google.android.exoplayer.TrackRenderer; -import com.google.android.exoplayer.ext.opus.LibopusAudioTrackRenderer; -import com.google.android.exoplayer.ext.vp9.LibvpxVideoTrackRenderer; -import com.google.android.exoplayer.ext.vp9.VpxDecoderException; -import com.google.android.exoplayer.ext.vp9.VpxVideoSurfaceView; -import com.google.android.exoplayer.extractor.ExtractorSampleSource; -import com.google.android.exoplayer.extractor.webm.WebmExtractor; -import com.google.android.exoplayer.upstream.DefaultAllocator; -import com.google.android.exoplayer.upstream.DefaultUriDataSource; -import com.google.android.exoplayer.util.PlayerControl; -import com.google.android.exoplayer.util.Util; - -import android.Manifest.permission; -import android.annotation.TargetApi; -import android.app.Activity; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.net.Uri; -import android.os.Bundle; -import android.os.Handler; -import android.view.MotionEvent; -import android.view.Surface; -import android.view.SurfaceView; -import android.view.View; -import android.view.View.OnTouchListener; -import android.widget.MediaController; -import android.widget.TextView; -import android.widget.Toast; - -/** - * Sample player that shows how to use ExoPlayer Extensions to playback VP9 Video and Opus Audio. - */ -public class PlayerActivity extends Activity implements - LibvpxVideoTrackRenderer.EventListener, ExoPlayer.Listener { - - /*package*/ static final String CONTENT_TYPE_EXTRA = "content_type"; - /*package*/ static final String USE_OPENGL_ID_EXTRA = "use_opengl"; - - private static final int BUFFER_SEGMENT_SIZE = 64 * 1024; - private static final int BUFFER_SEGMENT_COUNT = 160; - - private Uri contentUri; - private int contentType; - private boolean useOpenGL; - - private ExoPlayer player; - private Handler handler; - private MediaController mediaController; - private AspectRatioFrameLayout videoFrame; - private SurfaceView surfaceView; - private VpxVideoSurfaceView vpxVideoSurfaceView; - private TextView debugInfoView; - private String debugInfo; - private String playerState; - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - Intent intent = getIntent(); - contentUri = intent.getData(); - contentType = intent.getIntExtra(CONTENT_TYPE_EXTRA, - Util.inferContentType(contentUri.toString())); - useOpenGL = intent.getBooleanExtra(USE_OPENGL_ID_EXTRA, true); - - handler = new Handler(); - - setContentView(R.layout.activity_video_player); - View root = findViewById(R.id.root); - root.setOnTouchListener(new OnTouchListener() { - @Override - public boolean onTouch(View view, MotionEvent motionEvent) { - if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) { - toggleControlsVisibility(); - } else if (motionEvent.getAction() == MotionEvent.ACTION_UP) { - view.performClick(); - } - return true; - } - }); - - mediaController = new MediaController(this); - mediaController.setAnchorView(root); - videoFrame = (AspectRatioFrameLayout) findViewById(R.id.video_frame); - surfaceView = (SurfaceView) findViewById(R.id.surface_view); - vpxVideoSurfaceView = (VpxVideoSurfaceView) findViewById(R.id.vpx_surface_view); - debugInfoView = (TextView) findViewById(R.id.debug_info); - debugInfo = ""; - playerState = ""; - updateDebugInfoTextView(); - - if (!maybeRequestPermission()) { - startPlayback(); - } - } - - private void startPlayback() { - if (contentType != Util.TYPE_DASH) { - startBasicPlayback(); - } else { - startDashPlayback(); - } - } - - @Override - public void onPause() { - super.onPause(); - stopPlayback(); - } - - private void startBasicPlayback() { - player = ExoPlayer.Factory.newInstance(2); - player.addListener(this); - mediaController.setMediaPlayer(new PlayerControl(player)); - mediaController.setEnabled(true); - ExtractorSampleSource sampleSource = new ExtractorSampleSource( - contentUri, - new DefaultUriDataSource(this, Util.getUserAgent(this, "ExoPlayerExtWebMDemo")), - new DefaultAllocator(BUFFER_SEGMENT_SIZE), BUFFER_SEGMENT_SIZE * BUFFER_SEGMENT_COUNT, - new WebmExtractor()); - TrackRenderer videoRenderer = - new LibvpxVideoTrackRenderer(sampleSource, true, handler, this, 50); - if (useOpenGL) { - player.sendMessage(videoRenderer, LibvpxVideoTrackRenderer.MSG_SET_OUTPUT_BUFFER_RENDERER, - vpxVideoSurfaceView); - surfaceView.setVisibility(View.GONE); - } else { - player.sendMessage( - videoRenderer, LibvpxVideoTrackRenderer.MSG_SET_SURFACE, - surfaceView.getHolder().getSurface()); - vpxVideoSurfaceView.setVisibility(View.GONE); - } - TrackRenderer audioRenderer = new LibopusAudioTrackRenderer(sampleSource); - player.prepare(videoRenderer, audioRenderer); - player.setPlayWhenReady(true); - } - - private void startDashPlayback() { - playerState = "Initializing"; - updateDebugInfoTextView(); - final String userAgent = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like" - + " Gecko) Chrome/38.0.2125.104 Safari/537.36"; - DashRendererBuilder rendererBuilder = new DashRendererBuilder(contentUri.toString(), - userAgent, this); - rendererBuilder.build(); - } - - public void onRenderersBuilt(TrackRenderer[] renderers) { - surfaceView.setVisibility(View.GONE); - player = ExoPlayer.Factory.newInstance(renderers.length); - player.addListener(this); - mediaController.setMediaPlayer(new PlayerControl(player)); - mediaController.setEnabled(true); - player.sendMessage(renderers[0], LibvpxVideoTrackRenderer.MSG_SET_OUTPUT_BUFFER_RENDERER, - vpxVideoSurfaceView); - player.prepare(renderers); - player.setPlayWhenReady(true); - } - - @Override - public void onDroppedFrames(int count, long elapsed) { - // do nothing. - } - - @Override - public void onVideoSizeChanged(int width, int height) { - videoFrame.setAspectRatio(height == 0 ? 1 : (width * 1.0f) / height); - debugInfo = "Video: " + width + " x " + height; - updateDebugInfoTextView(); - } - - @Override - public void onDrawnToSurface(Surface surface) { - // do nothing. - } - - @Override - public void onDecoderError(VpxDecoderException e) { - debugInfo = "Libvpx decode failure. Giving up."; - updateDebugInfoTextView(); - } - - @Override - public void onPlayerStateChanged(boolean playWhenReady, int state) { - switch (player.getPlaybackState()) { - case ExoPlayer.STATE_BUFFERING: - playerState = "buffering"; - break; - case ExoPlayer.STATE_ENDED: - playerState = "ended"; - break; - case ExoPlayer.STATE_IDLE: - playerState = "idle"; - break; - case ExoPlayer.STATE_PREPARING: - playerState = "preparing"; - break; - case ExoPlayer.STATE_READY: - playerState = "ready"; - break; - } - updateDebugInfoTextView(); - } - - @Override - public void onPlayerError(ExoPlaybackException exception) { - debugInfo = "Exoplayer Playback error. Giving up."; - updateDebugInfoTextView(); - // TODO: show a retry button here. - } - - @Override - public void onPlayWhenReadyCommitted() { - // Do nothing. - } - - public Handler getMainHandler() { - return handler; - } - - // Permission management methods - - @Override - public void onRequestPermissionsResult(int requestCode, String[] permissions, - int[] grantResults) { - if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - startPlayback(); - } else { - Toast.makeText(getApplicationContext(), R.string.storage_permission_denied, - Toast.LENGTH_LONG).show(); - finish(); - } - } - - /** - * Checks whether it is necessary to ask for permission to read storage. If necessary, it also - * requests permission. - * - * @return true if a permission request is made. False if it is not necessary. - */ - @TargetApi(23) - private boolean maybeRequestPermission() { - if (requiresPermission(contentUri)) { - requestPermissions(new String[] {permission.READ_EXTERNAL_STORAGE}, 0); - return true; - } else { - return false; - } - } - - @TargetApi(23) - private boolean requiresPermission(Uri uri) { - return Util.SDK_INT >= 23 && Util.isLocalFileUri(uri) - && checkSelfPermission(permission.READ_EXTERNAL_STORAGE) - != PackageManager.PERMISSION_GRANTED; - } - - // Internal methods - - private void stopPlayback() { - if (player != null) { - player.stop(); - player.release(); - player = null; - } - } - - private void toggleControlsVisibility() { - if (mediaController != null && player != null) { - if (mediaController.isShowing()) { - mediaController.hide(); - } else { - mediaController.show(0); - } - } - } - - private void updateDebugInfoTextView() { - StringBuilder debugInfoText = new StringBuilder(); - debugInfoText.append( - getString(R.string.libvpx_version, LibvpxVideoTrackRenderer.getLibvpxVersion())); - debugInfoText.append(" "); - debugInfoText.append( - getString(R.string.libopus_version, LibopusAudioTrackRenderer.getLibopusVersion())); - debugInfoText.append("\n"); - debugInfoText.append(getString(R.string.current_path, contentUri.toString())); - debugInfoText.append(" "); - debugInfoText.append(debugInfo); - debugInfoText.append(" "); - debugInfoText.append(playerState); - debugInfoView.setText(debugInfoText.toString()); - } - -} diff --git a/demo_misc/vp9_opus_sw/src/main/java/com/google/android/exoplayer/demo/vp9opus/SampleChooserActivity.java b/demo_misc/vp9_opus_sw/src/main/java/com/google/android/exoplayer/demo/vp9opus/SampleChooserActivity.java deleted file mode 100644 index adf0ba1e31..0000000000 --- a/demo_misc/vp9_opus_sw/src/main/java/com/google/android/exoplayer/demo/vp9opus/SampleChooserActivity.java +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Copyright (C) 2014 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.exoplayer.demo.vp9opus; - -import com.google.android.exoplayer.util.Util; - -import android.app.Activity; -import android.content.Context; -import android.content.Intent; -import android.net.Uri; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -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; - -/** - * An activity for selecting from a number of samples. - */ -public class SampleChooserActivity extends Activity { - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.sample_chooser_activity); - - ListView sampleList = (ListView) findViewById(R.id.sample_list); - final SampleAdapter sampleAdapter = new SampleAdapter(this); - - sampleAdapter.add(new Header("DASH - VP9 Only")); - sampleAdapter.add(new Sample("Google Glass", - "http://demos.webmproject.org/dash/201410/vp9_glass/manifest_vp9.mpd", - Util.TYPE_DASH)); - sampleAdapter.add(new Header("DASH - VP9 and Opus")); - sampleAdapter.add(new Sample("Google Glass", - "http://demos.webmproject.org/dash/201410/vp9_glass/manifest_vp9_opus.mpd", - Util.TYPE_DASH)); - sampleAdapter.add(new Header("DASH - VP9 and Vorbis")); - sampleAdapter.add(new Sample("Google Glass", - "http://demos.webmproject.org/dash/201410/vp9_glass/manifest_vp9_vorbis.mpd", - Util.TYPE_DASH)); - - sampleList.setAdapter(sampleAdapter); - sampleList.setOnItemClickListener(new OnItemClickListener() { - @Override - public void onItemClick(AdapterView parent, View view, int position, long id) { - Object item = sampleAdapter.getItem(position); - if (item instanceof Sample) { - onSampleSelected((Sample) item); - } - } - }); - } - - private void onSampleSelected(Sample sample) { - Intent playerIntent = new Intent(this, PlayerActivity.class) - .setData(Uri.parse(sample.uri)) - .putExtra(PlayerActivity.CONTENT_TYPE_EXTRA, sample.type); - startActivity(playerIntent); - } - - private static class SampleAdapter extends ArrayAdapter { - - public SampleAdapter(Context context) { - super(context, 0); - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - View view = convertView; - if (view == null) { - int layoutId = getItemViewType(position) == 1 ? android.R.layout.simple_list_item_1 - : R.layout.sample_chooser_inline_header; - view = LayoutInflater.from(getContext()).inflate(layoutId, null, false); - } - Object item = getItem(position); - String name = null; - if (item instanceof Sample) { - name = ((Sample) item).description; - } else if (item instanceof Header) { - name = ((Header) item).name; - } - ((TextView) view).setText(name); - return view; - } - - @Override - public int getItemViewType(int position) { - return (getItem(position) instanceof Sample) ? 1 : 0; - } - - @Override - public int getViewTypeCount() { - return 2; - } - - } - - private static class Sample { - - public final String description; - public final String uri; - public final int type; - - public Sample(String description, String uri, int type) { - this.description = description; - this.uri = uri; - this.type = type; - } - - } - - private static class Header { - - public final String name; - - public Header(String name) { - this.name = name; - } - - } - -} diff --git a/demo_misc/vp9_opus_sw/src/main/project.properties b/demo_misc/vp9_opus_sw/src/main/project.properties deleted file mode 100644 index f85c9b372d..0000000000 --- a/demo_misc/vp9_opus_sw/src/main/project.properties +++ /dev/null @@ -1,17 +0,0 @@ -# This file is automatically generated by Android Tools. -# Do not modify this file -- YOUR CHANGES WILL BE ERASED! -# -# This file must be checked in Version Control Systems. -# -# To customize properties used by the Ant build system edit -# "ant.properties", and override values to adapt the script to your -# project structure. -# -# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): -#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt - -# Project target. -target=android-23 -android.library.reference.1=../../../../library/src/main -android.library.reference.2=../../../../extensions/opus/src/main -android.library.reference.3=../../../../extensions/vp9/src/main diff --git a/demo_misc/vp9_opus_sw/src/main/res/drawable-hdpi/ic_launcher.png b/demo_misc/vp9_opus_sw/src/main/res/drawable-hdpi/ic_launcher.png deleted file mode 100644 index 3e5716b8ad18f4c26bb4ceb5d53370160a6ff212..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2929 zcmV-%3y$=OP)99J2~|IeA-y_d zfntAen$65iA!RvWLJb+FaqczZn~(hBwnih`LlH3muw;@>v8EDI%A(dp_6n&lCKQ#G zaIR&OZ*dG{8w_?18+AyQAYFz)B;#)9kH6dhqa$;3a}t1r237*(feGFHVa=oy0O)~- zesH4IY~BDczy#>FGaErcH=Qj421EaYTSxzML4gC;hVwL^-Sdq|V8i<9f9G-c_V0ad z-%Gk;(z=)fM*yIkzkPVqbvt)`sUuoD73CTZ4dh z_X4ha{W%B`87y_6P2I4XlWHO$%Ox)SZ{eDC&E}5*>;{knAU_5P0AlxdAHS-#<70n2 zm*!JB1lZh3BO-fe%MRTCT_cUI1OsTcXK;0=gZA_^NC>P|C?W!e<=g3Hw?M^a2EamP zK(0GQ*|u9Beyhh*8M|<_@v!&3-y{Pv&yjSykoht)8qr1*9}|E`Z{!XXP0WZEIJ(n7 zvC#?$x|TlN_q|l4Qv`z{WsY9l0lU$&bbUF8)n2m-WFJ8bMtJHG0HTCNn^`AxHW!Y2 z$`({Al)dk6Vg|EB5_iEYZLloYdU-7zczH1M(lMd8uz~>c*+8;wTYuaM#GX*G41k^f zoh73e$5u&xJNuMa(VK;@_4D#oi9u$2Zs8oTDl%OlvyK~%VvNg42TW<>qJQ!gXG}_` z>y?aN5&kS)CkQT@#w8@vP#hMaYsv4D)PY?x2l4pKC?5dXFzx1y*P$+ zV$#s+;jqI58m;p!Yr4)m-EQ8hs4nQb$6Upfj~Czj&goC2gElm{EJ@I4wLn_fOI42_ z)9T%;x}4x#%(znUT!rt4!1%{)y;+YYH!PwiXdNm8cJ6}2n<01w` zKO8sM=UIxYHf;>TG1vAt(hIx;6;l(Ts{r|plc?|2LQwj8J)%}V9q>WNSYVuVy5Q+n~SY$C3cVKvM2mlj-0PNav1HSr& z&*Ht;zH6CqhIg$hKz1AyYJ8tdApkLy&N&ZeJy7H_7<;Y0vpP7LrH+j=>#*;hJ8{>? zZ^cxzxlB0q;~yVTX@SgbxWD;Is)84W#=083JR&ncdK9! zm(pM;2PYVCg%a#Egvi2iA*E>wX2#}C8?f)by}19=x1l{XBW1Kh>V-`!yyM)mSt!(8XzL<-nkvy zw{F3K!$Gz?OZ~h_EZC$in~VcvlVl)^q5e)j}Q9)L0}(tp(0#e>8I5-L9sAVdTqL;=sZk>ce%y-jrelW%ra%cH zAOVQd{EBQgux@1(RS(C7WZ|!%89-{_3OzuBPz|C8?db@;B!{Ne%&nI*;zz;dIpg&YaM`9n{g{}@TDCBRKxl2 z{6*|P^bF=+`}0oMl z8bWwI7nE{}SXQMV!m$%C;8#cffH>(5Th)=x0FqD->m`7k2MZ}X5YRc%(t?H3g26ks}7Rd+( zhUyS{iGt*guZWc5o_YQ+c;eV`^s)?V2?yv0q+$@OiVbU{cq8fQOfi@wn@MVwWIHeX z@Zcewzwow6JwizIK{C%%dwV_g5w~5^IwmzR)L|#u3_M~r}BL5AcaQKGXBZ;A8IblCsD-@y_ae- z&p|?{ZAoy7Yh+&fiTg^{u{232tti6G>W@ElAx#!`wC=k9?&z9p?_ff6q*BaGSG~@I z*)(~d0GhMi&N7rkc4wZJ6i+f%ZZMgpy_ez4uSq`%AlM&~qY0p;w+$6igZ3^GwIEZxE#zb^FRqB1<-@KxtaefSNlGk z-i!){g=5SIBv6KJ)H+Uc*YOOX9D31EZ@eb2WI}=Q-E_RO(8AosLU-v|0+4I+QZEWv z=A2Q{xUIAri~^Qbco5J^nfERl0{hFsII!1Gnk!xlpIBj&|GT5cCV*;e0;t9&fNE?4 bsK)*eyQy>SBxZs*00000NkvXXu0mjf)E8>! diff --git a/demo_misc/vp9_opus_sw/src/main/res/drawable-mdpi/ic_launcher.png b/demo_misc/vp9_opus_sw/src/main/res/drawable-mdpi/ic_launcher.png deleted file mode 100644 index a5d2a53b131511f9734eba6be2b6475177dcb5ed..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1798 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 N+`FT%S*~sy`9C%i_>2Gm diff --git a/demo_misc/vp9_opus_sw/src/main/res/drawable-xxhdpi/ic_launcher.png b/demo_misc/vp9_opus_sw/src/main/res/drawable-xxhdpi/ic_launcher.png deleted file mode 100644 index ef2f312fd4bff190a270ba2366d3c2159bc63493..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6846 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 diff --git a/demo_misc/vp9_opus_sw/src/main/res/layout/activity_video_player.xml b/demo_misc/vp9_opus_sw/src/main/res/layout/activity_video_player.xml deleted file mode 100644 index 176f28e6e6..0000000000 --- a/demo_misc/vp9_opus_sw/src/main/res/layout/activity_video_player.xml +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/demo_misc/vp9_opus_sw/src/main/res/layout/rows.xml b/demo_misc/vp9_opus_sw/src/main/res/layout/rows.xml deleted file mode 100644 index e645abd266..0000000000 --- a/demo_misc/vp9_opus_sw/src/main/res/layout/rows.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - diff --git a/demo_misc/vp9_opus_sw/src/main/res/layout/sample_chooser_activity.xml b/demo_misc/vp9_opus_sw/src/main/res/layout/sample_chooser_activity.xml deleted file mode 100644 index ae9be53796..0000000000 --- a/demo_misc/vp9_opus_sw/src/main/res/layout/sample_chooser_activity.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - diff --git a/demo_misc/vp9_opus_sw/src/main/res/layout/sample_chooser_inline_header.xml b/demo_misc/vp9_opus_sw/src/main/res/layout/sample_chooser_inline_header.xml deleted file mode 100644 index 8df32d76a1..0000000000 --- a/demo_misc/vp9_opus_sw/src/main/res/layout/sample_chooser_inline_header.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - diff --git a/demo_misc/vp9_opus_sw/src/main/res/values-v11/styles.xml b/demo_misc/vp9_opus_sw/src/main/res/values-v11/styles.xml deleted file mode 100644 index b7b29d5e5c..0000000000 --- a/demo_misc/vp9_opus_sw/src/main/res/values-v11/styles.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - diff --git a/extensions/README.md b/extensions/README.md deleted file mode 100644 index 5bb863c13e..0000000000 --- a/extensions/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Extensions # - -This folder contains optional ExoPlayer extensions. diff --git a/extensions/okhttp/README.md b/extensions/okhttp/README.md deleted file mode 100644 index 688bf8e08a..0000000000 --- a/extensions/okhttp/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# ExoPlayer OkHttp Extension # - -## Description ## - -The OkHttp Extension is an [HttpDataSource][] implementation using Square's [OkHttp][]. - -[HttpDataSource]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer/upstream/HttpDataSource.html -[OkHttp]: https://square.github.io/okhttp/ - diff --git a/extensions/okhttp/build.gradle b/extensions/okhttp/build.gradle deleted file mode 100644 index f7c3ce6256..0000000000 --- a/extensions/okhttp/build.gradle +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (C) 2014 The Android Open Source Project -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -apply plugin: 'com.android.library' - -android { - compileSdkVersion 23 - buildToolsVersion "23.0.1" - - defaultConfig { - minSdkVersion 9 - targetSdkVersion 23 - } - - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' - } - } - - lintOptions { - abortOnError false - } -} - -dependencies { - compile project(':library') - compile('com.squareup.okhttp3:okhttp:+') { - exclude group: 'org.json' - } -} diff --git a/extensions/okhttp/src/main/.classpath b/extensions/okhttp/src/main/.classpath deleted file mode 100644 index 7534a5f8cc..0000000000 --- a/extensions/okhttp/src/main/.classpath +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/extensions/okhttp/src/main/.project b/extensions/okhttp/src/main/.project deleted file mode 100644 index b36efd2579..0000000000 --- a/extensions/okhttp/src/main/.project +++ /dev/null @@ -1,33 +0,0 @@ - - - ExoPlayerExt-OkHttp - - - - - - com.android.ide.eclipse.adt.ResourceManagerBuilder - - - - - com.android.ide.eclipse.adt.PreCompilerBuilder - - - - - org.eclipse.jdt.core.javabuilder - - - - - com.android.ide.eclipse.adt.ApkBuilder - - - - - - com.android.ide.eclipse.adt.AndroidNature - org.eclipse.jdt.core.javanature - - diff --git a/extensions/okhttp/src/main/AndroidManifest.xml b/extensions/okhttp/src/main/AndroidManifest.xml deleted file mode 100644 index 8ca247dcaf..0000000000 --- a/extensions/okhttp/src/main/AndroidManifest.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - diff --git a/extensions/okhttp/src/main/java/com/google/android/exoplayer/ext/okhttp/OkHttpDataSource.java b/extensions/okhttp/src/main/java/com/google/android/exoplayer/ext/okhttp/OkHttpDataSource.java deleted file mode 100644 index 6163655a63..0000000000 --- a/extensions/okhttp/src/main/java/com/google/android/exoplayer/ext/okhttp/OkHttpDataSource.java +++ /dev/null @@ -1,372 +0,0 @@ -/* - * Copyright (C) 2014 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.exoplayer.ext.okhttp; - -import com.google.android.exoplayer.C; -import com.google.android.exoplayer.upstream.DataSpec; -import com.google.android.exoplayer.upstream.HttpDataSource; -import com.google.android.exoplayer.upstream.TransferListener; -import com.google.android.exoplayer.util.Assertions; -import com.google.android.exoplayer.util.Predicate; - -import okhttp3.CacheControl; -import okhttp3.HttpUrl; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.RequestBody; -import okhttp3.Response; - -import java.io.EOFException; -import java.io.IOException; -import java.io.InputStream; -import java.io.InterruptedIOException; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.atomic.AtomicReference; - -/** - * An {@link HttpDataSource} that delegates to Square's {@link OkHttpClient}. - */ -public class OkHttpDataSource implements HttpDataSource { - - private static final AtomicReference skipBufferReference = new AtomicReference<>(); - - private final OkHttpClient okHttpClient; - private final String userAgent; - private final Predicate contentTypePredicate; - private final TransferListener listener; - private final CacheControl cacheControl; - private final HashMap requestProperties; - - private DataSpec dataSpec; - private Response response; - private InputStream responseByteStream; - private boolean opened; - - private long bytesToSkip; - private long bytesToRead; - - private long bytesSkipped; - private long bytesRead; - - /** - * @param client An {@link OkHttpClient} for use by the source. - * @param userAgent The User-Agent string that should be used. - * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the - * predicate then a - * {@link com.google.android.exoplayer.upstream.HttpDataSource.InvalidContentTypeException} is - * thrown from {@link #open(DataSpec)}. - */ - public OkHttpDataSource(OkHttpClient client, String userAgent, - Predicate contentTypePredicate) { - this(client, userAgent, contentTypePredicate, null); - } - - /** - * @param client An {@link OkHttpClient} for use by the source. - * @param userAgent The User-Agent string that should be used. - * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the - * predicate then a - * {@link com.google.android.exoplayer.upstream.HttpDataSource.InvalidContentTypeException} is - * thrown from {@link #open(DataSpec)}. - * @param listener An optional listener. - */ - public OkHttpDataSource(OkHttpClient client, String userAgent, - Predicate contentTypePredicate, TransferListener listener) { - this(client, userAgent, contentTypePredicate, listener, null); - } - - /** - * @param client An {@link OkHttpClient} for use by the source. - * @param userAgent The User-Agent string that should be used. - * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the - * predicate then a - * {@link com.google.android.exoplayer.upstream.HttpDataSource.InvalidContentTypeException} is - * thrown from {@link #open(DataSpec)}. - * @param listener An optional listener. - * @param cacheControl An optional {@link CacheControl} which sets all requests' Cache-Control - * header. For example, you could force the network response for all requests. - * - */ - public OkHttpDataSource(OkHttpClient client, String userAgent, - Predicate contentTypePredicate, TransferListener listener, - CacheControl cacheControl) { - this.okHttpClient = Assertions.checkNotNull(client); - this.userAgent = Assertions.checkNotEmpty(userAgent); - this.contentTypePredicate = contentTypePredicate; - this.listener = listener; - this.cacheControl = cacheControl; - this.requestProperties = new HashMap<>(); - } - - @Override - public String getUri() { - return response == null ? null : response.request().url().toString(); - } - - @Override - public Map> getResponseHeaders() { - return response == null ? null : response.headers().toMultimap(); - } - - @Override - public void setRequestProperty(String name, String value) { - Assertions.checkNotNull(name); - Assertions.checkNotNull(value); - synchronized (requestProperties) { - requestProperties.put(name, value); - } - } - - @Override - public void clearRequestProperty(String name) { - Assertions.checkNotNull(name); - synchronized (requestProperties) { - requestProperties.remove(name); - } - } - - @Override - public void clearAllRequestProperties() { - synchronized (requestProperties) { - requestProperties.clear(); - } - } - - @Override - public long open(DataSpec dataSpec) throws HttpDataSourceException { - this.dataSpec = dataSpec; - this.bytesRead = 0; - this.bytesSkipped = 0; - Request request = makeRequest(dataSpec); - try { - response = okHttpClient.newCall(request).execute(); - responseByteStream = response.body().byteStream(); - } catch (IOException e) { - throw new HttpDataSourceException("Unable to connect to " + dataSpec.uri.toString(), e, - dataSpec); - } - - int responseCode = response.code(); - - // Check for a valid response code. - if (!response.isSuccessful()) { - Map> headers = request.headers().toMultimap(); - closeConnectionQuietly(); - throw new InvalidResponseCodeException(responseCode, headers, dataSpec); - } - - // Check for a valid content type. - String contentType = response.body().contentType().toString(); - if (contentTypePredicate != null && !contentTypePredicate.evaluate(contentType)) { - closeConnectionQuietly(); - throw new InvalidContentTypeException(contentType, dataSpec); - } - - // If we requested a range starting from a non-zero position and received a 200 rather than a - // 206, then the server does not support partial requests. We'll need to manually skip to the - // requested position. - bytesToSkip = responseCode == 200 && dataSpec.position != 0 ? dataSpec.position : 0; - - // Determine the length of the data to be read, after skipping. - long contentLength = response.body().contentLength(); - bytesToRead = dataSpec.length != C.LENGTH_UNBOUNDED ? dataSpec.length - : contentLength != -1 ? contentLength - bytesToSkip - : C.LENGTH_UNBOUNDED; - - opened = true; - if (listener != null) { - listener.onTransferStart(); - } - - return bytesToRead; - } - - @Override - public int read(byte[] buffer, int offset, int readLength) throws HttpDataSourceException { - try { - skipInternal(); - return readInternal(buffer, offset, readLength); - } catch (IOException e) { - throw new HttpDataSourceException(e, dataSpec); - } - } - - @Override - public void close() throws HttpDataSourceException { - if (opened) { - opened = false; - if (listener != null) { - listener.onTransferEnd(); - } - closeConnectionQuietly(); - } - } - - /** - * Returns the number of bytes that have been skipped since the most recent call to - * {@link #open(DataSpec)}. - * - * @return The number of bytes skipped. - */ - protected final long bytesSkipped() { - return bytesSkipped; - } - - /** - * Returns the number of bytes that have been read since the most recent call to - * {@link #open(DataSpec)}. - * - * @return The number of bytes read. - */ - protected final long bytesRead() { - return bytesRead; - } - - /** - * Returns the number of bytes that are still to be read for the current {@link DataSpec}. - *

- * If the total length of the data being read is known, then this length minus {@code bytesRead()} - * is returned. If the total length is unknown, {@link C#LENGTH_UNBOUNDED} is returned. - * - * @return The remaining length, or {@link C#LENGTH_UNBOUNDED}. - */ - protected final long bytesRemaining() { - return bytesToRead == C.LENGTH_UNBOUNDED ? bytesToRead : bytesToRead - bytesRead; - } - - /** - * Establishes a connection. - */ - private Request makeRequest(DataSpec dataSpec) { - long position = dataSpec.position; - long length = dataSpec.length; - boolean allowGzip = (dataSpec.flags & DataSpec.FLAG_ALLOW_GZIP) != 0; - - HttpUrl url = HttpUrl.parse(dataSpec.uri.toString()); - Request.Builder builder = new Request.Builder().url(url); - if (cacheControl != null) { - builder.cacheControl(cacheControl); - } - synchronized (requestProperties) { - for (Map.Entry property : requestProperties.entrySet()) { - builder.addHeader(property.getKey(), property.getValue()); - } - } - if (!(position == 0 && length == C.LENGTH_UNBOUNDED)) { - String rangeRequest = "bytes=" + position + "-"; - if (length != C.LENGTH_UNBOUNDED) { - rangeRequest += (position + length - 1); - } - builder.addHeader("Range", rangeRequest); - } - builder.addHeader("User-Agent", userAgent); - if (!allowGzip) { - builder.addHeader("Accept-Encoding", "identity"); - } - if (dataSpec.postBody != null) { - builder.post(RequestBody.create(null, dataSpec.postBody)); - } - return builder.build(); - } - - /** - * Skips any bytes that need skipping. Else does nothing. - *

- * This implementation is based roughly on {@code libcore.io.Streams.skipByReading()}. - * - * @throws InterruptedIOException If the thread is interrupted during the operation. - * @throws EOFException If the end of the input stream is reached before the bytes are skipped. - */ - private void skipInternal() throws IOException { - if (bytesSkipped == bytesToSkip) { - return; - } - - // Acquire the shared skip buffer. - byte[] skipBuffer = skipBufferReference.getAndSet(null); - if (skipBuffer == null) { - skipBuffer = new byte[4096]; - } - - while (bytesSkipped != bytesToSkip) { - int readLength = (int) Math.min(bytesToSkip - bytesSkipped, skipBuffer.length); - int read = responseByteStream.read(skipBuffer, 0, readLength); - if (Thread.interrupted()) { - throw new InterruptedIOException(); - } - if (read == -1) { - throw new EOFException(); - } - bytesSkipped += read; - if (listener != null) { - listener.onBytesTransferred(read); - } - } - - // Release the shared skip buffer. - skipBufferReference.set(skipBuffer); - } - - /** - * Reads up to {@code length} bytes of data and stores them into {@code buffer}, starting at - * index {@code offset}. - *

- * This method blocks until at least one byte of data can be read, the end of the opened range is - * detected, or an exception is thrown. - * - * @param buffer The buffer into which the read data should be stored. - * @param offset The start offset into {@code buffer} at which data should be written. - * @param readLength The maximum number of bytes to read. - * @return The number of bytes read, or {@link C#RESULT_END_OF_INPUT} if the end of the opened - * range is reached. - * @throws IOException If an error occurs reading from the source. - */ - private int readInternal(byte[] buffer, int offset, int readLength) throws IOException { - readLength = bytesToRead == C.LENGTH_UNBOUNDED ? readLength - : (int) Math.min(readLength, bytesToRead - bytesRead); - if (readLength == 0) { - // We've read all of the requested data. - return C.RESULT_END_OF_INPUT; - } - - int read = responseByteStream.read(buffer, offset, readLength); - if (read == -1) { - if (bytesToRead != C.LENGTH_UNBOUNDED && bytesToRead != bytesRead) { - // The server closed the connection having not sent sufficient data. - throw new EOFException(); - } - return C.RESULT_END_OF_INPUT; - } - - bytesRead += read; - if (listener != null) { - listener.onBytesTransferred(read); - } - return read; - } - - /** - * Closes the current connection quietly, if there is one. - */ - private void closeConnectionQuietly() { - response.body().close(); - response = null; - responseByteStream = null; - } - -} diff --git a/extensions/okhttp/src/main/project.properties b/extensions/okhttp/src/main/project.properties deleted file mode 100644 index b92a03b7ab..0000000000 --- a/extensions/okhttp/src/main/project.properties +++ /dev/null @@ -1,16 +0,0 @@ -# This file is automatically generated by Android Tools. -# Do not modify this file -- YOUR CHANGES WILL BE ERASED! -# -# This file must be checked in Version Control Systems. -# -# To customize properties used by the Ant build system edit -# "ant.properties", and override values to adapt the script to your -# project structure. -# -# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): -#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt - -# Project target. -target=android-23 -android.library=true -android.library.reference.1=../../../../library/src/main diff --git a/extensions/opus/README.md b/extensions/opus/README.md deleted file mode 100644 index 46bb6942e6..0000000000 --- a/extensions/opus/README.md +++ /dev/null @@ -1,131 +0,0 @@ -# ExoPlayer Opus Extension # - -## Description ## - -The Opus Extension is a [TrackRenderer][] implementation that helps you bundle libopus (the Opus decoding library) into your app and use it along with ExoPlayer to play Opus audio on Android devices. - -[TrackRenderer]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer/TrackRenderer.html - -## Build Instructions (Android Studio and Eclipse) ## - -Building the Opus Extension involves building libopus and JNI bindings using the Android NDK and linking it into your app. The following steps will tell you how to do that using Android Studio or Eclipse. - -* Checkout ExoPlayer along with Extensions - -``` -git clone https://github.com/google/ExoPlayer.git -``` - -* Set the following environment variables: - -``` -cd "" -EXOPLAYER_ROOT="$(pwd)" -OPUS_EXT_PATH="${EXOPLAYER_ROOT}/extensions/opus/src/main" -``` - -* Download the [Android NDK][] and set its location in an environment variable: - -``` -NDK_PATH="" -``` - -* Fetch libopus - -``` -cd "${OPUS_EXT_PATH}/jni" && \ -git clone git://git.opus-codec.org/opus.git libopus -``` - -* Run the script to convert arm assembly to NDK compatible format - -``` -cd ${OPUS_EXT_PATH}/jni && ./convert_android_asm.sh -``` - -### Android Studio ### - -For Android Studio, we build the native libraries from the command line and then Gradle will pick it up when building your app using Android Studio. - -* Build the JNI native libraries - -``` -cd "${OPUS_EXT_PATH}"/jni && \ -${NDK_PATH}/ndk-build APP_ABI=all -j4 -``` - -* In your project, you can add a dependency to the Opus Extension by using a rule like this: - -``` -// in settings.gradle -include ':..:ExoPlayer:library' -include ':..:ExoPlayer:opus-extension' - -// in build.gradle -dependencies { - compile project(':..:ExoPlayer:library') - compile project(':..:ExoPlayer:opus-extension') -} -``` - -* Now, when you build your app, the Opus extension will be built and the native libraries will be packaged along with the APK. - -### Eclipse ### - -* The following steps assume that you have installed Eclipse and configured it with the [Android SDK][] and [Android NDK ][]: - * Navigate to File->Import->General->Existing Projects into Workspace - * Select the root directory of the repository - * Import the following projects: - * ExoPlayerLib - * ExoPlayerExt-Opus - * If you are able to build ExoPlayerExt-Opus project, then you're all set. - * (Optional) To speed up the NDK build: - * Right click on ExoPlayerExt-Opus in the Project Explorer pane and choose Properties - * Click on C/C++ Build - * Uncheck `Use default build command` - * In `Build Command` enter: `ndk-build -j4` (adjust 4 to a reasonable number depending on the number of cores in your computer) - * Click Apply - -You can now create your own Android App project and add ExoPlayerLib along with ExoPlayerExt-Opus as a dependencies to use ExoPlayer along with the Opus Extension. - - -[Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html - -[Android NDK ]: http://tools.android.com/recent/usingthendkplugin -[Android SDK]: http://developer.android.com/sdk/installing/index.html?pkg=tools - -## Building for various Architectures ## - -### Android Studio ### - -The manual invocation of `ndk-build` will build the library for all architectures and the correct one will be picked up from the APK based on the device its running on. - -### Eclipse ### - -libopus can be built for the following architectures: - -* armeabi (the default - does not include neon optimizations) -* armeabi-v7a (choose this to enable neon optimizations) -* mips -* x86 -* all (will result in a larger binary but will cover all architectures) - -You can build for a specific architecture in two ways: - -* Method 1 (edit `Application.mk`) - * Edit `${OPUS_EXT_PATH}/jni/Application.mk` and add the following line `APP_ABI := ` (where `` is one of the above 4 architectures) -* Method 2 (pass NDK build flag) - * Right click on ExoPlayerExt-Opus in the Project Explorer pane and choose Properties - * Click on C/C++ Build - * Uncheck `Use default build command` - * In `Build Command` enter: `ndk-build APP_ABI=` (where `` is one of the above 4 architectures) - * Click Apply - -## Other Things to Note ## - -* Every time there is a change to the libopus checkout: - * Arm assembly should be converted by running `convert_android_asm.sh` - * Clean and re-build the project. -* If you want to use your own version of libopus, place it in `${OPUS_EXT_PATH}/jni/libopus`. diff --git a/extensions/opus/build.gradle b/extensions/opus/build.gradle deleted file mode 100644 index 9ccc1862aa..0000000000 --- a/extensions/opus/build.gradle +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright (C) 2014 The Android Open Source Project -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -apply plugin: 'com.android.library' - -android { - compileSdkVersion 23 - buildToolsVersion "23.0.1" - - defaultConfig { - minSdkVersion 9 - targetSdkVersion 23 - } - - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' - } - } - - lintOptions { - abortOnError false - } - - sourceSets.main { - jniLibs.srcDir 'src/main/libs' - jni.srcDirs = [] // Disable the automatic ndk-build call by Android Studio. - } -} - -dependencies { - compile project(':library') -} - diff --git a/extensions/opus/src/main/.classpath b/extensions/opus/src/main/.classpath deleted file mode 100644 index 503bb38b67..0000000000 --- a/extensions/opus/src/main/.classpath +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/extensions/opus/src/main/.cproject b/extensions/opus/src/main/.cproject deleted file mode 100644 index 22cc11ab57..0000000000 --- a/extensions/opus/src/main/.cproject +++ /dev/null @@ -1,57 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/extensions/opus/src/main/.project b/extensions/opus/src/main/.project deleted file mode 100644 index 9b65339b9c..0000000000 --- a/extensions/opus/src/main/.project +++ /dev/null @@ -1,97 +0,0 @@ - - - ExoPlayerExt-Opus - - - - - - org.eclipse.cdt.managedbuilder.core.genmakebuilder - clean,full,incremental, - - - ?children? - ?name?=outputEntries\|?children?=?name?=entry\\\\\\\|\\\|?name?=entry\\\\\\\|\\\|\|| - - - ?name? - - - - org.eclipse.cdt.make.core.append_environment - true - - - org.eclipse.cdt.make.core.buildArguments - - - - org.eclipse.cdt.make.core.buildCommand - ndk-build - - - org.eclipse.cdt.make.core.cleanBuildTarget - clean - - - org.eclipse.cdt.make.core.contents - org.eclipse.cdt.make.core.activeConfigSettings - - - org.eclipse.cdt.make.core.enableAutoBuild - false - - - org.eclipse.cdt.make.core.enableCleanBuild - true - - - org.eclipse.cdt.make.core.enableFullBuild - true - - - org.eclipse.cdt.make.core.stopOnError - true - - - org.eclipse.cdt.make.core.useDefaultBuildCmd - true - - - - - com.android.ide.eclipse.adt.ResourceManagerBuilder - - - - - com.android.ide.eclipse.adt.PreCompilerBuilder - - - - - org.eclipse.jdt.core.javabuilder - - - - - com.android.ide.eclipse.adt.ApkBuilder - - - - - org.eclipse.cdt.managedbuilder.core.ScannerConfigBuilder - full,incremental, - - - - - - com.android.ide.eclipse.adt.AndroidNature - org.eclipse.jdt.core.javanature - org.eclipse.cdt.core.cnature - org.eclipse.cdt.core.ccnature - org.eclipse.cdt.managedbuilder.core.managedBuildNature - org.eclipse.cdt.managedbuilder.core.ScannerConfigNature - - diff --git a/extensions/opus/src/main/.settings/org.eclipse.jdt.core.prefs b/extensions/opus/src/main/.settings/org.eclipse.jdt.core.prefs deleted file mode 100644 index d17b6724d1..0000000000 --- a/extensions/opus/src/main/.settings/org.eclipse.jdt.core.prefs +++ /dev/null @@ -1,12 +0,0 @@ -eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled -org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate -org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7 -org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve -org.eclipse.jdt.core.compiler.compliance=1.7 -org.eclipse.jdt.core.compiler.debug.lineNumber=generate -org.eclipse.jdt.core.compiler.debug.localVariable=generate -org.eclipse.jdt.core.compiler.debug.sourceFile=generate -org.eclipse.jdt.core.compiler.problem.assertIdentifier=error -org.eclipse.jdt.core.compiler.problem.enumIdentifier=error -org.eclipse.jdt.core.compiler.source=1.7 diff --git a/extensions/opus/src/main/AndroidManifest.xml b/extensions/opus/src/main/AndroidManifest.xml deleted file mode 100644 index 7c26ae79f3..0000000000 --- a/extensions/opus/src/main/AndroidManifest.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - diff --git a/extensions/opus/src/main/java/com/google/android/exoplayer/ext/opus/LibopusAudioTrackRenderer.java b/extensions/opus/src/main/java/com/google/android/exoplayer/ext/opus/LibopusAudioTrackRenderer.java deleted file mode 100644 index acffce3f97..0000000000 --- a/extensions/opus/src/main/java/com/google/android/exoplayer/ext/opus/LibopusAudioTrackRenderer.java +++ /dev/null @@ -1,417 +0,0 @@ -/* - * Copyright (C) 2014 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.exoplayer.ext.opus; - -import com.google.android.exoplayer.CodecCounters; -import com.google.android.exoplayer.ExoPlaybackException; -import com.google.android.exoplayer.ExoPlayer; -import com.google.android.exoplayer.MediaClock; -import com.google.android.exoplayer.MediaFormat; -import com.google.android.exoplayer.MediaFormatHolder; -import com.google.android.exoplayer.SampleSource; -import com.google.android.exoplayer.SampleSourceTrackRenderer; -import com.google.android.exoplayer.TrackRenderer; -import com.google.android.exoplayer.audio.AudioTrack; -import com.google.android.exoplayer.ext.opus.OpusDecoderWrapper.InputBuffer; -import com.google.android.exoplayer.ext.opus.OpusDecoderWrapper.OutputBuffer; -import com.google.android.exoplayer.util.MimeTypes; - -import android.os.Handler; - -import java.nio.ByteBuffer; -import java.nio.ByteOrder; -import java.util.List; - -/** - * Decodes and renders audio using the native Opus decoder. - */ -public final class LibopusAudioTrackRenderer extends SampleSourceTrackRenderer - implements MediaClock { - - /** - * Interface definition for a callback to be notified of {@link LibopusAudioTrackRenderer} events. - */ - public interface EventListener { - - /** - * Invoked when the {@link AudioTrack} fails to initialize. - * - * @param e The corresponding exception. - */ - void onAudioTrackInitializationError(AudioTrack.InitializationException e); - - /** - * Invoked when an {@link AudioTrack} write fails. - * - * @param e The corresponding exception. - */ - void onAudioTrackWriteError(AudioTrack.WriteException e); - - /** - * Invoked when decoding fails. - * - * @param e The corresponding exception. - */ - void onDecoderError(OpusDecoderException e); - - } - - /** - * The type of a message that can be passed to an instance of this class via - * {@link ExoPlayer#sendMessage} or {@link ExoPlayer#blockingSendMessage}. The message object - * should be a {@link Float} with 0 being silence and 1 being unity gain. - */ - public static final int MSG_SET_VOLUME = 1; - - public final CodecCounters codecCounters = new CodecCounters(); - - private final Handler eventHandler; - private final EventListener eventListener; - private final MediaFormatHolder formatHolder; - - private MediaFormat format; - private OpusDecoderWrapper decoder; - private InputBuffer inputBuffer; - private OutputBuffer outputBuffer; - - private long currentPositionUs; - private boolean allowPositionDiscontinuity; - private boolean inputStreamEnded; - private boolean outputStreamEnded; - private boolean sourceIsReady; - private boolean notifyDiscontinuityToDecoder; - - private AudioTrack audioTrack; - private int audioSessionId; - - /** - * @param source The upstream source from which the renderer obtains samples. - */ - public LibopusAudioTrackRenderer(SampleSource source) { - this(source, null, null); - } - - /** - * @param source The upstream source from which the renderer obtains samples. - * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be - * null if delivery of events is not required. - * @param eventListener A listener of events. May be null if delivery of events is not required. - */ - public LibopusAudioTrackRenderer(SampleSource source, Handler eventHandler, - EventListener eventListener) { - super(source); - this.eventHandler = eventHandler; - this.eventListener = eventListener; - this.audioSessionId = AudioTrack.SESSION_ID_NOT_SET; - this.audioTrack = new AudioTrack(); - formatHolder = new MediaFormatHolder(); - } - - /** - * Returns whether the underlying libopus library is available. - */ - public static boolean isLibopusAvailable() { - return OpusDecoder.isLibopusAvailable(); - } - - /** - * Returns the version of the underlying libopus library if available, otherwise {@code null}. - */ - public static String getLibopusVersion() { - return isLibopusAvailable() ? OpusDecoder.getLibopusVersion() : null; - } - - @Override - protected MediaClock getMediaClock() { - return this; - } - - @Override - protected boolean handlesTrack(MediaFormat mediaFormat) { - return MimeTypes.AUDIO_OPUS.equalsIgnoreCase(mediaFormat.mimeType); - } - - @Override - protected void doSomeWork(long positionUs, long elapsedRealtimeUs, boolean sourceIsReady) - throws ExoPlaybackException { - if (outputStreamEnded) { - return; - } - - // Try and read a format if we don't have one already. - if (format == null && !readFormat(positionUs)) { - // We can't make progress without one. - return; - } - - // If we don't have a decoder yet, we need to instantiate one. - if (decoder == null) { - // For opus, the format can contain upto 3 entries in initializationData in the following - // exact order: - // 1) Opus Header Information (required) - // 2) Codec Delay in nanoseconds (required if Seek Preroll is present) - // 3) Seek Preroll in nanoseconds (required if Codec Delay is present) - List initializationData = format.initializationData; - if (initializationData.size() < 1) { - throw new ExoPlaybackException("Missing initialization data"); - } - long codecDelayNs = -1; - long seekPreRollNs = -1; - if (initializationData.size() == 3) { - if (initializationData.get(1).length != 8 || initializationData.get(2).length != 8) { - throw new ExoPlaybackException("Invalid Codec Delay or Seek Preroll"); - } - codecDelayNs = - ByteBuffer.wrap(initializationData.get(1)).order(ByteOrder.LITTLE_ENDIAN).getLong(); - seekPreRollNs = - ByteBuffer.wrap(initializationData.get(2)).order(ByteOrder.LITTLE_ENDIAN).getLong(); - } - try { - decoder = new OpusDecoderWrapper(initializationData.get(0), codecDelayNs, seekPreRollNs); - } catch (OpusDecoderException e) { - notifyDecoderError(e); - throw new ExoPlaybackException(e); - } - decoder.start(); - codecCounters.codecInitCount++; - } - - // Rendering loop. - try { - renderBuffer(); - while (feedInputBuffer(positionUs)) {} - } catch (AudioTrack.InitializationException e) { - notifyAudioTrackInitializationError(e); - throw new ExoPlaybackException(e); - } catch (AudioTrack.WriteException e) { - notifyAudioTrackWriteError(e); - throw new ExoPlaybackException(e); - } catch (OpusDecoderException e) { - notifyDecoderError(e); - throw new ExoPlaybackException(e); - } - codecCounters.ensureUpdated(); - } - - private void renderBuffer() throws OpusDecoderException, AudioTrack.InitializationException, - AudioTrack.WriteException { - if (outputStreamEnded) { - return; - } - - if (outputBuffer == null) { - outputBuffer = decoder.dequeueOutputBuffer(); - if (outputBuffer == null) { - return; - } - } - - if (outputBuffer.getFlag(OpusDecoderWrapper.FLAG_END_OF_STREAM)) { - outputStreamEnded = true; - audioTrack.handleEndOfStream(); - decoder.releaseOutputBuffer(outputBuffer); - outputBuffer = null; - return; - } - - if (!audioTrack.isInitialized()) { - if (audioSessionId != AudioTrack.SESSION_ID_NOT_SET) { - audioTrack.initialize(audioSessionId); - } else { - audioSessionId = audioTrack.initialize(); - } - if (getState() == TrackRenderer.STATE_STARTED) { - audioTrack.play(); - } - } - - int handleBufferResult; - handleBufferResult = audioTrack.handleBuffer(outputBuffer.data, - outputBuffer.data.position(), outputBuffer.size, outputBuffer.timestampUs); - - // If we are out of sync, allow currentPositionUs to jump backwards. - if ((handleBufferResult & AudioTrack.RESULT_POSITION_DISCONTINUITY) != 0) { - allowPositionDiscontinuity = true; - } - - // Release the buffer if it was consumed. - if ((handleBufferResult & AudioTrack.RESULT_BUFFER_CONSUMED) != 0) { - decoder.releaseOutputBuffer(outputBuffer); - codecCounters.renderedOutputBufferCount++; - outputBuffer = null; - } - } - - private boolean feedInputBuffer(long positionUs) throws OpusDecoderException { - if (inputStreamEnded) { - return false; - } - - if (inputBuffer == null) { - inputBuffer = decoder.dequeueInputBuffer(); - if (inputBuffer == null) { - return false; - } - } - - int result = readSource(positionUs, formatHolder, inputBuffer.sampleHolder); - if (result == SampleSource.NOTHING_READ) { - return false; - } - if (result == SampleSource.FORMAT_READ) { - format = formatHolder.format; - return true; - } - if (result == SampleSource.END_OF_STREAM) { - inputBuffer.setFlag(OpusDecoderWrapper.FLAG_END_OF_STREAM); - decoder.queueInputBuffer(inputBuffer); - inputBuffer = null; - inputStreamEnded = true; - return false; - } - if (notifyDiscontinuityToDecoder) { - notifyDiscontinuityToDecoder = false; - inputBuffer.setFlag(OpusDecoderWrapper.FLAG_RESET_DECODER); - } - - decoder.queueInputBuffer(inputBuffer); - inputBuffer = null; - return true; - } - - private void flushDecoder() { - inputBuffer = null; - outputBuffer = null; - decoder.flush(); - notifyDiscontinuityToDecoder = true; - } - - @Override - protected boolean isEnded() { - return outputStreamEnded && !audioTrack.hasPendingData(); - } - - @Override - protected boolean isReady() { - return audioTrack.hasPendingData() - || (format != null && (sourceIsReady || outputBuffer != null)); - } - - @Override - public long getPositionUs() { - long newCurrentPositionUs = audioTrack.getCurrentPositionUs(isEnded()); - if (newCurrentPositionUs != AudioTrack.CURRENT_POSITION_NOT_SET) { - currentPositionUs = allowPositionDiscontinuity ? newCurrentPositionUs - : Math.max(currentPositionUs, newCurrentPositionUs); - allowPositionDiscontinuity = false; - } - return currentPositionUs; - } - - @Override - protected void onDiscontinuity(long positionUs) { - audioTrack.reset(); - currentPositionUs = positionUs; - allowPositionDiscontinuity = true; - inputStreamEnded = false; - outputStreamEnded = false; - sourceIsReady = false; - if (decoder != null) { - flushDecoder(); - } - } - - @Override - protected void onStarted() { - audioTrack.play(); - } - - @Override - protected void onStopped() { - audioTrack.pause(); - } - - @Override - protected void onDisabled() throws ExoPlaybackException { - inputBuffer = null; - outputBuffer = null; - format = null; - audioSessionId = AudioTrack.SESSION_ID_NOT_SET; - try { - if (decoder != null) { - decoder.release(); - decoder = null; - codecCounters.codecReleaseCount++; - } - audioTrack.release(); - } finally { - super.onDisabled(); - } - } - - private boolean readFormat(long positionUs) { - int result = readSource(positionUs, formatHolder, null); - if (result == SampleSource.FORMAT_READ) { - format = formatHolder.format; - audioTrack.configure(format.getFrameworkMediaFormatV16(), false); - return true; - } - return false; - } - - @Override - public void handleMessage(int messageType, Object message) throws ExoPlaybackException { - if (messageType == MSG_SET_VOLUME) { - audioTrack.setVolume((Float) message); - } else { - super.handleMessage(messageType, message); - } - } - - private void notifyAudioTrackInitializationError(final AudioTrack.InitializationException e) { - if (eventHandler != null && eventListener != null) { - eventHandler.post(new Runnable() { - @Override - public void run() { - eventListener.onAudioTrackInitializationError(e); - } - }); - } - } - - private void notifyAudioTrackWriteError(final AudioTrack.WriteException e) { - if (eventHandler != null && eventListener != null) { - eventHandler.post(new Runnable() { - @Override - public void run() { - eventListener.onAudioTrackWriteError(e); - } - }); - } - } - - private void notifyDecoderError(final OpusDecoderException e) { - if (eventHandler != null && eventListener != null) { - eventHandler.post(new Runnable() { - @Override - public void run() { - eventListener.onDecoderError(e); - } - }); - } - } - -} diff --git a/extensions/opus/src/main/java/com/google/android/exoplayer/ext/opus/OpusDecoder.java b/extensions/opus/src/main/java/com/google/android/exoplayer/ext/opus/OpusDecoder.java deleted file mode 100644 index d63da51700..0000000000 --- a/extensions/opus/src/main/java/com/google/android/exoplayer/ext/opus/OpusDecoder.java +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright (C) 2014 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.exoplayer.ext.opus; - -import com.google.android.exoplayer.ext.opus.OpusDecoderWrapper.OpusHeader; - -import java.nio.ByteBuffer; - -/** - * JNI Wrapper for the libopus Opus decoder. - */ -/* package */ class OpusDecoder { - - private static final boolean IS_AVAILABLE; - static { - boolean isAvailable; - try { - System.loadLibrary("opus"); - System.loadLibrary("opusJNI"); - isAvailable = true; - } catch (UnsatisfiedLinkError exception) { - isAvailable = false; - } - IS_AVAILABLE = isAvailable; - } - - private final long nativeDecoderContext; - - /** - * Creates the Opus Decoder. - * - * @param opusHeader OpusHeader used to initialize the decoder. - * @throws OpusDecoderException if the decoder initialization fails. - */ - public OpusDecoder(OpusHeader opusHeader) throws OpusDecoderException { - nativeDecoderContext = opusInit( - opusHeader.sampleRate, opusHeader.channelCount, opusHeader.numStreams, - opusHeader.numCoupled, opusHeader.gain, opusHeader.streamMap); - if (nativeDecoderContext == 0) { - throw new OpusDecoderException("Failed to initialize decoder"); - } - } - - /** - * Decodes an Opus Encoded Stream. - * - * @param inputBuffer buffer containing the encoded data. Must be allocated using allocateDirect. - * @param inputSize size of the input buffer. - * @param outputBuffer buffer to write the decoded data. Must be allocated using allocateDirect. - * @param outputSize Maximum capacity of the output buffer. - * @return number of decoded bytes. - * @throws OpusDecoderException if decode fails. - */ - public int decode(ByteBuffer inputBuffer, int inputSize, ByteBuffer outputBuffer, - int outputSize) throws OpusDecoderException { - int result = opusDecode(nativeDecoderContext, inputBuffer, inputSize, outputBuffer, outputSize); - if (result < 0) { - throw new OpusDecoderException("Decode error: " + opusGetErrorMessage(result)); - } - return result; - } - - /** - * Closes the native decoder. - */ - public void close() { - opusClose(nativeDecoderContext); - } - - /** - * Resets the native decode on discontinuity (during seek for example). - */ - public void reset() { - opusReset(nativeDecoderContext); - } - - /** - * Returns whether the underlying libopus library is available. - */ - public static boolean isLibopusAvailable() { - return IS_AVAILABLE; - } - - /** - * Returns the version string of the underlying libopus decoder. - */ - public static native String getLibopusVersion(); - - private native long opusInit(int sampleRate, int channelCount, int numStreams, int numCoupled, - int gain, byte[] streamMap); - private native int opusDecode(long decoder, ByteBuffer inputBuffer, int inputSize, - ByteBuffer outputBuffer, int outputSize); - private native void opusClose(long decoder); - private native void opusReset(long decoder); - private native String opusGetErrorMessage(int errorCode); - -} diff --git a/extensions/opus/src/main/java/com/google/android/exoplayer/ext/opus/OpusDecoderException.java b/extensions/opus/src/main/java/com/google/android/exoplayer/ext/opus/OpusDecoderException.java deleted file mode 100644 index 13e7739df9..0000000000 --- a/extensions/opus/src/main/java/com/google/android/exoplayer/ext/opus/OpusDecoderException.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright (C) 2014 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.exoplayer.ext.opus; - -/** - * Thrown when an Opus decoder error occurs. - */ -public class OpusDecoderException extends Exception { - - public OpusDecoderException(String message) { - super(message); - } - -} diff --git a/extensions/opus/src/main/java/com/google/android/exoplayer/ext/opus/OpusDecoderWrapper.java b/extensions/opus/src/main/java/com/google/android/exoplayer/ext/opus/OpusDecoderWrapper.java deleted file mode 100644 index d4f6c8e306..0000000000 --- a/extensions/opus/src/main/java/com/google/android/exoplayer/ext/opus/OpusDecoderWrapper.java +++ /dev/null @@ -1,400 +0,0 @@ -/* - * Copyright (C) 2014 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.exoplayer.ext.opus; - -import com.google.android.exoplayer.SampleHolder; - -import java.nio.ByteBuffer; -import java.util.LinkedList; - -/** - * Wraps {@link OpusDecoder}, exposing a higher level decoder interface. - */ -/* package */ class OpusDecoderWrapper extends Thread { - - public static final int FLAG_END_OF_STREAM = 1; - public static final int FLAG_RESET_DECODER = 2; - - private static final int INPUT_BUFFER_SIZE = 960 * 6; - private static final int OUTPUT_BUFFER_SIZE = 960 * 6 * 2; - private static final int NUM_BUFFERS = 16; - private static final int DEFAULT_SEEK_PRE_ROLL = 3840; - - private final Object lock; - private final OpusHeader opusHeader; - - private final LinkedList dequeuedInputBuffers; - private final LinkedList queuedInputBuffers; - private final LinkedList queuedOutputBuffers; - private final LinkedList dequeuedOutputBuffers; - private final InputBuffer[] availableInputBuffers; - private final OutputBuffer[] availableOutputBuffers; - private int availableInputBufferCount; - private int availableOutputBufferCount; - - private int skipSamples; - private boolean flushDecodedOutputBuffer; - private boolean released; - - private int seekPreRoll; - - private OpusDecoderException decoderException; - - /** - * @param headerBytes Opus header data that is used to initialize the decoder. For WebM Container, - * this comes from the CodecPrivate Track element. - * @param codecDelayNs Delay in nanoseconds added by the codec at the beginning. For WebM - * Container, this comes from the CodecDelay Track Element. Can be -1 in which case the value - * from the codec header will be used. - * @param seekPreRollNs Duration in nanoseconds of samples to discard when there is a - * discontinuity. For WebM Container, this comes from the SeekPreRoll Track Element. Can be -1 - * in which case the default value of 80ns will be used. - * @throws OpusDecoderException if an exception occurs when initializing the decoder. - */ - public OpusDecoderWrapper(byte[] headerBytes, long codecDelayNs, - long seekPreRollNs) throws OpusDecoderException { - lock = new Object(); - opusHeader = parseOpusHeader(headerBytes); - skipSamples = (codecDelayNs == -1) ? opusHeader.skipSamples - : nsToSamples(opusHeader, codecDelayNs); - seekPreRoll = (seekPreRoll == -1) ? DEFAULT_SEEK_PRE_ROLL - : nsToSamples(opusHeader, seekPreRollNs); - dequeuedInputBuffers = new LinkedList<>(); - queuedInputBuffers = new LinkedList<>(); - queuedOutputBuffers = new LinkedList<>(); - dequeuedOutputBuffers = new LinkedList<>(); - availableInputBuffers = new InputBuffer[NUM_BUFFERS]; - availableOutputBuffers = new OutputBuffer[NUM_BUFFERS]; - availableInputBufferCount = NUM_BUFFERS; - availableOutputBufferCount = NUM_BUFFERS; - for (int i = 0; i < NUM_BUFFERS; i++) { - availableInputBuffers[i] = new InputBuffer(); - availableOutputBuffers[i] = new OutputBuffer(); - } - } - - public InputBuffer dequeueInputBuffer() throws OpusDecoderException { - synchronized (lock) { - maybeThrowDecoderError(); - if (availableInputBufferCount == 0) { - return null; - } - InputBuffer inputBuffer = availableInputBuffers[--availableInputBufferCount]; - inputBuffer.reset(); - dequeuedInputBuffers.addLast(inputBuffer); - return inputBuffer; - } - } - - public void queueInputBuffer(InputBuffer inputBuffer) throws OpusDecoderException { - synchronized (lock) { - maybeThrowDecoderError(); - dequeuedInputBuffers.remove(inputBuffer); - queuedInputBuffers.addLast(inputBuffer); - maybeNotifyDecodeLoop(); - } - } - - public OutputBuffer dequeueOutputBuffer() throws OpusDecoderException { - synchronized (lock) { - maybeThrowDecoderError(); - if (queuedOutputBuffers.isEmpty()) { - return null; - } - OutputBuffer outputBuffer = queuedOutputBuffers.removeFirst(); - dequeuedOutputBuffers.add(outputBuffer); - return outputBuffer; - } - } - - public void releaseOutputBuffer(OutputBuffer outputBuffer) throws OpusDecoderException { - synchronized (lock) { - maybeThrowDecoderError(); - outputBuffer.reset(); - dequeuedOutputBuffers.remove(outputBuffer); - availableOutputBuffers[availableOutputBufferCount++] = outputBuffer; - maybeNotifyDecodeLoop(); - } - } - - public void flush() { - synchronized (lock) { - flushDecodedOutputBuffer = true; - while (!dequeuedInputBuffers.isEmpty()) { - availableInputBuffers[availableInputBufferCount++] = dequeuedInputBuffers.removeFirst(); - } - while (!queuedInputBuffers.isEmpty()) { - availableInputBuffers[availableInputBufferCount++] = queuedInputBuffers.removeFirst(); - } - while (!queuedOutputBuffers.isEmpty()) { - availableOutputBuffers[availableOutputBufferCount++] = queuedOutputBuffers.removeFirst(); - } - while (!dequeuedOutputBuffers.isEmpty()) { - availableOutputBuffers[availableOutputBufferCount++] = dequeuedOutputBuffers.removeFirst(); - } - } - } - - public void release() { - synchronized (lock) { - released = true; - lock.notify(); - } - try { - join(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - } - - private void maybeThrowDecoderError() throws OpusDecoderException { - if (decoderException != null) { - throw decoderException; - } - } - - /** - * Notifies the decode loop if there exists a queued input buffer and an available output buffer - * to decode into. - *

- * Should only be called whilst synchronized on the lock object. - */ - private void maybeNotifyDecodeLoop() { - if (!queuedInputBuffers.isEmpty() && availableOutputBufferCount > 0) { - lock.notify(); - } - } - - @Override - public void run() { - OpusDecoder decoder = null; - try { - decoder = new OpusDecoder(opusHeader); - while (decodeBuffer(decoder)) { - // Do nothing. - } - } catch (OpusDecoderException e) { - synchronized (lock) { - decoderException = e; - } - } catch (InterruptedException e) { - // Shouldn't ever happen. - } finally { - if (decoder != null) { - decoder.close(); - } - } - } - - private boolean decodeBuffer(OpusDecoder decoder) throws InterruptedException, - OpusDecoderException { - InputBuffer inputBuffer; - OutputBuffer outputBuffer; - - // Wait until we have an input buffer to decode, and an output buffer to decode into. - synchronized (lock) { - while (!released && (queuedInputBuffers.isEmpty() || availableOutputBufferCount == 0)) { - lock.wait(); - } - if (released) { - return false; - } - inputBuffer = queuedInputBuffers.removeFirst(); - outputBuffer = availableOutputBuffers[--availableOutputBufferCount]; - flushDecodedOutputBuffer = false; - } - - // Decode. - boolean skipBuffer = false; - if (inputBuffer.getFlag(FLAG_END_OF_STREAM)) { - outputBuffer.setFlag(FLAG_END_OF_STREAM); - } else { - if (inputBuffer.getFlag(FLAG_RESET_DECODER)) { - decoder.reset(); - // When seeking to 0, skip number of samples as specified in opus header. When seeking to - // any other time, skip number of samples as specified by seek preroll. - skipSamples = (inputBuffer.sampleHolder.timeUs == 0) ? opusHeader.skipSamples : seekPreRoll; - } - SampleHolder sampleHolder = inputBuffer.sampleHolder; - sampleHolder.data.position(sampleHolder.data.position() - sampleHolder.size); - outputBuffer.timestampUs = sampleHolder.timeUs; - outputBuffer.size = decoder.decode(sampleHolder.data, sampleHolder.size, - outputBuffer.data, outputBuffer.data.capacity()); - outputBuffer.data.position(0); - if (skipSamples > 0) { - int bytesPerSample = opusHeader.channelCount * 2; - int skipBytes = skipSamples * bytesPerSample; - if (outputBuffer.size <= skipBytes) { - skipSamples -= outputBuffer.size / bytesPerSample; - outputBuffer.size = 0; - skipBuffer = true; - } else { - skipSamples = 0; - outputBuffer.size -= skipBytes; - outputBuffer.data.position(skipBytes); - } - } - } - - synchronized (lock) { - if (flushDecodedOutputBuffer - || inputBuffer.sampleHolder.isDecodeOnly() - || skipBuffer) { - // In the following cases, we make the output buffer available again rather than queuing it - // to be consumed: - // 1) A flush occured whilst we were decoding. - // 2) The input sample has decodeOnly flag set. - // 3) We skip the entire buffer due to skipSamples being greater than bytes decoded. - outputBuffer.reset(); - availableOutputBuffers[availableOutputBufferCount++] = outputBuffer; - } else { - // Queue the decoded output buffer to be consumed. - queuedOutputBuffers.addLast(outputBuffer); - } - // Make the input buffer available again. - availableInputBuffers[availableInputBufferCount++] = inputBuffer; - } - - return true; - } - - private static OpusHeader parseOpusHeader(byte[] headerBytes) throws OpusDecoderException { - final int maxChannelCount = 8; - final int maxChannelCountWithDefaultLayout = 2; - final int headerSize = 19; - final int headerChannelCountOffset = 9; - final int headerSkipSamplesOffset = 10; - final int headerGainOffset = 16; - final int headerChannelMappingOffset = 18; - final int headerNumStreamsOffset = headerSize; - final int headerNumCoupledOffset = headerNumStreamsOffset + 1; - final int headerStreamMapOffset = headerNumStreamsOffset + 2; - OpusHeader opusHeader = new OpusHeader(); - try { - // Opus streams are always decoded at 48000 hz. - opusHeader.sampleRate = 48000; - opusHeader.channelCount = headerBytes[headerChannelCountOffset]; - if (opusHeader.channelCount > maxChannelCount) { - throw new OpusDecoderException("Invalid channel count: " + opusHeader.channelCount); - } - opusHeader.skipSamples = readLittleEndian16(headerBytes, headerSkipSamplesOffset); - opusHeader.gain = readLittleEndian16(headerBytes, headerGainOffset); - opusHeader.channelMapping = headerBytes[headerChannelMappingOffset]; - - if (opusHeader.channelMapping == 0) { - // If there is no channel mapping, use the defaults. - if (opusHeader.channelCount > maxChannelCountWithDefaultLayout) { - throw new OpusDecoderException("Invalid Header, missing stream map."); - } - opusHeader.numStreams = 1; - opusHeader.numCoupled = (opusHeader.channelCount > 1) ? 1 : 0; - opusHeader.streamMap[0] = 0; - opusHeader.streamMap[1] = 1; - } else { - // Read the channel mapping. - opusHeader.numStreams = headerBytes[headerNumStreamsOffset]; - opusHeader.numCoupled = headerBytes[headerNumCoupledOffset]; - for (int i = 0; i < opusHeader.channelCount; i++) { - opusHeader.streamMap[i] = headerBytes[headerStreamMapOffset + i]; - } - } - return opusHeader; - } catch (ArrayIndexOutOfBoundsException e) { - throw new OpusDecoderException("Header size is too small."); - } - } - - private static int readLittleEndian16(byte[] input, int offset) { - int value = input[offset]; - value |= input[offset + 1] << 8; - return value; - } - - private static int nsToSamples(OpusHeader opusHeader, long ns) { - return (int) (ns * opusHeader.sampleRate / 1000000000); - } - - /* package */ static final class InputBuffer { - - public final SampleHolder sampleHolder; - - public int flags; - - public InputBuffer() { - sampleHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_DIRECT); - sampleHolder.data = ByteBuffer.allocateDirect(INPUT_BUFFER_SIZE); - } - - public void reset() { - sampleHolder.clearData(); - flags = 0; - } - - public void setFlag(int flag) { - flags |= flag; - } - - public boolean getFlag(int flag) { - return (flags & flag) == flag; - } - - } - - /* package */ static final class OutputBuffer { - - public ByteBuffer data; - public int size; - public long timestampUs; - public int flags; - - public OutputBuffer() { - data = ByteBuffer.allocateDirect(OUTPUT_BUFFER_SIZE); - } - - public void reset() { - data.clear(); - size = 0; - flags = 0; - } - - public void setFlag(int flag) { - flags |= flag; - } - - public boolean getFlag(int flag) { - return (flags & flag) == flag; - } - - } - - /* package */ static final class OpusHeader { - - public int sampleRate; - public int channelCount; - public int skipSamples; - public int gain; - public int channelMapping; - public int numStreams; - public int numCoupled; - public byte[] streamMap; - - public OpusHeader() { - streamMap = new byte[8]; - } - - } - -} diff --git a/extensions/opus/src/main/jni/Android.mk b/extensions/opus/src/main/jni/Android.mk deleted file mode 100644 index 892715e9c6..0000000000 --- a/extensions/opus/src/main/jni/Android.mk +++ /dev/null @@ -1,33 +0,0 @@ -# -# Copyright (C) 2014 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. -# - -WORKING_DIR := $(call my-dir) -include $(CLEAR_VARS) - -# build libopus.so -LOCAL_PATH := $(WORKING_DIR) -include libopus.mk - -# build libopusJNI.so -include $(CLEAR_VARS) -LOCAL_PATH := $(WORKING_DIR) -LOCAL_MODULE := libopusJNI -LOCAL_ARM_MODE := arm -LOCAL_CPP_EXTENSION := .cc -LOCAL_SRC_FILES := opus_jni.cc -LOCAL_LDLIBS := -llog -lz -lm -LOCAL_SHARED_LIBRARIES := libopus -include $(BUILD_SHARED_LIBRARY) diff --git a/extensions/opus/src/main/jni/Application.mk b/extensions/opus/src/main/jni/Application.mk deleted file mode 100644 index 7dc417cda1..0000000000 --- a/extensions/opus/src/main/jni/Application.mk +++ /dev/null @@ -1,20 +0,0 @@ -# -# Copyright (C) 2014 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. -# - -APP_OPTIM := release -APP_STL := gnustl_static -APP_CPPFLAGS := -frtti -APP_PLATFORM := android-9 diff --git a/extensions/opus/src/main/jni/convert_android_asm.sh b/extensions/opus/src/main/jni/convert_android_asm.sh deleted file mode 100755 index c789c66a1a..0000000000 --- a/extensions/opus/src/main/jni/convert_android_asm.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/bin/bash -# -# Copyright (C) 2014 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. -# - -set -e -ASM_CONVERTER="./libopus/celt/arm/arm2gnu.pl" - -if [[ ! -x "${ASM_CONVERTER}" ]]; then - echo "Please make sure you have checked out libopus." - exit -fi - -while read file; do - # This check is required because the ASM conversion script doesn't seem to be - # idempotent. - if [[ ! "${file}" =~ .*_gnu\.s$ ]]; then - gnu_file="${file%.s}_gnu.s" - ${ASM_CONVERTER} "${file}" > "${gnu_file}" - # The ASM conversion script replaces includes with *_gnu.S. So, replace - # occurences of "*-gnu.S" with "*_gnu.s". - perl -pi -e "s/-gnu\.S/_gnu\.s/g" "${gnu_file}" - rm -f "${file}" - fi -done < <(find . -iname '*.s') - -# Generate armopts.s from armopts.s.in -sed \ - -e "s/@OPUS_ARM_MAY_HAVE_EDSP@/1/g" \ - -e "s/@OPUS_ARM_MAY_HAVE_MEDIA@/1/g" \ - -e "s/@OPUS_ARM_MAY_HAVE_NEON@/1/g" \ - libopus/celt/arm/armopts.s.in > libopus/celt/arm/armopts.s.temp -${ASM_CONVERTER} "libopus/celt/arm/armopts.s.temp" > "libopus/celt/arm/armopts_gnu.s" -rm "libopus/celt/arm/armopts.s.temp" -echo "Converted all ASM files and generated armopts.s successfully." diff --git a/extensions/opus/src/main/jni/libopus.mk b/extensions/opus/src/main/jni/libopus.mk deleted file mode 100644 index 2eb5476e66..0000000000 --- a/extensions/opus/src/main/jni/libopus.mk +++ /dev/null @@ -1,50 +0,0 @@ -# -# Copyright (C) 2014 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. -# - -LOCAL_PATH := $(call my-dir)/libopus - -include $(CLEAR_VARS) - -include $(LOCAL_PATH)/celt_headers.mk -include $(LOCAL_PATH)/celt_sources.mk -include $(LOCAL_PATH)/opus_headers.mk -include $(LOCAL_PATH)/opus_sources.mk -include $(LOCAL_PATH)/silk_headers.mk -include $(LOCAL_PATH)/silk_sources.mk - -LOCAL_MODULE := libopus -LOCAL_ARM_MODE := arm -LOCAL_CFLAGS := -DOPUS_BUILD -DFIXED_POINT -DUSE_ALLOCA -DHAVE_LRINT \ - -DHAVE_LRINTF -LOCAL_C_INCLUDES := $(LOCAL_PATH)/include $(LOCAL_PATH)/src \ - $(LOCAL_PATH)/silk $(LOCAL_PATH)/celt \ - $(LOCAL_PATH)/silk/fixed -LOCAL_SRC_FILES := $(CELT_SOURCES) $(OPUS_SOURCES) $(OPUS_SOURCES_FLOAT) \ - $(SILK_SOURCES) $(SILK_SOURCES_FIXED) - -ifneq ($(findstring armeabi-v7a, $(TARGET_ARCH_ABI)),) -LOCAL_SRC_FILES += $(CELT_SOURCES_ARM) -LOCAL_SRC_FILES += celt/arm/armopts_gnu.s.neon -LOCAL_SRC_FILES += $(subst .s,_gnu.s.neon,$(CELT_SOURCES_ARM_ASM)) -LOCAL_CFLAGS += -DOPUS_ARM_ASM -DOPUS_ARM_INLINE_ASM -DOPUS_ARM_INLINE_EDSP \ - -DOPUS_ARM_INLINE_MEDIA -DOPUS_ARM_INLINE_NEON \ - -DOPUS_ARM_MAY_HAVE_NEON -DOPUS_ARM_MAY_HAVE_MEDIA \ - -DOPUS_ARM_MAY_HAVE_EDSP -endif - -LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include - -include $(BUILD_SHARED_LIBRARY) diff --git a/extensions/opus/src/main/jni/opus_jni.cc b/extensions/opus/src/main/jni/opus_jni.cc deleted file mode 100644 index e4ee1c60e3..0000000000 --- a/extensions/opus/src/main/jni/opus_jni.cc +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright (C) 2014 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. - */ - -#include - -#include - -#include - -#include "opus.h" // NOLINT -#include "opus_multistream.h" // NOLINT - -#define LOG_TAG "libopus_native" -#define LOGE(...) ((void)__android_log_print(ANDROID_LOG_ERROR, LOG_TAG, \ - __VA_ARGS__)) - -#define FUNC(RETURN_TYPE, NAME, ...) \ - extern "C" { \ - JNIEXPORT RETURN_TYPE \ - Java_com_google_android_exoplayer_ext_opus_OpusDecoder_ ## NAME \ - (JNIEnv* env, jobject thiz, ##__VA_ARGS__);\ - } \ - JNIEXPORT RETURN_TYPE \ - Java_com_google_android_exoplayer_ext_opus_OpusDecoder_ ## NAME \ - (JNIEnv* env, jobject thiz, ##__VA_ARGS__)\ - -jint JNI_OnLoad(JavaVM* vm, void* reserved) { - JNIEnv* env; - if (vm->GetEnv(reinterpret_cast(&env), JNI_VERSION_1_6) != JNI_OK) { - return -1; - } - return JNI_VERSION_1_6; -} - -static int channelCount; - -FUNC(jlong, opusInit, jint sampleRate, jint channelCount, jint numStreams, - jint numCoupled, jint gain, jbyteArray jStreamMap) { - int status = OPUS_INVALID_STATE; - ::channelCount = channelCount; - jbyte* streamMapBytes = env->GetByteArrayElements(jStreamMap, 0); - uint8_t* streamMap = reinterpret_cast(streamMapBytes); - OpusMSDecoder* decoder = opus_multistream_decoder_create( - sampleRate, channelCount, numStreams, numCoupled, streamMap, &status); - env->ReleaseByteArrayElements(jStreamMap, streamMapBytes, 0); - if (!decoder || status != OPUS_OK) { - LOGE("Failed to create Opus Decoder; status=%s", opus_strerror(status)); - return 0; - } - status = opus_multistream_decoder_ctl(decoder, OPUS_SET_GAIN(gain)); - if (status != OPUS_OK) { - LOGE("Failed to set Opus header gain; status=%s", opus_strerror(status)); - return 0; - } - return reinterpret_cast(decoder); -} - -FUNC(jint, opusDecode, jlong jDecoder, jobject jInputBuffer, jint inputSize, - jobject jOutputBuffer, jint outputSize) { - OpusMSDecoder* decoder = reinterpret_cast(jDecoder); - const uint8_t* inputBuffer = - reinterpret_cast( - env->GetDirectBufferAddress(jInputBuffer)); - int16_t* outputBuffer = reinterpret_cast( - env->GetDirectBufferAddress(jOutputBuffer)); - int numFrames = opus_multistream_decode(decoder, inputBuffer, inputSize, - outputBuffer, outputSize, 0); - return (numFrames < 0) ? numFrames : numFrames * 2 * channelCount; -} - -FUNC(void, opusClose, jlong jDecoder) { - OpusMSDecoder* decoder = reinterpret_cast(jDecoder); - opus_multistream_decoder_destroy(decoder); -} - -FUNC(void, opusReset, jlong jDecoder) { - OpusMSDecoder* decoder = reinterpret_cast(jDecoder); - opus_multistream_decoder_ctl(decoder, OPUS_RESET_STATE); -} - -FUNC(jstring, getLibopusVersion) { - return env->NewStringUTF(opus_get_version_string()); -} - -FUNC(jstring, opusGetErrorMessage, jint errorCode) { - return env->NewStringUTF(opus_strerror(errorCode)); -} diff --git a/extensions/opus/src/main/proguard.cfg b/extensions/opus/src/main/proguard.cfg deleted file mode 100644 index 15e910b1e9..0000000000 --- a/extensions/opus/src/main/proguard.cfg +++ /dev/null @@ -1,6 +0,0 @@ -# Proguard rules specific to the Opus extension. - -# This prevents the names of native methods from being obfuscated. --keepclasseswithmembernames class * { - native ; -} diff --git a/extensions/opus/src/main/project.properties b/extensions/opus/src/main/project.properties deleted file mode 100644 index b92a03b7ab..0000000000 --- a/extensions/opus/src/main/project.properties +++ /dev/null @@ -1,16 +0,0 @@ -# This file is automatically generated by Android Tools. -# Do not modify this file -- YOUR CHANGES WILL BE ERASED! -# -# This file must be checked in Version Control Systems. -# -# To customize properties used by the Ant build system edit -# "ant.properties", and override values to adapt the script to your -# project structure. -# -# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): -#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt - -# Project target. -target=android-23 -android.library=true -android.library.reference.1=../../../../library/src/main diff --git a/extensions/opus/src/main/res/.README.txt b/extensions/opus/src/main/res/.README.txt deleted file mode 100644 index c27147ce56..0000000000 --- a/extensions/opus/src/main/res/.README.txt +++ /dev/null @@ -1,2 +0,0 @@ -This file is needed to make sure the res directory is present. -The file is ignored by the Android toolchain because its name starts with a dot. diff --git a/extensions/vp9/README.md b/extensions/vp9/README.md deleted file mode 100644 index f47aa1225a..0000000000 --- a/extensions/vp9/README.md +++ /dev/null @@ -1,137 +0,0 @@ -# ExoPlayer VP9 Extension # - -## Description ## - -The VP9 Extension is a [TrackRenderer][] implementation that helps you bundle libvpx (the VP9 decoding library) into your app and use it along with ExoPlayer to play VP9 video on Android devices. - -[TrackRenderer]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer/TrackRenderer.html - -## Build Instructions (Android Studio and Eclipse) ## - -Building the VP9 Extension involves building libvpx and JNI bindings using the Android NDK and linking it into your app. The following steps will tell you how to do that using Android Studio or Eclipse. - -* Checkout ExoPlayer along with Extensions - -``` -git clone https://github.com/google/ExoPlayer.git -``` - -* Set the following environment variables: - -``` -cd "" -EXOPLAYER_ROOT="$(pwd)" -VP9_EXT_PATH="${EXOPLAYER_ROOT}/extensions/vp9/src/main" -``` - -* Download the [Android NDK][] and set its location in an environment variable: - -``` -NDK_PATH="" -``` - -* Fetch libvpx and libyuv - -``` -cd "${VP9_EXT_PATH}/jni" && \ -git clone https://chromium.googlesource.com/webm/libvpx libvpx && \ -git clone https://chromium.googlesource.com/libyuv/libyuv libyuv -``` - -* Run a script that generates necessary configuration files for libvpx - -``` -cd ${VP9_EXT_PATH}/jni && \ -./generate_libvpx_android_configs.sh "${NDK_PATH}" -``` - -### Android Studio ### - -For Android Studio, we build the native libraries from the command line and then Gradle will pick it up when building your app using Android Studio. - -* Build the JNI native libraries - -``` -cd "${VP9_EXT_PATH}"/jni && \ -${NDK_PATH}/ndk-build APP_ABI=all -j4 -``` - -* In your project, you can add a dependency to the VP9 Extension by using a the following rule - -``` -// in settings.gradle -include ':..:ExoPlayer:library' -include ':..:ExoPlayer:vp9-extension' - -// in build.gradle -dependencies { - compile project(':..:ExoPlayer:library') - compile project(':..:ExoPlayer:vp9-extension') -} -``` - -* Now, when you build your app, the VP9 extension will be built and the native libraries will be packaged along with the APK. - -### Eclipse ### - -* The following steps assume that you have installed Eclipse and configured it with the [Android SDK][] and [Android NDK ][]: - * Navigate to File->Import->General->Existing Projects into Workspace - * Select the root directory of the repository - * Import the following projects: - * ExoPlayerLib - * ExoPlayerExt-VP9 - * If you are able to build ExoPlayerExt-VP9 project, then you're all set. - * (Optional) To speed up the NDK build: - * Right click on ExoPlayerExt-VP9 in the Project Explorer pane and choose Properties - * Click on C/C++ Build - * Uncheck `Use default build command` - * In `Build Command` enter: `ndk-build -j4` (adjust 4 to a reasonable number depending on the number of cores in your computer) - * Click Apply - -You can now create your own Android App project and add ExoPlayerLib along with ExoPlayerExt-VP9 as a dependencies to use ExoPlayer along with the VP9 Extension. - - -[Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html - -[Android NDK ]: http://tools.android.com/recent/usingthendkplugin -[Android SDK]: http://developer.android.com/sdk/installing/index.html?pkg=tools - -## Building for various Architectures ## - -### Android Studio ### - -The manual invocation of `ndk-build` will build the library for all architectures and the correct one will be picked up from the APK based on the device its running on. - -### Eclipse ### - -libvpx is optimized for various architectures (like neon, x86, etc.). The `generate_libvpx_android_configs.sh` script generates Android configurations for the following architectures: - -* armeabi (the default - does not include neon optimizations) -* armeabi-v7a (choose this to enable neon optimizations) -* mips -* x86 -* arm64-v8a -* mips64 -* x86_64 -* all (will result in a larger binary but will cover all architectures) - -You can build for a specific architecture in two ways: - -* Method 1 (edit `Application.mk`) - * Edit `${VP9_EXT_PATH}/jni/Application.mk` and add the following line `APP_ABI := ` (where `` is one of the above 7 architectures) -* Method 2 (pass NDK build flag) - * Right click on ExoPlayerExt-VP9 in the Project Explorer pane and choose Properties - * Click on C/C++ Build - * Uncheck `Use default build command` - * In `Build Command` enter: `ndk-build APP_ABI=` (where `` is one of the above 7 architectures) - * Click Apply - -## Other Things to Note ## - -* Every time there is a change to the libvpx checkout: - * Android config scripts should be re-generated by running `generate_libvpx_android_configs.sh` - * Clean and re-build the project. -* If you want to use your own version of libvpx or libyuv, place it in `${VP9_EXT_PATH}/jni/libvpx` or `${VP9_EXT_PATH}/jni/libyuv` respectively. - diff --git a/extensions/vp9/build.gradle b/extensions/vp9/build.gradle deleted file mode 100644 index 9ccc1862aa..0000000000 --- a/extensions/vp9/build.gradle +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright (C) 2014 The Android Open Source Project -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -apply plugin: 'com.android.library' - -android { - compileSdkVersion 23 - buildToolsVersion "23.0.1" - - defaultConfig { - minSdkVersion 9 - targetSdkVersion 23 - } - - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' - } - } - - lintOptions { - abortOnError false - } - - sourceSets.main { - jniLibs.srcDir 'src/main/libs' - jni.srcDirs = [] // Disable the automatic ndk-build call by Android Studio. - } -} - -dependencies { - compile project(':library') -} - diff --git a/extensions/vp9/src/androidTest/.classpath b/extensions/vp9/src/androidTest/.classpath deleted file mode 100644 index 5b30f3b49b..0000000000 --- a/extensions/vp9/src/androidTest/.classpath +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/extensions/vp9/src/androidTest/.project b/extensions/vp9/src/androidTest/.project deleted file mode 100644 index 8e0bfa4eb9..0000000000 --- a/extensions/vp9/src/androidTest/.project +++ /dev/null @@ -1,45 +0,0 @@ - - - ExoPlayerExt-VP9Tests - - - ExoPlayerLib - - - - com.android.ide.eclipse.adt.ResourceManagerBuilder - - - - - com.android.ide.eclipse.adt.PreCompilerBuilder - - - - - org.eclipse.jdt.core.javabuilder - - - - - com.android.ide.eclipse.adt.ApkBuilder - - - - - - com.android.ide.eclipse.adt.AndroidNature - org.eclipse.jdt.core.javanature - - - - 0 - - 14 - - org.eclipse.ui.ide.multiFilter - 1.0-name-matches-true-false-BUILD - - - - diff --git a/extensions/vp9/src/androidTest/AndroidManifest.xml b/extensions/vp9/src/androidTest/AndroidManifest.xml deleted file mode 100644 index a2859b3c3c..0000000000 --- a/extensions/vp9/src/androidTest/AndroidManifest.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - - - - diff --git a/extensions/vp9/src/androidTest/assets/bear-vp9-odd-dimensions.webm b/extensions/vp9/src/androidTest/assets/bear-vp9-odd-dimensions.webm deleted file mode 100644 index 4d65a906faff6549488f688d9250abc7bf4abdf5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 31548 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 z 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 z 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 ob8n!HXj5&V982Lh8G2Zw+d zX#pMlUoKKZ5U@a3;JwQK^eX=ugu-pE|2rmHe;x=_Oe9(n2m}<6umh9}Of3L>4+I1W z6&e~58WI5lf&-UoiH-AhnZ^7;*n5{P$$Mv)ezpAqS~Z@?cx2;c9>YwouXC;$N8 zfu0Lr;IDel&)&$MdY7-$+KK*(KP60i>A&;Od^-WWf7V~sm-7|; zTLGe9wYU6_`aSzmzaPKSKk;w*-~PA$fFIT0`ESTu_fx`N{_Nko-}Z09oA|Txjrf!P znf_jX&!3{-FF^S(=GxnW|L}wKE?^E&@xAIWN}2O|>eAEw2N-JREW8-d%nG~*s99F;6;^17nf|0bU1G`>U6 zeQsx~E0)h14U-4jvi=UVoo(0Bd3B!;F?iB`Jk2FW1A&%zeJLkhmg)wrtU9!n_`%rv z^FZA@jCWLgX{_K%EnT<*9Yr?W?1_jIJ!uw-iH}v}SfV7WU!KLLg0T~JI71ulw7WIe z^{Jb2HYr#Cwk#c3D`^s_pUK9p8mLmB+hr{QQ=&=lLvU0U7$RFG`<}0F+`Irqt9=0$ ziZ#U3s_1O<(A&opcGU;6{lN>4 z)Tx>$H}*y70R!tkfXnt@!oYv?nXwe8Ht!_SJZmt&fg}zT`!I@ukMor|3gN_!Z#Dvv zZf~Yco(XSi?!n%|dI(Ng%%QHi1F($RMOug78y>3k1Te}v!~8*m-*f2Bw#cL;=FC|8 zQ7C%)+K#I)GGrM4{8k}5&d5mAQD4nAoAc}Y1o6~jW5)z+h53kgLx z%Dn1od>wTqkd<6f8E2l_MDU=_gJbIe_xopCX?>phN~@&S9iFfuE6tjL!jum5NJQY736QZ`Z0E)Ew~md zQ(&@~7!+|(XmF)j+H5XQ4qz#_TbLioNT|J&ECWl-4hlKvdXB>(n7C{x67}U@u3643 zNIrIMC-ZtHBUQ?7|99d58@W!EVA~i@mj{|VQwH%_G%VE=m6ISiwLv3K{JTvw9o|Z6 z2BxMbHVZUt+W0>*ZTt3rih~Y9LLAHS`UG)4d3Uy^i069eBG0RC3vsoDb5Hj$^Qe5} z5=OC7uN&9p{2pi-YlghCk{yhtW>O;JUX3*0VV-%^5INL-5Pq0Sz7_dBFB7gn0RjZd zg(Xrn;lq7)cCqEqRn7E^9>B(f?-1i$gw{@Yg=&nU>DcqrbPe>616N=R$=A$9VJ~G# z^vHMsdWSFYQ&6VZ_8QR9cxVx&=Ar7Hvf%Ls5Z9_#EFD5d`D7gTAY*4VcYru{IXypIOyzPo9Jx`+Z>R`~vJEPqqPOITX)H0pc zt;hsrX$>d{h_~6I4_G;Hlip;Gi?7M$N8o{?U{BZ9ebGDN#f|sZ zttsC3@9CAHY_W1tT<}2uE?i~Yk9o4Z32oZhDwz}p2Bb(=hoP8}8T(mXGUw?N*@6=iQ}$5%7E7I3@^a?NFwaiwO;kSG%D)a;nQ(V`vuUY^ChMBt&;p+~#N|pc@?=KtByj2@39ht> zl-Mj_>z5^llci741`_@TH|Ayi9ekRyyNMzKvlO&*0l_|ehg!1}`s2?>&A^I-I$l<7 zDPlxB5gI9%iZfnfG)n(-P$1GsmIO|R1vce2ST1)457(~}Z7vOKQ3Ik3eIg{S-cClp ztz)G@=~0v<3N-*gNYjV|C2q4*UmP_e^{W!*`MvWSYIY_W!_fF<*J!;qp;4(fDLjWp zVf&U4*n-0%IesTO8qTpme8HneSaiKOn=Axc4p5O}079e-oD!nliw~^T4~O88x{>uH z`4ivuGvqsS2|DW(&0G@qr;?eG z!8{ZsbJY%BeM=nth8+`%4 z7;L2E^j{Po(!*okBGv0XnWDj{pnQU$UtyHr4`h3lTh3b7aN4V6=|!s0u|O0XuVN{Y zB-;LAv4`f}FTMwXNS|0D%^9_OLlMRH{BrLFJ3r1Lj4yyFj{~Cw`S<|3&f6?v1xU7JNXS00K<1;8O{m(8TK~pzPja#K`pzS0-51)EkwZ_;sUj3tVE^)|*$V z<-gd>BiM}nSXGprS>wZijva&WzaT+uRE*U$`bEXeWE`g+XGDp07C0v9k3bV2MxqHn2zALO;!)XImSI;DzdoWSd%-erIIP%x!oGtHn-m23lwU-nlJc3R@L)y=p7jZw&`i9%_3Zp4J(6jE+se>t z!&dw`wTR)~{!4Cv*#LfSifclj%B(fP!TKl@rgJO~c}m5@K0JAO`&Sp-5PJw@+e#0; zc^xF4mpQqu44}FttDoTI`j-A$aM@e=sVazp$-f_+9LTt?5l*{5hzC?9kj7Yf=Z`n} zEoJa$Fpm|Cw6^82*e+4s_us^rAHzs>1)7So?hkq(tY$X8>_+h4Km&;&p`Vd>m4&fM9mX@4zX_!k-ec_pyidF~P<1Rvk z5-$A8{goq<3>m^=&5R}+w(1^|%F7Fs(z~&SwVV7Xuaf+OgX5~-_PEC6t}pBBn=xQF zc*c!lNugjUCAYPD8}dEzGkTpXO@^kFnV~p2KszFOs|n|f_9J;+;R1hGJvu08lq*qg zIzP8~B1PTj45pXj@OQp{H~F)`Cr(5^el2KQ7P%il9oaq?{+&1w`jQ0t$%O@g>0 zo`{1FH+H|n2&Geq7f412vsFW(M55R~)g#?}K6}%ii0~O9|CuYm;Fl!+xUI*f z8tqom;*9jS`+~X|YX&_ezpwlH2+;cC;w@SjjGemD8Kk&s6J;DeZvKX*fn~PK8iK5J ziUN~v;mad;FXFkuSlKOVH!;ADW&w$!3BixBO>EzDUSdS)my44zRS*MiOo1*wW7(O#Ux zf3ws)!+=j8YVL#JGT*Zr0}3CruG|C{7Rl!Lt-_+$G{}{ex53I(|HEi-Qh=SAP)nwNSR9*ky)0_ zS}Lza%_U1Q@Y=A}UWLPRf-T?xQKvky=PKhg>6rd7Jl&)BjpQ+*+hNN@mS!0l_2Zh$ z3XO+gWn4`ZOER$K`kr~9|70<^xM4i zyOKtHu&p$+IZzvWN-CdDe;BXydSoU_-kgj)@Xb}-3M7^0J&^IH%F3E}9&!n$*Ndom z;v@HgSld%%KAiI*Cg_FU#Z`E@o2r1Uu9@Nsffl6Ox`tB+(Ffh{HJVdT66v9cZtmfzo4l`P4eDxWY+$ytI;^C8S2t%bv=Na!-7g zQ(oJ(VcdOr#BQ2rCMO@k6!Jgvc5s&_@MnW*LYEWa=*tXZO@`m%IVjvZsQu?3TN$AFsJbkqNLf=eu-d2)< zA~cQagTGBoi!RRAAj#u7(;OjJF$ca3X6*Sap+yAvl7?x6;1o)#l6$2{#uc7#7% z4p__ZWYd=P=$J%;6x=2}qB0FyoEKv7a`*L!`NIMf^R#l#mEkhO0P4Sp?TpfXGx+$! zZ%laEia{T6#9LwqKXOb%w*@i&K3kI{rcVMu zc9rOL3fK@A)d+_h(vARbUQ+EQv%`{l7H%b#ecc>b*tJUhtNZ=TBJRT4_HK zl%1VPPJnp;F2jf=2^v6ECZs0K-Au{MDEHi@^gZU=@um|B{lWP~1wS}-ZGkm<6k@_-O4xLYnsM+3Fz za9NLPlHeR6CnNmfZTaHfgajA4E6w3S z930L`9!0$jYg10B4KO=oykk1O(^A2GFe@6ClslDl-2L2xV&IBN73y&nTp)jV>uX77 zMq+q+tT(IrqpoHHe0|Lq{EMfRq_kZXb>FTzPRlBZE2IXCpxTq*#2xb<6$PjOYs>JhR`cJ)eKpNQTcmW~X@dk-{Hua9g^b%k%@w=p zK`izQkpkl2+?*GSlZ&F^XdKYYvCA;0u)PVl1cq`r>9FS1+PQ-2{VGwpImO{_iJfvz z{b#LbtrP`;>3D_;aW{)n8{$Y>^(D?ld4&33PPY6Ua+xv2@XtL@-SBhaCy2ZsZ@7`$84%WGwqy83q$hs3-fX>s0Mi)fR_@6Wgrh;Ubi!Kjp4~c)K!dB7AB~|z)qem zeTQaxR$z$K?uP;CQWVKAL$k(mS7L|m@ zn+mRG&nD5aYl3*z@=oioZnxV~?Tg3#@X5(D*yPh4$HIxC=@hMLd2&GS>@`ywOi3!i@9uRswC2SCFCR=@&Nkgn; zsVJ=09ut7KU;&4h7^uoC1c^4f*zka;D~b@{Ga-k_MwkJ;)XJz8RmRP=aTuyJQr;p3 zFS~sSdX&ykTX3W}_O;7t33?=T7>`R9pB6>LzagJd-H8yASkI@x%3Nt){i5u3GX`S? z|LCPcj;WpYAvDSl%f`KGy$2Gqv>{e`1;;E@!vkt}lQt{n?ZglqpSLz4eR|(ukBSz_ zUI}x_sXr`x#G!?;UwuutMOB*1k7`zA~pc%tzxcjC@0YawcKj9Nw%28PA;m zhV_X1%GoSxuGI>ejz^eP#D{{jk&wty#`w4>?BV^KnCRkL4_bf#7#Gq)EU@6&4vNG7 zp`9~@36{dcr^vXw6i&)kM4TtDi|LkSzHwbbdO6ykyTLA6{x6RXN0W!efK?(kw5}= zg0BvjZvE5uy~+D=f%S2~hM~HLykp1WQs;hzfyPGwkBFKeNo2UbLMB>thO>qo7@uV= z86Mf^DGoErVw#(Z-%(Wk%f7!B)UA_i3l~?10S;%BZr*p>Fx_g$bmMiY_(-$eSXKjn7VeLgFOHSVYvr&cBP;nDjIxlV8 z9*P#$;$y%j6(ua!;onun@ON85-Y>d~dk?@NOg)Jq|Oy=m(e?`8+Gja*gX>^Gu zpem_Fb8}WxH_j!Eg-e>>b@QPFPwkRs>#;_$G;=E+?91Edk@XKlKW=z&saFVFY@^+7)NwtKQ3g zomMV@2zgG=^E|nhP>-z;fca1HyV7g2H2uvA8CPOQWp%`&$W+4%LBBL}C{XNAiM6gW zL^5Phqi$&eN0uy)m*KIBP255Hw_J}6!4wML+qHZAud}0xIXh_uZ!;F^&c(|lzaq5i z7UiU&&TJkD25#OY;$EU>;~&W`d}@$(W|cM?hIGS;;LW{}@L9?cdd6GP6vvSpyUVH~ z)-Ty{y-d+&#eGoq$H})b>a>u%vT~s&rBiJJ7p1@UsDwQ+bUobDAs}ZgX{>GZ?&V_p zt=?hK>ShrB?tL&2qb^B&1jzk#2mZVNy*kCx4QtG)ZH_gqM*UsWh|< zwQce->AhSv^j3~K=$q(dxZO)07qIH&ab6@odo2ScHr@9Sb&iD=F;P+S(b93 zZQ4VQHs^KJo%RX$zAnGZqAHV0i?i6d0ssD_N2Vmm%PF5nIZYy@j_F!8oq_Dty;hkj z*eypfqvun#@-g!!jSW&zHZaYq)ThIBV)AI&g)S=Gqm390>WWw6u>N%czERic1R|r| z$BU-$IsvehLEQdp&+4|^vK#QKZVMURz_Hh7s&{Sd8FKGiU8gI z-5h=BrgBEbDBFs4e9~f;-)+5Wm0MMbO1tSx>up8VS|Hg9q*7SX(dZ#aa_C=kK-p2^ zs|z;x)i_DYg62W)qqYFYJ*^4RjRk~Z%ptwtrLc~Yh@9-zSLe>KN{W@96VgnG0FF%_ zL&)yrbe0{~KQfx_FD0AC z4%qNdLqX?TTI?`7CtaE@ys5eF>}RR8!SsrhurgmAtW()>`g)U1enMR8t@rF&%2&8W!+*&iW>;D8Xo}inag@oP8GCSH%F;4;&0fR3T(0MEsRf zVPHTz`SU~()A*L=38JUa6mjIV3n}paY2Xdsf^_LnG%b3?KBom&Sb|J*ENDh~xkkH! z8Dm+-OiI8O(6yO!vU9_^rE-Z5o;tOWbig`pAr=Pz&I3e8x`UC^$AhRnx6PcA(@;_O zUO|JetU7~ScN6PpJv-Gd-K4?(NA6O|Lod}q9Y=BQ&IR1adI65rZ_RsFN?J}XVphRmllDW4ad zvV~1=$MoNMt!Jc}3p}Q)wWLH92Ykv7?PvmHJHq5;ddZqmwd8;7ZvU_tD_d1I;1SWuvVM z)X$So=>)bfGG6CkR{u-3FdSuwCoeV>O^S7+l!#YbAyksTb`NrqF}6k0ZOoO%QCE z9NMBW3An{1t&QXP49J#9Pk?R0SwopUtzA66!*24iL?BpW$OeJ&gOD@@qJw~+i4$3J102%vsDG*jc_A&ipZ(#)ABW9!)O)jSB>X364jmmwx z5!g4bb7<$vxkQ%KT+>|c5kM9xe;t!c5#H=QP!tN z3TBi7fFVb8DP^bF=LBw!C-vWdCpSCB*qUXp& zhm0Azi-MI&ex+E6!&|;eUM;aE?q+Cis$sZy&zy?Ly`fJ0DoE}2Ls>3W3dD#bxKY9J zu#WPA@+zlO7=&a(13r~2$O7sqpH-0;uG!Q|dr?=yI(~(aZJL6O#=+3I*&U`@BN@hO zr}SeK)aW%Qb}b?)mzK|%roT_7ae?JzEV}NH6=*ZgCPQ$aR3%+TRnP$GeStl5b^Om> zUNgtpLrH(k_v-GXwn;k&6;Z@RxF6=vbsa_Yae76W(h4gcuFS~X zJu(V|v-ehS8wUKrywguz+$xAMk^56@N;kV7y0a;i;89%IN1+fSWTwCQ976 zYvk1*qIa$PXh;c#&VC}&vtL=G9tO;o`c{Q7Vai%#nM+w}A=)J*)d#c97}fULS_wF* zhE=vONe?IyHc?Fzp39n!D?tnKJzUDurrB5)W_yI<#eKZ?_j97$%k={_lY^&GE3qAG z|E@=fF1hjVBa7&#L|J=46T)cd2*;049CK<Al&oJTdl0F&@ zT%#!*N_(b!s_dqt;myKK)*7@4^qrt*Y1jgxqCgHK_d$MiP=Fw^H+PrdiA+X+15XCu zLCcF?eM}Q2GN7+u7xI^vf~q5wZHxJR+5@3w{9j$Xcs4=mgOr238QTFbd7IJ@Gp{>Yg4RtueHIFYCf4cj0LiQ@ zJV&42_R6OKIgtMzRw6blOwRfEYI?O?GA`viA?_K`2(nXy9(yoEpj%t?LP z1&;~Cjm0EH^{(E4!wC*6HzvQl><|$BI7GuuhGv|-Wp`qB zKZH6mr75x{(x-{vH>)K@qT7S1i#niI-E%tkBR9LFWiz~h6O z&Wd6L>PUO|hJ~fkT2RQ4=gvm~#b>Q)9oNyl%B+M9^$u`=UDRW*1ZB`gT=>Gm!qYEt zCsdiwmyDBz$Wg)&J1Bb(Eo_o`x5mceKrZ6&XUBB;DH!a@eav=9+hLkI6Y3>rZwuvv z7G%-Gt|!^SwrwRi1~c%Cim8kmz1|+hTKMptPG%di^f=MujitzANH1BVQtS_a899jz74>c{ygy&Npc{psXa4-$_>HuR2Q~2$5Ygj5q0_J2>Zta2FzQPQ=b) z;?I=N$H4t<7FE@EwSj&e^M(D_$Wb8`NM2NuAV`>%MlsSpp@#IRFe5kw!`};ucz6e# zop2nmDqkbKB{u<*l&RL}87p%4#AMfQ(G^%A=H^9M_R)FG&SKpd zqJjn0R*2rdSj8t4JNcO;e#cyt$nQ1@{(t&3W3|z z8Bpoz7U2XPC#SiBK)L8FM;!9?*c4F3`zW$&GF0{huTo|%B`=+x*utSQ{3gN&MbzB~5jQps(_8Eh>Jr4xyM)RAvB42p6LS({E};Gb!A!rWV)9v2kXi{QAB+ z)V+d8kaY&f>Zm4k3}OSWWO53E0e0<~7m<-L%zALdI#zLTnisek>M8-|XvMR%o+M1!{wsAOys`mx=*ftYB2Oq$`?R_-hLA*^-JkC|nE~3irMt3ZJ33^(6 zhd8RaYmDO@wbSc`vUCD{7MNn9IJp%J3l3TOrYP58GRAc2`iq9~ z#;XEZ{gpVb32^%lFHT~!b3_sa5<%T8!^yEsA+KznmF<;S{vCCAH3ubxp^l9qi5=x( ztI#f#E)D#iO3%hGB+@bsrF+l>LiDitw@dbvkmco$_2(V|#y!5|w^t~I&lprl$bHBA zr38+~5*F64zaN;l(p>cm|H)hg6*Eqd3C66SF=~{uqFP2octe`5imp$>Yuc}+gWKW2 zMF>)G8-a2m%bN(7G&_qJT3EvRkk)KN{C2_~9L`>;YE@Eg8nnk-S@z+wjvmA6>$4fkV`OaQe|YT=-4U()6!Y9{ln;RYWs$rr zCL9ipvz$C2Vq3h>DGVz=8jHRJ6KyD7UQ3;}f%>@Go$5|CK|y=Km_~LEkTPAX3=B<9 zS;7W@WL9YPU$feQh_vIKiNt7KuXPwg8=Z)W0|m)Zy|C(qnS4SIMGe#_O5O{JeeU`Y zO90e7eeZYj-b%HJtd1OK`sw1Jr<2pBm%tvVM^?!cGpZeQZ$fas|0!@s;DHx#)U=VO z4tv(ON61=oruiy9eZyDA!v8f2SR|Y7@bTy5&8#=}$()i__zR-K%wSAT_&&4MA@d)h z11Tb!nCDZZhc`mxnKB+V2_(p=<&1B+k9b=2u7r6pXjLi0QobmqF>Bq{mUNAht@4Y8 zNMcFyTXSB5V16>`0wEY6rRgG}N!1pssTt^A>NUuxm$?45GWbP-PRZ}L;*sF}+-$S9 zpQ|~L`#8rpyniDXVM=7wy(l1>FsTU+up{hbf}-SRNS)(XNaF$w#MDadYx@#OosSA+2_AH^UPop@WbyP3Qd#s_3$s z^`8HNbEpfA)A0!3{c7)pD-nMd`y-rCrpp zZvn_!HKVc_QKY1Qi)z*4e?hyLF6&5^y8*#~lT5ioMk)S6j-S7p7lLbUJP zF7}V<MFTJIh876 z4Kl<=pLfFPxhQkR!68&o$TK*j2X%06scbo}d1+j}C-33P4b=4qW(kQE12aco%B0(B zKHAv=Rf+d64|o@ltC@zk22V{!Z1&5fuw$JMQC@=Z*oRE+OE%ve1!h1%!tm}iZgkCc z;CVd8+hNX7+1V8|8g}m_7o=i+J#x9?wvKx&;Ax6nERyx7ZJ?3khgLfF{2DaR@i%QE zZ{Bhm;7=W@<`LHW7W_bo%$~oq-UX&rTx`5r6TQhO!O>JkuzxDXHqk(mRCh2z%g=Qe zzuw=z(Z2Yw5TKDQ6Mb()9{@7FbJ0`7GBh_MoHxdH7B_C6pT{zax58yd$dA;#(ay{| z>BP8e$=7NrA&{Kw%%#$vhLH~GQkDan-ZU0>imeg@u@+vXkYVx$wD<67ni$M`4->QA|fQbXUBQpYT zPViS39b@QX6BA?8G`N!oEwDP~h*l#tJWDs>cFEYBpl0@nh3@%WLxTdoT^RFC8-gpF zhR$>HHHaDb_V~K33ULc(0@=OBBarY5pG;e~Q=9lmnTrr~(s|Z_2QmOQk+f*_j!NQm zxRnRBmj&1R8Xh(WvuHoE7VO^p5~y1JQ~#}0_pFE^n@Ln8lRFWzmeqb>k;!wfdv-Tk z%GUXf5hf5yD_h@sB4bI_(nDm`&_uhx7AZ=&+YtU+U50~CD7FNgOxqcyuthZR?+AU@eqH^;n3&vRtpbSXC%ga@x|790SGsQ(9$kM(MJF%$RHQ z9RL@S2EDz9q8>^nOlQ^}-d>udP`Z59yE4YQC4{(Fi8VcFXNv}t0nk4|Fp93i0xG1*a)w{t%-3JWo&GpcMO^%kW zs~f){^WUT4(q87?R1)N0QS{;=Oaq`ubaGNPP7B1V46o5o!(M3@oH#Lrapy$jGiJW_ z-3YVj)||1AmlFt$le0(56S^yzr3j;bUp9 zRVU#YiOU@--W*~*sh95t<$AqOH5 z#2d}r8Q^|KxSYT^LTGL*4KK*Ck8ahN{rbJhJ7o!vV(FMNoa3We&RIpRo_7+`3Iz~u zLbfi_b!#GxO}P1e@?IibjI5tF2k7l*jKW__X8MbZnAQEXs>&|*)Zxd;nGB^m4!JjK z;5uaz%(eBf-?Qx(TGt6|WXjgQr4}ck!ghGk2;10{S+Y#G(b~p(m@~myHjb~9Szt#= z78mN6!0~J=^Ut(34$7}pTlvN>11?1q$g4kj`awec*kv)zUN=1H@9mhjkf3ySARPxy z!$_6mx-&6klueemI-j@xgcY=inOH}W9R&(ptL1|qWfCxk)B(npjoHa}9RBYZLBuU? zLQEJw_1GVXVO9Il#fQ-MA4EUP83i9dJPEZ4c_Q!p`aZ&v*%IKW&DLcn7J47c%dqcy zgbFq!PE7m~D|}$PJFn#JUtswQXo1CRr-G{jm%ZhbqtxIYx9GT}1(4qRsfIla-I`O1 z`i5}j*B6Enc>yc+67yrP8T1-X(dS2Z#1gW3DvH=W!{-ejUwSqo?g)6u^B)@G*LMqu zm&_-+B*l%Lnf&RE%W|Pln1=MpOt?Q*?Oj9sus0L4Y&%e z`FK?*vXQiWUr1sR3br7W1QH!itud-o61dBBxA_Hy@KYe&t@mVAGae?RuI64x1>*PE z8@7Gv9gjKjUK-rQFq6h$v0wHU&5`#G*7t``zPU$(w0>em1`KoO02pn)XJ23?R+=o{ zkW=rU(3>qJt2&N5-o(J)d$*c>gra(h#p&juk)gs{IW8Ey87_F#;&4AWE~kIV(gK*b zl5Y#4znM)aow;zivrGy9NzbKv!!;g@+;QyQiGNgb3MaEnacC!Fh)0%K-(VnV-`RyBDkyQpe%8%s(BE4v}+LRna{8 ziOeKtJrUB1ygzWz_t7(oPjOyah6k(-van<6IMs!Z&`yT276t>g%+}jFm2|um$)b)i zjV3)V(HC-|OL^42q|kpzXXu@x*VIYAU}-em*cl$R>DZ3jXr%_i&o@ zKYdcG*?%Y)vzp&wDAm7oUxFq0O=tO@5)M&^T)8o<>tK1yxG z5D#ilE2Y0Syx$E}AU-Wunm`daPZlsCH@&&J?fRtVLKy7JNLUClY0Tc&GDPq~X9{vEHd^n&YuVmO$OK* zPHa&>R4FOlS7mf+ahe1mB-vsxZOTm>w9l#k#(~0BUOie62_&^>k>2N-`@NoEox#7i z`e7*dg>N^Se;YH&JQe$1tRt;=bA+_KYLB*fMDVZYgQ%PO3-JVKHQ_{*-yPy~Fu;2x zzacu&IK`R&@& zI`d#mexiy?q7|B3Kxj>(yE%*b_execE&I~CcX=7g?p5Fvkkt(PNUS<;VmW%6gVF7* zYNL(h9B*l#=CbYjjfE2afyVfdd~92UA!mAb6w)Yao%t1(+DZG?oF;#-Z2UW>i4jiP z_2=Q#kMkE9acwubx0Z9zYXqA!ye9Qi8t(KqyP{Cv<8BC%`xBb+#MG_V?i(;&N$b9I zLp3{iw?a2#oBY^4zb_>hQkAg=W|tdK_gjN4&Z1${p$nRv6<6jE5@mhq%sZfw1AXtz z5i}<2`HPK0V+kY^sVsbed^s2cU18`Zg~d(n?EijLe6n+9)@bGV-4`;*xLES(yUAbw z*GU*H7D%IxzZF7?jXb;XYiLM0xdcoY_TXVslX#-F2Pa`_Y0kUtJ(fHE-7FbEz%j^b z0j@h82pIr6pGYtfUrl$ZJc-C4Vz`ii8~O1s*Rzsk<95laz}nv#{1Jgb!!eexF??F( ztOha}Q-OP85M*;9Avywy!h$1q^MmDuCZZ_W>JUydDc07@5xa8yUNPMx-GAH3=%>zv zs_XPsUv+}mMZ!HO`|JRk4}z%SXdvaL2bQLpwr@ekQ0*qZ>OGg8iU_zGn9F!MD}D1m zhFG#p4*1kP@bu;})e$woo>`Z*%j%DQA6$sPkEYsvBIDRrMV2IAg>~ZH0;Ik&0&~8! z1_xg5A5a|mSxTJY0R*!N6!&|Yqg>j}7dZDbg&I=qJ=Slr?~*zMgrf4e8h9dSIvrxMsdFi^Okz&6|*ufaOT7s2}d;wPe#$jpEFbMa4_dHtWkB$j~Nq9{=*d=Aiko0{3py#C}lj9c(+y|OX_E*8Ur1CTq;^R!s ze3IrDu+dE9zG$+k?DPYSg$7M(#aTf*F^Rqz%xx6G5~LP$|q<9WqLokqh zZ4GlS_*EM}c<66KDTq~jJ|1!Vd&p|hiDCdYiA5gPL-1-#v9qX|w=uev9*;LXXsRW! zbOy~@$biy7u+x*FWOI-A*;Zm(Hpr$a*J0q;E)sQx7cB&_w@D3cNP=P^YRs1cC%+6< z9&fFU*(q;87s6bqgxsLyMOBEyCmd4&{Z1{@?J1JZ3>9hz!ONU6WUj}EEk(?KcT&?` zQxn!&-QNVE_tTIbRS_Pm0OS-OO~VAYF$y;s$8PV-<{U9HBgM>z>&;6r#5Q~`*&-q& zqocpR4bCDCW*a%YxgfBNt1e+qzkD#-BPw55da-)+kv^vNNOb99v%OJ$zJj1!sB zG%%`4JpD4a0Sj6O)m$h6Bm%WVEG0=35LAB96S1Bn)zH%u4`^F2icrlnYoDC9zwd?6LPtP^+Y09yHv~EB0zy%GS)IX_3C&G0U znn}U)v=M1!Ct^gM7_hgd_~pOp>J<2E5Xd-0GF2kf5Y|F-gjY59@ZT3V z8WFB(KGYFk)ehDwHe}-h!U)e~P#eZ4OV@sRu5pcWhq~W@J$N@BY+m9UMe$%9BLaAd z)k3OrZgVU@z~}f8(fp-jpoy;I%j&|9j1XQL!EeUXHumUkk&kM>gO>>dDNPnvEDkR9 zCr1P6ktUc#Zo;SAs^vG|U4#slR@qhTA($~WeT2X$Pr)l|c`%SX!E*d0Fy#^?57tglyn;|&+U6dB*dkz9SzA`e zrr*-T-P9&{AQ7^7LA#zIAYdvrZ$*9`2ffU8mhLED>ak(FrbLVMl_jvhX%d&g(F0t>CO`iswmae@`BKw*x3a{9P>moujB4>ffe{Umz zp6qs{oj0{W+~ogZ>Kq#@>zZ!8W81dTv6GJNq+{E*ZCf4mj%}x-j&0kvb#lL7p7R%~ z)~cHGs!?N3M~C1$a6~`SFkm*@I0K-Qjiyu0MZ`)W)P)QM5(!h_Xi)h@BXOVKoCL9` zQ8f}tleupcvf^B=%>6{fW2UWNrvCM01q46VT9hW!=~~cdE@GS7nJ4nX6?CAxPSmq*M`>0#qZz`Cbc`_@jff#n4Ws^7FyWuR?x};#R{X*Q=HpzH? z`wcs`!uS-?!{GN7H_BFqDVpDiu5fq=Qmn8^@lvKUWqpZUZu`VVi1tT`2t~U#-`OB9 zUfkVI=*kXCNi&yd6aDjdTDo1ap8)qIqL5(hhRf6At(K8!p9{H(-q3_+ab}d41{*$8 zpxG2#DzmlAaCddwpEa;Fl*iY<36)%k$phUw`JU!&wQw?tti+-SO%(l9(6)fQ(_9oV z_?5tZ%bR<=0BP0jRq&(C4VEpRK51{|pXMdY^g`guM?q-9@r$HsD46J`ZCDkUaEI6q zFfnz2YED)4SES(1pIp$}EfSSIZxN$tje8zg}CYb>(hKD84JC% zi5sYgO%UHn#wb21?)=Iz199`W-dLBN*S5KZ)-AZ$(UBCWL1pDuM+AP&z zMos{!yX(#rV>=2P=i4JsU`a%%fYc{7){x5;Cu}%=-J@m=NJ(o_!)%z;OD4-KG)82a zCpMNUN8A-I%-K==VOmnv^#qe4eA}&OoG$h1(m)qet(1vu|2^uT`&C`}O03JEvvJ*{ z>6?{kf4wAJy|9Xih%Z4K$;(?vWlY*vMM=x@MS9aEV6&*BKYt@!-7C))kEgu`m+Jgr zp!|c&;7xNaw*0lBNL$G>-Y%1rd2?r3%-!OJ!jIF%{OeU` zWKhIxK|r3C3t0KtMc_71kFD#+XE9>&M0EWpiVrfZ0M=q8PRBRP)QfPs`M56#39hWLAF6$KKKkw7yNsB5N0zRFe0HA4r_ZuTBr`1I-9!Z}U^q*TFN+ z5bB1H=}UH`TBM2JjZDF_nG-^goBx8(W8;{aZ)# zD-vCO2bq8_&QVhiZSjppiF!7AN;^!UB;pz75g*N&wV~ON2MWSuyyK?wz!R?ytY4$S z)M+-<9EM&{>Ow{y?tL+h1x`kWhQbU(LbgA#2n1DKvxouzvu;5#ISQl(w)|(@k_Zk! zMwSI&1F-Xd+yhVm#3mSv>esdTX{&|t6YqSO)8gR6$*j|PA=zi9GRvazULpZNA<#eo zb$}8g03MkOK)CSknFjb7K+~n33-9|zbjzuId3jw5W&NT@8+wPbaV5sj`TPki?C1I% z$cB;PE8sD`$d|=n6B?439or$dZG&!@F--asbdLxhg3w!i7hi=QRTRagvKW#TtT3np zVsS!B(Ou@|U@C3LgbA>QADZN3D5XzU+7X+XbX2rn$%patdT)##haQNZnPK%&XSP-8 zk`EkicW6O3>s_L`vPCbpz>;TWkvUgrBBOd^-a#gWIR^j@zxo`~bU)lrMJY%a3=kp= zp>8gpMuiRfkoJbbAn02_wmDnDC56t+->49OP8fkQEc;t4k@PZnHV0JK2y-9 z8qd=GW8{w@=DQp|Fi!Kp15&X8L&Dd7^8+C!yclqLR>3%0cW z$CcdkGiuG!QV^bjj7S!(@t}T(?Bz<&Z}g7X5HBaZl4Vt{3Y(s0wpShmc_?zQpxYpD z&6NBJgG1)5^wzfON0U^kNif`TK!*JoVamKeu%kzDU?`#>d8^z@ok8&ghSVlXR&$iO zsHg(-p^44%xI;$zxmkKaihi#K9Y4L=2sNW{maOMHxGI4FkAOND02Z<&fDlMPS`EO; zpi#3<9+L%`PZa9w2gzoNofjNwPr1P+hnCWGY!d5)jab3R>e%R=)L8a|tDilDr4xMZ z*TETCng;)B(X4Y4q!(EKAR2{z;(|Av_>!R0F2(wkYIO^ZMAx6YF5dk!9T`^<_+u1? zpM3+VBGB3zgSV;5*G;P@Vt-GTq9cW%S+|nJZ0^8& z9)kbVuqGh+9aZH(K>mMG#rTdYZul{1zsp1_u#42exq!LKytJ*gwt7~vCgWB9cvPBT z03n(}E;TbgbrWa&}01b~6$U!iX&M!)7TCi31gNNX!d zDO#Vx%H|$I^`3@3eN7IY`;kbGS}PLpwx$x|PP`(>aiZ;ZZYZ9Z**RF%xHi5*3Al2N zxVXpBy%YPv`*vr+;u%&2MCgL_rd8Mv29@i^r53;D)@;rBLCU}*4oOry@9YnDV^_*F zO&-zYcdOUS?7gkWk15VGORK3AhBWhd0rJ`S79a&R@5|pog$x7?2izb5P>@9dgcE>b zAA9HCuq^x-W7~?TmIiYOLczqTIpDDuKTcm)##H8C!x0;`m#tW^_|~(;QA28`k@7fv zU~z85K~=G^OqckiHyW!ge$<}T{lpQc2fgvRSRzx6!A_?ap)x%yMWYZUhv>eLb)RD6 zcEE$p!O=v%oqbiPJ}Fe%&FD?bd+dn7HNZ%9l4y}0k#XN3j4eV9M!al4P?F+}ei^`G*sDcZ%W;+Vz0f1)OvVX7n zT}{jaqFvE#L%x&tjMqX>T*Jd};|W#u?Y&g-S=)VH!fA`V&?XCms3hu0i{k}W{nf~b zZIj1bK$twAyJ)bppv-a~BaI?Mcn4;?+9k+O-MS_+3=U7(^L-0TKIdmAWZKqv&53?9 zFPbciePys!eU&!`y*Mm@C&*MA%Ft*XnSjyF9P1EE(0yWiCo{iFer%)irp%u=GI}Yh z-Bv5;nVo_+i7?O?LS+&MZg4*UE?DF7znaXms*osd_Ay&#Se6OVma++jT1=R&e#fJr zq;?+ONHv>-?g)0Gwwjx-aH-deWv|&?o6aTgk~SB|?Ka)KR3l5B9Mpq@4zmfr<%Au{ za*<#*f?Op1aN^@6zm;_T zms&9vPbobR94r{93TO5k>7S)lRFbPOdox#m_gWgUF%yezQanlMN!bJ zAW&SZhAMMkm|)^Rt(_v6w|=zOvZJxM2?4LxZM8wlhg%ha7fwW*1@cMqgfin61xUF! zw;e#g?no%DCbG=ssC&F2``2#;^7+E7c7lY0o(*Qg_Bf_^rVBW_EYcE%#}{#j^vy#6MU(;9vXBd;#p zz}RW|6(%*%T?5btUwZ+p$kFgOw=)bAX*+5l5K6E*v{ng)<|s+4$&OB@dz>WVm3;tf za>|7Wwl?oc5bRVM>JaL~l$?82luILUJ0TBJ8eR(?RvMv|;%Kjr@VG7RgQqG|RC!lJ zeN%x-|0ll7s*Uj)L8eW^;JX^)f?ckJ^;~YP_E{md4X z*wa1OGrTHUF=znH&K34yfY-i%*9ZpW((0G%6-)X4AYPteug@Kxsav5YDNm$G_a;3;x|{>PdPHMuKfs#|?f1H%GW%YLW@VQoqMM~9g;F3*UHK zP=NKWjWmOy_}YAR#(3$u%wieG4??{Z7#j~r?!a82z3rwA04S^%2!b7ui1K|5!w2Bg zD-?ypluvCMmp+3)K437;cw@RD5ailH($r?;bmR*81YqKuC4)qI;H^pfBK0@*m_6k}#X)CGs_x7o|Fudnjos+%EDJKZQ4I=rNe3wS4mX&coWvuXXGyp<|IR^KK zP6k|73&d<-ydLiEZS%a*q+E6>)T}baoOyRn_qbpHOHM}YEY`0=%8d^UHOq-mX9vpW zO3n)QR8(gcmB1y+M(=5L&oj}ylk^R&P@f_|Db$%_fmS;qT~no=ra89lXIEyyLsRQh(R;ok_yyD1xcv^~2n-Slng&^OjwsXhqfwo*E&GP+By*+2dIuDPp% zznX%(OKiChrgpPp*#fC?jP)^$dj9woaIX9{y{fcG|5|0Pak8Ha5ePFf2&8k$@`L~Q zvW@WnIz4Rqvhde(3v0H3IH3%Xak3XBI4p^n6vEDrvnk{$fcS;&kHEU(5poSfjt(_R zANp?_ZBh!Y7P8IhCn>x*C)Y%l!a&a_x6~Di$*D^D#$8ClRo@q*R&jp>x>$#+z+WJ8 z2sys#Jy<&K{7x&K^#SMb$^P{qKJj;d-wgy2446R$z#y{$un}Sg0s!J`o5{r^Wdr7@q&cYlhG!y>W!#sx}es;EI^Ddmo5y_-k~Pw?aPPvU_M^7{bER z`;=%2^E4Nz$+P9s<+`E3PC}}M^tNUxkO1D?<%0KUBhg?-D47Fw0MSEjI*Z=njT({>rubw1fd0Hd8x{I)^0sI+Q23fv#!%FK z^BKL!>AObr#K9v47k~?zb2VjG`EOd@U^oy!?ij3V((WJ5o|3I4Opfd3a8^( z|28!gD`3}${_%IlL>v^l!Xc2>6Sof**EKAHBFrz%^o+)MYKwkX5tTp?i-0=p?<6w; z@KKyOiC(*FQ79+;HOq=#%Yx{b;#5B_eFn{==laTizn+JWbqep08*c|lz2k+Qa%&CY z8?LM2F-EeEJ3it)@A`onlCmOOP;#P) z$FQd_xcJHbK~f7=1g!X2)XX|e-KNtHZJ%NQb4&&5BkcaKiSGRjklpEHHmTiv8aq~` ze%1qSXT7sQ*VrBKD_I64fjoZH=L0ad{4n545anwwnS6AVrOb7vd`@a@2e?Lvn zE9ZZhz2IVr6p$Le*IM-Oj3~=cArxYNc8Y?sc}6I66>l#>WjLlgM)UFyRRjGj^RqWJ zC1%EuFc{q8aVToywGc+eD9n=U+v6!}|6Fy8LX2@IhyP|NxkGR;K%a2W78O5y$8Vu88hhTUuJ7yf9V+d>|0JCE{U%5 z{eht?Ws>iR2AIbE4aH^@8yf z0>vXKm?%_MVHDzdbf+Bmnj*3Ryv*F)CiEp7g;~op#Fm=LSWnPEh^;t{S$ZdOw#=f6 zSCQAhmAQRHVlVLwi^qFc1`2Orb`vX9KP|6@q2pr?nohy$SAbbmP#Dv$aGaVwd(o#d zulQ9j4Uz9p&81bDmQmnU00?Y#3SE|Lv?gRhXx3_h!^82(aROhmkD+UPoLLb}w$aR= zW%d4yQNQK9QvJ4>)Mksj%zpMT&Y?2-)(1B-dyu@f8c+dMHw{2*hqYK-irtUZaTZB0sc8K{fL(~;g)b{F7$YEjb8hG4uBzk7=TrOGC(fHwlQ5N5Tr>^mx$i03R zA-x^QaSfSb_)yks`79s=qDtQ;(i^4Iqe)!}?$4<*=6n1Q5-o{c&ND(%$H|2MniBqn45BZ~OO?d(r%sbNLbz<7vJ~gVzbj>i0$W z>foftMzl#Mcnky$K^8U3=tVh|HnXMcEy-_+HNhXp{%%S4|M52zbO7x4cSN{{kLDz( z3f$O5CWj*3V68eF9b_62qF?~;W*!NJ_KNRQmtHYKE;12p;%J~;@@!^r!0yD(-!mH2 zf#Jjv&CgCIWm{Z*=Qh;4AJ$II$fyCUbiORIjA^`DY$*u_Q>Hs&7U7O}=W0-K!e zDux?6Ruend$yDz8l6NQIvD^%l6F&?FjeHzp#<*4JDsO)!pz0I2lNHuc1?;}ZS&8=B zmmJWG5)5h?Jj`kt7U>gRqoKaGIan<_5Qc(Q65i+O(X(!Q3`|ZT*dQymK9<2VJ(=RC z_f3*)h|o1rpZ6qDJootPmYS6816`yt){89{Km|WriG%pW>mBp^DzCo&^sniy_@`$| zCb*5C91H!lIQ?RlK%Qq%Aq_j+MEjR>S`8Uo=(l!yB0|;ZiI@YS&S71wvVS=nHu(PV zBOL%dYcD;`jZpEg<;-p=>pZ2UU;ul%dPE=_SzZ3|0c;ln!>@71?(NXe$qbod$f+Tr z_j{!0e+3U%v{9=go{ml53>PFek{33y{x(|?s!<;g7THVbHP9_LNO6m<&<_ z^o^_=?GD^O$Lz_1c>iuvR{s&lM2zpWvjGTaJRE59_Tm($`u5*hm)+_7u)kDChI&Tc`FIuOTM8W_{`C$UZug&Z zp9>D(u9N`(ZpT9v{@t zGFufq*PMMyx!0=_u;6C}jF=fln}yZ~g;|5_sU%%K-u02Pp=GHawH-ai=Fke2Xw7vi zTqd*MH`V{{XHC0gq3?UhXdEeea2b((itW=J*L5Z8plJy;@P#BQrN*e7roF zhB*D&pdTo{ZsI(06jyHIu#7TlXumBxSJLuB+ z6qzwnnv@dHj#S-e7!B2O0smzhTRFhoEvwxUS+3FVLoNw!F%*o)pHI$ z(`LtD@tP<&|X|QyZ$pS{l*Zg3H+i6}b4X$dz;@ADmg-pyL4GFoI5iTo99CRVk z&~7x8ta%Hbs~o{Rx?ir~#j$MDo)R>_DQlC@MpG@WAbqR0^Io?5=-<&KM*C_XpJ&w^ zB-KIAiwhP?;E0j<&1{p~C$h4Y)5Krw%#c~CL?%lFMwYFrSpY2p5r)Q`*<^?m#h)aPtbop1TL^dC2emRi5W?Rj@9HuiJ>6J}j z3@F)jYx*rHVub{Yw)We!QO`E~Em&2R?k&$oO>%`hJH{Lt&$Fjt{$LQ$x!q%+LPNbq z5<-#01p(3Ixp4Gl(Jb7RYyJB1L{>uLDNB-3sJbYIhb61$SBqFK$m(HF{Dgc~7>G9WaM3+Ot*vou?6-wp;ATWQ_nanB|=8_8BSUEzmX zK3y}`1Xu8@d5ktpZ?x1JUu|q|)n}d$z(}ySpUNP4Up`?BS#{;SB>3=xcVWVmiU;h_ z>4&UK#XbA_xFWpFQraA|Z#)?OO}A~HRk(Nq1d1L>VTCjA+Z@z5cC2=d|DpGa|AWMc z00iU+0Ac@20w_tgWR!)TkH62FkkG!<8Ex1lY^cvC8i)SzBOBbn{P=VQVYmjoCI}~o z^`4)4(|S=4f4WE%N6#tbj0=Dy#m@RIYQN#w5u50Xe@Rl9ZZFx>g1JBctBd)T#F^0V zJ(sQrkxo;%Eo`MCYl~?iPnU3BWur405Fq-QYVDn>~A1=wrWY`3QNxm;>TFL&EuY~O&ESY0e z*0r`DLA%?a{x{TOc5AiV>2@8rl$4NrF34FQ@@k=gxW7ux?et%|sF z%UE~=lSdVImVR&)+mzre(^lxuLnN&vFeG9-rATf05ds2~hQ)#+XXtc?;A?U3?Np)E zTLDl=BDbOvoyE9%}-^qtf!V$ zjOe4;lPOZ#TvjP>gBq#hkS|X3gt>r zRtTC&7CXc0pXkq`thvT>Md$B#2%m>bYmL62Cq4bt0c!$2cxtUg>YT%~gx#tL$|Z># z8|*$l`lkX~>a+BYuEuB6sd!MJ_#Q%*l1IVv?0s zUUU&>%2dy>B_GTC`-M|m!fYPbwRf2Neh<-}^tfe>mU^f6=Ka7uJb9r)1T}7k{@t># za$)3=vVESLnt8{$A&KLMdnTu!M&3p_Ic-%8Tt&>d-fQ%ITe%fh~WC%@KDkdJu& zuXB%%H;I={&c{rj$Ro`!ffV1nm0!G19|FPM&R;XcW2~<2cTPOv+0#2a-apz4BvO z&%=%fir+~iE-5fKaSnS0?D^}Zm{ou5uH}Ykl%;lVe!?9-P+=m0v*M z63`n)jpP)v@9o}XyH4T7I1Z;4NRUXkv~C%hek0nPIt{UUrwJQvq;y8)EAt=KCqXL~ zuP8=X@TCyj0z4VH1X#vqgkpML+^Sh?dyN-QUN*rgX+Gan9bc1%FXpHnI=?mO!V-)7 zAWOSJRF7z^`mN*h}mn{HOl1WM=v;f zgAw7us(%{FCRlaJxk*oyi z>g(x+$Z4vB^hEBp${&GZZwSXKlJ=t4q-LKPh=jQ~UgRc-&SZyoh~Qnj$9)r!OS|i% zgTbsSPuCn#8K?}HJM=;$r080pf~7UGbBGK|(~jJK@OtQg1xk^ZE-IF@4K!(QJ^2#0 zp^Fmi1&|}cGCQEtS?fsNVwGegMZKl46LgmrkDw-7%;9R|vq^I$pIPej-uxX(oZ8k+ zr3nh8_fgGiBBpwW%kctyBhbmz4BRMs5N8eu|JpTS6^1XTxOIxe|0@0AeONfUSQ&Le zEk&29SODTqoR;ODmtga z_iCIv?V!WKN>L@t&1s>bzuPJU&po^lcwleWRPcja!o|U^fg=>rx6`Bl@PPL6n;I!p zXfB+x!d_ShbLBSMM+D%y8(rUnSZt0v;HkXPbG`>+Tp^(`(GMm8 z?i0&wA+2fZM;~wW-_6D4P#)7hrW*HAxryvXLLs@Ev&~kxOy|gqfH1E*6)rRB59}?4|ey+Xi^1%Ph;O z-yUd!;d;w8&r;Vz*n+}tR0}rObHS>{4r;)o`7lN$_yz|UiF}qTv1*Elyrjo&C84kW zwZfroivScSo85-xgg2arHyomx3I*#1RS+%#2u4hdTbB>^`O_

9zMZ)&A@^kN>!4}?!xX|KTGPm$**$5 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 - - - - - - - - - diff --git a/extensions/vp9/src/main/.cproject b/extensions/vp9/src/main/.cproject deleted file mode 100644 index 8548e48eb9..0000000000 --- a/extensions/vp9/src/main/.cproject +++ /dev/null @@ -1,57 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/extensions/vp9/src/main/.project b/extensions/vp9/src/main/.project deleted file mode 100644 index 3811626cb3..0000000000 --- a/extensions/vp9/src/main/.project +++ /dev/null @@ -1,97 +0,0 @@ - - - ExoPlayerExt-VP9 - - - - - - org.eclipse.cdt.managedbuilder.core.genmakebuilder - clean,full,incremental, - - - ?children? - ?name?=outputEntries\|?children?=?name?=entry\\\\\\\|\\\|?name?=entry\\\\\\\|\\\|\|| - - - ?name? - - - - org.eclipse.cdt.make.core.append_environment - true - - - org.eclipse.cdt.make.core.buildArguments - - - - org.eclipse.cdt.make.core.buildCommand - ndk-build - - - org.eclipse.cdt.make.core.cleanBuildTarget - clean - - - org.eclipse.cdt.make.core.contents - org.eclipse.cdt.make.core.activeConfigSettings - - - org.eclipse.cdt.make.core.enableAutoBuild - false - - - org.eclipse.cdt.make.core.enableCleanBuild - true - - - org.eclipse.cdt.make.core.enableFullBuild - true - - - org.eclipse.cdt.make.core.stopOnError - true - - - org.eclipse.cdt.make.core.useDefaultBuildCmd - true - - - - - com.android.ide.eclipse.adt.ResourceManagerBuilder - - - - - com.android.ide.eclipse.adt.PreCompilerBuilder - - - - - org.eclipse.jdt.core.javabuilder - - - - - com.android.ide.eclipse.adt.ApkBuilder - - - - - org.eclipse.cdt.managedbuilder.core.ScannerConfigBuilder - full,incremental, - - - - - - com.android.ide.eclipse.adt.AndroidNature - org.eclipse.jdt.core.javanature - org.eclipse.cdt.core.cnature - org.eclipse.cdt.core.ccnature - org.eclipse.cdt.managedbuilder.core.managedBuildNature - org.eclipse.cdt.managedbuilder.core.ScannerConfigNature - - diff --git a/extensions/vp9/src/main/.settings/org.eclipse.jdt.core.prefs b/extensions/vp9/src/main/.settings/org.eclipse.jdt.core.prefs deleted file mode 100644 index d17b6724d1..0000000000 --- a/extensions/vp9/src/main/.settings/org.eclipse.jdt.core.prefs +++ /dev/null @@ -1,12 +0,0 @@ -eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled -org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate -org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7 -org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve -org.eclipse.jdt.core.compiler.compliance=1.7 -org.eclipse.jdt.core.compiler.debug.lineNumber=generate -org.eclipse.jdt.core.compiler.debug.localVariable=generate -org.eclipse.jdt.core.compiler.debug.sourceFile=generate -org.eclipse.jdt.core.compiler.problem.assertIdentifier=error -org.eclipse.jdt.core.compiler.problem.enumIdentifier=error -org.eclipse.jdt.core.compiler.source=1.7 diff --git a/extensions/vp9/src/main/AndroidManifest.xml b/extensions/vp9/src/main/AndroidManifest.xml deleted file mode 100644 index 2869352679..0000000000 --- a/extensions/vp9/src/main/AndroidManifest.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer/ext/vp9/LibvpxVideoTrackRenderer.java b/extensions/vp9/src/main/java/com/google/android/exoplayer/ext/vp9/LibvpxVideoTrackRenderer.java deleted file mode 100644 index f20f877c5a..0000000000 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer/ext/vp9/LibvpxVideoTrackRenderer.java +++ /dev/null @@ -1,493 +0,0 @@ -/* - * Copyright (C) 2014 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.exoplayer.ext.vp9; - -import com.google.android.exoplayer.CodecCounters; -import com.google.android.exoplayer.ExoPlaybackException; -import com.google.android.exoplayer.ExoPlayer; -import com.google.android.exoplayer.MediaFormat; -import com.google.android.exoplayer.MediaFormatHolder; -import com.google.android.exoplayer.SampleSource; -import com.google.android.exoplayer.SampleSourceTrackRenderer; -import com.google.android.exoplayer.TrackRenderer; -import com.google.android.exoplayer.ext.vp9.VpxDecoderWrapper.VpxInputBuffer; -import com.google.android.exoplayer.util.MimeTypes; - -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.os.Handler; -import android.os.SystemClock; -import android.view.Surface; - -/** - * Decodes and renders video using the native VP9 decoder. - */ -public final class LibvpxVideoTrackRenderer extends SampleSourceTrackRenderer { - - /** - * Interface definition for a callback to be notified of {@link LibvpxVideoTrackRenderer} events. - */ - public interface EventListener { - - /** - * Invoked to report the number of frames dropped by the renderer. Dropped frames are reported - * whenever the renderer is stopped having dropped frames, and optionally, whenever the count - * reaches a specified threshold whilst the renderer is started. - * - * @param count The number of dropped frames. - * @param elapsed The duration in milliseconds over which the frames were dropped. This - * duration is timed from when the renderer was started or from when dropped frames were - * last reported (whichever was more recent), and not from when the first of the reported - * drops occurred. - */ - void onDroppedFrames(int count, long elapsed); - - /** - * Invoked each time there's a change in the size of the video being rendered. - * - * @param width The video width in pixels. - * @param height The video height in pixels. - */ - void onVideoSizeChanged(int width, int height); - - /** - * Invoked when a frame is rendered to a surface for the first time following that surface - * having been set as the target for the renderer. - * - * @param surface The surface to which a first frame has been rendered. - */ - void onDrawnToSurface(Surface surface); - - /** - * Invoked when one of the following happens: libvpx initialization failure, decoder error, - * renderer error. - * - * @param e The corresponding exception. - */ - void onDecoderError(VpxDecoderException e); - - } - - /** - * The type of a message that can be passed to an instance of this class via - * {@link ExoPlayer#sendMessage} or {@link ExoPlayer#blockingSendMessage}. The message object - * should be the target {@link Surface}, or null. - */ - public static final int MSG_SET_SURFACE = 1; - /** - * The type of a message that can be passed to an instance of this class via - * {@link ExoPlayer#sendMessage} or {@link ExoPlayer#blockingSendMessage}. The message object - * should be the target {@link VpxOutputBufferRenderer}, or null. - */ - public static final int MSG_SET_OUTPUT_BUFFER_RENDERER = 2; - - public final CodecCounters codecCounters = new CodecCounters(); - - private final boolean scaleToFit; - private final Handler eventHandler; - private final EventListener eventListener; - private final int maxDroppedFrameCountToNotify; - private final MediaFormatHolder formatHolder; - - private MediaFormat format; - private VpxDecoderWrapper decoder; - private VpxInputBuffer inputBuffer; - private VpxOutputBuffer outputBuffer; - - private Bitmap bitmap; - private boolean drawnToSurface; - private boolean renderedFirstFrame; - private Surface surface; - private VpxOutputBufferRenderer outputBufferRenderer; - private int outputMode; - - private boolean inputStreamEnded; - private boolean outputStreamEnded; - private boolean sourceIsReady; - private int previousWidth; - private int previousHeight; - - private int droppedFrameCount; - private long droppedFrameAccumulationStartTimeMs; - - /** - * @param source The upstream source from which the renderer obtains samples. - * @param scaleToFit Boolean that indicates if video frames should be scaled to fit when - * rendering. - */ - public LibvpxVideoTrackRenderer(SampleSource source, boolean scaleToFit) { - this(source, scaleToFit, null, null, 0); - } - - /** - * @param source The upstream source from which the renderer obtains samples. - * @param scaleToFit Boolean that indicates if video frames should be scaled to fit when - * rendering. - * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be - * null if delivery of events is not required. - * @param eventListener A listener of events. May be null if delivery of events is not required. - * @param maxDroppedFrameCountToNotify The maximum number of frames that can be dropped between - * invocations of {@link EventListener#onDroppedFrames(int, long)}. - */ - public LibvpxVideoTrackRenderer(SampleSource source, boolean scaleToFit, - Handler eventHandler, EventListener eventListener, int maxDroppedFrameCountToNotify) { - super(source); - this.scaleToFit = scaleToFit; - this.eventHandler = eventHandler; - this.eventListener = eventListener; - this.maxDroppedFrameCountToNotify = maxDroppedFrameCountToNotify; - previousWidth = -1; - previousHeight = -1; - formatHolder = new MediaFormatHolder(); - outputMode = VpxDecoder.OUTPUT_MODE_UNKNOWN; - } - - /** - * Returns whether the underlying libvpx library is available. - */ - public static boolean isLibvpxAvailable() { - return VpxDecoder.isLibvpxAvailable(); - } - - /** - * Returns the version of the underlying libvpx library if available, otherwise {@code null}. - */ - public static String getLibvpxVersion() { - return isLibvpxAvailable() ? VpxDecoder.getLibvpxVersion() : null; - } - - @Override - protected boolean handlesTrack(MediaFormat mediaFormat) { - return MimeTypes.VIDEO_VP9.equalsIgnoreCase(mediaFormat.mimeType); - } - - @Override - protected void doSomeWork(long positionUs, long elapsedRealtimeUs, boolean sourceIsReady) - throws ExoPlaybackException { - if (outputStreamEnded) { - return; - } - - // Try and read a format if we don't have one already. - if (format == null && !readFormat(positionUs)) { - // We can't make progress without one. - return; - } - - // If we don't have a decoder yet, we need to instantiate one. - if (decoder == null) { - decoder = new VpxDecoderWrapper(outputMode); - decoder.start(); - codecCounters.codecInitCount++; - } - - // Rendering loop. - try { - processOutputBuffer(positionUs, elapsedRealtimeUs); - while (feedInputBuffer(positionUs)) {} - } catch (VpxDecoderException e) { - notifyDecoderError(e); - throw new ExoPlaybackException(e); - } - codecCounters.ensureUpdated(); - } - - private void processOutputBuffer(long positionUs, long elapsedRealtimeUs) - throws VpxDecoderException { - if (outputStreamEnded) { - return; - } - - if (outputBuffer == null) { - outputBuffer = decoder.dequeueOutputBuffer(); - if (outputBuffer == null) { - return; - } - } - - if (outputBuffer.flags == VpxDecoderWrapper.FLAG_END_OF_STREAM) { - outputStreamEnded = true; - decoder.releaseOutputBuffer(outputBuffer); - outputBuffer = null; - return; - } - - long elapsedSinceStartOfLoop = SystemClock.elapsedRealtime() * 1000 - elapsedRealtimeUs; - long timeToRenderUs = outputBuffer.timestampUs - positionUs - elapsedSinceStartOfLoop; - - if (timeToRenderUs < -30000 || outputBuffer.timestampUs < positionUs) { - // Drop frame if we are too late. - codecCounters.droppedOutputBufferCount++; - droppedFrameCount++; - if (droppedFrameCount == maxDroppedFrameCountToNotify) { - notifyAndResetDroppedFrameCount(); - } - decoder.releaseOutputBuffer(outputBuffer); - outputBuffer = null; - return; - } - - // If we have not rendered any frame so far (either initially or immediately following a seek), - // render one frame irrespective of the state. - if (!renderedFirstFrame) { - renderBuffer(); - renderedFirstFrame = true; - return; - } - - // Do nothing if we are not playing or if we are too early to render the next frame. - if (getState() != TrackRenderer.STATE_STARTED || timeToRenderUs > 30000) { - return; - } - - if (timeToRenderUs > 11000) { - try { - // Subtracting 10000 rather than 11000 ensures that the sleep time will be at least 1ms. - Thread.sleep((timeToRenderUs - 10000) / 1000); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - } - renderBuffer(); - } - - private void renderBuffer() { - codecCounters.renderedOutputBufferCount++; - notifyIfVideoSizeChanged(outputBuffer); - if (outputBuffer.mode == VpxDecoder.OUTPUT_MODE_RGB && surface != null) { - renderRgbFrame(outputBuffer, scaleToFit); - if (!drawnToSurface) { - drawnToSurface = true; - notifyDrawnToSurface(surface); - } - outputBuffer.release(); - } else if (outputBuffer.mode == VpxDecoder.OUTPUT_MODE_YUV && outputBufferRenderer != null) { - // The renderer will release the buffer. - outputBufferRenderer.setOutputBuffer(outputBuffer); - } else { - outputBuffer.release(); - } - outputBuffer = null; - } - - private void renderRgbFrame(VpxOutputBuffer outputBuffer, boolean scale) { - if (bitmap == null || bitmap.getWidth() != outputBuffer.width - || bitmap.getHeight() != outputBuffer.height) { - bitmap = Bitmap.createBitmap(outputBuffer.width, outputBuffer.height, Bitmap.Config.RGB_565); - } - bitmap.copyPixelsFromBuffer(outputBuffer.data); - Canvas canvas = surface.lockCanvas(null); - if (scale) { - canvas.scale(((float) canvas.getWidth()) / outputBuffer.width, - ((float) canvas.getHeight()) / outputBuffer.height); - } - canvas.drawBitmap(bitmap, 0, 0, null); - surface.unlockCanvasAndPost(canvas); - } - - private boolean feedInputBuffer(long positionUs) throws VpxDecoderException { - if (inputStreamEnded) { - return false; - } - - if (inputBuffer == null) { - inputBuffer = decoder.dequeueInputBuffer(); - if (inputBuffer == null) { - return false; - } - } - - int result = readSource(positionUs, formatHolder, inputBuffer.sampleHolder); - if (result == SampleSource.NOTHING_READ) { - return false; - } - if (result == SampleSource.FORMAT_READ) { - format = formatHolder.format; - return true; - } - if (result == SampleSource.END_OF_STREAM) { - inputBuffer.flags = VpxDecoderWrapper.FLAG_END_OF_STREAM; - decoder.queueInputBuffer(inputBuffer); - inputBuffer = null; - inputStreamEnded = true; - return false; - } - - inputBuffer.width = format.width; - inputBuffer.height = format.height; - decoder.queueInputBuffer(inputBuffer); - inputBuffer = null; - return true; - } - - private void flushDecoder() { - inputBuffer = null; - if (outputBuffer != null) { - decoder.releaseOutputBuffer(outputBuffer); - outputBuffer = null; - } - decoder.flush(); - } - - @Override - protected boolean isEnded() { - return outputStreamEnded; - } - - @Override - protected boolean isReady() { - return format != null && (sourceIsReady || outputBuffer != null) && renderedFirstFrame; - } - - @Override - protected void onDiscontinuity(long positionUs) { - sourceIsReady = false; - inputStreamEnded = false; - outputStreamEnded = false; - renderedFirstFrame = false; - if (decoder != null) { - flushDecoder(); - } - } - - @Override - protected void onStarted() { - droppedFrameCount = 0; - droppedFrameAccumulationStartTimeMs = SystemClock.elapsedRealtime(); - } - - @Override - protected void onStopped() { - notifyAndResetDroppedFrameCount(); - } - - @Override - protected void onDisabled() throws ExoPlaybackException { - inputBuffer = null; - outputBuffer = null; - format = null; - try { - if (decoder != null) { - decoder.release(); - decoder = null; - codecCounters.codecReleaseCount++; - } - } finally { - super.onDisabled(); - } - } - - private boolean readFormat(long positionUs) { - int result = readSource(positionUs, formatHolder, null); - if (result == SampleSource.FORMAT_READ) { - format = formatHolder.format; - return true; - } - return false; - } - - @Override - public void handleMessage(int messageType, Object message) throws ExoPlaybackException { - if (messageType == MSG_SET_SURFACE) { - setSurface((Surface) message); - } else if (messageType == MSG_SET_OUTPUT_BUFFER_RENDERER) { - setOutputBufferRenderer((VpxOutputBufferRenderer) message); - } else { - super.handleMessage(messageType, message); - } - } - - private void setSurface(Surface surface) { - if (this.surface == surface) { - return; - } - this.surface = surface; - outputBufferRenderer = null; - outputMode = (surface != null) ? VpxDecoder.OUTPUT_MODE_RGB : VpxDecoder.OUTPUT_MODE_UNKNOWN; - if (decoder != null) { - decoder.setOutputMode(outputMode); - } - drawnToSurface = false; - } - - private void setOutputBufferRenderer(VpxOutputBufferRenderer outputBufferRenderer) { - if (this.outputBufferRenderer == outputBufferRenderer) { - return; - } - this.outputBufferRenderer = outputBufferRenderer; - surface = null; - outputMode = (outputBufferRenderer != null) - ? VpxDecoder.OUTPUT_MODE_YUV : VpxDecoder.OUTPUT_MODE_UNKNOWN; - if (decoder != null) { - decoder.setOutputMode(outputMode); - } - } - - private void notifyIfVideoSizeChanged(final VpxOutputBuffer outputBuffer) { - if (previousWidth == -1 || previousHeight == -1 - || previousWidth != outputBuffer.width || previousHeight != outputBuffer.height) { - previousWidth = outputBuffer.width; - previousHeight = outputBuffer.height; - if (eventHandler != null && eventListener != null) { - eventHandler.post(new Runnable() { - @Override - public void run() { - eventListener.onVideoSizeChanged(outputBuffer.width, outputBuffer.height); - } - }); - } - } - } - - private void notifyAndResetDroppedFrameCount() { - if (eventHandler != null && eventListener != null && droppedFrameCount > 0) { - long now = SystemClock.elapsedRealtime(); - final int countToNotify = droppedFrameCount; - final long elapsedToNotify = now - droppedFrameAccumulationStartTimeMs; - droppedFrameCount = 0; - droppedFrameAccumulationStartTimeMs = now; - eventHandler.post(new Runnable() { - @Override - public void run() { - eventListener.onDroppedFrames(countToNotify, elapsedToNotify); - } - }); - } - } - - private void notifyDrawnToSurface(final Surface surface) { - if (eventHandler != null && eventListener != null) { - eventHandler.post(new Runnable() { - @Override - public void run() { - eventListener.onDrawnToSurface(surface); - } - }); - } - } - - private void notifyDecoderError(final VpxDecoderException e) { - if (eventHandler != null && eventListener != null) { - eventHandler.post(new Runnable() { - @Override - public void run() { - eventListener.onDecoderError(e); - } - }); - } - } - -} diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer/ext/vp9/VpxDecoder.java b/extensions/vp9/src/main/java/com/google/android/exoplayer/ext/vp9/VpxDecoder.java deleted file mode 100644 index 3e54003633..0000000000 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer/ext/vp9/VpxDecoder.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright (C) 2014 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.exoplayer.ext.vp9; - -import java.nio.ByteBuffer; - -/** - * JNI Wrapper for the libvpx VP9 decoder. - */ -/* package */ class VpxDecoder { - - private static final boolean IS_AVAILABLE; - static { - boolean isAvailable; - try { - System.loadLibrary("vpx"); - System.loadLibrary("vpxJNI"); - isAvailable = true; - } catch (UnsatisfiedLinkError exception) { - isAvailable = false; - } - IS_AVAILABLE = isAvailable; - } - - public static final int OUTPUT_MODE_UNKNOWN = -1; - public static final int OUTPUT_MODE_YUV = 0; - public static final int OUTPUT_MODE_RGB = 1; - - private final long vpxDecContext; - - /** - * Creates the VP9 Decoder. - * - * @throws VpxDecoderException if the decoder fails to initialize. - */ - public VpxDecoder() throws VpxDecoderException { - vpxDecContext = vpxInit(); - if (vpxDecContext == 0) { - throw new VpxDecoderException("Failed to initialize decoder"); - } - } - - /** - * Decodes a vp9 encoded frame and converts it to RGB565. - * - * @param encoded The encoded buffer. - * @param size Size of the encoded buffer. - * @param outputBuffer The buffer into which the decoded frame should be written. - * @return 0 on success with a frame to render. 1 on success without a frame to render. - * @throws VpxDecoderException on decode failure. - */ - public int decode(ByteBuffer encoded, int size, VpxOutputBuffer outputBuffer) - throws VpxDecoderException { - if (vpxDecode(vpxDecContext, encoded, size) != 0) { - throw new VpxDecoderException("Decode error: " + vpxGetErrorMessage(vpxDecContext)); - } - return vpxGetFrame(vpxDecContext, outputBuffer); - } - - /** - * Destroys the decoder. - */ - public void close() { - vpxClose(vpxDecContext); - } - - /** - * Returns whether the underlying libvpx library is available. - */ - public static boolean isLibvpxAvailable() { - return IS_AVAILABLE; - } - - /** - * Returns the version string of the underlying libvpx decoder. - */ - public static native String getLibvpxVersion(); - - private native long vpxInit(); - private native long vpxClose(long context); - private native long vpxDecode(long context, ByteBuffer encoded, int length); - private native int vpxGetFrame(long context, VpxOutputBuffer outputBuffer); - private native String vpxGetErrorMessage(long context); - -} diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer/ext/vp9/VpxDecoderException.java b/extensions/vp9/src/main/java/com/google/android/exoplayer/ext/vp9/VpxDecoderException.java deleted file mode 100644 index 1afa01a6c7..0000000000 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer/ext/vp9/VpxDecoderException.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright (C) 2014 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.exoplayer.ext.vp9; - -/** - * Thrown when a libvpx decoder error occurs. - */ -public class VpxDecoderException extends Exception { - - public VpxDecoderException(String message) { - super(message); - } - -} diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer/ext/vp9/VpxDecoderWrapper.java b/extensions/vp9/src/main/java/com/google/android/exoplayer/ext/vp9/VpxDecoderWrapper.java deleted file mode 100644 index 5dff64a846..0000000000 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer/ext/vp9/VpxDecoderWrapper.java +++ /dev/null @@ -1,264 +0,0 @@ -/* - * Copyright (C) 2014 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.exoplayer.ext.vp9; - -import com.google.android.exoplayer.SampleHolder; -import com.google.android.exoplayer.util.Assertions; - -import java.nio.ByteBuffer; -import java.util.LinkedList; - -/** - * Wraps {@link VpxDecoder}, exposing a higher level decoder interface. - */ -/* package */ final class VpxDecoderWrapper extends Thread { - - public static final int FLAG_END_OF_STREAM = 1; - - private static final int INPUT_BUFFER_SIZE = 768 * 1024; // Value based on cs/SoftVpx.cpp. - /** - * The number of input buffers and the number of output buffers. The track renderer may limit the - * minimum possible value due to requiring multiple output buffers to be dequeued at a time for it - * to make progress. - */ - private static final int NUM_BUFFERS = 16; - - private final Object lock; - - private final LinkedList queuedInputBuffers; - private final LinkedList queuedOutputBuffers; - private final VpxInputBuffer[] availableInputBuffers; - private final VpxOutputBuffer[] availableOutputBuffers; - private int availableInputBufferCount; - private int availableOutputBufferCount; - private VpxInputBuffer dequeuedInputBuffer; - - private boolean flushDecodedOutputBuffer; - private boolean released; - private int outputMode; - - private VpxDecoderException decoderException; - - /** - * @param outputMode One of OUTPUT_MODE_* constants from {@link VpxDecoderWrapper} - * depending on the desired output mode. - */ - public VpxDecoderWrapper(int outputMode) { - lock = new Object(); - this.outputMode = outputMode; - queuedInputBuffers = new LinkedList<>(); - queuedOutputBuffers = new LinkedList<>(); - availableInputBuffers = new VpxInputBuffer[NUM_BUFFERS]; - availableOutputBuffers = new VpxOutputBuffer[NUM_BUFFERS]; - availableInputBufferCount = NUM_BUFFERS; - availableOutputBufferCount = NUM_BUFFERS; - for (int i = 0; i < NUM_BUFFERS; i++) { - availableInputBuffers[i] = new VpxInputBuffer(); - availableOutputBuffers[i] = new VpxOutputBuffer(this); - } - } - - public void setOutputMode(int outputMode) { - this.outputMode = outputMode; - } - - public VpxInputBuffer dequeueInputBuffer() throws VpxDecoderException { - synchronized (lock) { - maybeThrowDecoderError(); - Assertions.checkState(dequeuedInputBuffer == null); - if (availableInputBufferCount == 0) { - return null; - } - VpxInputBuffer inputBuffer = availableInputBuffers[--availableInputBufferCount]; - inputBuffer.flags = 0; - inputBuffer.sampleHolder.clearData(); - dequeuedInputBuffer = inputBuffer; - return inputBuffer; - } - } - - public void queueInputBuffer(VpxInputBuffer inputBuffer) throws VpxDecoderException { - synchronized (lock) { - maybeThrowDecoderError(); - Assertions.checkArgument(inputBuffer == dequeuedInputBuffer); - queuedInputBuffers.addLast(inputBuffer); - maybeNotifyDecodeLoop(); - dequeuedInputBuffer = null; - } - } - - public VpxOutputBuffer dequeueOutputBuffer() throws VpxDecoderException { - synchronized (lock) { - maybeThrowDecoderError(); - if (queuedOutputBuffers.isEmpty()) { - return null; - } - return queuedOutputBuffers.removeFirst(); - } - } - - public void releaseOutputBuffer(VpxOutputBuffer outputBuffer) { - synchronized (lock) { - availableOutputBuffers[availableOutputBufferCount++] = outputBuffer; - maybeNotifyDecodeLoop(); - } - } - - /** - * Flushes input/output buffers that have not been dequeued yet and returns ownership of any - * dequeued input buffer to the decoder. Flushes any pending output currently in the decoder. The - * caller is still responsible for releasing any dequeued output buffers. - */ - public void flush() { - synchronized (lock) { - flushDecodedOutputBuffer = true; - if (dequeuedInputBuffer != null) { - availableInputBuffers[availableInputBufferCount++] = dequeuedInputBuffer; - dequeuedInputBuffer = null; - } - while (!queuedInputBuffers.isEmpty()) { - availableInputBuffers[availableInputBufferCount++] = queuedInputBuffers.removeFirst(); - } - while (!queuedOutputBuffers.isEmpty()) { - availableOutputBuffers[availableOutputBufferCount++] = queuedOutputBuffers.removeFirst(); - } - } - } - - public void release() { - synchronized (lock) { - released = true; - lock.notify(); - } - try { - join(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - } - - private void maybeThrowDecoderError() throws VpxDecoderException { - if (decoderException != null) { - throw decoderException; - } - } - - /** - * Notifies the decode loop if there exists a queued input buffer and an available output buffer - * to decode into. - *

- * Should only be called whilst synchronized on the lock object. - */ - private void maybeNotifyDecodeLoop() { - if (canDecodeBuffer()) { - lock.notify(); - } - } - - @Override - public void run() { - VpxDecoder decoder = null; - try { - decoder = new VpxDecoder(); - while (decodeBuffer(decoder)) { - // Do nothing. - } - } catch (VpxDecoderException e) { - synchronized (lock) { - decoderException = e; - } - } catch (InterruptedException e) { - // Shouldn't ever happen. - } finally { - if (decoder != null) { - decoder.close(); - } - } - } - - private boolean decodeBuffer(VpxDecoder decoder) throws InterruptedException, - VpxDecoderException { - VpxInputBuffer inputBuffer; - VpxOutputBuffer outputBuffer; - - // Wait until we have an input buffer to decode, and an output buffer to decode into. - synchronized (lock) { - while (!released && !canDecodeBuffer()) { - lock.wait(); - } - if (released) { - return false; - } - inputBuffer = queuedInputBuffers.removeFirst(); - outputBuffer = availableOutputBuffers[--availableOutputBufferCount]; - flushDecodedOutputBuffer = false; - } - - // Decode. - int decodeResult = -1; - if (inputBuffer.flags == FLAG_END_OF_STREAM) { - outputBuffer.flags = FLAG_END_OF_STREAM; - } else { - SampleHolder sampleHolder = inputBuffer.sampleHolder; - outputBuffer.timestampUs = sampleHolder.timeUs; - outputBuffer.flags = 0; - outputBuffer.mode = outputMode; - sampleHolder.data.position(sampleHolder.data.position() - sampleHolder.size); - decodeResult = decoder.decode(sampleHolder.data, sampleHolder.size, outputBuffer); - } - - synchronized (lock) { - if (flushDecodedOutputBuffer - || inputBuffer.sampleHolder.isDecodeOnly() - || decodeResult == 1) { - // In the following cases, we make the output buffer available again rather than queuing it - // to be consumed: - // 1) A flush occured whilst we were decoding. - // 2) The input sample has decodeOnly flag set. - // 3) The decode succeeded, but we did not get any frame back for rendering (happens in case - // of an unpacked altref frame). - availableOutputBuffers[availableOutputBufferCount++] = outputBuffer; - } else { - // Queue the decoded output buffer to be consumed. - queuedOutputBuffers.addLast(outputBuffer); - } - // Make the input buffer available again. - availableInputBuffers[availableInputBufferCount++] = inputBuffer; - } - - return true; - } - - private boolean canDecodeBuffer() { - return !queuedInputBuffers.isEmpty() && availableOutputBufferCount > 0; - } - - /* package */ static final class VpxInputBuffer { - - public final SampleHolder sampleHolder; - - public int width; - public int height; - public int flags; - - public VpxInputBuffer() { - sampleHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_DIRECT); - sampleHolder.data = ByteBuffer.allocateDirect(INPUT_BUFFER_SIZE); - } - - } - -} diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer/ext/vp9/VpxOutputBuffer.java b/extensions/vp9/src/main/java/com/google/android/exoplayer/ext/vp9/VpxOutputBuffer.java deleted file mode 100644 index 1a2b40a069..0000000000 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer/ext/vp9/VpxOutputBuffer.java +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Copyright (C) 2015 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.exoplayer.ext.vp9; - -import java.nio.ByteBuffer; - -/** - * OutputBuffer for storing the video frame. - */ -public final class VpxOutputBuffer { - - public static final int COLORSPACE_UNKNOWN = 0; - public static final int COLORSPACE_BT601 = 1; - public static final int COLORSPACE_BT709 = 2; - - private final VpxDecoderWrapper decoder; - - /* package */ VpxOutputBuffer(VpxDecoderWrapper decoder) { - this.decoder = decoder; - } - - /** - * RGB buffer for RGB mode. - */ - public ByteBuffer data; - public long timestampUs; - public int width; - public int height; - public int flags; - /** - * YUV planes for YUV mode. - */ - public ByteBuffer[] yuvPlanes; - public int[] yuvStrides; - public int mode; - public int colorspace; - - /** - * Releases the buffer back to the decoder, allowing it to be reused. - */ - public void release() { - decoder.releaseOutputBuffer(this); - } - - /** - * Resizes the buffer based on the given dimensions. Called via JNI after decoding completes. - */ - /* package */ void initForRgbFrame(int width, int height) { - this.width = width; - this.height = height; - int minimumRgbSize = width * height * 2; - if (data == null || data.capacity() < minimumRgbSize) { - data = ByteBuffer.allocateDirect(minimumRgbSize); - yuvPlanes = null; - } - data.position(0); - data.limit(minimumRgbSize); - } - - /** - * Resizes the buffer based on the given stride. Called via JNI after decoding completes. - */ - /* package */ void initForYuvFrame(int width, int height, int yStride, int uvStride, - int colorspace) { - this.width = width; - this.height = height; - this.colorspace = colorspace; - int yLength = yStride * height; - int uvLength = uvStride * ((height + 1) / 2); - int minimumYuvSize = yLength + (uvLength * 2); - if (data == null || data.capacity() < minimumYuvSize) { - data = ByteBuffer.allocateDirect(minimumYuvSize); - } - data.limit(minimumYuvSize); - if (yuvPlanes == null) { - yuvPlanes = new ByteBuffer[3]; - } - // Rewrapping has to be done on every frame since the stride might have changed. - data.position(0); - yuvPlanes[0] = data.slice(); - yuvPlanes[0].limit(yLength); - data.position(yLength); - yuvPlanes[1] = data.slice(); - yuvPlanes[1].limit(uvLength); - data.position(yLength + uvLength); - yuvPlanes[2] = data.slice(); - yuvPlanes[2].limit(uvLength); - if (yuvStrides == null) { - yuvStrides = new int[3]; - } - yuvStrides[0] = yStride; - yuvStrides[1] = uvStride; - yuvStrides[2] = uvStride; - } - -} diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer/ext/vp9/VpxOutputBufferRenderer.java b/extensions/vp9/src/main/java/com/google/android/exoplayer/ext/vp9/VpxOutputBufferRenderer.java deleted file mode 100644 index 0ab1fc475e..0000000000 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer/ext/vp9/VpxOutputBufferRenderer.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright (C) 2015 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.exoplayer.ext.vp9; - -/** - * Renders the {@link VpxOutputBuffer}. - */ -public interface VpxOutputBufferRenderer { - - /** - * Sets the output buffer to be rendered. The renderer is responsible for releasing the buffer. - */ - void setOutputBuffer(VpxOutputBuffer outputBuffer); - -} diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer/ext/vp9/VpxRenderer.java b/extensions/vp9/src/main/java/com/google/android/exoplayer/ext/vp9/VpxRenderer.java deleted file mode 100644 index 508e77deac..0000000000 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer/ext/vp9/VpxRenderer.java +++ /dev/null @@ -1,244 +0,0 @@ -/* - * Copyright (C) 2014 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.exoplayer.ext.vp9; - -import android.opengl.GLES20; -import android.opengl.GLSurfaceView; - -import java.nio.ByteBuffer; -import java.nio.ByteOrder; -import java.nio.FloatBuffer; -import java.util.concurrent.atomic.AtomicReference; - -import javax.microedition.khronos.egl.EGLConfig; -import javax.microedition.khronos.opengles.GL10; - -/** - * GLSurfaceView.Renderer implementation that can render YUV Frames returned by libvpx after - * decoding. It does the YUV to RGB color conversion in the Fragment Shader. - */ -/* package */ class VpxRenderer implements GLSurfaceView.Renderer { - - private static final float[] kColorConversion601 = { - 1.164f, 1.164f, 1.164f, - 0.0f, -0.392f, 2.017f, - 1.596f, -0.813f, 0.0f, - }; - - private static final float[] kColorConversion709 = { - 1.164f, 1.164f, 1.164f, - 0.0f, -0.213f, 2.112f, - 1.793f, -0.533f, 0.0f, - }; - - private static final String VERTEX_SHADER = - "varying vec2 interp_tc;\n" - + "attribute vec4 in_pos;\n" - + "attribute vec2 in_tc;\n" - + "void main() {\n" - + " gl_Position = in_pos;\n" - + " interp_tc = in_tc;\n" - + "}\n"; - private static final String[] TEXTURE_UNIFORMS = {"y_tex", "u_tex", "v_tex"}; - private static final String FRAGMENT_SHADER = - "precision mediump float;\n" - + "varying vec2 interp_tc;\n" - + "uniform sampler2D y_tex;\n" - + "uniform sampler2D u_tex;\n" - + "uniform sampler2D v_tex;\n" - + "uniform mat3 mColorConversion;\n" - + "void main() {\n" - + " vec3 yuv;" - + " yuv.x = texture2D(y_tex, interp_tc).r - 0.0625;\n" - + " yuv.y = texture2D(u_tex, interp_tc).r - 0.5;\n" - + " yuv.z = texture2D(v_tex, interp_tc).r - 0.5;\n" - + " gl_FragColor = vec4(mColorConversion * yuv, 1.0);" - + "}\n"; - private static final FloatBuffer TEXTURE_VERTICES = nativeFloatBuffer( - -1.0f, 1.0f, - -1.0f, -1.0f, - 1.0f, 1.0f, - 1.0f, -1.0f); - private final int[] yuvTextures = new int[3]; - private final AtomicReference pendingOutputBufferReference; - - private int program; - private int texLocation; - private int colorMatrixLocation; - private FloatBuffer textureCoords; - private int previousWidth; - private int previousStride; - - private VpxOutputBuffer renderedOutputBuffer; // Accessed only from the GL thread. - - public VpxRenderer() { - previousWidth = -1; - previousStride = -1; - pendingOutputBufferReference = new AtomicReference<>(); - } - - /** - * Set a frame to be rendered. This should be followed by a call to - * VpxVideoSurfaceView.requestRender() to actually render the frame. - * - * @param outputBuffer OutputBuffer containing the YUV Frame to be rendered - */ - public void setFrame(VpxOutputBuffer outputBuffer) { - VpxOutputBuffer oldPendingOutputBuffer = pendingOutputBufferReference.getAndSet(outputBuffer); - if (oldPendingOutputBuffer != null) { - // The old pending output buffer will never be used for rendering, so release it now. - oldPendingOutputBuffer.release(); - } - } - - @Override - public void onSurfaceCreated(GL10 unused, EGLConfig config) { - // Create the GL program. - program = GLES20.glCreateProgram(); - - // Add the vertex and fragment shaders. - addShader(GLES20.GL_VERTEX_SHADER, VERTEX_SHADER, program); - addShader(GLES20.GL_FRAGMENT_SHADER, FRAGMENT_SHADER, program); - - // Link the GL program. - GLES20.glLinkProgram(program); - int[] result = new int[] { - GLES20.GL_FALSE - }; - result[0] = GLES20.GL_FALSE; - GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, result, 0); - abortUnless(result[0] == GLES20.GL_TRUE, GLES20.glGetProgramInfoLog(program)); - GLES20.glUseProgram(program); - int posLocation = GLES20.glGetAttribLocation(program, "in_pos"); - GLES20.glEnableVertexAttribArray(posLocation); - GLES20.glVertexAttribPointer( - posLocation, 2, GLES20.GL_FLOAT, false, 0, TEXTURE_VERTICES); - texLocation = GLES20.glGetAttribLocation(program, "in_tc"); - GLES20.glEnableVertexAttribArray(texLocation); - checkNoGLES2Error(); - colorMatrixLocation = GLES20.glGetUniformLocation(program, "mColorConversion"); - checkNoGLES2Error(); - setupTextures(); - checkNoGLES2Error(); - } - - @Override - public void onSurfaceChanged(GL10 unused, int width, int height) { - GLES20.glViewport(0, 0, width, height); - } - - @Override - public void onDrawFrame(GL10 unused) { - VpxOutputBuffer pendingOutputBuffer = pendingOutputBufferReference.getAndSet(null); - if (pendingOutputBuffer == null && renderedOutputBuffer == null) { - // There is no output buffer to render at the moment. - return; - } - if (pendingOutputBuffer != null) { - if (renderedOutputBuffer != null) { - renderedOutputBuffer.release(); - } - renderedOutputBuffer = pendingOutputBuffer; - } - VpxOutputBuffer outputBuffer = renderedOutputBuffer; - // Set color matrix. Assume BT709 if the color space is unknown. - float[] colorConversion = outputBuffer.colorspace == VpxOutputBuffer.COLORSPACE_BT601 - ? kColorConversion601 : kColorConversion709; - GLES20.glUniformMatrix3fv(colorMatrixLocation, 1, false, colorConversion, 0); - - for (int i = 0; i < 3; i++) { - int h = (i == 0) ? outputBuffer.height : (outputBuffer.height + 1) / 2; - GLES20.glActiveTexture(GLES20.GL_TEXTURE0 + i); - GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, yuvTextures[i]); - GLES20.glPixelStorei(GLES20.GL_UNPACK_ALIGNMENT, 1); - GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_LUMINANCE, - outputBuffer.yuvStrides[i], h, 0, GLES20.GL_LUMINANCE, GLES20.GL_UNSIGNED_BYTE, - outputBuffer.yuvPlanes[i]); - } - // Set cropping of stride if either width or stride has changed. - if (previousWidth != outputBuffer.width || previousStride != outputBuffer.yuvStrides[0]) { - float crop = (float) outputBuffer.width / outputBuffer.yuvStrides[0]; - textureCoords = nativeFloatBuffer( - 0.0f, 0.0f, - 0.0f, 1.0f, - crop, 0.0f, - crop, 1.0f); - GLES20.glVertexAttribPointer( - texLocation, 2, GLES20.GL_FLOAT, false, 0, textureCoords); - previousWidth = outputBuffer.width; - previousStride = outputBuffer.yuvStrides[0]; - } - GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); - GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); - checkNoGLES2Error(); - } - - private void addShader(int type, String source, int program) { - int[] result = new int[] { - GLES20.GL_FALSE - }; - int shader = GLES20.glCreateShader(type); - GLES20.glShaderSource(shader, source); - GLES20.glCompileShader(shader); - GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, result, 0); - abortUnless(result[0] == GLES20.GL_TRUE, - GLES20.glGetShaderInfoLog(shader) + ", source: " + source); - GLES20.glAttachShader(program, shader); - GLES20.glDeleteShader(shader); - - checkNoGLES2Error(); - } - - private void setupTextures() { - GLES20.glGenTextures(3, yuvTextures, 0); - for (int i = 0; i < 3; i++) { - GLES20.glUniform1i(GLES20.glGetUniformLocation(program, TEXTURE_UNIFORMS[i]), i); - GLES20.glActiveTexture(GLES20.GL_TEXTURE0 + i); - GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, yuvTextures[i]); - GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, - GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR); - GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, - GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); - GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, - GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE); - GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, - GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE); - } - checkNoGLES2Error(); - } - - private void abortUnless(boolean condition, String msg) { - if (!condition) { - throw new RuntimeException(msg); - } - } - - private void checkNoGLES2Error() { - int error = GLES20.glGetError(); - if (error != GLES20.GL_NO_ERROR) { - throw new RuntimeException("GLES20 error: " + error); - } - } - - private static FloatBuffer nativeFloatBuffer(float... array) { - FloatBuffer buffer = ByteBuffer.allocateDirect(array.length * 4).order( - ByteOrder.nativeOrder()).asFloatBuffer(); - buffer.put(array); - buffer.flip(); - return buffer; - } - -} diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer/ext/vp9/VpxVideoSurfaceView.java b/extensions/vp9/src/main/java/com/google/android/exoplayer/ext/vp9/VpxVideoSurfaceView.java deleted file mode 100644 index 47d0770abe..0000000000 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer/ext/vp9/VpxVideoSurfaceView.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright (C) 2014 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.exoplayer.ext.vp9; - -import android.annotation.TargetApi; -import android.content.Context; -import android.opengl.GLSurfaceView; -import android.util.AttributeSet; - -/** - * A GLSurfaceView extension that scales itself to the given aspect ratio. - */ -@TargetApi(11) -public class VpxVideoSurfaceView extends GLSurfaceView implements VpxOutputBufferRenderer { - - private final VpxRenderer renderer; - - public VpxVideoSurfaceView(Context context) { - this(context, null); - } - - public VpxVideoSurfaceView(Context context, AttributeSet attrs) { - super(context, attrs); - renderer = new VpxRenderer(); - setPreserveEGLContextOnPause(true); - setEGLContextClientVersion(2); - setRenderer(renderer); - setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY); - } - - @Override - public void setOutputBuffer(VpxOutputBuffer outputBuffer) { - renderer.setFrame(outputBuffer); - requestRender(); - } - -} diff --git a/extensions/vp9/src/main/jni/Android.mk b/extensions/vp9/src/main/jni/Android.mk deleted file mode 100644 index f2d8a99a90..0000000000 --- a/extensions/vp9/src/main/jni/Android.mk +++ /dev/null @@ -1,42 +0,0 @@ -# -# Copyright (C) 2014 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. -# - -WORKING_DIR := $(call my-dir) -include $(CLEAR_VARS) -LIBVPX_ROOT := $(WORKING_DIR)/libvpx -LIBYUV_ROOT := $(WORKING_DIR)/libyuv - -# build libyuv_static.a -LOCAL_PATH := $(WORKING_DIR) -include $(LIBYUV_ROOT)/Android.mk - -# build libvpx.so -LOCAL_PATH := $(WORKING_DIR) -include libvpx.mk - -# build libvpxJNI.so -include $(CLEAR_VARS) -LOCAL_PATH := $(WORKING_DIR) -LOCAL_MODULE := libvpxJNI -LOCAL_ARM_MODE := arm -LOCAL_CPP_EXTENSION := .cc -LOCAL_SRC_FILES := vpx_jni.cc -LOCAL_LDLIBS := -llog -lz -lm -LOCAL_SHARED_LIBRARIES := libvpx -LOCAL_STATIC_LIBRARIES := libyuv_static cpufeatures -include $(BUILD_SHARED_LIBRARY) - -$(call import-module,android/cpufeatures) diff --git a/extensions/vp9/src/main/jni/Application.mk b/extensions/vp9/src/main/jni/Application.mk deleted file mode 100644 index 7dc417cda1..0000000000 --- a/extensions/vp9/src/main/jni/Application.mk +++ /dev/null @@ -1,20 +0,0 @@ -# -# Copyright (C) 2014 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. -# - -APP_OPTIM := release -APP_STL := gnustl_static -APP_CPPFLAGS := -frtti -APP_PLATFORM := android-9 diff --git a/extensions/vp9/src/main/jni/generate_libvpx_android_configs.sh b/extensions/vp9/src/main/jni/generate_libvpx_android_configs.sh deleted file mode 100755 index 951dcc0dfe..0000000000 --- a/extensions/vp9/src/main/jni/generate_libvpx_android_configs.sh +++ /dev/null @@ -1,124 +0,0 @@ -#!/bin/bash -# -# Copyright (C) 2014 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. -# - -# a bash script that generates the necessary config files for libvpx android ndk -# builds. - -set -e - -if [ $# -ne 1 ]; then - echo "Usage: ${0} " - exit -fi - -ndk="${1}" -shift 1 - -# configuration parameters common to all architectures -common_params="--disable-examples --disable-docs --enable-realtime-only" -common_params+=" --disable-vp8 --disable-vp9-encoder --disable-webm-io" -common_params+=" --disable-vp10 --disable-libyuv --disable-runtime-cpu-detect" - -# configuration parameters for various architectures -arch[0]="armeabi-v7a" -config[0]="--target=armv7-android-gcc --sdk-path=$ndk --enable-neon" -config[0]+=" --enable-neon-asm" - -arch[1]="armeabi" -config[1]="--target=armv7-android-gcc --sdk-path=$ndk --disable-neon" -config[1]+=" --disable-neon-asm --disable-media" - -arch[2]="mips" -config[2]="--force-target=mips32-android-gcc --sdk-path=$ndk" - -arch[3]="x86" -config[3]="--force-target=x86-android-gcc --sdk-path=$ndk --disable-sse2" -config[3]+=" --disable-sse3 --disable-ssse3 --disable-sse4_1 --disable-avx" -config[3]+=" --disable-avx2 --enable-pic" - -arch[4]="arm64-v8a" -config[4]="--force-target=armv8-android-gcc --sdk-path=$ndk --disable-neon" -config[4]+=" --disable-neon-asm" - -arch[5]="x86_64" -config[5]="--force-target=x86_64-android-gcc --sdk-path=$ndk --disable-sse2" -config[5]+=" --disable-sse3 --disable-ssse3 --disable-sse4_1 --disable-avx" -config[5]+=" --disable-avx2 --enable-pic --disable-neon --disable-neon-asm" - -arch[6]="mips64" -config[6]="--force-target=mips64-android-gcc --sdk-path=$ndk" - -limit=$((${#arch[@]} - 1)) - -# list of files allowed after running configure in each arch directory. -# everything else will be removed. -allowed_files="libvpx_srcs.txt vpx_config.c vpx_config.h vpx_scale_rtcd.h" -allowed_files+=" vp8_rtcd.h vp9_rtcd.h vpx_version.h vpx_config.asm" -allowed_files+=" vpx_dsp_rtcd.h" - -remove_trailing_whitespace() { - perl -pi -e 's/\s\+$//' "$@" -} - -convert_asm() { - for i in $(seq 0 ${limit}); do - while read file; do - case "${file}" in - *.asm.s) - # Some files may already have been processed (there are duplicated - # .asm.s files for vp8 in the armeabi/armeabi-v7a configurations). - file="libvpx/${file}" - if [[ ! -e "${file}" ]]; then - asm_file="${file%.s}" - cat "${asm_file}" | libvpx/build/make/ads2gas.pl > "${file}" - remove_trailing_whitespace "${file}" - rm "${asm_file}" - fi - ;; - esac - done < libvpx_android_configs/${arch[${i}]}/libvpx_srcs.txt - done -} - -extglob_status="$(shopt extglob | cut -f2)" -shopt -s extglob -for i in $(seq 0 ${limit}); do - mkdir -p "libvpx_android_configs/${arch[${i}]}" - pushd "libvpx_android_configs/${arch[${i}]}" - - # configure and make - echo "build_android_configs: " - echo "configure ${config[${i}]} ${common_params}" - ../../libvpx/configure ${config[${i}]} ${common_params} - rm -f libvpx_srcs.txt - make libvpx_srcs.txt - - # remove files that aren't needed - rm -rf !(${allowed_files// /|}) - remove_trailing_whitespace * - - popd -done - -# restore extglob status as it was before -if [[ "${extglob_status}" == "off" ]]; then - shopt -u extglob -fi - -convert_asm - -echo "Generated android config files." diff --git a/extensions/vp9/src/main/jni/libvpx.mk b/extensions/vp9/src/main/jni/libvpx.mk deleted file mode 100644 index 369b3b7f94..0000000000 --- a/extensions/vp9/src/main/jni/libvpx.mk +++ /dev/null @@ -1,51 +0,0 @@ -# -# Copyright (C) 2014 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. -# - -LOCAL_PATH := $(call my-dir) -include $(CLEAR_VARS) -CONFIG_DIR := $(LOCAL_PATH)/libvpx_android_configs/$(TARGET_ARCH_ABI) -libvpx_source_dir := $(LOCAL_PATH)/libvpx - -LOCAL_MODULE := libvpx -LOCAL_MODULE_CLASS := STATIC_LIBRARIES -LOCAL_CFLAGS := -DHAVE_CONFIG_H=vpx_config.h -LOCAL_ARM_MODE := arm -LOCAL_CFLAGS += -O3 - -# config specific include should go first to pick up the config specific rtcd. -LOCAL_C_INCLUDES := $(CONFIG_DIR) $(libvpx_source_dir) - -# generate source file list -libvpx_codec_srcs := $(sort $(shell cat $(CONFIG_DIR)/libvpx_srcs.txt)) -LOCAL_SRC_FILES := libvpx_android_configs/$(TARGET_ARCH_ABI)/vpx_config.c -LOCAL_SRC_FILES += $(addprefix libvpx/, $(filter-out vpx_config.c, \ - $(filter %.c, $(libvpx_codec_srcs)))) - -# include assembly files if they exist -# "%.asm.s" covers neon assembly and "%.asm" covers x86 assembly -LOCAL_SRC_FILES += $(addprefix libvpx/, \ - $(filter %.asm.s %.asm, $(libvpx_codec_srcs))) - -ifneq ($(findstring armeabi-v7a, $(TARGET_ARCH_ABI)),) -# append .neon to *_neon.c and *.s -LOCAL_SRC_FILES := $(subst _neon.c,_neon.c.neon,$(LOCAL_SRC_FILES)) -LOCAL_SRC_FILES := $(subst .s,.s.neon,$(LOCAL_SRC_FILES)) -endif - -LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/libvpx \ - $(LOCAL_PATH)/libvpx/vpx - -include $(BUILD_SHARED_LIBRARY) diff --git a/extensions/vp9/src/main/jni/vpx_jni.cc b/extensions/vp9/src/main/jni/vpx_jni.cc deleted file mode 100644 index a3abe24399..0000000000 --- a/extensions/vp9/src/main/jni/vpx_jni.cc +++ /dev/null @@ -1,176 +0,0 @@ -/* - * Copyright (C) 2014 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. - */ - -#include -#include - -#include - -#include -#include -#include -#include - -#include "libyuv.h" // NOLINT - -#define VPX_CODEC_DISABLE_COMPAT 1 -#include "vpx/vpx_decoder.h" -#include "vpx/vp8dx.h" - -#define LOG_TAG "LIBVPX_DEC" -#define LOGE(...) ((void)__android_log_print(ANDROID_LOG_ERROR, LOG_TAG, \ - __VA_ARGS__)) - -#define FUNC(RETURN_TYPE, NAME, ...) \ - extern "C" { \ - JNIEXPORT RETURN_TYPE \ - Java_com_google_android_exoplayer_ext_vp9_VpxDecoder_ ## NAME \ - (JNIEnv* env, jobject thiz, ##__VA_ARGS__);\ - } \ - JNIEXPORT RETURN_TYPE \ - Java_com_google_android_exoplayer_ext_vp9_VpxDecoder_ ## NAME \ - (JNIEnv* env, jobject thiz, ##__VA_ARGS__)\ - -// JNI references for VpxOutputBuffer class. -static jmethodID initForRgbFrame; -static jmethodID initForYuvFrame; -static jfieldID dataField; -static jfieldID outputModeField; - -jint JNI_OnLoad(JavaVM* vm, void* reserved) { - JNIEnv* env; - if (vm->GetEnv(reinterpret_cast(&env), JNI_VERSION_1_6) != JNI_OK) { - return -1; - } - return JNI_VERSION_1_6; -} - -FUNC(jlong, vpxInit) { - vpx_codec_ctx_t* context = new vpx_codec_ctx_t(); - vpx_codec_dec_cfg_t cfg = {0}; - cfg.threads = android_getCpuCount(); - if (vpx_codec_dec_init(context, &vpx_codec_vp9_dx_algo, &cfg, 0)) { - LOGE("ERROR: Fail to initialize libvpx decoder."); - return 0; - } - - // Populate JNI References. - const jclass outputBufferClass = env->FindClass( - "com/google/android/exoplayer/ext/vp9/VpxOutputBuffer"); - initForYuvFrame = env->GetMethodID(outputBufferClass, "initForYuvFrame", - "(IIIII)V"); - initForRgbFrame = env->GetMethodID(outputBufferClass, "initForRgbFrame", - "(II)V"); - dataField = env->GetFieldID(outputBufferClass, "data", - "Ljava/nio/ByteBuffer;"); - outputModeField = env->GetFieldID(outputBufferClass, "mode", "I"); - - return reinterpret_cast(context); -} - -FUNC(jlong, vpxDecode, jlong jContext, jobject encoded, jint len) { - vpx_codec_ctx_t* const context = reinterpret_cast(jContext); - const uint8_t* const buffer = - reinterpret_cast(env->GetDirectBufferAddress(encoded)); - const vpx_codec_err_t status = - vpx_codec_decode(context, buffer, len, NULL, 0); - if (status != VPX_CODEC_OK) { - LOGE("ERROR: vpx_codec_decode() failed, status= %d", status); - return -1; - } - return 0; -} - -FUNC(jlong, vpxClose, jlong jContext) { - vpx_codec_ctx_t* const context = reinterpret_cast(jContext); - vpx_codec_destroy(context); - delete context; - return 0; -} - -FUNC(jint, vpxGetFrame, jlong jContext, jobject jOutputBuffer) { - vpx_codec_ctx_t* const context = reinterpret_cast(jContext); - vpx_codec_iter_t iter = NULL; - const vpx_image_t* const img = vpx_codec_get_frame(context, &iter); - - if (img == NULL) { - return 1; - } - - const int kOutputModeYuv = 0; - const int kOutputModeRgb = 1; - - int outputMode = env->GetIntField(jOutputBuffer, outputModeField); - if (outputMode == kOutputModeRgb) { - // resize buffer if required. - env->CallVoidMethod(jOutputBuffer, initForRgbFrame, img->d_w, img->d_h); - - // get pointer to the data buffer. - const jobject dataObject = env->GetObjectField(jOutputBuffer, dataField); - uint8_t* const dst = - reinterpret_cast(env->GetDirectBufferAddress(dataObject)); - - libyuv::I420ToRGB565(img->planes[VPX_PLANE_Y], img->stride[VPX_PLANE_Y], - img->planes[VPX_PLANE_U], img->stride[VPX_PLANE_U], - img->planes[VPX_PLANE_V], img->stride[VPX_PLANE_V], - dst, img->d_w * 2, img->d_w, img->d_h); - } else if (outputMode == kOutputModeYuv) { - const int kColorspaceUnknown = 0; - const int kColorspaceBT601 = 1; - const int kColorspaceBT709 = 2; - - int colorspace = kColorspaceUnknown; - switch (img->cs) { - case VPX_CS_BT_601: - colorspace = kColorspaceBT601; - break; - case VPX_CS_BT_709: - colorspace = kColorspaceBT709; - break; - default: - break; - } - - // resize buffer if required. - env->CallVoidMethod(jOutputBuffer, initForYuvFrame, img->d_w, img->d_h, - img->stride[VPX_PLANE_Y], img->stride[VPX_PLANE_U], - colorspace); - - // get pointer to the data buffer. - const jobject dataObject = env->GetObjectField(jOutputBuffer, dataField); - jbyte* const data = - reinterpret_cast(env->GetDirectBufferAddress(dataObject)); - - // TODO: This copy can be eliminated by using external frame buffers. NOLINT - // This is insignificant for smaller videos but takes ~1.5ms for 1080p - // clips. So this should eventually be gotten rid of. - const uint64_t y_length = img->stride[VPX_PLANE_Y] * img->d_h; - const uint64_t uv_length = img->stride[VPX_PLANE_U] * ((img->d_h + 1) / 2); - memcpy(data, img->planes[VPX_PLANE_Y], y_length); - memcpy(data + y_length, img->planes[VPX_PLANE_U], uv_length); - memcpy(data + y_length + uv_length, img->planes[VPX_PLANE_V], uv_length); - } - return 0; -} - -FUNC(jstring, getLibvpxVersion) { - return env->NewStringUTF(vpx_codec_version_str()); -} - -FUNC(jstring, vpxGetErrorMessage, jlong jContext) { - vpx_codec_ctx_t* const context = reinterpret_cast(jContext); - return env->NewStringUTF(vpx_codec_error(context)); -} diff --git a/extensions/vp9/src/main/proguard.cfg b/extensions/vp9/src/main/proguard.cfg deleted file mode 100644 index 28ec41a6b3..0000000000 --- a/extensions/vp9/src/main/proguard.cfg +++ /dev/null @@ -1,11 +0,0 @@ -# Proguard rules specific to the VP9 extension. - -# This prevents the names of native methods from being obfuscated. --keepclasseswithmembernames class * { - native ; -} - -# Some members of this class are being accessed from native methods. Keep them unobfuscated. --keep class com.google.android.exoplayer.ext.vp9.VpxDecoderWrapper$OutputBuffer { - *; -} diff --git a/extensions/vp9/src/main/project.properties b/extensions/vp9/src/main/project.properties deleted file mode 100644 index b92a03b7ab..0000000000 --- a/extensions/vp9/src/main/project.properties +++ /dev/null @@ -1,16 +0,0 @@ -# This file is automatically generated by Android Tools. -# Do not modify this file -- YOUR CHANGES WILL BE ERASED! -# -# This file must be checked in Version Control Systems. -# -# To customize properties used by the Ant build system edit -# "ant.properties", and override values to adapt the script to your -# project structure. -# -# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): -#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt - -# Project target. -target=android-23 -android.library=true -android.library.reference.1=../../../../library/src/main diff --git a/extensions/vp9/src/main/res/.README.txt b/extensions/vp9/src/main/res/.README.txt deleted file mode 100644 index c27147ce56..0000000000 --- a/extensions/vp9/src/main/res/.README.txt +++ /dev/null @@ -1,2 +0,0 @@ -This file is needed to make sure the res directory is present. -The file is ignored by the Android toolchain because its name starts with a dot. diff --git a/library/src/androidTest/.classpath b/library/src/androidTest/.classpath deleted file mode 100644 index be2dd156ff..0000000000 --- a/library/src/androidTest/.classpath +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/library/src/androidTest/.project b/library/src/androidTest/.project deleted file mode 100644 index 888dce8656..0000000000 --- a/library/src/androidTest/.project +++ /dev/null @@ -1,62 +0,0 @@ - - - ExoPlayerLibTests - - - ExoPlayerLib - - - - com.android.ide.eclipse.adt.ResourceManagerBuilder - - - - - com.android.ide.eclipse.adt.PreCompilerBuilder - - - - - org.eclipse.jdt.core.javabuilder - - - - - com.android.ide.eclipse.adt.ApkBuilder - - - - - - com.android.ide.eclipse.adt.AndroidNature - org.eclipse.jdt.core.javanature - - - - libs/dexmaker-1.2.jar - 1 - $%7BPARENT-3-PROJECT_LOC%7D/third_party/dexmaker/dexmaker-1.2.jar - - - libs/dexmaker-mockito-1.2.jar - 1 - $%7BPARENT-3-PROJECT_LOC%7D/third_party/dexmaker/dexmaker-mockito-1.2.jar - - - libs/mockito-all-1.9.5.jar - 1 - $%7BPARENT-3-PROJECT_LOC%7D/third_party/mockito/mockito-all-1.9.5.jar - - - - - 1425657306619 - - 14 - - org.eclipse.ui.ide.multiFilter - 1.0-name-matches-true-false-BUILD - - - - diff --git a/library/src/androidTest/.settings/org.eclipse.jdt.core.prefs b/library/src/androidTest/.settings/org.eclipse.jdt.core.prefs deleted file mode 100644 index 64cef5023a..0000000000 --- a/library/src/androidTest/.settings/org.eclipse.jdt.core.prefs +++ /dev/null @@ -1,4 +0,0 @@ -eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7 -org.eclipse.jdt.core.compiler.compliance=1.7 -org.eclipse.jdt.core.compiler.source=1.7 diff --git a/library/src/androidTest/libs/.README.txt b/library/src/androidTest/libs/.README.txt deleted file mode 100644 index 3f37353a9d..0000000000 --- a/library/src/androidTest/libs/.README.txt +++ /dev/null @@ -1 +0,0 @@ -This file is needed to make sure the libs directory is present. diff --git a/library/src/androidTest/project.properties b/library/src/androidTest/project.properties index dfce3e9cd6..ac2c332477 100644 --- a/library/src/androidTest/project.properties +++ b/library/src/androidTest/project.properties @@ -13,4 +13,4 @@ # Project target. target=android-23 android.library=false -android.library.reference.1=../main +android.library.reference.1=../experimental diff --git a/library/src/main/.classpath b/library/src/main/.classpath deleted file mode 100644 index 26c3fb17cd..0000000000 --- a/library/src/main/.classpath +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/library/src/main/.project b/library/src/main/.project deleted file mode 100644 index 21ae416a8b..0000000000 --- a/library/src/main/.project +++ /dev/null @@ -1,33 +0,0 @@ - - - ExoPlayerLib - - - - - - com.android.ide.eclipse.adt.ResourceManagerBuilder - - - - - com.android.ide.eclipse.adt.PreCompilerBuilder - - - - - org.eclipse.jdt.core.javabuilder - - - - - com.android.ide.eclipse.adt.ApkBuilder - - - - - - com.android.ide.eclipse.adt.AndroidNature - org.eclipse.jdt.core.javanature - - diff --git a/library/src/main/.settings/org.eclipse.jdt.core.prefs b/library/src/main/.settings/org.eclipse.jdt.core.prefs deleted file mode 100644 index 64cef5023a..0000000000 --- a/library/src/main/.settings/org.eclipse.jdt.core.prefs +++ /dev/null @@ -1,4 +0,0 @@ -eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7 -org.eclipse.jdt.core.compiler.compliance=1.7 -org.eclipse.jdt.core.compiler.source=1.7 diff --git a/library/src/main/java/com/google/android/exoplayer/C.java b/library/src/main/java/com/google/android/exoplayer/C.java index 622da04caf..a49246a07f 100644 --- a/library/src/main/java/com/google/android/exoplayer/C.java +++ b/library/src/main/java/com/google/android/exoplayer/C.java @@ -27,15 +27,14 @@ import android.media.MediaExtractor; public final class C { /** - * Represents an unknown microsecond time or duration. + * Special microsecond constant representing an unknown time or duration. */ public static final long UNKNOWN_TIME_US = -1L; /** - * Represents a microsecond duration whose exact value is unknown, but which should match the - * longest of some other known durations. + * Special microsecond constant representing the end of a source. */ - public static final long MATCH_LONGEST_US = -2L; + public static final long END_OF_SOURCE_US = -2L; /** * The number of microseconds in one second. diff --git a/library/src/main/java/com/google/android/exoplayer/DummyTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/DummyTrackRenderer.java index c1e91b2904..a7a06f9a96 100644 --- a/library/src/main/java/com/google/android/exoplayer/DummyTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/DummyTrackRenderer.java @@ -25,8 +25,8 @@ package com.google.android.exoplayer; public final class DummyTrackRenderer extends TrackRenderer { @Override - protected boolean doPrepare(long positionUs) throws ExoPlaybackException { - return true; + protected void doPrepare(SampleSource sampleSource) throws ExoPlaybackException { + // Do nothing. } @Override @@ -49,11 +49,6 @@ public final class DummyTrackRenderer extends TrackRenderer { throw new IllegalStateException(); } - @Override - protected void seekTo(long positionUs) { - throw new IllegalStateException(); - } - @Override protected void doSomeWork(long positionUs, long elapsedRealtimeUs) { throw new IllegalStateException(); @@ -64,14 +59,4 @@ public final class DummyTrackRenderer extends TrackRenderer { throw new IllegalStateException(); } - @Override - protected long getDurationUs() { - throw new IllegalStateException(); - } - - @Override - protected long getBufferedPositionUs() { - throw new IllegalStateException(); - } - } diff --git a/library/src/main/java/com/google/android/exoplayer/ExoPlayer.java b/library/src/main/java/com/google/android/exoplayer/ExoPlayer.java index 5d265a64ff..5319f88ed2 100644 --- a/library/src/main/java/com/google/android/exoplayer/ExoPlayer.java +++ b/library/src/main/java/com/google/android/exoplayer/ExoPlayer.java @@ -85,7 +85,7 @@ import android.os.Looper; * *

The possible playback state transitions are shown below. Transitions can be triggered either * by changes in the state of the {@link TrackRenderer}s being used, or as a result of - * {@link #prepare(TrackRenderer[])}, {@link #stop()} or {@link #release()} being invoked.

+ * {@link #prepare(SampleSource)}, {@link #stop()} or {@link #release()} being invoked.

*

ExoPlayer playback state transitions

@@ -95,7 +95,7 @@ public interface ExoPlayer { /** * A factory for instantiating ExoPlayer instances. */ - public static final class Factory { + static final class Factory { /** * The default minimum duration of data that must be buffered for playback to start or resume @@ -117,16 +117,16 @@ public interface ExoPlayer { *

* Must be invoked from a thread that has an associated {@link Looper}. * - * @param rendererCount The number of {@link TrackRenderer}s that will be passed to - * {@link #prepare(TrackRenderer[])}. + * @param renderers The {@link TrackRenderer}s that will be used by the instance. * @param minBufferMs A minimum duration of data that must be buffered for playback to start * or resume following a user action such as a seek. * @param minRebufferMs A minimum duration of data that must be buffered for playback to resume * after a player invoked rebuffer (i.e. a rebuffer that occurs due to buffer depletion, and * not due to a user action such as starting playback or seeking). */ - public static ExoPlayer newInstance(int rendererCount, int minBufferMs, int minRebufferMs) { - return new ExoPlayerImpl(rendererCount, minBufferMs, minRebufferMs); + public static ExoPlayer newInstance(TrackRenderer[] renderers, int minBufferMs, + int minRebufferMs) { + return new ExoPlayerImpl(renderers, minBufferMs, minRebufferMs); } /** @@ -134,11 +134,10 @@ public interface ExoPlayer { *

* Must be invoked from a thread that has an associated {@link Looper}. * - * @param rendererCount The number of {@link TrackRenderer}s that will be passed to - * {@link #prepare(TrackRenderer[])}. + * @param renderers The {@link TrackRenderer}s that will be used by the instance. */ - public static ExoPlayer newInstance(int rendererCount) { - return new ExoPlayerImpl(rendererCount, DEFAULT_MIN_BUFFER_MS, DEFAULT_MIN_REBUFFER_MS); + public static ExoPlayer newInstance(TrackRenderer... renderers) { + return new ExoPlayerImpl(renderers, DEFAULT_MIN_BUFFER_MS, DEFAULT_MIN_REBUFFER_MS); } } @@ -146,7 +145,7 @@ public interface ExoPlayer { /** * Interface definition for a callback to be notified of changes in player state. */ - public interface Listener { + interface Listener { /** * Invoked when the value returned from either {@link ExoPlayer#getPlayWhenReady()} or * {@link ExoPlayer#getPlaybackState()} changes. @@ -183,7 +182,7 @@ public interface ExoPlayer { * Messages can be delivered to a component via {@link ExoPlayer#sendMessage} and * {@link ExoPlayer#blockingSendMessage}. */ - public interface ExoPlayerComponent { + interface ExoPlayerComponent { /** * Handles a message delivered to the component. Invoked on the playback thread. @@ -199,49 +198,49 @@ public interface ExoPlayer { /** * The player is neither prepared or being prepared. */ - public static final int STATE_IDLE = 1; + static final int STATE_IDLE = 1; /** * The player is being prepared. */ - public static final int STATE_PREPARING = 2; + static final int STATE_PREPARING = 2; /** * The player is prepared but not able to immediately play from the current position. The cause * is {@link TrackRenderer} specific, but this state typically occurs when more data needs * to be buffered for playback to start. */ - public static final int STATE_BUFFERING = 3; + static final int STATE_BUFFERING = 3; /** * The player is prepared and able to immediately play from the current position. The player will * be playing if {@link #setPlayWhenReady(boolean)} returns true, and paused otherwise. */ - public static final int STATE_READY = 4; + static final int STATE_READY = 4; /** * The player has finished playing the media. */ - public static final int STATE_ENDED = 5; + static final int STATE_ENDED = 5; /** * A value that can be passed as the second argument to {@link #setSelectedTrack(int, int)} to * disable the renderer. */ - public static final int TRACK_DISABLED = -1; + static final int TRACK_DISABLED = -1; /** * A value that can be passed as the second argument to {@link #setSelectedTrack(int, int)} to * select the default track. */ - public static final int TRACK_DEFAULT = 0; + static final int TRACK_DEFAULT = 0; /** * Represents an unknown time or duration. */ - public static final long UNKNOWN_TIME = -1; + static final long UNKNOWN_TIME = -1; /** * Gets the {@link Looper} associated with the playback thread. * * @return The {@link Looper} associated with the playback thread. */ - public Looper getPlaybackLooper(); + Looper getPlaybackLooper(); /** * Register a listener to receive events from the player. The listener's methods will be invoked @@ -249,29 +248,28 @@ public interface ExoPlayer { * * @param listener The listener to register. */ - public void addListener(Listener listener); + void addListener(Listener listener); /** * Unregister a listener. The listener will no longer receive events from the player. * * @param listener The listener to unregister. */ - public void removeListener(Listener listener); + void removeListener(Listener listener); /** * Returns the current state of the player. * * @return One of the {@code STATE} constants defined in this interface. */ - public int getPlaybackState(); + int getPlaybackState(); /** * Prepares the player for playback. * - * @param renderers The {@link TrackRenderer}s to use. The number of renderers must match the - * value that was passed to the {@link ExoPlayer.Factory#newInstance} method. + * @param sampleSource The {@link SampleSource} to play. */ - public void prepare(TrackRenderer... renderers); + void prepare(SampleSource sampleSource); /** * Returns the number of tracks exposed by the specified renderer. @@ -279,7 +277,7 @@ public interface ExoPlayer { * @param rendererIndex The index of the renderer. * @return The number of tracks. */ - public int getTrackCount(int rendererIndex); + int getTrackCount(int rendererIndex); /** * Returns the format of a track. @@ -288,7 +286,7 @@ public interface ExoPlayer { * @param trackIndex The index of the track. * @return The format of the track. */ - public MediaFormat getTrackFormat(int rendererIndex, int trackIndex); + MediaFormat getTrackFormat(int rendererIndex, int trackIndex); /** * Selects a track for the specified renderer. @@ -297,7 +295,7 @@ public interface ExoPlayer { * @param trackIndex The index of the track. A negative value or a value greater than or equal to * the renderer's track count will disable the renderer. */ - public void setSelectedTrack(int rendererIndex, int trackIndex); + void setSelectedTrack(int rendererIndex, int trackIndex); /** * Returns the index of the currently selected track for the specified renderer. @@ -306,7 +304,7 @@ public interface ExoPlayer { * @return The selected track. A negative value or a value greater than or equal to the renderer's * track count indicates that the renderer is disabled. */ - public int getSelectedTrack(int rendererIndex); + int getSelectedTrack(int rendererIndex); /** * Sets whether playback should proceed when {@link #getPlaybackState()} == {@link #STATE_READY}. @@ -315,14 +313,14 @@ public interface ExoPlayer { * * @param playWhenReady Whether playback should proceed when ready. */ - public void setPlayWhenReady(boolean playWhenReady); + void setPlayWhenReady(boolean playWhenReady); /** * Whether playback will proceed when {@link #getPlaybackState()} == {@link #STATE_READY}. * * @return Whether playback will proceed when ready. */ - public boolean getPlayWhenReady(); + boolean getPlayWhenReady(); /** * Whether the current value of {@link ExoPlayer#getPlayWhenReady()} has been reflected by the @@ -330,14 +328,14 @@ public interface ExoPlayer { * * @return True if the current value has been reflected. False otherwise. */ - public boolean isPlayWhenReadyCommitted(); + boolean isPlayWhenReadyCommitted(); /** * Seeks to a position specified in milliseconds. * * @param positionMs The seek position. */ - public void seekTo(long positionMs); + void seekTo(long positionMs); /** * Stops playback. Use {@code setPlayWhenReady(false)} rather than this method if the intention @@ -351,14 +349,14 @@ public interface ExoPlayer { * to play another video from its start, then {@code seekTo(0)} should be called after stopping * the player and before preparing it for the next video. */ - public void stop(); + void stop(); /** * Releases the player. This method must be called when the player is no longer required. *

* The player must not be used after calling this method. */ - public void release(); + void release(); /** * Sends a message to a specified component. The message is delivered to the component on the @@ -369,7 +367,7 @@ public interface ExoPlayer { * @param messageType An integer that can be used to identify the type of the message. * @param message The message object. */ - public void sendMessage(ExoPlayerComponent target, int messageType, Object message); + void sendMessage(ExoPlayerComponent target, int messageType, Object message); /** * Blocking variant of {@link #sendMessage(ExoPlayerComponent, int, Object)} that does not return @@ -379,7 +377,7 @@ public interface ExoPlayer { * @param messageType An integer that can be used to identify the type of the message. * @param message The message object. */ - public void blockingSendMessage(ExoPlayerComponent target, int messageType, Object message); + void blockingSendMessage(ExoPlayerComponent target, int messageType, Object message); /** * Gets the duration of the track in milliseconds. @@ -387,14 +385,14 @@ public interface ExoPlayer { * @return The duration of the track in milliseconds, or {@link ExoPlayer#UNKNOWN_TIME} if the * duration is not known. */ - public long getDuration(); + long getDuration(); /** * Gets the current playback position in milliseconds. * * @return The current playback position in milliseconds. */ - public long getCurrentPosition(); + long getCurrentPosition(); /** * Gets an estimate of the absolute position in milliseconds up to which data is buffered. @@ -402,7 +400,7 @@ public interface ExoPlayer { * @return An estimate of the absolute position in milliseconds up to which data is buffered, * or {@link ExoPlayer#UNKNOWN_TIME} if no estimate is available. */ - public long getBufferedPosition(); + long getBufferedPosition(); /** * Gets an estimate of the percentage into the media up to which data is buffered. @@ -410,6 +408,6 @@ public interface ExoPlayer { * @return An estimate of the percentage into the media up to which data is buffered. 0 if the * duration of the media is not known or if no estimate is available. */ - public int getBufferedPercentage(); + int getBufferedPercentage(); } diff --git a/library/src/main/java/com/google/android/exoplayer/ExoPlayerImpl.java b/library/src/main/java/com/google/android/exoplayer/ExoPlayerImpl.java index 5450a2b94c..141266badc 100644 --- a/library/src/main/java/com/google/android/exoplayer/ExoPlayerImpl.java +++ b/library/src/main/java/com/google/android/exoplayer/ExoPlayerImpl.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer; +import com.google.android.exoplayer.util.Assertions; + import android.annotation.SuppressLint; import android.os.Handler; import android.os.Looper; @@ -44,8 +46,7 @@ import java.util.concurrent.CopyOnWriteArraySet; /** * Constructs an instance. Must be invoked from a thread that has an associated {@link Looper}. * - * @param rendererCount The number of {@link TrackRenderer}s that will be passed to - * {@link #prepare(TrackRenderer[])}. + * @param renderers The {@link TrackRenderer}s belonging to this instance. * @param minBufferMs A minimum duration of data that must be buffered for playback to start * or resume following a user action such as a seek. * @param minRebufferMs A minimum duration of data that must be buffered for playback to resume @@ -53,21 +54,23 @@ import java.util.concurrent.CopyOnWriteArraySet; * not due to a user action such as starting playback or seeking). */ @SuppressLint("HandlerLeak") - public ExoPlayerImpl(int rendererCount, int minBufferMs, int minRebufferMs) { + public ExoPlayerImpl(TrackRenderer[] renderers, int minBufferMs, int minRebufferMs) { Log.i(TAG, "Init " + ExoPlayerLibraryInfo.VERSION); + Assertions.checkNotNull(renderers); + Assertions.checkState(renderers.length > 0); this.playWhenReady = false; this.playbackState = STATE_IDLE; this.listeners = new CopyOnWriteArraySet<>(); - this.trackFormats = new MediaFormat[rendererCount][]; - this.selectedTrackIndices = new int[rendererCount]; + this.trackFormats = new MediaFormat[renderers.length][]; + this.selectedTrackIndices = new int[renderers.length]; eventHandler = new Handler() { @Override public void handleMessage(Message msg) { ExoPlayerImpl.this.handleEvent(msg); } }; - internalPlayer = new ExoPlayerImplInternal(eventHandler, playWhenReady, selectedTrackIndices, - minBufferMs, minRebufferMs); + internalPlayer = new ExoPlayerImplInternal(renderers, minBufferMs, minRebufferMs, + playWhenReady, selectedTrackIndices, eventHandler); } @Override @@ -91,9 +94,9 @@ import java.util.concurrent.CopyOnWriteArraySet; } @Override - public void prepare(TrackRenderer... renderers) { + public void prepare(SampleSource source) { Arrays.fill(trackFormats, null); - internalPlayer.prepare(renderers); + internalPlayer.prepare(source); } @Override diff --git a/library/src/main/java/com/google/android/exoplayer/ExoPlayerImplInternal.java b/library/src/main/java/com/google/android/exoplayer/ExoPlayerImplInternal.java index e8a79efa04..4bb73065bb 100644 --- a/library/src/main/java/com/google/android/exoplayer/ExoPlayerImplInternal.java +++ b/library/src/main/java/com/google/android/exoplayer/ExoPlayerImplInternal.java @@ -30,6 +30,7 @@ import android.os.SystemClock; import android.util.Log; import android.util.Pair; +import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -38,6 +39,9 @@ import java.util.concurrent.atomic.AtomicInteger; /** * Implements the internal behavior of {@link ExoPlayerImpl}. */ +// TODO[REFACTOR]: Make sure renderer errors that will prevent prepare from being called again are +// always propagated properly. +// TODO[REFACTOR]: Distinguish source and renderer errors in ExoPlaybackException. /* package */ final class ExoPlayerImplInternal implements Handler.Callback { private static final String TAG = "ExoPlayerImplInternal"; @@ -63,21 +67,21 @@ import java.util.concurrent.atomic.AtomicInteger; private static final int RENDERING_INTERVAL_MS = 10; private static final int IDLE_INTERVAL_MS = 1000; - private final Handler handler; - private final HandlerThread internalPlaybackThread; - private final Handler eventHandler; + private final TrackRenderer[] renderers; + private final TrackRenderer rendererMediaClockSource; + private final MediaClock rendererMediaClock; private final StandaloneMediaClock standaloneMediaClock; - private final AtomicInteger pendingSeekCount; + private final long minBufferUs; + private final long minRebufferUs; private final List enabledRenderers; private final MediaFormat[][] trackFormats; private final int[] selectedTrackIndices; - private final long minBufferUs; - private final long minRebufferUs; - - private TrackRenderer[] renderers; - private TrackRenderer rendererMediaClockSource; - private MediaClock rendererMediaClock; + private final Handler handler; + private final HandlerThread internalPlaybackThread; + private final Handler eventHandler; + private final AtomicInteger pendingSeekCount; + private SampleSource source; private boolean released; private boolean playWhenReady; private boolean rebuffering; @@ -91,16 +95,31 @@ import java.util.concurrent.atomic.AtomicInteger; private volatile long positionUs; private volatile long bufferedPositionUs; - public ExoPlayerImplInternal(Handler eventHandler, boolean playWhenReady, - int[] selectedTrackIndices, int minBufferMs, int minRebufferMs) { - this.eventHandler = eventHandler; - this.playWhenReady = playWhenReady; + public ExoPlayerImplInternal(TrackRenderer[] renderers, int minBufferMs, int minRebufferMs, + boolean playWhenReady, int[] selectedTrackIndices, Handler eventHandler) { + this.renderers = renderers; this.minBufferUs = minBufferMs * 1000L; this.minRebufferUs = minRebufferMs * 1000L; + this.playWhenReady = playWhenReady; this.selectedTrackIndices = Arrays.copyOf(selectedTrackIndices, selectedTrackIndices.length); + this.eventHandler = eventHandler; this.state = ExoPlayer.STATE_IDLE; - this.durationUs = TrackRenderer.UNKNOWN_TIME_US; - this.bufferedPositionUs = TrackRenderer.UNKNOWN_TIME_US; + this.durationUs = C.UNKNOWN_TIME_US; + this.bufferedPositionUs = C.UNKNOWN_TIME_US; + + MediaClock rendererMediaClock = null; + TrackRenderer rendererMediaClockSource = null; + for (int i = 0; i < renderers.length; i++) { + MediaClock mediaClock = renderers[i].getMediaClock(); + if (mediaClock != null) { + Assertions.checkState(rendererMediaClock == null); + rendererMediaClock = mediaClock; + rendererMediaClockSource = renderers[i]; + break; + } + } + this.rendererMediaClock = rendererMediaClock; + this.rendererMediaClockSource = rendererMediaClockSource; standaloneMediaClock = new StandaloneMediaClock(); pendingSeekCount = new AtomicInteger(); @@ -123,17 +142,16 @@ import java.util.concurrent.atomic.AtomicInteger; } public long getBufferedPosition() { - return bufferedPositionUs == TrackRenderer.UNKNOWN_TIME_US ? ExoPlayer.UNKNOWN_TIME + return bufferedPositionUs == C.UNKNOWN_TIME_US ? ExoPlayer.UNKNOWN_TIME : bufferedPositionUs / 1000; } public long getDuration() { - return durationUs == TrackRenderer.UNKNOWN_TIME_US ? ExoPlayer.UNKNOWN_TIME - : durationUs / 1000; + return durationUs == C.UNKNOWN_TIME_US ? ExoPlayer.UNKNOWN_TIME : durationUs / 1000; } - public void prepare(TrackRenderer... renderers) { - handler.obtainMessage(MSG_PREPARE, renderers).sendToTarget(); + public void prepare(SampleSource sampleSource) { + handler.obtainMessage(MSG_PREPARE, sampleSource).sendToTarget(); } public void setPlayWhenReady(boolean playWhenReady) { @@ -198,7 +216,7 @@ import java.util.concurrent.atomic.AtomicInteger; try { switch (msg.what) { case MSG_PREPARE: { - prepareInternal((TrackRenderer[]) msg.obj); + prepareInternal((SampleSource) msg.obj); return true; } case MSG_INCREMENTAL_PREPARE: { @@ -241,6 +259,11 @@ import java.util.concurrent.atomic.AtomicInteger; eventHandler.obtainMessage(MSG_ERROR, e).sendToTarget(); stopInternal(); return true; + } catch (IOException e) { + Log.e(TAG, "Source track renderer error.", e); + eventHandler.obtainMessage(MSG_ERROR, new ExoPlaybackException(e)).sendToTarget(); + stopInternal(); + return true; } catch (RuntimeException e) { Log.e(TAG, "Internal runtime error.", e); eventHandler.obtainMessage(MSG_ERROR, new ExoPlaybackException(e, true)).sendToTarget(); @@ -256,47 +279,29 @@ import java.util.concurrent.atomic.AtomicInteger; } } - private void prepareInternal(TrackRenderer[] renderers) throws ExoPlaybackException { + private void prepareInternal(SampleSource sampleSource) throws ExoPlaybackException, IOException { resetInternal(); - this.renderers = renderers; - Arrays.fill(trackFormats, null); - for (int i = 0; i < renderers.length; i++) { - MediaClock mediaClock = renderers[i].getMediaClock(); - if (mediaClock != null) { - Assertions.checkState(rendererMediaClock == null); - rendererMediaClock = mediaClock; - rendererMediaClockSource = renderers[i]; - } - } setState(ExoPlayer.STATE_PREPARING); + this.source = sampleSource; incrementalPrepareInternal(); } - private void incrementalPrepareInternal() throws ExoPlaybackException { + private void incrementalPrepareInternal() throws ExoPlaybackException, IOException { long operationStartTimeMs = SystemClock.elapsedRealtime(); - boolean prepared = true; - for (int rendererIndex = 0; rendererIndex < renderers.length; rendererIndex++) { - TrackRenderer renderer = renderers[rendererIndex]; - if (renderer.getState() == TrackRenderer.STATE_UNPREPARED) { - int state = renderer.prepare(positionUs); - if (state == TrackRenderer.STATE_UNPREPARED) { - renderer.maybeThrowError(); - prepared = false; - } - } - } - - if (!prepared) { - // We're still waiting for some sources to be prepared. + if (!source.prepare(positionUs)) { + // We're still waiting for the source to be prepared. scheduleNextOperation(MSG_INCREMENTAL_PREPARE, operationStartTimeMs, PREPARE_INTERVAL_MS); return; } - long durationUs = 0; + this.durationUs = source.getDurationUs(); + this.bufferedPositionUs = source.getBufferedPositionUs(); + boolean allRenderersEnded = true; boolean allRenderersReadyOrEnded = true; for (int rendererIndex = 0; rendererIndex < renderers.length; rendererIndex++) { TrackRenderer renderer = renderers[rendererIndex]; + renderer.prepare(source); int rendererTrackCount = renderer.getTrackCount(); MediaFormat[] rendererTrackFormats = new MediaFormat[rendererTrackCount]; for (int trackIndex = 0; trackIndex < rendererTrackCount; trackIndex++) { @@ -304,36 +309,22 @@ import java.util.concurrent.atomic.AtomicInteger; } trackFormats[rendererIndex] = rendererTrackFormats; if (rendererTrackCount > 0) { - if (durationUs == TrackRenderer.UNKNOWN_TIME_US) { - // We've already encountered a track for which the duration is unknown, so the media - // duration is unknown regardless of the duration of this track. - } else { - long trackDurationUs = renderer.getDurationUs(); - if (trackDurationUs == TrackRenderer.UNKNOWN_TIME_US) { - durationUs = TrackRenderer.UNKNOWN_TIME_US; - } else if (trackDurationUs == TrackRenderer.MATCH_LONGEST_US) { - // Do nothing. - } else { - durationUs = Math.max(durationUs, trackDurationUs); - } - } int trackIndex = selectedTrackIndices[rendererIndex]; if (0 <= trackIndex && trackIndex < rendererTrackFormats.length) { renderer.enable(trackIndex, positionUs, false); enabledRenderers.add(renderer); allRenderersEnded = allRenderersEnded && renderer.isEnded(); - allRenderersReadyOrEnded = allRenderersReadyOrEnded && rendererReadyOrEnded(renderer); + allRenderersReadyOrEnded = allRenderersReadyOrEnded && isReadyOrEnded(renderer); } } } - this.durationUs = durationUs; - if (allRenderersEnded - && (durationUs == TrackRenderer.UNKNOWN_TIME_US || durationUs <= positionUs)) { + if (allRenderersEnded && (durationUs == C.UNKNOWN_TIME_US || durationUs <= positionUs)) { // We don't expect this case, but handle it anyway. state = ExoPlayer.STATE_ENDED; } else { - state = allRenderersReadyOrEnded ? ExoPlayer.STATE_READY : ExoPlayer.STATE_BUFFERING; + state = allRenderersReadyOrEnded && haveSufficientBuffer() ? ExoPlayer.STATE_READY + : ExoPlayer.STATE_BUFFERING; } // Fire an event indicating that the player has been prepared, passing the initial state and @@ -347,26 +338,17 @@ import java.util.concurrent.atomic.AtomicInteger; handler.sendEmptyMessage(MSG_DO_SOME_WORK); } - private boolean rendererReadyOrEnded(TrackRenderer renderer) { - if (renderer.isEnded()) { - return true; - } - if (!renderer.isReady()) { - return false; - } - if (state == ExoPlayer.STATE_READY) { - return true; - } - long rendererDurationUs = renderer.getDurationUs(); - long rendererBufferedPositionUs = renderer.getBufferedPositionUs(); + private boolean isReadyOrEnded(TrackRenderer renderer) { + return renderer.isReady() || renderer.isEnded(); + } + + private boolean haveSufficientBuffer() { long minBufferDurationUs = rebuffering ? minRebufferUs : minBufferUs; return minBufferDurationUs <= 0 - || rendererBufferedPositionUs == TrackRenderer.UNKNOWN_TIME_US - || rendererBufferedPositionUs == TrackRenderer.END_OF_TRACK_US - || rendererBufferedPositionUs >= positionUs + minBufferDurationUs - || (rendererDurationUs != TrackRenderer.UNKNOWN_TIME_US - && rendererDurationUs != TrackRenderer.MATCH_LONGEST_US - && rendererBufferedPositionUs >= rendererDurationUs); + || bufferedPositionUs == C.UNKNOWN_TIME_US + || bufferedPositionUs == C.END_OF_SOURCE_US + || bufferedPositionUs >= positionUs + minBufferDurationUs + || (durationUs != C.UNKNOWN_TIME_US && bufferedPositionUs >= durationUs); } private void setPlayWhenReadyInternal(boolean playWhenReady) throws ExoPlaybackException { @@ -415,14 +397,15 @@ import java.util.concurrent.atomic.AtomicInteger; elapsedRealtimeUs = SystemClock.elapsedRealtime() * 1000; } - private void doSomeWork() throws ExoPlaybackException { + private void doSomeWork() throws ExoPlaybackException, IOException { TraceUtil.beginSection("doSomeWork"); long operationStartTimeMs = SystemClock.elapsedRealtime(); - long bufferedPositionUs = durationUs != TrackRenderer.UNKNOWN_TIME_US ? durationUs - : Long.MAX_VALUE; + updatePositionUs(); + bufferedPositionUs = source.getBufferedPositionUs(); + source.continueBuffering(positionUs); + boolean allRenderersEnded = true; boolean allRenderersReadyOrEnded = true; - updatePositionUs(); for (int i = 0; i < enabledRenderers.size(); i++) { TrackRenderer renderer = enabledRenderers.get(i); // TODO: Each renderer should return the maximum delay before which it wishes to be @@ -430,40 +413,20 @@ import java.util.concurrent.atomic.AtomicInteger; // invocation of this method. renderer.doSomeWork(positionUs, elapsedRealtimeUs); allRenderersEnded = allRenderersEnded && renderer.isEnded(); - // Determine whether the renderer is ready (or ended). If it's not, throw an error that's // preventing the renderer from making progress, if such an error exists. - boolean rendererReadyOrEnded = rendererReadyOrEnded(renderer); + boolean rendererReadyOrEnded = isReadyOrEnded(renderer); if (!rendererReadyOrEnded) { renderer.maybeThrowError(); } allRenderersReadyOrEnded = allRenderersReadyOrEnded && rendererReadyOrEnded; - - if (bufferedPositionUs == TrackRenderer.UNKNOWN_TIME_US) { - // We've already encountered a track for which the buffered position is unknown. Hence the - // media buffer position unknown regardless of the buffered position of this track. - } else { - long rendererDurationUs = renderer.getDurationUs(); - long rendererBufferedPositionUs = renderer.getBufferedPositionUs(); - if (rendererBufferedPositionUs == TrackRenderer.UNKNOWN_TIME_US) { - bufferedPositionUs = TrackRenderer.UNKNOWN_TIME_US; - } else if (rendererBufferedPositionUs == TrackRenderer.END_OF_TRACK_US - || (rendererDurationUs != TrackRenderer.UNKNOWN_TIME_US - && rendererDurationUs != TrackRenderer.MATCH_LONGEST_US - && rendererBufferedPositionUs >= rendererDurationUs)) { - // This track is fully buffered. - } else { - bufferedPositionUs = Math.min(bufferedPositionUs, rendererBufferedPositionUs); - } - } } - this.bufferedPositionUs = bufferedPositionUs; - if (allRenderersEnded - && (durationUs == TrackRenderer.UNKNOWN_TIME_US || durationUs <= positionUs)) { + if (allRenderersEnded && (durationUs == C.UNKNOWN_TIME_US || durationUs <= positionUs)) { setState(ExoPlayer.STATE_ENDED); stopRenderers(); - } else if (state == ExoPlayer.STATE_BUFFERING && allRenderersReadyOrEnded) { + } else if (state == ExoPlayer.STATE_BUFFERING && allRenderersReadyOrEnded + && haveSufficientBuffer()) { setState(ExoPlayer.STATE_READY); if (playWhenReady) { startRenderers(); @@ -512,9 +475,9 @@ import java.util.concurrent.atomic.AtomicInteger; for (int i = 0; i < enabledRenderers.size(); i++) { TrackRenderer renderer = enabledRenderers.get(i); ensureStopped(renderer); - renderer.seekTo(positionUs); } setState(ExoPlayer.STATE_BUFFERING); + source.seekToUs(positionUs); handler.sendEmptyMessage(MSG_DO_SOME_WORK); } finally { pendingSeekCount.decrementAndGet(); @@ -544,21 +507,20 @@ import java.util.concurrent.atomic.AtomicInteger; return; } for (int i = 0; i < renderers.length; i++) { - TrackRenderer renderer = renderers[i]; - stopAndDisable(renderer); - release(renderer); + unprepare(renderers[i]); } - renderers = null; - rendererMediaClock = null; - rendererMediaClockSource = null; enabledRenderers.clear(); + source = null; } - private void stopAndDisable(TrackRenderer renderer) { + private void unprepare(TrackRenderer renderer) { try { ensureStopped(renderer); if (renderer.getState() == TrackRenderer.STATE_ENABLED) { renderer.disable(); + renderer.unprepare(); + } else if (renderer.getState() == TrackRenderer.STATE_PREPARED) { + renderer.unprepare(); } } catch (ExoPlaybackException e) { // There's nothing we can do. @@ -569,18 +531,6 @@ import java.util.concurrent.atomic.AtomicInteger; } } - private void release(TrackRenderer renderer) { - try { - renderer.release(); - } catch (ExoPlaybackException e) { - // There's nothing we can do. - Log.e(TAG, "Release failed.", e); - } catch (RuntimeException e) { - // Ditto. - Log.e(TAG, "Release failed.", e); - } - } - private void sendMessageInternal(int what, Object obj) throws ExoPlaybackException { try { @@ -612,9 +562,7 @@ import java.util.concurrent.atomic.AtomicInteger; TrackRenderer renderer = renderers[rendererIndex]; int rendererState = renderer.getState(); - if (rendererState == TrackRenderer.STATE_UNPREPARED - || rendererState == TrackRenderer.STATE_RELEASED - || renderer.getTrackCount() == 0) { + if (rendererState == TrackRenderer.STATE_UNPREPARED || renderer.getTrackCount() == 0) { return; } diff --git a/library/src/main/java/com/google/android/exoplayer/FrameworkSampleSource.java b/library/src/main/java/com/google/android/exoplayer/FrameworkSampleSource.java index eb54971a39..1cc8a0d99f 100644 --- a/library/src/main/java/com/google/android/exoplayer/FrameworkSampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/FrameworkSampleSource.java @@ -15,7 +15,6 @@ */ package com.google.android.exoplayer; -import com.google.android.exoplayer.SampleSource.SampleSourceReader; import com.google.android.exoplayer.drm.DrmInitData; import com.google.android.exoplayer.drm.DrmInitData.SchemeInitData; import com.google.android.exoplayer.extractor.ExtractorSampleSource; @@ -54,7 +53,7 @@ import java.util.UUID; // through use of a background thread, or through changes to the framework's MediaExtractor API). @Deprecated @TargetApi(16) -public final class FrameworkSampleSource implements SampleSource, SampleSourceReader { +public final class FrameworkSampleSource implements SampleSource { private static final int ALLOWED_FLAGS_MASK = C.SAMPLE_FLAG_SYNC | C.SAMPLE_FLAG_ENCRYPTED; @@ -72,13 +71,13 @@ public final class FrameworkSampleSource implements SampleSource, SampleSourceRe private final long fileDescriptorOffset; private final long fileDescriptorLength; - private IOException preparationError; private MediaExtractor extractor; private MediaFormat[] trackFormats; private boolean prepared; - private int remainingReleaseCount; + private long durationUs; + private int enabledTrackCount; private int[] trackStates; - private boolean[] pendingDiscontinuities; + private boolean[] pendingResets; private long lastSeekPositionUs; private long pendingSeekPositionUs; @@ -120,39 +119,39 @@ public final class FrameworkSampleSource implements SampleSource, SampleSourceRe } @Override - public SampleSourceReader register() { - remainingReleaseCount++; - return this; + public boolean prepare(long positionUs) throws IOException { + if (prepared) { + return true; + } + extractor = new MediaExtractor(); + if (context != null) { + extractor.setDataSource(context, uri, headers); + } else { + extractor.setDataSource(fileDescriptor, fileDescriptorOffset, fileDescriptorLength); + } + durationUs = C.UNKNOWN_TIME_US; + trackStates = new int[extractor.getTrackCount()]; + pendingResets = new boolean[trackStates.length]; + trackFormats = new MediaFormat[trackStates.length]; + for (int i = 0; i < trackStates.length; i++) { + trackFormats[i] = createMediaFormat(extractor.getTrackFormat(i)); + long trackDurationUs = trackFormats[i].durationUs; + if (trackDurationUs > durationUs) { + durationUs = trackDurationUs; + } + } + prepared = true; + return true; } @Override - public boolean prepare(long positionUs) { - if (!prepared) { - if (preparationError != null) { - return false; - } + public boolean isPrepared() { + return prepared; + } - extractor = new MediaExtractor(); - try { - if (context != null) { - extractor.setDataSource(context, uri, headers); - } else { - extractor.setDataSource(fileDescriptor, fileDescriptorOffset, fileDescriptorLength); - } - } catch (IOException e) { - preparationError = e; - return false; - } - - trackStates = new int[extractor.getTrackCount()]; - pendingDiscontinuities = new boolean[trackStates.length]; - trackFormats = new MediaFormat[trackStates.length]; - for (int i = 0; i < trackStates.length; i++) { - trackFormats[i] = createMediaFormat(extractor.getTrackFormat(i)); - } - prepared = true; - } - return true; + @Override + public long getDurationUs() { + return C.UNKNOWN_TIME_US; } @Override @@ -168,45 +167,40 @@ public final class FrameworkSampleSource implements SampleSource, SampleSourceRe } @Override - public void enable(int track, long positionUs) { + public void continueBuffering(long positionUs) { + // MediaExtractor takes care of buffering. Do nothing. + } + + @Override + public TrackStream enable(int track, long positionUs) { Assertions.checkState(prepared); Assertions.checkState(trackStates[track] == TRACK_STATE_DISABLED); + enabledTrackCount++; trackStates[track] = TRACK_STATE_ENABLED; extractor.selectTrack(track); seekToUsInternal(positionUs, positionUs != 0); + return new TrackStreamImpl(track); } - @Override - public boolean continueBuffering(int track, long positionUs) { - // MediaExtractor takes care of buffering and blocks until it has samples, so we can always - // return true here. Although note that the blocking behavior is itself as bug, as per the - // TODO further up this file. This method will need to return something else as part of fixing - // the TODO. - return true; - } - - @Override - public long readDiscontinuity(int track) { - if (pendingDiscontinuities[track]) { - pendingDiscontinuities[track] = false; + /* package */ long readReset(int track) { + if (pendingResets[track]) { + pendingResets[track] = false; return lastSeekPositionUs; } - return NO_DISCONTINUITY; + return TrackStream.NO_RESET; } - @Override - public int readData(int track, long positionUs, MediaFormatHolder formatHolder, - SampleHolder sampleHolder) { + /* package */ int readData(int track, MediaFormatHolder formatHolder, SampleHolder sampleHolder) { Assertions.checkState(prepared); Assertions.checkState(trackStates[track] != TRACK_STATE_DISABLED); - if (pendingDiscontinuities[track]) { - return NOTHING_READ; + if (pendingResets[track]) { + return TrackStream.NOTHING_READ; } if (trackStates[track] != TRACK_STATE_FORMAT_SENT) { formatHolder.format = trackFormats[track]; formatHolder.drmInitData = Util.SDK_INT >= 18 ? getDrmInitDataV18() : null; trackStates[track] = TRACK_STATE_FORMAT_SENT; - return FORMAT_READ; + return TrackStream.FORMAT_READ; } int extractorTrackIndex = extractor.getSampleTrackIndex(); if (extractorTrackIndex == track) { @@ -224,53 +218,53 @@ public final class FrameworkSampleSource implements SampleSource, SampleSourceRe } pendingSeekPositionUs = C.UNKNOWN_TIME_US; extractor.advance(); - return SAMPLE_READ; + return TrackStream.SAMPLE_READ; } else { - return extractorTrackIndex < 0 ? END_OF_STREAM : NOTHING_READ; + return extractorTrackIndex < 0 ? TrackStream.END_OF_STREAM : TrackStream.NOTHING_READ; } } - @Override - public void disable(int track) { + /* package */ void disable(int track) { Assertions.checkState(prepared); Assertions.checkState(trackStates[track] != TRACK_STATE_DISABLED); extractor.unselectTrack(track); - pendingDiscontinuities[track] = false; trackStates[track] = TRACK_STATE_DISABLED; - } - - @Override - public void maybeThrowError() throws IOException { - if (preparationError != null) { - throw preparationError; - } + pendingResets[track] = false; + enabledTrackCount--; } @Override public void seekToUs(long positionUs) { Assertions.checkState(prepared); + if (enabledTrackCount == 0) { + return; + } seekToUsInternal(positionUs, false); } @Override public long getBufferedPositionUs() { Assertions.checkState(prepared); + if (enabledTrackCount == 0) { + return C.END_OF_SOURCE_US; + } + long bufferedDurationUs = extractor.getCachedDuration(); if (bufferedDurationUs == -1) { - return TrackRenderer.UNKNOWN_TIME_US; - } else { - long sampleTime = extractor.getSampleTime(); - return sampleTime == -1 ? TrackRenderer.END_OF_TRACK_US : sampleTime + bufferedDurationUs; + return C.UNKNOWN_TIME_US; } + + long sampleTime = extractor.getSampleTime(); + return sampleTime == -1 ? C.END_OF_SOURCE_US : sampleTime + bufferedDurationUs; } @Override public void release() { - Assertions.checkState(remainingReleaseCount > 0); - if (--remainingReleaseCount == 0 && extractor != null) { + if (extractor != null) { extractor.release(); extractor = null; } + prepared = false; } @TargetApi(18) @@ -297,7 +291,7 @@ public final class FrameworkSampleSource implements SampleSource, SampleSourceRe extractor.seekTo(positionUs, MediaExtractor.SEEK_TO_PREVIOUS_SYNC); for (int i = 0; i < trackStates.length; ++i) { if (trackStates[i] != TRACK_STATE_DISABLED) { - pendingDiscontinuities[i] = true; + pendingResets[i] = true; } } } @@ -341,4 +335,43 @@ public final class FrameworkSampleSource implements SampleSource, SampleSourceRe return format.containsKey(key) ? format.getInteger(key) : MediaFormat.NO_VALUE; } + private final class TrackStreamImpl implements TrackStream { + + private final int track; + + public TrackStreamImpl(int track) { + this.track = track; + } + + @Override + public boolean isReady() { + // MediaExtractor takes care of buffering and blocks until it has samples, so we can always + // return true here. Although note that the blocking behavior is itself as bug, as per the + // TODO further up this file. This method will need to return something else as part of fixing + // the TODO. + return true; + } + + @Override + public void maybeThrowError() throws IOException { + // Do nothing. + } + + @Override + public long readReset() { + return FrameworkSampleSource.this.readReset(track); + } + + @Override + public int readData(MediaFormatHolder formatHolder, SampleHolder sampleHolder) { + return FrameworkSampleSource.this.readData(track, formatHolder, sampleHolder); + } + + @Override + public void disable() { + FrameworkSampleSource.this.disable(track); + } + + } + } diff --git a/library/src/main/java/com/google/android/exoplayer/MediaCodecAudioTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/MediaCodecAudioTrackRenderer.java index afe93e6a7a..fbef69b660 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaCodecAudioTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecAudioTrackRenderer.java @@ -99,15 +99,13 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer implem private long lastFeedElapsedRealtimeMs; /** - * @param source The upstream source from which the renderer obtains samples. * @param mediaCodecSelector A decoder selector. */ - public MediaCodecAudioTrackRenderer(SampleSource source, MediaCodecSelector mediaCodecSelector) { - this(source, mediaCodecSelector, null, true); + public MediaCodecAudioTrackRenderer(MediaCodecSelector mediaCodecSelector) { + this(mediaCodecSelector, null, true); } /** - * @param source The upstream source from which the renderer obtains samples. * @param mediaCodecSelector A decoder selector. * @param drmSessionManager For use with encrypted content. May be null if support for encrypted * content is not required. @@ -117,25 +115,23 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer implem * permitted to play clear regions of encrypted media files before {@code drmSessionManager} * has obtained the keys necessary to decrypt encrypted regions of the media. */ - public MediaCodecAudioTrackRenderer(SampleSource source, MediaCodecSelector mediaCodecSelector, + public MediaCodecAudioTrackRenderer(MediaCodecSelector mediaCodecSelector, DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys) { - this(source, mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, null, null); + this(mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, null, null); } /** - * @param source The upstream source from which the renderer obtains samples. * @param mediaCodecSelector A decoder selector. * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be * null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. */ - public MediaCodecAudioTrackRenderer(SampleSource source, MediaCodecSelector mediaCodecSelector, - Handler eventHandler, EventListener eventListener) { - this(source, mediaCodecSelector, null, true, eventHandler, eventListener); + public MediaCodecAudioTrackRenderer(MediaCodecSelector mediaCodecSelector, Handler eventHandler, + EventListener eventListener) { + this(mediaCodecSelector, null, true, eventHandler, eventListener); } /** - * @param source The upstream source from which the renderer obtains samples. * @param mediaCodecSelector A decoder selector. * @param drmSessionManager For use with encrypted content. May be null if support for encrypted * content is not required. @@ -148,15 +144,14 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer implem * null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. */ - public MediaCodecAudioTrackRenderer(SampleSource source, MediaCodecSelector mediaCodecSelector, + public MediaCodecAudioTrackRenderer(MediaCodecSelector mediaCodecSelector, DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys, Handler eventHandler, EventListener eventListener) { - this(source, mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, eventHandler, + this(mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, eventHandler, eventListener, null, AudioManager.STREAM_MUSIC); } /** - * @param source The upstream source from which the renderer obtains samples. * @param mediaCodecSelector A decoder selector. * @param drmSessionManager For use with encrypted content. May be null if support for encrypted * content is not required. @@ -172,11 +167,11 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer implem * default capabilities (no encoded audio passthrough support) should be assumed. * @param streamType The type of audio stream for the {@link AudioTrack}. */ - public MediaCodecAudioTrackRenderer(SampleSource source, MediaCodecSelector mediaCodecSelector, + public MediaCodecAudioTrackRenderer(MediaCodecSelector mediaCodecSelector, DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys, Handler eventHandler, EventListener eventListener, AudioCapabilities audioCapabilities, int streamType) { - super(source, mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, eventHandler, + super(mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, eventHandler, eventListener); this.eventListener = eventListener; this.audioSessionId = AudioTrack.SESSION_ID_NOT_SET; @@ -305,8 +300,8 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer implem } @Override - protected void onDiscontinuity(long positionUs) throws ExoPlaybackException { - super.onDiscontinuity(positionUs); + protected void onReset(long positionUs) throws ExoPlaybackException { + super.onReset(positionUs); audioTrack.reset(); currentPositionUs = positionUs; allowPositionDiscontinuity = true; diff --git a/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java index 0f1d42e5e1..077fd4f4b7 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer; import com.google.android.exoplayer.MediaCodecUtil.DecoderQueryException; +import com.google.android.exoplayer.SampleSource.TrackStream; import com.google.android.exoplayer.drm.DrmInitData; import com.google.android.exoplayer.drm.DrmSessionManager; import com.google.android.exoplayer.util.Assertions; @@ -230,7 +231,6 @@ public abstract class MediaCodecTrackRenderer extends SampleSourceTrackRenderer private boolean waitingForFirstSyncFrame; /** - * @param source The upstream source from which the renderer obtains samples. * @param mediaCodecSelector A decoder selector. * @param drmSessionManager For use with encrypted media. May be null if support for encrypted * media is not required. @@ -243,10 +243,9 @@ public abstract class MediaCodecTrackRenderer extends SampleSourceTrackRenderer * null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. */ - public MediaCodecTrackRenderer(SampleSource source, MediaCodecSelector mediaCodecSelector, + public MediaCodecTrackRenderer(MediaCodecSelector mediaCodecSelector, DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys, Handler eventHandler, EventListener eventListener) { - super(source); Assertions.checkState(Util.SDK_INT >= 16); this.mediaCodecSelector = Assertions.checkNotNull(mediaCodecSelector); this.drmSessionManager = drmSessionManager; @@ -451,7 +450,7 @@ public abstract class MediaCodecTrackRenderer extends SampleSourceTrackRenderer } @Override - protected void onDiscontinuity(long positionUs) throws ExoPlaybackException { + protected void onReset(long positionUs) throws ExoPlaybackException { sourceState = SOURCE_STATE_NOT_READY; inputStreamEnded = false; outputStreamEnded = false; @@ -477,7 +476,7 @@ public abstract class MediaCodecTrackRenderer extends SampleSourceTrackRenderer ? (sourceState == SOURCE_STATE_NOT_READY ? SOURCE_STATE_READY : sourceState) : SOURCE_STATE_NOT_READY; if (format == null) { - readFormat(positionUs); + readFormat(); } maybeInitCodec(); if (codec != null) { @@ -491,9 +490,9 @@ public abstract class MediaCodecTrackRenderer extends SampleSourceTrackRenderer codecCounters.ensureUpdated(); } - private void readFormat(long positionUs) throws ExoPlaybackException { - int result = readSource(positionUs, formatHolder, null); - if (result == SampleSource.FORMAT_READ) { + private void readFormat() throws ExoPlaybackException { + int result = readSource(formatHolder, null); + if (result == TrackStream.FORMAT_READ) { onInputFormatChanged(formatHolder); } } @@ -568,7 +567,7 @@ public abstract class MediaCodecTrackRenderer extends SampleSourceTrackRenderer int result; if (waitingForKeys) { // We've already read an encrypted sample into sampleHolder, and are waiting for keys. - result = SampleSource.SAMPLE_READ; + result = TrackStream.SAMPLE_READ; } else { // For adaptive reconfiguration OMX decoders expect all reconfiguration data to be supplied // at the start of the buffer that also contains the first frame in the new format. @@ -579,16 +578,16 @@ public abstract class MediaCodecTrackRenderer extends SampleSourceTrackRenderer } codecReconfigurationState = RECONFIGURATION_STATE_QUEUE_PENDING; } - result = readSource(positionUs, formatHolder, sampleHolder); - if (firstFeed && sourceState == SOURCE_STATE_READY && result == SampleSource.NOTHING_READ) { + result = readSource(formatHolder, sampleHolder); + if (firstFeed && sourceState == SOURCE_STATE_READY && result == TrackStream.NOTHING_READ) { sourceState = SOURCE_STATE_READY_READ_MAY_FAIL; } } - if (result == SampleSource.NOTHING_READ) { + if (result == TrackStream.NOTHING_READ) { return false; } - if (result == SampleSource.FORMAT_READ) { + if (result == TrackStream.FORMAT_READ) { if (codecReconfigurationState == RECONFIGURATION_STATE_QUEUE_PENDING) { // We received two formats in a row. Clear the current buffer of any reconfiguration data // associated with the first format. @@ -598,7 +597,7 @@ public abstract class MediaCodecTrackRenderer extends SampleSourceTrackRenderer onInputFormatChanged(formatHolder); return true; } - if (result == SampleSource.END_OF_STREAM) { + if (result == TrackStream.END_OF_STREAM) { if (codecReconfigurationState == RECONFIGURATION_STATE_QUEUE_PENDING) { // We received a new format immediately before the end of the stream. We need to clear // the corresponding reconfiguration data from the current buffer, but re-write it into diff --git a/library/src/main/java/com/google/android/exoplayer/MediaCodecVideoTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/MediaCodecVideoTrackRenderer.java index d5e82482ff..9f12c6702a 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaCodecVideoTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecVideoTrackRenderer.java @@ -128,34 +128,30 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer { /** * @param context A context. - * @param source The upstream source from which the renderer obtains samples. * @param mediaCodecSelector A decoder selector. * @param videoScalingMode The scaling mode to pass to * {@link MediaCodec#setVideoScalingMode(int)}. */ - public MediaCodecVideoTrackRenderer(Context context, SampleSource source, - MediaCodecSelector mediaCodecSelector, int videoScalingMode) { - this(context, source, mediaCodecSelector, videoScalingMode, 0); + public MediaCodecVideoTrackRenderer(Context context, MediaCodecSelector mediaCodecSelector, + int videoScalingMode) { + this(context, mediaCodecSelector, videoScalingMode, 0); } /** * @param context A context. - * @param source The upstream source from which the renderer obtains samples. * @param mediaCodecSelector A decoder selector. * @param videoScalingMode The scaling mode to pass to * {@link MediaCodec#setVideoScalingMode(int)}. * @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer * can attempt to seamlessly join an ongoing playback. */ - public MediaCodecVideoTrackRenderer(Context context, SampleSource source, - MediaCodecSelector mediaCodecSelector, int videoScalingMode, long allowedJoiningTimeMs) { - this(context, source, mediaCodecSelector, videoScalingMode, allowedJoiningTimeMs, null, null, - -1); + public MediaCodecVideoTrackRenderer(Context context, MediaCodecSelector mediaCodecSelector, + int videoScalingMode, long allowedJoiningTimeMs) { + this(context, mediaCodecSelector, videoScalingMode, allowedJoiningTimeMs, null, null, -1); } /** * @param context A context. - * @param source The upstream source from which the renderer obtains samples. * @param mediaCodecSelector A decoder selector. * @param videoScalingMode The scaling mode to pass to * {@link MediaCodec#setVideoScalingMode(int)}. @@ -167,16 +163,15 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer { * @param maxDroppedFrameCountToNotify The maximum number of frames that can be dropped between * invocations of {@link EventListener#onDroppedFrames(int, long)}. */ - public MediaCodecVideoTrackRenderer(Context context, SampleSource source, - MediaCodecSelector mediaCodecSelector, int videoScalingMode, long allowedJoiningTimeMs, - Handler eventHandler, EventListener eventListener, int maxDroppedFrameCountToNotify) { - this(context, source, mediaCodecSelector, videoScalingMode, allowedJoiningTimeMs, null, false, + public MediaCodecVideoTrackRenderer(Context context, MediaCodecSelector mediaCodecSelector, + int videoScalingMode, long allowedJoiningTimeMs, Handler eventHandler, + EventListener eventListener, int maxDroppedFrameCountToNotify) { + this(context, mediaCodecSelector, videoScalingMode, allowedJoiningTimeMs, null, false, eventHandler, eventListener, maxDroppedFrameCountToNotify); } /** * @param context A context. - * @param source The upstream source from which the renderer obtains samples. * @param mediaCodecSelector A decoder selector. * @param videoScalingMode The scaling mode to pass to * {@link MediaCodec#setVideoScalingMode(int)}. @@ -195,11 +190,11 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer { * @param maxDroppedFrameCountToNotify The maximum number of frames that can be dropped between * invocations of {@link EventListener#onDroppedFrames(int, long)}. */ - public MediaCodecVideoTrackRenderer(Context context, SampleSource source, - MediaCodecSelector mediaCodecSelector, int videoScalingMode, long allowedJoiningTimeMs, - DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys, - Handler eventHandler, EventListener eventListener, int maxDroppedFrameCountToNotify) { - super(source, mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, eventHandler, + public MediaCodecVideoTrackRenderer(Context context, MediaCodecSelector mediaCodecSelector, + int videoScalingMode, long allowedJoiningTimeMs, DrmSessionManager drmSessionManager, + boolean playClearSamplesWithoutKeys, Handler eventHandler, EventListener eventListener, + int maxDroppedFrameCountToNotify) { + super(mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, eventHandler, eventListener); this.frameReleaseTimeHelper = new VideoFrameReleaseTimeHelper(context); this.videoScalingMode = videoScalingMode; @@ -235,8 +230,8 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer { } @Override - protected void onDiscontinuity(long positionUs) throws ExoPlaybackException { - super.onDiscontinuity(positionUs); + protected void onReset(long positionUs) throws ExoPlaybackException { + super.onReset(positionUs); renderedFirstFrame = false; consecutiveDroppedFrameCount = 0; joiningDeadlineUs = -1; diff --git a/library/src/main/java/com/google/android/exoplayer/MediaFormat.java b/library/src/main/java/com/google/android/exoplayer/MediaFormat.java index 9978221a74..6722fbb099 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaFormat.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaFormat.java @@ -58,9 +58,7 @@ public final class MediaFormat { */ public final int maxInputSize; /** - * The duration in microseconds, or {@link C#UNKNOWN_TIME_US} if the duration is unknown, or - * {@link C#MATCH_LONGEST_US} if the duration should match the duration of the longest track whose - * duration is known. + * The duration in microseconds, or {@link C#UNKNOWN_TIME_US} if the duration is unknown. */ public final long durationUs; /** diff --git a/library/src/main/java/com/google/android/exoplayer/MultiSampleSource.java b/library/src/main/java/com/google/android/exoplayer/MultiSampleSource.java new file mode 100644 index 0000000000..dc36db96f4 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/MultiSampleSource.java @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2014 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.exoplayer; + +import java.io.IOException; + +/** + * Combines multiple {@link SampleSource} instances. + */ +public class MultiSampleSource implements SampleSource { + + private final SampleSource[] sources; + + private boolean prepared; + private long durationUs; + private SampleSource[] trackSources; + private int[] trackIndices; + + public MultiSampleSource(SampleSource... sources) { + this.sources = sources; + } + + @Override + public boolean prepare(long positionUs) throws IOException { + if (this.prepared) { + return true; + } + boolean prepared = true; + for (int i = 0; i < sources.length; i++) { + prepared &= sources[i].prepare(positionUs); + } + if (prepared) { + this.prepared = true; + this.durationUs = C.UNKNOWN_TIME_US; + int trackCount = 0; + for (int i = 0; i < sources.length; i++) { + trackCount += sources[i].getTrackCount(); + if (sources[i].getDurationUs() > durationUs) { + durationUs = sources[i].getDurationUs(); + } + } + trackSources = new SampleSource[trackCount]; + trackIndices = new int[trackCount]; + int index = 0; + for (int i = 0; i < sources.length; i++) { + int thisSourceTrackCount = sources[i].getTrackCount(); + for (int j = 0; j < thisSourceTrackCount; j++) { + trackSources[index] = sources[i]; + trackIndices[index++] = j; + } + } + } + return prepared; + } + + @Override + public boolean isPrepared() { + return prepared; + } + + @Override + public int getTrackCount() { + return trackSources.length; + } + + @Override + public MediaFormat getFormat(int track) { + return trackSources[track].getFormat(trackIndices[track]); + } + + @Override + public TrackStream enable(int track, long positionUs) { + return trackSources[track].enable(trackIndices[track], positionUs); + } + + @Override + public void continueBuffering(long positionUs) { + for (int i = 0; i < sources.length; i++) { + sources[i].continueBuffering(positionUs); + } + } + + @Override + public void seekToUs(long positionUs) { + for (int i = 0; i < sources.length; i++) { + sources[i].seekToUs(positionUs); + } + } + + @Override + public long getDurationUs() { + return durationUs; + } + + @Override + public long getBufferedPositionUs() { + long bufferedPositionUs = durationUs != C.UNKNOWN_TIME_US ? durationUs : Long.MAX_VALUE; + for (int i = 0; i < sources.length; i++) { + long rendererBufferedPositionUs = sources[i].getBufferedPositionUs(); + if (rendererBufferedPositionUs == C.UNKNOWN_TIME_US) { + return C.UNKNOWN_TIME_US; + } else if (rendererBufferedPositionUs == C.END_OF_SOURCE_US) { + // This source is fully buffered. + } else { + bufferedPositionUs = Math.min(bufferedPositionUs, rendererBufferedPositionUs); + } + } + return bufferedPositionUs == Long.MAX_VALUE ? C.UNKNOWN_TIME_US : bufferedPositionUs; + } + + @Override + public void release() { + for (int i = 0; i < sources.length; i++) { + sources[i].release(); + } + prepared = false; + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/SampleSource.java b/library/src/main/java/com/google/android/exoplayer/SampleSource.java index c122532d56..4c30a9d5d3 100644 --- a/library/src/main/java/com/google/android/exoplayer/SampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/SampleSource.java @@ -18,196 +18,189 @@ package com.google.android.exoplayer; import java.io.IOException; /** - * A source of media samples. + * A source of media. *

* A {@link SampleSource} may expose one or multiple tracks. The number of tracks and each track's - * media format can be queried using {@link SampleSourceReader#getTrackCount()} and - * {@link SampleSourceReader#getFormat(int)} respectively. + * media format can be queried using {@link #getTrackCount()} and {@link #getFormat(int)} + * respectively. */ public interface SampleSource { /** - * The end of stream has been reached. - */ - public static final int END_OF_STREAM = -1; - /** - * Neither a sample nor a format was read in full. This may be because insufficient data is - * buffered upstream. If multiple tracks are enabled, this return value may indicate that the - * next piece of data to be returned from the {@link SampleSource} corresponds to a different - * track than the one for which data was requested. - */ - public static final int NOTHING_READ = -2; - /** - * A sample was read. - */ - public static final int SAMPLE_READ = -3; - /** - * A format was read. - */ - public static final int FORMAT_READ = -4; - /** - * Returned from {@link SampleSourceReader#readDiscontinuity(int)} to indicate no discontinuity. - */ - public static final long NO_DISCONTINUITY = Long.MIN_VALUE; - - /** - * A consumer of samples should call this method to register themselves and gain access to the - * source through the returned {@link SampleSourceReader}. + * Prepares the source. *

- * {@link SampleSourceReader#release()} should be called on the returned object when access is no - * longer required. + * If preparation cannot complete immediately then the call will return {@code false} rather than + * block. The method can be called repeatedly until the return value indicates success. * - * @return A {@link SampleSourceReader} that provides access to the source. + * @param positionUs The player's current playback position. + * @return True if the source was prepared, false otherwise. + * @throws IOException If there's an error preparing the source. */ - public SampleSourceReader register(); + boolean prepare(long positionUs) throws IOException; /** - * An interface providing read access to a {@link SampleSource}. + * Returns whether the source is prepared. + * + * @return True if the source is prepared. False otherwise. */ - public interface SampleSourceReader { + boolean isPrepared(); + + /** + * Returns the duration of the source. + *

+ * This method should only be called after the source has been prepared. + * + * @return The duration of the source in microseconds, or {@link C#UNKNOWN_TIME_US} if the + * duration is not known. + */ + long getDurationUs(); + + /** + * Returns the number of tracks exposed by the source. + *

+ * This method should only be called after the source has been prepared. + * + * @return The number of tracks. + */ + int getTrackCount(); + + /** + * Returns the format of the specified track. + *

+ * Note that whilst the format of a track will remain constant, the format of the actual media + * stream may change dynamically. An example of this is where the track is adaptive (i.e. + * {@link MediaFormat#adaptive} is true). Hence the track formats returned through this method + * should not be used to configure decoders. Decoder configuration should be performed using the + * formats obtained when reading the media stream through calls to + * {@link TrackStream#readData(MediaFormatHolder, SampleHolder)}. + *

+ * This method should only be called after the source has been prepared. + * + * @param track The track index. + * @return The format of the specified track. + */ + MediaFormat getFormat(int track); + + /** + * Indicates to the source that it should continue buffering data for its enabled tracks. + *

+ * This method should only be called after the source has been prepared. + * + * @param positionUs The current playback position. + */ + void continueBuffering(long positionUs); + + /** + * Returns an estimate of the position up to which data is buffered for the enabled tracks. + *

+ * This method should only be called after the source has been prepared. + * + * @return An estimate of the absolute position in microseconds up to which data is buffered, + * or {@link C#END_OF_SOURCE_US} if the track is fully buffered, or {@link C#UNKNOWN_TIME_US} + * if no estimate is available. If no tracks are enabled then {@link C#END_OF_SOURCE_US} is + * returned. + */ + long getBufferedPositionUs(); + + /** + * Seeks to the specified time in microseconds. + *

+ * This method should only be called after the source has been prepared. + * + * @param positionUs The seek position in microseconds. + */ + void seekToUs(long positionUs); + + /** + * Enables the specified track. Returning a {@link TrackStream} from which the track's data can + * be read. + *

+ * This method should only be called after the source has been prepared, and when the specified + * track is disabled. + * + * @param track The track to enable. + * @param positionUs The current playback position in microseconds. + * @return A {@link TrackStream} from which the enabled track's data can be read. + */ + TrackStream enable(int track, long positionUs); + + /** + * Releases the source. + *

+ * This method should be called when the source is no longer required. + */ + void release(); + + /** + * A stream of data corresponding to a single {@link SampleSource} track. + */ + interface TrackStream { /** - * If the source is currently having difficulty preparing or loading samples, then this method - * throws the underlying error. Otherwise does nothing. + * The end of stream has been reached. + */ + static final int END_OF_STREAM = -1; + /** + * Nothing was read. + */ + static final int NOTHING_READ = -2; + /** + * A sample was read. + */ + static final int SAMPLE_READ = -3; + /** + * A format was read. + */ + static final int FORMAT_READ = -4; + /** + * Returned from {@link #readReset()} to indicate no reset is required. + */ + static final long NO_RESET = Long.MIN_VALUE; + + /** + * Returns whether data is available to be read. + *

+ * Note: If the stream has ended then {@link #END_OF_STREAM} can always be read from + * {@link #readData(MediaFormatHolder, SampleHolder)}. Hence an ended stream is always ready. + * + * @return True if data is available to be read. False otherwise. + */ + boolean isReady(); + + /** + * If there's an underlying error preventing data from being read, it's thrown by this method. + * If not, this method does nothing. * * @throws IOException The underlying error. */ - public void maybeThrowError() throws IOException; + void maybeThrowError() throws IOException; /** - * Prepares the source. - *

- * Preparation may require reading from the data source (e.g. to determine the available tracks - * and formats). If insufficient data is available then the call will return {@code false} - * rather than block. The method can be called repeatedly until the return value indicates - * success. + * Attempts to read a pending reset. * - * @param positionUs The player's current playback position. - * @return True if the source was prepared, false otherwise. + * @return If a reset was read then the position after the reset. Else {@link #NO_RESET}. */ - public boolean prepare(long positionUs); + long readReset(); /** - * Returns the number of tracks exposed by the source. - *

- * This method should only be called after the source has been prepared. - * - * @return The number of tracks. - */ - public int getTrackCount(); - - /** - * Returns the format of the specified track. - *

- * Note that whilst the format of a track will remain constant, the format of the actual media - * stream may change dynamically. An example of this is where the track is adaptive - * (i.e. @link {@link MediaFormat#adaptive} is true). Hence the track formats returned through - * this method should not be used to configure decoders. Decoder configuration should be - * performed using the formats obtained when reading the media stream through calls to - * {@link #readData(int, long, MediaFormatHolder, SampleHolder)}. - *

- * This method should only be called after the source has been prepared. - * - * @param track The track index. - * @return The format of the specified track. - */ - public MediaFormat getFormat(int track); - - /** - * Enable the specified track. This allows the track's format and samples to be read from - * {@link #readData(int, long, MediaFormatHolder, SampleHolder)}. - *

- * This method should only be called after the source has been prepared, and when the specified - * track is disabled. - * - * @param track The track to enable. - * @param positionUs The player's current playback position. - */ - public void enable(int track, long positionUs); - - /** - * Indicates to the source that it should still be buffering data for the specified track. - *

- * This method should only be called when the specified track is enabled. - * - * @param track The track to continue buffering. - * @param positionUs The current playback position. - * @return True if the track has available samples, or if the end of the stream has been - * reached. False if more data needs to be buffered for samples to become available. - */ - public boolean continueBuffering(int track, long positionUs); - - /** - * Attempts to read a pending discontinuity from the source. - *

- * This method should only be called when the specified track is enabled. - * - * @param track The track from which to read. - * @return If a discontinuity was read then the playback position after the discontinuity. Else - * {@link #NO_DISCONTINUITY}. - */ - public long readDiscontinuity(int track); - - /** - * Attempts to read a sample or a new format from the source. - *

- * This method should only be called when the specified track is enabled. - *

- * Note that where multiple tracks are enabled, {@link #NOTHING_READ} may be returned if the - * next piece of data to be read from the {@link SampleSource} corresponds to a different track - * than the one for which data was requested. + * Attempts to read the next format or sample. *

* This method will always return {@link #NOTHING_READ} in the case that there's a pending - * discontinuity to be read from {@link #readDiscontinuity(int)} for the specified track. + * discontinuity to be read from {@link #readReset} for the specified track. * - * @param track The track from which to read. - * @param positionUs The current playback position. - * @param formatHolder A {@link MediaFormatHolder} object to populate in the case of a new - * format. - * @param sampleHolder A {@link SampleHolder} object to populate in the case of a new sample. - * If the caller requires the sample data then it must ensure that {@link SampleHolder#data} + * @param formatHolder A {@link MediaFormatHolder} to populate in the case of a new format. + * @param sampleHolder A {@link SampleHolder} to populate in the case of a new sample. If the + * caller requires the sample data then it must ensure that {@link SampleHolder#data} * references a valid output buffer. - * @return The result, which can be {@link #SAMPLE_READ}, {@link #FORMAT_READ}, - * {@link #NOTHING_READ} or {@link #END_OF_STREAM}. + * @return The result, which can be {@link #END_OF_STREAM}, {@link #NOTHING_READ}, + * {@link #FORMAT_READ} or {@link #SAMPLE_READ}. */ - public int readData(int track, long positionUs, MediaFormatHolder formatHolder, - SampleHolder sampleHolder); + int readData(MediaFormatHolder formatHolder, SampleHolder sampleHolder); /** - * Seeks to the specified time in microseconds. - *

- * This method should only be called when at least one track is enabled. - * - * @param positionUs The seek position in microseconds. + * Disables the track. */ - public void seekToUs(long positionUs); - - /** - * Returns an estimate of the position up to which data is buffered. - *

- * This method should only be called when at least one track is enabled. - * - * @return An estimate of the absolute position in microseconds up to which data is buffered, - * or {@link TrackRenderer#END_OF_TRACK_US} if data is buffered to the end of the stream, - * or {@link TrackRenderer#UNKNOWN_TIME_US} if no estimate is available. - */ - public long getBufferedPositionUs(); - - /** - * Disable the specified track. - *

- * This method should only be called when the specified track is enabled. - * - * @param track The track to disable. - */ - public void disable(int track); - - /** - * Releases the {@link SampleSourceReader}. - *

- * This method should be called when access to the {@link SampleSource} is no longer required. - */ - public void release(); + void disable(); } diff --git a/library/src/main/java/com/google/android/exoplayer/SampleSourceTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/SampleSourceTrackRenderer.java index 08edae3bc3..cd97fa3329 100644 --- a/library/src/main/java/com/google/android/exoplayer/SampleSourceTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/SampleSourceTrackRenderer.java @@ -16,7 +16,7 @@ package com.google.android.exoplayer; import com.google.android.exoplayer.MediaCodecUtil.DecoderQueryException; -import com.google.android.exoplayer.SampleSource.SampleSourceReader; +import com.google.android.exoplayer.SampleSource.TrackStream; import java.io.IOException; import java.util.Arrays; @@ -27,167 +27,78 @@ import java.util.Arrays; */ public abstract class SampleSourceTrackRenderer extends TrackRenderer { - private final SampleSourceReader[] sources; - - private int[] handledSourceIndices; - private int[] handledSourceTrackIndices; - - private SampleSourceReader enabledSource; - private int enabledSourceTrackIndex; - - private long durationUs; - - /** - * @param sources One or more upstream sources from which the renderer can obtain samples. - */ - public SampleSourceTrackRenderer(SampleSource... sources) { - this.sources = new SampleSourceReader[sources.length]; - for (int i = 0; i < sources.length; i++) { - this.sources[i] = sources[i].register(); - } - } + private SampleSource source; + private TrackStream trackStream; + private int[] handledTrackIndices; @Override - protected final boolean doPrepare(long positionUs) throws ExoPlaybackException { - boolean allSourcesPrepared = true; - for (int i = 0; i < sources.length; i++) { - allSourcesPrepared &= sources[i].prepare(positionUs); - } - if (!allSourcesPrepared) { - return false; - } - // The sources are all prepared. - int totalSourceTrackCount = 0; - for (int i = 0; i < sources.length; i++) { - totalSourceTrackCount += sources[i].getTrackCount(); - } - long durationUs = 0; + protected final void doPrepare(SampleSource source) throws ExoPlaybackException { + int sourceTrackCount = source.getTrackCount(); + int[] handledTrackIndices = new int[sourceTrackCount]; int handledTrackCount = 0; - int[] handledSourceIndices = new int[totalSourceTrackCount]; - int[] handledTrackIndices = new int[totalSourceTrackCount]; - int sourceCount = sources.length; - for (int sourceIndex = 0; sourceIndex < sourceCount; sourceIndex++) { - SampleSourceReader source = sources[sourceIndex]; - int sourceTrackCount = source.getTrackCount(); - for (int trackIndex = 0; trackIndex < sourceTrackCount; trackIndex++) { - MediaFormat format = source.getFormat(trackIndex); - boolean handlesTrack; - try { - handlesTrack = handlesTrack(format); - } catch (DecoderQueryException e) { - throw new ExoPlaybackException(e); - } - if (handlesTrack) { - handledSourceIndices[handledTrackCount] = sourceIndex; - handledTrackIndices[handledTrackCount] = trackIndex; - handledTrackCount++; - if (durationUs == TrackRenderer.UNKNOWN_TIME_US) { - // We've already encountered a track for which the duration is unknown, so the media - // duration is unknown regardless of the duration of this track. - } else { - long trackDurationUs = format.durationUs; - if (trackDurationUs == TrackRenderer.UNKNOWN_TIME_US) { - durationUs = TrackRenderer.UNKNOWN_TIME_US; - } else if (trackDurationUs == TrackRenderer.MATCH_LONGEST_US) { - // Do nothing. - } else { - durationUs = Math.max(durationUs, trackDurationUs); - } - } - } + for (int trackIndex = 0; trackIndex < sourceTrackCount; trackIndex++) { + MediaFormat format = source.getFormat(trackIndex); + boolean handlesTrack; + try { + handlesTrack = handlesTrack(format); + } catch (DecoderQueryException e) { + throw new ExoPlaybackException(e); + } + if (handlesTrack) { + handledTrackIndices[handledTrackCount] = trackIndex; + handledTrackCount++; } } - this.durationUs = durationUs; - this.handledSourceIndices = Arrays.copyOf(handledSourceIndices, handledTrackCount); - this.handledSourceTrackIndices = Arrays.copyOf(handledTrackIndices, handledTrackCount); - return true; + this.source = source; + this.handledTrackIndices = Arrays.copyOf(handledTrackIndices, handledTrackCount); } @Override protected void onEnabled(int track, long positionUs, boolean joining) throws ExoPlaybackException { - positionUs = shiftInputPosition(positionUs); - enabledSource = sources[handledSourceIndices[track]]; - enabledSourceTrackIndex = handledSourceTrackIndices[track]; - enabledSource.enable(enabledSourceTrackIndex, positionUs); - onDiscontinuity(positionUs); - } - - @Override - protected final void seekTo(long positionUs) throws ExoPlaybackException { - positionUs = shiftInputPosition(positionUs); - enabledSource.seekToUs(positionUs); - checkForDiscontinuity(positionUs); + trackStream = source.enable(handledTrackIndices[track], positionUs); + onReset(positionUs); } @Override protected final void doSomeWork(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { - positionUs = shiftInputPosition(positionUs); - boolean sourceIsReady = enabledSource.continueBuffering(enabledSourceTrackIndex, positionUs); - positionUs = checkForDiscontinuity(positionUs); - doSomeWork(positionUs, elapsedRealtimeUs, sourceIsReady); + // TODO[REFACTOR]: Consider splitting reading of resets into a separate method? + long resetPositionUs = trackStream.readReset(); + if (resetPositionUs != TrackStream.NO_RESET) { + onReset(resetPositionUs); + return; + } + doSomeWork(positionUs, elapsedRealtimeUs, trackStream.isReady()); } @Override - protected long getBufferedPositionUs() { - return enabledSource.getBufferedPositionUs(); - } - - @Override - protected long getDurationUs() { - return durationUs; - } - - @Override - protected void maybeThrowError() throws ExoPlaybackException { - if (enabledSource != null) { - maybeThrowError(enabledSource); - } else { - int sourceCount = sources.length; - for (int i = 0; i < sourceCount; i++) { - maybeThrowError(sources[i]); - } + protected void maybeThrowError() throws IOException { + if (source != null) { + trackStream.maybeThrowError(); } } @Override protected void onDisabled() throws ExoPlaybackException { - enabledSource.disable(enabledSourceTrackIndex); - enabledSource = null; + trackStream.disable(); + trackStream = null; } @Override - protected void onReleased() throws ExoPlaybackException { - int sourceCount = sources.length; - for (int i = 0; i < sourceCount; i++) { - sources[i].release(); - } + protected void onUnprepared() { + source = null; + handledTrackIndices = null; } @Override protected final int getTrackCount() { - return handledSourceTrackIndices.length; + return handledTrackIndices.length; } @Override protected final MediaFormat getFormat(int track) { - SampleSourceReader source = sources[handledSourceIndices[track]]; - return source.getFormat(handledSourceTrackIndices[track]); - } - - /** - * Shifts positions passed to {@link #onEnabled(int, long, boolean)}, {@link #seekTo(long)} and - * {@link #doSomeWork(long, long)}. - *

- * The default implementation does not modify the position. Except in very specific cases, - * subclasses should not override this method. - * - * @param positionUs The position in microseconds. - * @return The adjusted position in microseconds. - */ - protected long shiftInputPosition(long positionUs) { - return positionUs; + return source.getFormat(handledTrackIndices[track]); } // Methods to be called by subclasses. @@ -195,18 +106,16 @@ public abstract class SampleSourceTrackRenderer extends TrackRenderer { /** * Reads from the enabled upstream source. * - * @param positionUs The current playback position. * @param formatHolder A {@link MediaFormatHolder} object to populate in the case of a new format. * @param sampleHolder A {@link SampleHolder} object to populate in the case of a new sample. * If the caller requires the sample data then it must ensure that {@link SampleHolder#data} * references a valid output buffer. - * @return The result, which can be {@link SampleSource#SAMPLE_READ}, - * {@link SampleSource#FORMAT_READ}, {@link SampleSource#NOTHING_READ} or - * {@link SampleSource#END_OF_STREAM}. + * @return The result, which can be {@link TrackStream#SAMPLE_READ}, + * {@link TrackStream#FORMAT_READ}, {@link TrackStream#NOTHING_READ} or + * {@link TrackStream#END_OF_STREAM}. */ - protected final int readSource(long positionUs, MediaFormatHolder formatHolder, - SampleHolder sampleHolder) { - return enabledSource.readData(enabledSourceTrackIndex, positionUs, formatHolder, sampleHolder); + protected final int readSource(MediaFormatHolder formatHolder, SampleHolder sampleHolder) { + return trackStream.readData(formatHolder, sampleHolder); } // Abstract methods. @@ -221,14 +130,12 @@ public abstract class SampleSourceTrackRenderer extends TrackRenderer { protected abstract boolean handlesTrack(MediaFormat mediaFormat) throws DecoderQueryException; /** - * Invoked when a discontinuity is encountered. Also invoked when the renderer is enabled, for - * convenience. + * Invoked when a reset is encountered. Also invoked when the renderer is enabled. * - * @param positionUs The playback position after the discontinuity, or the position at which - * the renderer is being enabled. - * @throws ExoPlaybackException If an error occurs handling the discontinuity. + * @param positionUs The playback position in microseconds. + * @throws ExoPlaybackException If an error occurs handling the reset. */ - protected abstract void onDiscontinuity(long positionUs) throws ExoPlaybackException; + protected abstract void onReset(long positionUs) throws ExoPlaybackException; /** * Called by {@link #doSomeWork(long, long)}. @@ -237,31 +144,11 @@ public abstract class SampleSourceTrackRenderer extends TrackRenderer { * 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. - * @param sourceIsReady The result of the most recent call to - * {@link SampleSourceReader#continueBuffering(int, long)}. + * @param sourceIsReady The result of the most recent call to {@link TrackStream#isReady()}. * @throws ExoPlaybackException If an error occurs. * @throws ExoPlaybackException */ protected abstract void doSomeWork(long positionUs, long elapsedRealtimeUs, boolean sourceIsReady) throws ExoPlaybackException; - // Private methods. - - private long checkForDiscontinuity(long positionUs) throws ExoPlaybackException { - long discontinuityPositionUs = enabledSource.readDiscontinuity(enabledSourceTrackIndex); - if (discontinuityPositionUs != SampleSource.NO_DISCONTINUITY) { - onDiscontinuity(discontinuityPositionUs); - return discontinuityPositionUs; - } - return positionUs; - } - - private void maybeThrowError(SampleSourceReader source) throws ExoPlaybackException { - try { - source.maybeThrowError(); - } catch (IOException e) { - throw new ExoPlaybackException(e); - } - } - } diff --git a/library/src/main/java/com/google/android/exoplayer/SingleSampleSource.java b/library/src/main/java/com/google/android/exoplayer/SingleSampleSource.java index ec7a8dcbb7..16ddd6f428 100644 --- a/library/src/main/java/com/google/android/exoplayer/SingleSampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/SingleSampleSource.java @@ -15,7 +15,7 @@ */ package com.google.android.exoplayer; -import com.google.android.exoplayer.SampleSource.SampleSourceReader; +import com.google.android.exoplayer.SampleSource.TrackStream; import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DataSpec; import com.google.android.exoplayer.upstream.Loader; @@ -31,7 +31,7 @@ import java.util.Arrays; /** * A {@link SampleSource} that loads the data at a given {@link Uri} as a single sample. */ -public final class SingleSampleSource implements SampleSource, SampleSourceReader, Loader.Callback, +public final class SingleSampleSource implements SampleSource, TrackStream, Loader.Callback, Loadable { /** @@ -77,8 +77,10 @@ public final class SingleSampleSource implements SampleSource, SampleSourceReade } @Override - public SampleSourceReader register() { - return this; + public void maybeThrowError() throws IOException { + if (currentLoadableException != null && currentLoadableExceptionCount > minLoadableRetryCount) { + throw currentLoadableException; + } } @Override @@ -89,6 +91,16 @@ public final class SingleSampleSource implements SampleSource, SampleSourceReade return true; } + @Override + public boolean isPrepared() { + return loader != null; + } + + @Override + public long getDurationUs() { + return format.durationUs; + } + @Override public int getTrackCount() { return 1; @@ -100,33 +112,30 @@ public final class SingleSampleSource implements SampleSource, SampleSourceReade } @Override - public void enable(int track, long positionUs) { + public TrackStream enable(int track, long positionUs) { state = STATE_SEND_FORMAT; clearCurrentLoadableException(); maybeStartLoading(); + return this; + } + + @Override + public void continueBuffering(long positionUs) { + maybeStartLoading(); } @Override - public boolean continueBuffering(int track, long positionUs) { - maybeStartLoading(); + public boolean isReady() { return loadingFinished; } @Override - public void maybeThrowError() throws IOException { - if (currentLoadableException != null && currentLoadableExceptionCount > minLoadableRetryCount) { - throw currentLoadableException; - } + public long readReset() { + return TrackStream.NO_RESET; } @Override - public long readDiscontinuity(int track) { - return NO_DISCONTINUITY; - } - - @Override - public int readData(int track, long positionUs, MediaFormatHolder formatHolder, - SampleHolder sampleHolder) { + public int readData(MediaFormatHolder formatHolder, SampleHolder sampleHolder) { if (state == STATE_END_OF_STREAM) { return END_OF_STREAM; } else if (state == STATE_SEND_FORMAT) { @@ -158,11 +167,11 @@ public final class SingleSampleSource implements SampleSource, SampleSourceReade @Override public long getBufferedPositionUs() { - return loadingFinished ? TrackRenderer.END_OF_TRACK_US : 0; + return state == STATE_END_OF_STREAM || loadingFinished ? C.END_OF_SOURCE_US : 0; } @Override - public void disable(int track) { + public void disable() { state = STATE_END_OF_STREAM; } diff --git a/library/src/main/java/com/google/android/exoplayer/TrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/TrackRenderer.java index e2fd0301b4..7bd05199f1 100644 --- a/library/src/main/java/com/google/android/exoplayer/TrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/TrackRenderer.java @@ -18,6 +18,8 @@ package com.google.android.exoplayer; import com.google.android.exoplayer.ExoPlayer.ExoPlayerComponent; import com.google.android.exoplayer.util.Assertions; +import java.io.IOException; + /** * Renders a single component of media. * @@ -31,24 +33,6 @@ import com.google.android.exoplayer.util.Assertions; */ public abstract class TrackRenderer implements ExoPlayerComponent { - /** - * Represents an unknown time or duration. Equal to {@link C#UNKNOWN_TIME_US}. - */ - public static final long UNKNOWN_TIME_US = C.UNKNOWN_TIME_US; // -1 - /** - * Represents a time or duration that should match the duration of the longest track whose - * duration is known. Equal to {@link C#MATCH_LONGEST_US}. - */ - public static final long MATCH_LONGEST_US = C.MATCH_LONGEST_US; // -2 - /** - * Represents the time of the end of the track. - */ - public static final long END_OF_TRACK_US = -3; - - /** - * The renderer has been released and should not be used. - */ - protected static final int STATE_RELEASED = -1; /** * The renderer has not yet been prepared. */ @@ -98,37 +82,30 @@ public abstract class TrackRenderer implements ExoPlayerComponent { } /** - * Prepares the renderer. This method is non-blocking, and hence it may be necessary to call it - * more than once in order to transition the renderer into the prepared state. + * Prepares the renderer to read from the provided {@link SampleSource}. + *

+ * The {@link SampleSource} must itself be prepared before it is passed to this method. * - * @param positionUs The player's current playback position. - * @return The current state (one of the STATE_* constants), for convenience. + * @param sampleSource The {@link SampleSource} from which to read. * @throws ExoPlaybackException If an error occurs. */ - /* package */ final int prepare(long positionUs) throws ExoPlaybackException { + /* package */ final void prepare(SampleSource sampleSource) throws ExoPlaybackException { Assertions.checkState(state == STATE_UNPREPARED); - state = doPrepare(positionUs) ? STATE_PREPARED : STATE_UNPREPARED; - return state; + Assertions.checkState(sampleSource.isPrepared()); + doPrepare(sampleSource); + state = STATE_PREPARED; } /** - * Invoked to make progress when the renderer is in the {@link #STATE_UNPREPARED} state. This - * method will be called repeatedly until {@code true} is returned. - *

- * This method should return quickly, and should not block if the renderer is currently unable to - * make any useful progress. + * Called when the renderer is prepared. * - * @param positionUs The player's current playback position. - * @return True if the renderer is now prepared. False otherwise. + * @param sampleSource The {@link SampleSource} from which to read. * @throws ExoPlaybackException If an error occurs. */ - protected abstract boolean doPrepare(long positionUs) throws ExoPlaybackException; + protected abstract void doPrepare(SampleSource sampleSource) throws ExoPlaybackException; /** * Returns the number of tracks exposed by the renderer. - *

- * This method may be called when the renderer is in the following states: - * {@link #STATE_PREPARED}, {@link #STATE_ENABLED}, {@link #STATE_STARTED} * * @return The number of tracks. */ @@ -136,9 +113,6 @@ public abstract class TrackRenderer implements ExoPlayerComponent { /** * Returns the format of the specified track. - *

- * This method may be called when the renderer is in the following states: - * {@link #STATE_PREPARED}, {@link #STATE_ENABLED}, {@link #STATE_STARTED} * * @param track The track index. * @return The format of the specified track. @@ -243,26 +217,24 @@ public abstract class TrackRenderer implements ExoPlayerComponent { } /** - * Releases the renderer. + * Unprepares the renderer. * * @throws ExoPlaybackException If an error occurs. */ - /* package */ final void release() throws ExoPlaybackException { - Assertions.checkState(state != STATE_ENABLED - && state != STATE_STARTED - && state != STATE_RELEASED); - state = STATE_RELEASED; - onReleased(); + /* package */ final void unprepare() throws ExoPlaybackException { + Assertions.checkState(state == STATE_PREPARED); + state = STATE_UNPREPARED; + onUnprepared(); } /** - * Called when the renderer is released. + * Called when the renderer is unprepared. *

* The default implementation is a no-op. * * @throws ExoPlaybackException If an error occurs. */ - protected void onReleased() throws ExoPlaybackException { + protected void onUnprepared() throws ExoPlaybackException { // Do nothing. } @@ -322,46 +294,14 @@ public abstract class TrackRenderer implements ExoPlayerComponent { /** * Throws an error that's preventing the renderer from making progress or buffering more data at * this point in time. - * - * @throws ExoPlaybackException An error that's preventing the renderer from making progress or - * buffering more data. - */ - protected abstract void maybeThrowError() throws ExoPlaybackException; - - /** - * Returns the duration of the media being rendered. - *

- * This method may be called when the renderer is in the following states: - * {@link #STATE_PREPARED}, {@link #STATE_ENABLED}, {@link #STATE_STARTED} - * - * @return The duration of the track in microseconds, or {@link #MATCH_LONGEST_US} if - * the track's duration should match that of the longest track whose duration is known, or - * or {@link #UNKNOWN_TIME_US} if the duration is not known. - */ - protected abstract long getDurationUs(); - - /** - * Returns an estimate of the absolute position in microseconds up to which data is buffered. - *

- * This method may be called when the renderer is in the following states: - * {@link #STATE_ENABLED}, {@link #STATE_STARTED} - * - * @return An estimate of the absolute position in microseconds up to which data is buffered, - * or {@link #END_OF_TRACK_US} if the track is fully buffered, or {@link #UNKNOWN_TIME_US} if - * no estimate is available. - */ - protected abstract long getBufferedPositionUs(); - - /** - * Seeks to a specified time in the track. *

* This method may be called when the renderer is in the following states: * {@link #STATE_ENABLED} * - * @param positionUs The desired playback position in microseconds. - * @throws ExoPlaybackException If an error occurs. + * @throws IOException An error that's preventing the renderer from making progress or buffering + * more data. */ - protected abstract void seekTo(long positionUs) throws ExoPlaybackException; + protected abstract void maybeThrowError() throws IOException; @Override public void handleMessage(int what, Object object) throws ExoPlaybackException { diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/BaseChunkSampleSourceEventListener.java b/library/src/main/java/com/google/android/exoplayer/chunk/BaseChunkSampleSourceEventListener.java index 9bf75da420..995baa591b 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/BaseChunkSampleSourceEventListener.java +++ b/library/src/main/java/com/google/android/exoplayer/chunk/BaseChunkSampleSourceEventListener.java @@ -17,7 +17,7 @@ package com.google.android.exoplayer.chunk; import com.google.android.exoplayer.C; import com.google.android.exoplayer.SampleSource; -import com.google.android.exoplayer.SampleSource.SampleSourceReader; +import com.google.android.exoplayer.SampleSource.TrackStream; import java.io.IOException; @@ -91,7 +91,7 @@ public interface BaseChunkSampleSourceEventListener { /** * Invoked when the downstream format changes (i.e. when the format being supplied to the - * caller of {@link SampleSourceReader#readData} changes). + * caller of {@link TrackStream#readData} changes). * * @param sourceId The id of the reporting {@link SampleSource}. * @param format The format. diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java b/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java index f7d4e12dfc..9dc73721a5 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java @@ -21,8 +21,7 @@ import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.MediaFormatHolder; import com.google.android.exoplayer.SampleHolder; import com.google.android.exoplayer.SampleSource; -import com.google.android.exoplayer.SampleSource.SampleSourceReader; -import com.google.android.exoplayer.TrackRenderer; +import com.google.android.exoplayer.SampleSource.TrackStream; import com.google.android.exoplayer.extractor.DefaultTrackOutput; import com.google.android.exoplayer.upstream.Loader; import com.google.android.exoplayer.upstream.Loader.Loadable; @@ -40,7 +39,7 @@ import java.util.List; * A {@link SampleSource} that loads media in {@link Chunk}s, which are themselves obtained from a * {@link ChunkSource}. */ -public class ChunkSampleSource implements SampleSource, SampleSourceReader, Loader.Callback { +public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Callback { /** * Interface definition for a callback to be notified of {@link ChunkSampleSource} events. @@ -53,9 +52,8 @@ public class ChunkSampleSource implements SampleSource, SampleSourceReader, Load public static final int DEFAULT_MIN_LOADABLE_RETRY_COUNT = 3; private static final int STATE_IDLE = 0; - private static final int STATE_INITIALIZED = 1; - private static final int STATE_PREPARED = 2; - private static final int STATE_ENABLED = 3; + private static final int STATE_PREPARED = 1; + private static final int STATE_ENABLED = 2; private static final long NO_RESET_PENDING = Long.MIN_VALUE; @@ -76,8 +74,9 @@ public class ChunkSampleSource implements SampleSource, SampleSourceReader, Load private long lastSeekPositionUs; private long pendingResetPositionUs; private long lastPerformedBufferOperation; - private boolean pendingDiscontinuity; + private boolean pendingReset; + private long durationUs; private Loader loader; private boolean loadingFinished; private IOException currentLoadableException; @@ -145,41 +144,47 @@ public class ChunkSampleSource implements SampleSource, SampleSourceReader, Load } @Override - public SampleSourceReader register() { - Assertions.checkState(state == STATE_IDLE); - state = STATE_INITIALIZED; - return this; - } - - @Override - public boolean prepare(long positionUs) { - Assertions.checkState(state == STATE_INITIALIZED || state == STATE_PREPARED); - if (state == STATE_PREPARED) { + public boolean prepare(long positionUs) throws IOException { + if (state != STATE_IDLE) { return true; - } else if (!chunkSource.prepare()) { + } + if (!chunkSource.prepare()) { + maybeThrowError(); return false; } + durationUs = C.UNKNOWN_TIME_US; if (chunkSource.getTrackCount() > 0) { loader = new Loader("Loader:" + chunkSource.getFormat(0).mimeType); + durationUs = chunkSource.getFormat(0).durationUs; } state = STATE_PREPARED; return true; } + @Override + public boolean isPrepared() { + return state != STATE_IDLE; + } + + @Override + public long getDurationUs() { + return durationUs; + } + @Override public int getTrackCount() { - Assertions.checkState(state == STATE_PREPARED || state == STATE_ENABLED); + Assertions.checkState(state != STATE_IDLE); return chunkSource.getTrackCount(); } @Override public MediaFormat getFormat(int track) { - Assertions.checkState(state == STATE_PREPARED || state == STATE_ENABLED); + Assertions.checkState(state != STATE_IDLE); return chunkSource.getFormat(track); } @Override - public void enable(int track, long positionUs) { + public TrackStream enable(int track, long positionUs) { Assertions.checkState(state == STATE_PREPARED); Assertions.checkState(enabledTrackCount++ == 0); state = STATE_ENABLED; @@ -189,12 +194,13 @@ public class ChunkSampleSource implements SampleSource, SampleSourceReader, Load downstreamMediaFormat = null; downstreamPositionUs = positionUs; lastSeekPositionUs = positionUs; - pendingDiscontinuity = false; + pendingReset = false; restartFrom(positionUs); + return this; } @Override - public void disable(int track) { + public void disable() { Assertions.checkState(state == STATE_ENABLED); Assertions.checkState(--enabledTrackCount == 0); state = STATE_PREPARED; @@ -214,30 +220,35 @@ public class ChunkSampleSource implements SampleSource, SampleSourceReader, Load } @Override - public boolean continueBuffering(int track, long positionUs) { - Assertions.checkState(state == STATE_ENABLED); + public void continueBuffering(long positionUs) { + Assertions.checkState(state != STATE_IDLE); + if (state == STATE_PREPARED) { + return; + } downstreamPositionUs = positionUs; chunkSource.continueBuffering(positionUs); updateLoadControl(); + } + + @Override + public boolean isReady() { + Assertions.checkState(state == STATE_ENABLED); return loadingFinished || !sampleQueue.isEmpty(); } @Override - public long readDiscontinuity(int track) { - if (pendingDiscontinuity) { - pendingDiscontinuity = false; + public long readReset() { + if (pendingReset) { + pendingReset = false; return lastSeekPositionUs; } - return NO_DISCONTINUITY; + return TrackStream.NO_RESET; } @Override - public int readData(int track, long positionUs, MediaFormatHolder formatHolder, - SampleHolder sampleHolder) { + public int readData(MediaFormatHolder formatHolder, SampleHolder sampleHolder) { Assertions.checkState(state == STATE_ENABLED); - downstreamPositionUs = positionUs; - - if (pendingDiscontinuity || isPendingReset()) { + if (pendingReset || isPendingReset()) { return NOTHING_READ; } @@ -284,15 +295,12 @@ public class ChunkSampleSource implements SampleSource, SampleSourceReader, Load @Override public void seekToUs(long positionUs) { - Assertions.checkState(state == STATE_ENABLED); - - long currentPositionUs = isPendingReset() ? pendingResetPositionUs : downstreamPositionUs; - downstreamPositionUs = positionUs; - lastSeekPositionUs = positionUs; - if (currentPositionUs == positionUs) { + Assertions.checkState(state != STATE_IDLE); + if (state == STATE_PREPARED) { return; } - + downstreamPositionUs = positionUs; + lastSeekPositionUs = positionUs; // If we're not pending a reset, see if we can seek within the sample queue. boolean seekInsideBuffer = !isPendingReset() && sampleQueue.skipToKeyframeBefore(positionUs); if (seekInsideBuffer) { @@ -307,7 +315,7 @@ public class ChunkSampleSource implements SampleSource, SampleSourceReader, Load restartFrom(positionUs); } // Either way, we need to send a discontinuity to the downstream components. - pendingDiscontinuity = true; + pendingReset = true; } @Override @@ -321,11 +329,11 @@ public class ChunkSampleSource implements SampleSource, SampleSourceReader, Load @Override public long getBufferedPositionUs() { - Assertions.checkState(state == STATE_ENABLED); - if (isPendingReset()) { + Assertions.checkState(state != STATE_IDLE); + if (state != STATE_ENABLED || loadingFinished) { + return C.END_OF_SOURCE_US; + } else if (isPendingReset()) { return pendingResetPositionUs; - } else if (loadingFinished) { - return TrackRenderer.END_OF_TRACK_US; } else { long largestParsedTimestampUs = sampleQueue.getLargestParsedTimestampUs(); return largestParsedTimestampUs == Long.MIN_VALUE ? downstreamPositionUs diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/SingleSampleChunkSource.java b/library/src/main/java/com/google/android/exoplayer/chunk/SingleSampleChunkSource.java index 12fd645fa9..9e52a31d0b 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/SingleSampleChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer/chunk/SingleSampleChunkSource.java @@ -41,8 +41,7 @@ public final class SingleSampleChunkSource implements ChunkSource { * @param dataSpec Defines the location of the sample. * @param format The format of the sample. * @param durationUs The duration of the sample in microseconds, or {@link C#UNKNOWN_TIME_US} if - * the duration is unknown, or {@link C#MATCH_LONGEST_US} if the duration should match the - * duration of the longest track whose duration is known. + * the duration is unknown. * @param mediaFormat The sample media format. May be null. */ public SingleSampleChunkSource(DataSource dataSource, DataSpec dataSpec, Format format, diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/ExtractorSampleSource.java b/library/src/main/java/com/google/android/exoplayer/extractor/ExtractorSampleSource.java index cb1b229a94..14f3ef618d 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/ExtractorSampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/ExtractorSampleSource.java @@ -21,8 +21,6 @@ import com.google.android.exoplayer.MediaFormatHolder; import com.google.android.exoplayer.ParserException; import com.google.android.exoplayer.SampleHolder; import com.google.android.exoplayer.SampleSource; -import com.google.android.exoplayer.SampleSource.SampleSourceReader; -import com.google.android.exoplayer.TrackRenderer; import com.google.android.exoplayer.drm.DrmInitData; import com.google.android.exoplayer.upstream.Allocator; import com.google.android.exoplayer.upstream.DataSource; @@ -39,6 +37,7 @@ import android.util.SparseArray; import java.io.EOFException; import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; /** @@ -66,8 +65,7 @@ import java.util.List; * constructor. When reading a new stream, the first {@link Extractor} that returns {@code true} * from {@link Extractor#sniff(ExtractorInput)} will be used. */ -public final class ExtractorSampleSource implements SampleSource, SampleSourceReader, - ExtractorOutput, Loader.Callback { +public final class ExtractorSampleSource implements SampleSource, ExtractorOutput, Loader.Callback { /** * Thrown if the input format could not recognized. @@ -177,12 +175,11 @@ public final class ExtractorSampleSource implements SampleSource, SampleSourceRe private boolean prepared; private int enabledTrackCount; private MediaFormat[] mediaFormats; - private long maxTrackDurationUs; + private long durationUs; private boolean[] pendingMediaFormat; - private boolean[] pendingDiscontinuities; + private boolean[] pendingResets; private boolean[] trackEnabledStates; - private int remainingReleaseCount; private long downstreamPositionUs; private long lastSeekPositionUs; private long pendingResetPositionUs; @@ -252,12 +249,6 @@ public final class ExtractorSampleSource implements SampleSource, SampleSourceRe pendingResetPositionUs = NO_RESET_PENDING; } - @Override - public SampleSourceReader register() { - remainingReleaseCount++; - return this; - } - @Override public boolean prepare(long positionUs) { if (prepared) { @@ -272,15 +263,15 @@ public final class ExtractorSampleSource implements SampleSource, SampleSourceRe if (seekMap != null && tracksBuilt && haveFormatsForAllTracks()) { int trackCount = sampleQueues.size(); trackEnabledStates = new boolean[trackCount]; - pendingDiscontinuities = new boolean[trackCount]; + pendingResets = new boolean[trackCount]; pendingMediaFormat = new boolean[trackCount]; mediaFormats = new MediaFormat[trackCount]; - maxTrackDurationUs = C.UNKNOWN_TIME_US; + durationUs = C.UNKNOWN_TIME_US; for (int i = 0; i < trackCount; i++) { MediaFormat format = sampleQueues.valueAt(i).getFormat(); mediaFormats[i] = format; - if (format.durationUs != C.UNKNOWN_TIME_US && format.durationUs > maxTrackDurationUs) { - maxTrackDurationUs = format.durationUs; + if (format.durationUs > durationUs) { + durationUs = format.durationUs; } } prepared = true; @@ -290,6 +281,16 @@ public final class ExtractorSampleSource implements SampleSource, SampleSourceRe return false; } + @Override + public boolean isPrepared() { + return prepared; + } + + @Override + public long getDurationUs() { + return durationUs; + } + @Override public int getTrackCount() { return sampleQueues.size(); @@ -302,13 +303,13 @@ public final class ExtractorSampleSource implements SampleSource, SampleSourceRe } @Override - public void enable(int track, long positionUs) { + public TrackStream enable(int track, long positionUs) { Assertions.checkState(prepared); Assertions.checkState(!trackEnabledStates[track]); enabledTrackCount++; trackEnabledStates[track] = true; pendingMediaFormat[track] = true; - pendingDiscontinuities[track] = false; + pendingResets[track] = false; if (enabledTrackCount == 1) { // Treat all enables in non-seekable media as being from t=0. positionUs = !seekMap.isSeekable() ? 0 : positionUs; @@ -316,10 +317,10 @@ public final class ExtractorSampleSource implements SampleSource, SampleSourceRe lastSeekPositionUs = positionUs; restartFrom(positionUs); } + return new TrackStreamImpl(track); } - @Override - public void disable(int track) { + /* package */ void disable(int track) { Assertions.checkState(prepared); Assertions.checkState(trackEnabledStates[track]); enabledTrackCount--; @@ -336,37 +337,37 @@ public final class ExtractorSampleSource implements SampleSource, SampleSourceRe } @Override - public boolean continueBuffering(int track, long playbackPositionUs) { + public void continueBuffering(long playbackPositionUs) { Assertions.checkState(prepared); - Assertions.checkState(trackEnabledStates[track]); + if (enabledTrackCount == 0) { + return; + } downstreamPositionUs = playbackPositionUs; discardSamplesForDisabledTracks(downstreamPositionUs); if (loadingFinished) { - return true; + return; } maybeStartLoading(); - if (isPendingReset()) { - return false; - } - return !sampleQueues.valueAt(track).isEmpty(); } - @Override - public long readDiscontinuity(int track) { - if (pendingDiscontinuities[track]) { - pendingDiscontinuities[track] = false; + /* package */ boolean isReady(int track) { + Assertions.checkState(prepared); + Assertions.checkState(trackEnabledStates[track]); + return !sampleQueues.valueAt(track).isEmpty(); + + } + + /* package */ long readReset(int track) { + if (pendingResets[track]) { + pendingResets[track] = false; return lastSeekPositionUs; } - return NO_DISCONTINUITY; + return TrackStream.NO_RESET; } - @Override - public int readData(int track, long playbackPositionUs, MediaFormatHolder formatHolder, - SampleHolder sampleHolder) { - downstreamPositionUs = playbackPositionUs; - - if (pendingDiscontinuities[track] || isPendingReset()) { - return NOTHING_READ; + /* package */ int readData(int track, MediaFormatHolder formatHolder, SampleHolder sampleHolder) { + if (pendingResets[track] || isPendingReset()) { + return TrackStream.NOTHING_READ; } InternalTrackOutput sampleQueue = sampleQueues.valueAt(track); @@ -374,7 +375,7 @@ public final class ExtractorSampleSource implements SampleSource, SampleSourceRe formatHolder.format = sampleQueue.getFormat(); formatHolder.drmInitData = drmInitData; pendingMediaFormat[track] = false; - return FORMAT_READ; + return TrackStream.FORMAT_READ; } if (sampleQueue.getSample(sampleHolder)) { @@ -386,18 +387,17 @@ public final class ExtractorSampleSource implements SampleSource, SampleSourceRe havePendingNextSampleUs = false; } sampleHolder.timeUs += sampleTimeOffsetUs; - return SAMPLE_READ; + return TrackStream.SAMPLE_READ; } if (loadingFinished) { - return END_OF_STREAM; + return TrackStream.END_OF_STREAM; } - return NOTHING_READ; + return TrackStream.NOTHING_READ; } - @Override - public void maybeThrowError() throws IOException { + /* package */ void maybeThrowError() throws IOException { if (currentLoadableException == null) { return; } @@ -420,38 +420,30 @@ public final class ExtractorSampleSource implements SampleSource, SampleSourceRe @Override public void seekToUs(long positionUs) { Assertions.checkState(prepared); - Assertions.checkState(enabledTrackCount > 0); - // Treat all seeks into non-seekable media as being to t=0. - positionUs = !seekMap.isSeekable() ? 0 : positionUs; - - long currentPositionUs = isPendingReset() ? pendingResetPositionUs : downstreamPositionUs; - downstreamPositionUs = positionUs; - lastSeekPositionUs = positionUs; - if (currentPositionUs == positionUs) { + if (enabledTrackCount == 0) { return; } - + // Treat all seeks into non-seekable media as being to t=0. + downstreamPositionUs = !seekMap.isSeekable() ? 0 : positionUs; + lastSeekPositionUs = downstreamPositionUs; // If we're not pending a reset, see if we can seek within the sample queues. boolean seekInsideBuffer = !isPendingReset(); for (int i = 0; seekInsideBuffer && i < sampleQueues.size(); i++) { seekInsideBuffer &= sampleQueues.valueAt(i).skipToKeyframeBefore(positionUs); } - // If we failed to seek within the sample queues, we need to restart. if (!seekInsideBuffer) { restartFrom(positionUs); } // Either way, we need to send discontinuities to the downstream components. - for (int i = 0; i < pendingDiscontinuities.length; i++) { - pendingDiscontinuities[i] = true; - } + Arrays.fill(pendingResets, true); } @Override public long getBufferedPositionUs() { - if (loadingFinished) { - return TrackRenderer.END_OF_TRACK_US; + if (enabledTrackCount == 0 || loadingFinished) { + return C.END_OF_SOURCE_US; } else if (isPendingReset()) { return pendingResetPositionUs; } else { @@ -467,11 +459,11 @@ public final class ExtractorSampleSource implements SampleSource, SampleSourceRe @Override public void release() { - Assertions.checkState(remainingReleaseCount > 0); - if (--remainingReleaseCount == 0 && loader != null) { + if (loader != null) { loader.release(); loader = null; } + prepared = false; } // Loader.Callback implementation. @@ -561,7 +553,7 @@ public final class ExtractorSampleSource implements SampleSource, SampleSourceRe sampleQueues.valueAt(i).clear(); } loadable = createLoadableFromStart(); - } else if (!seekMap.isSeekable() && maxTrackDurationUs == C.UNKNOWN_TIME_US) { + } else if (!seekMap.isSeekable() && durationUs == C.UNKNOWN_TIME_US) { // We're playing a non-seekable stream with unknown duration. Assume it's live, and // therefore that the data at the uri is a continuously shifting window of the latest // available media. For this case there's no way to continue loading from where a previous @@ -594,7 +586,7 @@ public final class ExtractorSampleSource implements SampleSource, SampleSourceRe loadable = createLoadableFromStart(); } else { Assertions.checkState(isPendingReset()); - if (maxTrackDurationUs != C.UNKNOWN_TIME_US && pendingResetPositionUs >= maxTrackDurationUs) { + if (durationUs != C.UNKNOWN_TIME_US && pendingResetPositionUs >= durationUs) { loadingFinished = true; pendingResetPositionUs = NO_RESET_PENDING; return; @@ -654,6 +646,41 @@ public final class ExtractorSampleSource implements SampleSource, SampleSourceRe return Math.min((errorCount - 1) * 1000, 5000); } + private final class TrackStreamImpl implements TrackStream { + + private final int track; + + public TrackStreamImpl(int track) { + this.track = track; + } + + @Override + public boolean isReady() { + return ExtractorSampleSource.this.isReady(track); + } + + @Override + public void maybeThrowError() throws IOException { + ExtractorSampleSource.this.maybeThrowError(); + } + + @Override + public long readReset() { + return ExtractorSampleSource.this.readReset(track); + } + + @Override + public int readData(MediaFormatHolder formatHolder, SampleHolder sampleHolder) { + return ExtractorSampleSource.this.readData(track, formatHolder, sampleHolder); + } + + @Override + public void disable() { + ExtractorSampleSource.this.disable(track); + } + + } + /** * Extension of {@link DefaultTrackOutput} that increments a shared counter of the total number * of extracted samples. diff --git a/library/src/main/java/com/google/android/exoplayer/hls/HlsSampleSource.java b/library/src/main/java/com/google/android/exoplayer/hls/HlsSampleSource.java index 63b0740418..53908b685e 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/HlsSampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/HlsSampleSource.java @@ -21,8 +21,6 @@ import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.MediaFormatHolder; import com.google.android.exoplayer.SampleHolder; import com.google.android.exoplayer.SampleSource; -import com.google.android.exoplayer.SampleSource.SampleSourceReader; -import com.google.android.exoplayer.TrackRenderer; import com.google.android.exoplayer.chunk.BaseChunkSampleSourceEventListener; import com.google.android.exoplayer.chunk.Chunk; import com.google.android.exoplayer.chunk.ChunkOperationHolder; @@ -42,7 +40,7 @@ import java.util.LinkedList; /** * A {@link SampleSource} for HLS streams. */ -public final class HlsSampleSource implements SampleSource, SampleSourceReader, Loader.Callback { +public final class HlsSampleSource implements SampleSource, Loader.Callback { /** * Interface definition for a callback to be notified of {@link HlsSampleSource} events. @@ -72,7 +70,6 @@ public final class HlsSampleSource implements SampleSource, SampleSourceReader, private final Handler eventHandler; private final EventListener eventListener; - private int remainingReleaseCount; private boolean prepared; private boolean loadControlRegistered; private int trackCount; @@ -84,7 +81,7 @@ public final class HlsSampleSource implements SampleSource, SampleSourceReader, // Indexed by track (as exposed by this source). private MediaFormat[] trackFormats; private boolean[] trackEnabledStates; - private boolean[] pendingDiscontinuities; + private boolean[] pendingResets; private MediaFormat[] downstreamMediaFormats; // Maps track index (as exposed by this source) to the corresponding chunk source track index for // primary tracks, or to -1 otherwise. @@ -137,16 +134,11 @@ public final class HlsSampleSource implements SampleSource, SampleSourceReader, } @Override - public SampleSourceReader register() { - remainingReleaseCount++; - return this; - } - - @Override - public boolean prepare(long positionUs) { + public boolean prepare(long positionUs) throws IOException { if (prepared) { return true; } else if (!chunkSource.prepare()) { + maybeThrowError(); return false; } if (!extractors.isEmpty()) { @@ -179,9 +171,20 @@ public final class HlsSampleSource implements SampleSource, SampleSourceReader, downstreamPositionUs = positionUs; } maybeStartLoading(); + maybeThrowError(); return false; } + @Override + public boolean isPrepared() { + return prepared; + } + + @Override + public long getDurationUs() { + return chunkSource.getDurationUs(); + } + @Override public int getTrackCount() { Assertions.checkState(prepared); @@ -195,11 +198,11 @@ public final class HlsSampleSource implements SampleSource, SampleSourceReader, } @Override - public void enable(int track, long positionUs) { + public TrackStream enable(int track, long positionUs) { Assertions.checkState(prepared); setTrackEnabledState(track, true); downstreamMediaFormats[track] = null; - pendingDiscontinuities[track] = false; + pendingResets[track] = false; downstreamFormat = null; boolean wasLoadControlRegistered = loadControlRegistered; if (!loadControlRegistered) { @@ -216,9 +219,7 @@ public final class HlsSampleSource implements SampleSource, SampleSourceReader, // renderers receive a discontinuity event. chunkSource.selectTrack(chunkSourceTrack); seekToInternal(positionUs); - return; - } - if (enabledTrackCount == 1) { + } else if (enabledTrackCount == 1) { lastSeekPositionUs = positionUs; if (wasLoadControlRegistered && downstreamPositionUs == positionUs) { // TODO: Address [Internal: b/21743989] to remove the need for this kind of hack. @@ -231,10 +232,10 @@ public final class HlsSampleSource implements SampleSource, SampleSourceReader, restartFrom(positionUs); } } + return new TrackStreamImpl(track); } - @Override - public void disable(int track) { + /* package */ void disable(int track) { Assertions.checkState(prepared); setTrackEnabledState(track, false); if (enabledTrackCount == 0) { @@ -254,14 +255,20 @@ public final class HlsSampleSource implements SampleSource, SampleSourceReader, } @Override - public boolean continueBuffering(int track, long playbackPositionUs) { + public void continueBuffering(long playbackPositionUs) { Assertions.checkState(prepared); - Assertions.checkState(trackEnabledStates[track]); + if (enabledTrackCount == 0) { + return; + } downstreamPositionUs = playbackPositionUs; if (!extractors.isEmpty()) { discardSamplesForDisabledTracks(getCurrentExtractor(), downstreamPositionUs); } maybeStartLoading(); + } + + /* package */ boolean isReady(int track) { + Assertions.checkState(trackEnabledStates[track]); if (loadingFinished) { return true; } @@ -281,28 +288,24 @@ public final class HlsSampleSource implements SampleSource, SampleSourceReader, return false; } - @Override - public long readDiscontinuity(int track) { - if (pendingDiscontinuities[track]) { - pendingDiscontinuities[track] = false; + /* package */ long readReset(int track) { + if (pendingResets[track]) { + pendingResets[track] = false; return lastSeekPositionUs; } - return NO_DISCONTINUITY; + return TrackStream.NO_RESET; } - @Override - public int readData(int track, long playbackPositionUs, MediaFormatHolder formatHolder, - SampleHolder sampleHolder) { + /* package */ int readData(int track, MediaFormatHolder formatHolder, SampleHolder sampleHolder) { Assertions.checkState(prepared); - downstreamPositionUs = playbackPositionUs; - if (pendingDiscontinuities[track] || isPendingReset()) { - return NOTHING_READ; + if (pendingResets[track] || isPendingReset()) { + return TrackStream.NOTHING_READ; } HlsExtractorWrapper extractor = getCurrentExtractor(); if (!extractor.isPrepared()) { - return NOTHING_READ; + return TrackStream.NOTHING_READ; } if (downstreamFormat == null || !downstreamFormat.equals(extractor.format)) { @@ -324,7 +327,7 @@ public final class HlsSampleSource implements SampleSource, SampleSourceReader, // next one for the current read. extractor = extractors.get(++extractorIndex); if (!extractor.isPrepared()) { - return NOTHING_READ; + return TrackStream.NOTHING_READ; } } @@ -332,24 +335,23 @@ public final class HlsSampleSource implements SampleSource, SampleSourceReader, if (mediaFormat != null && !mediaFormat.equals(downstreamMediaFormats[track])) { formatHolder.format = mediaFormat; downstreamMediaFormats[track] = mediaFormat; - return FORMAT_READ; + return TrackStream.FORMAT_READ; } if (extractor.getSample(extractorTrack, sampleHolder)) { boolean decodeOnly = sampleHolder.timeUs < lastSeekPositionUs; sampleHolder.flags |= decodeOnly ? C.SAMPLE_FLAG_DECODE_ONLY : 0; - return SAMPLE_READ; + return TrackStream.SAMPLE_READ; } if (loadingFinished) { - return END_OF_STREAM; + return TrackStream.END_OF_STREAM; } - return NOTHING_READ; + return TrackStream.NOTHING_READ; } - @Override - public void maybeThrowError() throws IOException { + /* package */ void maybeThrowError() throws IOException { if (currentLoadableException != null && currentLoadableExceptionCount > minLoadableRetryCount) { throw currentLoadableException; } else if (currentLoadable == null) { @@ -360,29 +362,21 @@ public final class HlsSampleSource implements SampleSource, SampleSourceReader, @Override public void seekToUs(long positionUs) { Assertions.checkState(prepared); - Assertions.checkState(enabledTrackCount > 0); - // Treat all seeks into live streams as being to t=0. - positionUs = chunkSource.isLive() ? 0 : positionUs; - - // Ignore seeks to the current position. - long currentPositionUs = isPendingReset() ? pendingResetPositionUs : downstreamPositionUs; - downstreamPositionUs = positionUs; - lastSeekPositionUs = positionUs; - if (currentPositionUs == positionUs) { + if (enabledTrackCount == 0) { return; } - - seekToInternal(positionUs); + seekToInternal(chunkSource.isLive() ? 0 : positionUs); } @Override public long getBufferedPositionUs() { Assertions.checkState(prepared); - Assertions.checkState(enabledTrackCount > 0); - if (isPendingReset()) { + if (enabledTrackCount == 0) { + return C.END_OF_SOURCE_US; + } else if (isPendingReset()) { return pendingResetPositionUs; } else if (loadingFinished) { - return TrackRenderer.END_OF_TRACK_US; + return C.END_OF_SOURCE_US; } else { long largestParsedTimestampUs = extractors.getLast().getLargestParsedTimestampUs(); if (extractors.size() > 1) { @@ -398,8 +392,7 @@ public final class HlsSampleSource implements SampleSource, SampleSourceReader, @Override public void release() { - Assertions.checkState(remainingReleaseCount > 0); - if (--remainingReleaseCount == 0 && loader != null) { + if (loader != null) { if (loadControlRegistered) { loadControl.unregister(this); loadControlRegistered = false; @@ -407,6 +400,7 @@ public final class HlsSampleSource implements SampleSource, SampleSourceReader, loader.release(); loader = null; } + prepared = false; } // Loader.Callback implementation. @@ -531,7 +525,7 @@ public final class HlsSampleSource implements SampleSource, SampleSourceReader, // Instantiate the necessary internal data-structures. trackFormats = new MediaFormat[trackCount]; trackEnabledStates = new boolean[trackCount]; - pendingDiscontinuities = new boolean[trackCount]; + pendingResets = new boolean[trackCount]; downstreamMediaFormats = new MediaFormat[trackCount]; chunkSourceTrackIndices = new int[trackCount]; extractorTrackIndices = new int[trackCount]; @@ -596,7 +590,7 @@ public final class HlsSampleSource implements SampleSource, SampleSourceReader, private void seekToInternal(long positionUs) { lastSeekPositionUs = positionUs; downstreamPositionUs = positionUs; - Arrays.fill(pendingDiscontinuities, true); + Arrays.fill(pendingResets, true); chunkSource.seek(); restartFrom(positionUs); } @@ -823,4 +817,39 @@ public final class HlsSampleSource implements SampleSource, SampleSourceReader, } } + private final class TrackStreamImpl implements TrackStream { + + private final int track; + + public TrackStreamImpl(int track) { + this.track = track; + } + + @Override + public boolean isReady() { + return HlsSampleSource.this.isReady(track); + } + + @Override + public void maybeThrowError() throws IOException { + HlsSampleSource.this.maybeThrowError(); + } + + @Override + public long readReset() { + return HlsSampleSource.this.readReset(track); + } + + @Override + public int readData(MediaFormatHolder formatHolder, SampleHolder sampleHolder) { + return HlsSampleSource.this.readData(track, formatHolder, sampleHolder); + } + + @Override + public void disable() { + HlsSampleSource.this.disable(track); + } + + } + } diff --git a/library/src/main/java/com/google/android/exoplayer/metadata/MetadataTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/metadata/MetadataTrackRenderer.java index 5bd3ffbf5f..0337a76ca7 100644 --- a/library/src/main/java/com/google/android/exoplayer/metadata/MetadataTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/metadata/MetadataTrackRenderer.java @@ -19,7 +19,7 @@ import com.google.android.exoplayer.ExoPlaybackException; import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.MediaFormatHolder; import com.google.android.exoplayer.SampleHolder; -import com.google.android.exoplayer.SampleSource; +import com.google.android.exoplayer.SampleSource.TrackStream; import com.google.android.exoplayer.SampleSourceTrackRenderer; import com.google.android.exoplayer.TrackRenderer; import com.google.android.exoplayer.util.Assertions; @@ -67,7 +67,6 @@ public final class MetadataTrackRenderer extends SampleSourceTrackRenderer im private T pendingMetadata; /** - * @param source A source from which samples containing metadata can be read. * @param metadataParser A parser for parsing the metadata. * @param metadataRenderer The metadata renderer to receive the parsed metadata. * @param metadataRendererLooper The looper associated with the thread on which metadataRenderer @@ -76,9 +75,8 @@ public final class MetadataTrackRenderer extends SampleSourceTrackRenderer im * obtained using {@link android.app.Activity#getMainLooper()}. Null may be passed if the * renderer should be invoked directly on the player's internal rendering thread. */ - public MetadataTrackRenderer(SampleSource source, MetadataParser metadataParser, + public MetadataTrackRenderer(MetadataParser metadataParser, MetadataRenderer metadataRenderer, Looper metadataRendererLooper) { - super(source); this.metadataParser = Assertions.checkNotNull(metadataParser); this.metadataRenderer = Assertions.checkNotNull(metadataRenderer); this.metadataHandler = metadataRendererLooper == null ? null @@ -93,7 +91,7 @@ public final class MetadataTrackRenderer extends SampleSourceTrackRenderer im } @Override - protected void onDiscontinuity(long positionUs) { + protected void onReset(long positionUs) { pendingMetadata = null; inputStreamEnded = false; } @@ -103,15 +101,15 @@ public final class MetadataTrackRenderer extends SampleSourceTrackRenderer im throws ExoPlaybackException { if (!inputStreamEnded && pendingMetadata == null) { sampleHolder.clearData(); - int result = readSource(positionUs, formatHolder, sampleHolder); - if (result == SampleSource.SAMPLE_READ) { + int result = readSource(formatHolder, sampleHolder); + if (result == TrackStream.SAMPLE_READ) { pendingMetadataTimestamp = sampleHolder.timeUs; try { pendingMetadata = metadataParser.parse(sampleHolder.data.array(), sampleHolder.size); } catch (IOException e) { throw new ExoPlaybackException(e); } - } else if (result == SampleSource.END_OF_STREAM) { + } else if (result == TrackStream.END_OF_STREAM) { inputStreamEnded = true; } } @@ -128,11 +126,6 @@ public final class MetadataTrackRenderer extends SampleSourceTrackRenderer im super.onDisabled(); } - @Override - protected long getBufferedPositionUs() { - return TrackRenderer.END_OF_TRACK_US; - } - @Override protected boolean isEnded() { return inputStreamEnded; diff --git a/library/src/main/java/com/google/android/exoplayer/text/TextTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/text/TextTrackRenderer.java index 865966ad53..04a4de0bcb 100644 --- a/library/src/main/java/com/google/android/exoplayer/text/TextTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/text/TextTrackRenderer.java @@ -19,7 +19,7 @@ import com.google.android.exoplayer.ExoPlaybackException; import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.MediaFormatHolder; import com.google.android.exoplayer.SampleHolder; -import com.google.android.exoplayer.SampleSource; +import com.google.android.exoplayer.SampleSource.TrackStream; import com.google.android.exoplayer.SampleSourceTrackRenderer; import com.google.android.exoplayer.TrackRenderer; import com.google.android.exoplayer.util.Assertions; @@ -124,7 +124,6 @@ public final class TextTrackRenderer extends SampleSourceTrackRenderer implement private int nextSubtitleEventIndex; /** - * @param source A source from which samples containing subtitle data can be read. * @param textRenderer The text renderer. * @param textRendererLooper The looper associated with the thread on which textRenderer should be * invoked. If the renderer makes use of standard Android UI components, then this should @@ -134,25 +133,8 @@ public final class TextTrackRenderer extends SampleSourceTrackRenderer implement * @param subtitleParsers {@link SubtitleParser}s to parse text samples, in order of decreasing * priority. If omitted, the default parsers will be used. */ - public TextTrackRenderer(SampleSource source, TextRenderer textRenderer, - Looper textRendererLooper, SubtitleParser... subtitleParsers) { - this(new SampleSource[] {source}, textRenderer, textRendererLooper, subtitleParsers); - } - - /** - * @param sources Sources from which samples containing subtitle data can be read. - * @param textRenderer The text renderer. - * @param textRendererLooper The looper associated with the thread on which textRenderer should be - * invoked. If the renderer makes use of standard Android UI components, then this should - * normally be the looper associated with the applications' main thread, which can be - * obtained using {@link android.app.Activity#getMainLooper()}. Null may be passed if the - * renderer should be invoked directly on the player's internal rendering thread. - * @param subtitleParsers {@link SubtitleParser}s to parse text samples, in order of decreasing - * priority. If omitted, the default parsers will be used. - */ - public TextTrackRenderer(SampleSource[] sources, TextRenderer textRenderer, - Looper textRendererLooper, SubtitleParser... subtitleParsers) { - super(sources); + public TextTrackRenderer(TextRenderer textRenderer, Looper textRendererLooper, + SubtitleParser... subtitleParsers) { this.textRenderer = Assertions.checkNotNull(textRenderer); this.textRendererHandler = textRendererLooper == null ? null : new Handler(textRendererLooper, this); @@ -188,7 +170,7 @@ public final class TextTrackRenderer extends SampleSourceTrackRenderer implement } @Override - protected void onDiscontinuity(long positionUs) { + protected void onReset(long positionUs) { inputStreamEnded = false; subtitle = null; nextSubtitle = null; @@ -243,12 +225,12 @@ public final class TextTrackRenderer extends SampleSourceTrackRenderer implement // Try and read the next subtitle from the source. SampleHolder sampleHolder = parserHelper.getSampleHolder(); sampleHolder.clearData(); - int result = readSource(positionUs, formatHolder, sampleHolder); - if (result == SampleSource.FORMAT_READ) { + int result = readSource(formatHolder, sampleHolder); + if (result == TrackStream.FORMAT_READ) { parserHelper.setFormat(formatHolder.format); - } else if (result == SampleSource.SAMPLE_READ) { + } else if (result == TrackStream.SAMPLE_READ) { parserHelper.startParseOperation(); - } else if (result == SampleSource.END_OF_STREAM) { + } else if (result == TrackStream.END_OF_STREAM) { inputStreamEnded = true; } } @@ -265,12 +247,6 @@ public final class TextTrackRenderer extends SampleSourceTrackRenderer implement super.onDisabled(); } - @Override - protected long getBufferedPositionUs() { - // Don't block playback whilst subtitles are loading. - return END_OF_TRACK_US; - } - @Override protected boolean isEnded() { return inputStreamEnded && (subtitle == null || getNextEventTime() == Long.MAX_VALUE); diff --git a/library/src/main/java/com/google/android/exoplayer/text/eia608/Eia608TrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/text/eia608/Eia608TrackRenderer.java index 4004846621..9ec391dc4a 100644 --- a/library/src/main/java/com/google/android/exoplayer/text/eia608/Eia608TrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/text/eia608/Eia608TrackRenderer.java @@ -20,7 +20,7 @@ import com.google.android.exoplayer.ExoPlaybackException; import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.MediaFormatHolder; import com.google.android.exoplayer.SampleHolder; -import com.google.android.exoplayer.SampleSource; +import com.google.android.exoplayer.SampleSource.TrackStream; import com.google.android.exoplayer.SampleSourceTrackRenderer; import com.google.android.exoplayer.TrackRenderer; import com.google.android.exoplayer.text.Cue; @@ -68,7 +68,6 @@ public final class Eia608TrackRenderer extends SampleSourceTrackRenderer impleme private String lastRenderedCaption; /** - * @param source A source from which samples containing EIA-608 closed captions can be read. * @param textRenderer The text renderer. * @param textRendererLooper The looper associated with the thread on which textRenderer should be * invoked. If the renderer makes use of standard Android UI components, then this should @@ -76,9 +75,7 @@ public final class Eia608TrackRenderer extends SampleSourceTrackRenderer impleme * obtained using {@link android.app.Activity#getMainLooper()}. Null may be passed if the * renderer should be invoked directly on the player's internal rendering thread. */ - public Eia608TrackRenderer(SampleSource source, TextRenderer textRenderer, - Looper textRendererLooper) { - super(source); + public Eia608TrackRenderer(TextRenderer textRenderer, Looper textRendererLooper) { this.textRenderer = Assertions.checkNotNull(textRenderer); textRendererHandler = textRendererLooper == null ? null : new Handler(textRendererLooper, this); eia608Parser = new Eia608Parser(); @@ -100,7 +97,7 @@ public final class Eia608TrackRenderer extends SampleSourceTrackRenderer impleme } @Override - protected void onDiscontinuity(long positionUs) { + protected void onReset(long positionUs) { inputStreamEnded = false; pendingCaptionLists.clear(); clearPendingSample(); @@ -116,12 +113,12 @@ public final class Eia608TrackRenderer extends SampleSourceTrackRenderer impleme maybeParsePendingSample(positionUs); } - int result = inputStreamEnded ? SampleSource.END_OF_STREAM : SampleSource.SAMPLE_READ; - while (!isSamplePending() && result == SampleSource.SAMPLE_READ) { - result = readSource(positionUs, formatHolder, sampleHolder); - if (result == SampleSource.SAMPLE_READ) { + int result = inputStreamEnded ? TrackStream.END_OF_STREAM : TrackStream.SAMPLE_READ; + while (!isSamplePending() && result == TrackStream.SAMPLE_READ) { + result = readSource(formatHolder, sampleHolder); + if (result == TrackStream.SAMPLE_READ) { maybeParsePendingSample(positionUs); - } else if (result == SampleSource.END_OF_STREAM) { + } else if (result == TrackStream.END_OF_STREAM) { inputStreamEnded = true; } } @@ -141,11 +138,6 @@ public final class Eia608TrackRenderer extends SampleSourceTrackRenderer impleme } } - @Override - protected long getBufferedPositionUs() { - return TrackRenderer.END_OF_TRACK_US; - } - @Override protected boolean isEnded() { return inputStreamEnded; diff --git a/playbacktests/build.gradle b/playbacktests/build.gradle deleted file mode 100644 index ff294ad0b5..0000000000 --- a/playbacktests/build.gradle +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (C) 2014 The Android Open Source Project -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -apply plugin: 'com.android.application' - -android { - compileSdkVersion 23 - buildToolsVersion "23.0.1" - - defaultConfig { - minSdkVersion 16 - targetSdkVersion 23 - } - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' - } - } - - lintOptions { - abortOnError false - } -} - -dependencies { - compile project(':library') -} diff --git a/playbacktests/src/main/.classpath b/playbacktests/src/main/.classpath deleted file mode 100644 index 3ae82311ba..0000000000 --- a/playbacktests/src/main/.classpath +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/playbacktests/src/main/.project b/playbacktests/src/main/.project deleted file mode 100644 index 7bce46ef78..0000000000 --- a/playbacktests/src/main/.project +++ /dev/null @@ -1,53 +0,0 @@ - - - ExoPlayerPlaybackTests - - - - - - com.android.ide.eclipse.adt.ResourceManagerBuilder - - - - - com.android.ide.eclipse.adt.PreCompilerBuilder - - - - - org.eclipse.jdt.core.javabuilder - - - - - com.android.ide.eclipse.adt.ApkBuilder - - - - - - com.android.ide.eclipse.adt.AndroidNature - org.eclipse.jdt.core.javanature - - - - 1363908154650 - - 22 - - org.eclipse.ui.ide.multiFilter - 1.0-name-matches-false-false-BUILD - - - - 1363908154652 - - 10 - - org.eclipse.ui.ide.multiFilter - 1.0-name-matches-true-false-build - - - - diff --git a/playbacktests/src/main/AndroidManifest.xml b/playbacktests/src/main/AndroidManifest.xml deleted file mode 100644 index 9aeb5b1478..0000000000 --- a/playbacktests/src/main/AndroidManifest.xml +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/gts/DashTest.java b/playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/gts/DashTest.java deleted file mode 100644 index b50b17ac6b..0000000000 --- a/playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/gts/DashTest.java +++ /dev/null @@ -1,578 +0,0 @@ -/* - * Copyright (C) 2014 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.exoplayer.playbacktests.gts; - -import com.google.android.exoplayer.CodecCounters; -import com.google.android.exoplayer.DefaultLoadControl; -import com.google.android.exoplayer.ExoPlayer; -import com.google.android.exoplayer.LoadControl; -import com.google.android.exoplayer.MediaCodecAudioTrackRenderer; -import com.google.android.exoplayer.MediaCodecSelector; -import com.google.android.exoplayer.MediaCodecUtil; -import com.google.android.exoplayer.MediaCodecVideoTrackRenderer; -import com.google.android.exoplayer.TrackRenderer; -import com.google.android.exoplayer.chunk.ChunkSampleSource; -import com.google.android.exoplayer.chunk.ChunkSource; -import com.google.android.exoplayer.chunk.FormatEvaluator; -import com.google.android.exoplayer.chunk.VideoFormatSelectorUtil; -import com.google.android.exoplayer.dash.DashChunkSource; -import com.google.android.exoplayer.dash.DashTrackSelector; -import com.google.android.exoplayer.dash.mpd.AdaptationSet; -import com.google.android.exoplayer.dash.mpd.MediaPresentationDescription; -import com.google.android.exoplayer.dash.mpd.MediaPresentationDescriptionParser; -import com.google.android.exoplayer.dash.mpd.Period; -import com.google.android.exoplayer.dash.mpd.Representation; -import com.google.android.exoplayer.playbacktests.util.ActionSchedule; -import com.google.android.exoplayer.playbacktests.util.CodecCountersUtil; -import com.google.android.exoplayer.playbacktests.util.ExoHostedTest; -import com.google.android.exoplayer.playbacktests.util.HostActivity; -import com.google.android.exoplayer.playbacktests.util.LogcatLogger; -import com.google.android.exoplayer.playbacktests.util.MetricsLogger; -import com.google.android.exoplayer.playbacktests.util.TestUtil; -import com.google.android.exoplayer.upstream.DataSource; -import com.google.android.exoplayer.upstream.DefaultAllocator; -import com.google.android.exoplayer.upstream.DefaultUriDataSource; -import com.google.android.exoplayer.util.Assertions; -import com.google.android.exoplayer.util.MimeTypes; -import com.google.android.exoplayer.util.Util; - -import android.annotation.TargetApi; -import android.media.MediaCodec; -import android.os.Bundle; -import android.os.Handler; -import android.test.ActivityInstrumentationTestCase2; -import android.util.Log; -import android.view.Surface; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - -/** - * Tests DASH playbacks using {@link ExoPlayer}. - */ -public final class DashTest extends ActivityInstrumentationTestCase2 { - - private static final String TAG = "DashTest"; - - private static final long MAX_PLAYING_TIME_DISCREPANCY_MS = 2000; - private static final float MAX_DROPPED_VIDEO_FRAME_FRACTION = 0.01f; - private static final int MAX_CONSECUTIVE_DROPPED_VIDEO_FRAMES = 10; - - private static final long MAX_ADDITIONAL_TIME_MS = 180000; - private static final int MIN_LOADABLE_RETRY_COUNT = 10; - - private static final String MANIFEST_URL_PREFIX = "https://storage.googleapis.com/exoplayer-test-" - + "media-1/gen-2/screens/dash-vod-single-segment/"; - private static final String H264_MANIFEST = "manifest-h264.mpd"; - private static final String H265_MANIFEST = "manifest-h265.mpd"; - private static final String VP9_MANIFEST = "manifest-vp9.mpd"; - private static final int AAC_AUDIO_FRAME_COUNT = 5524; - private static final int VIDEO_FRAME_COUNT = 3841; - private static final int VORBIS_AUDIO_FRAME_COUNT = 7773; - - private static final String AAC_AUDIO_REPRESENTATION_ID = "141"; - private static final String H264_BASELINE_240P_VIDEO_REPRESENTATION_ID = "avc-baseline-240"; - private static final String H264_BASELINE_480P_VIDEO_REPRESENTATION_ID = "avc-baseline-480"; - private static final String H264_MAIN_240P_VIDEO_REPRESENTATION_ID = "avc-main-240"; - private static final String H264_MAIN_480P_VIDEO_REPRESENTATION_ID = "avc-main-480"; - // The highest quality H264 format mandated by the Android CDD. - private static final String H264_CDD_FIXED = Util.SDK_INT < 23 - ? H264_BASELINE_480P_VIDEO_REPRESENTATION_ID : H264_MAIN_480P_VIDEO_REPRESENTATION_ID; - // Multiple H264 formats mandated by the Android CDD. - private static final String[] H264_CDD_ADAPTIVE = Util.SDK_INT < 23 - ? new String[] { - H264_BASELINE_240P_VIDEO_REPRESENTATION_ID, - H264_BASELINE_480P_VIDEO_REPRESENTATION_ID} - : new String[] { - H264_BASELINE_240P_VIDEO_REPRESENTATION_ID, - H264_BASELINE_480P_VIDEO_REPRESENTATION_ID, - H264_MAIN_240P_VIDEO_REPRESENTATION_ID, - H264_MAIN_480P_VIDEO_REPRESENTATION_ID}; - - private static final String H265_BASELINE_288P_VIDEO_REPRESENTATION_ID = "hevc-main-288"; - private static final String H265_BASELINE_360P_VIDEO_REPRESENTATION_ID = "hevc-main-360"; - // The highest quality H265 format mandated by the Android CDD. - private static final String H265_CDD_FIXED = H265_BASELINE_360P_VIDEO_REPRESENTATION_ID; - // Multiple H265 formats mandated by the Android CDD. - private static final String[] H265_CDD_ADAPTIVE = - new String[] { - H265_BASELINE_288P_VIDEO_REPRESENTATION_ID, - H265_BASELINE_360P_VIDEO_REPRESENTATION_ID}; - - private static final String VORBIS_AUDIO_REPRESENTATION_ID = "2"; - private static final String VP9_180P_VIDEO_REPRESENTATION_ID = "0"; - private static final String VP9_360P_VIDEO_REPRESENTATION_ID = "1"; - // The highest quality VP9 format mandated by the Android CDD. - private static final String VP9_CDD_FIXED = VP9_360P_VIDEO_REPRESENTATION_ID; - // Multiple VP9 formats mandated by the Android CDD. - private static final String[] VP9_CDD_ADAPTIVE = - new String[] { - VP9_180P_VIDEO_REPRESENTATION_ID, - VP9_360P_VIDEO_REPRESENTATION_ID}; - - // Whether adaptive tests should enable video formats beyond those mandated by the Android CDD - // if the device advertises support for them. - private static final boolean ALLOW_ADDITIONAL_VIDEO_FORMATS = Util.SDK_INT >= 21; - - private static final ActionSchedule SEEKING_SCHEDULE = new ActionSchedule.Builder(TAG) - .delay(10000).seek(15000) - .delay(10000).seek(30000).seek(31000).seek(32000).seek(33000).seek(34000) - .delay(1000).pause().delay(1000).play() - .delay(1000).pause().seek(100000).delay(1000).play() - .build(); - private static final ActionSchedule RENDERER_DISABLING_SCHEDULE = new ActionSchedule.Builder(TAG) - // Wait 10 seconds, disable the video renderer, wait another 5 seconds and enable it again. - .delay(10000).disableRenderer(DashHostedTest.VIDEO_RENDERER_INDEX) - .delay(10000).enableRenderer(DashHostedTest.VIDEO_RENDERER_INDEX) - // Ditto for the audio renderer. - .delay(10000).disableRenderer(DashHostedTest.AUDIO_RENDERER_INDEX) - .delay(10000).enableRenderer(DashHostedTest.AUDIO_RENDERER_INDEX) - // Wait 10 seconds, then disable and enable the video renderer 5 times in quick succession. - .delay(10000).disableRenderer(DashHostedTest.VIDEO_RENDERER_INDEX) - .enableRenderer(DashHostedTest.VIDEO_RENDERER_INDEX) - .disableRenderer(DashHostedTest.VIDEO_RENDERER_INDEX) - .enableRenderer(DashHostedTest.VIDEO_RENDERER_INDEX) - .disableRenderer(DashHostedTest.VIDEO_RENDERER_INDEX) - .enableRenderer(DashHostedTest.VIDEO_RENDERER_INDEX) - .disableRenderer(DashHostedTest.VIDEO_RENDERER_INDEX) - .enableRenderer(DashHostedTest.VIDEO_RENDERER_INDEX) - .disableRenderer(DashHostedTest.VIDEO_RENDERER_INDEX) - .enableRenderer(DashHostedTest.VIDEO_RENDERER_INDEX) - // Ditto for the audio renderer. - .delay(10000).disableRenderer(DashHostedTest.AUDIO_RENDERER_INDEX) - .enableRenderer(DashHostedTest.AUDIO_RENDERER_INDEX) - .disableRenderer(DashHostedTest.AUDIO_RENDERER_INDEX) - .enableRenderer(DashHostedTest.AUDIO_RENDERER_INDEX) - .disableRenderer(DashHostedTest.AUDIO_RENDERER_INDEX) - .enableRenderer(DashHostedTest.AUDIO_RENDERER_INDEX) - .disableRenderer(DashHostedTest.AUDIO_RENDERER_INDEX) - .enableRenderer(DashHostedTest.AUDIO_RENDERER_INDEX) - .disableRenderer(DashHostedTest.AUDIO_RENDERER_INDEX) - .enableRenderer(DashHostedTest.AUDIO_RENDERER_INDEX) - .build(); - - public DashTest() { - super(HostActivity.class); - } - - // H264 CDD. - - public void testH264Fixed() throws IOException { - if (Util.SDK_INT < 16) { - // Pass. - return; - } - String testName = "testH264Fixed"; - testDashPlayback(getActivity(), testName, AAC_AUDIO_FRAME_COUNT, VIDEO_FRAME_COUNT, - H264_MANIFEST, AAC_AUDIO_REPRESENTATION_ID, false, H264_CDD_FIXED); - } - - public void testH264Adaptive() throws IOException { - if (Util.SDK_INT < 16 || shouldSkipAdaptiveTest(MimeTypes.VIDEO_H264)) { - // Pass. - return; - } - String testName = "testH264Adaptive"; - testDashPlayback(getActivity(), testName, AAC_AUDIO_FRAME_COUNT, VIDEO_FRAME_COUNT, - H264_MANIFEST, AAC_AUDIO_REPRESENTATION_ID, ALLOW_ADDITIONAL_VIDEO_FORMATS, - H264_CDD_ADAPTIVE); - } - - public void testH264AdaptiveWithSeeking() throws IOException { - if (Util.SDK_INT < 16 || shouldSkipAdaptiveTest(MimeTypes.VIDEO_H264)) { - // Pass. - return; - } - String testName = "testH264AdaptiveWithSeeking"; - testDashPlayback(getActivity(), testName, SEEKING_SCHEDULE, false, AAC_AUDIO_FRAME_COUNT, - VIDEO_FRAME_COUNT, H264_MANIFEST, AAC_AUDIO_REPRESENTATION_ID, - ALLOW_ADDITIONAL_VIDEO_FORMATS, H264_CDD_ADAPTIVE); - } - - public void testH264AdaptiveWithRendererDisabling() throws IOException { - if (Util.SDK_INT < 16 || shouldSkipAdaptiveTest(MimeTypes.VIDEO_H264)) { - // Pass. - return; - } - String testName = "testH264AdaptiveWithRendererDisabling"; - testDashPlayback(getActivity(), testName, RENDERER_DISABLING_SCHEDULE, false, - AAC_AUDIO_FRAME_COUNT, VIDEO_FRAME_COUNT, H264_MANIFEST, AAC_AUDIO_REPRESENTATION_ID, - ALLOW_ADDITIONAL_VIDEO_FORMATS, H264_CDD_ADAPTIVE); - } - - // H265 CDD. - - public void testH265Fixed() throws IOException { - if (Util.SDK_INT < 21) { - // Pass. - return; - } - String testName = "testH265Fixed"; - testDashPlayback(getActivity(), testName, AAC_AUDIO_FRAME_COUNT, VIDEO_FRAME_COUNT, - H265_MANIFEST, AAC_AUDIO_REPRESENTATION_ID, false, H265_CDD_FIXED); - } - - public void testH265Adaptive() throws IOException { - if (Util.SDK_INT < 21 || shouldSkipAdaptiveTest(MimeTypes.VIDEO_H265)) { - // Pass. - return; - } - String testName = "testH265Adaptive"; - testDashPlayback(getActivity(), testName, AAC_AUDIO_FRAME_COUNT, VIDEO_FRAME_COUNT, - H265_MANIFEST, AAC_AUDIO_REPRESENTATION_ID, ALLOW_ADDITIONAL_VIDEO_FORMATS, - H265_CDD_ADAPTIVE); - } - - public void testH265AdaptiveWithSeeking() throws IOException { - if (Util.SDK_INT < 21 || shouldSkipAdaptiveTest(MimeTypes.VIDEO_H265)) { - // Pass. - return; - } - String testName = "testH265AdaptiveWithSeeking"; - testDashPlayback(getActivity(), testName, SEEKING_SCHEDULE, false, AAC_AUDIO_FRAME_COUNT, - VIDEO_FRAME_COUNT, H265_MANIFEST, AAC_AUDIO_REPRESENTATION_ID, - ALLOW_ADDITIONAL_VIDEO_FORMATS, H265_CDD_ADAPTIVE); - } - - public void testH265AdaptiveWithRendererDisabling() throws IOException { - if (Util.SDK_INT < 21 || shouldSkipAdaptiveTest(MimeTypes.VIDEO_H265)) { - // Pass. - return; - } - String testName = "testH265AdaptiveWithRendererDisabling"; - testDashPlayback(getActivity(), testName, RENDERER_DISABLING_SCHEDULE, false, - AAC_AUDIO_FRAME_COUNT, VIDEO_FRAME_COUNT, H265_MANIFEST, AAC_AUDIO_REPRESENTATION_ID, - ALLOW_ADDITIONAL_VIDEO_FORMATS, H265_CDD_ADAPTIVE); - } - - // VP9 (CDD). - - public void testVp9Fixed360p() throws IOException { - if (Util.SDK_INT < 16) { - // Pass. - return; - } - String testName = "testVp9Fixed360p"; - testDashPlayback(getActivity(), testName, VORBIS_AUDIO_FRAME_COUNT, VIDEO_FRAME_COUNT, - VP9_MANIFEST, VORBIS_AUDIO_REPRESENTATION_ID, false, VP9_CDD_FIXED); - } - - public void testVp9Adaptive() throws IOException { - if (Util.SDK_INT < 16 || shouldSkipAdaptiveTest(MimeTypes.VIDEO_VP9)) { - // Pass. - return; - } - String testName = "testVp9Adaptive"; - testDashPlayback(getActivity(), testName, VORBIS_AUDIO_FRAME_COUNT, VIDEO_FRAME_COUNT, - VP9_MANIFEST, VORBIS_AUDIO_REPRESENTATION_ID, ALLOW_ADDITIONAL_VIDEO_FORMATS, - VP9_CDD_ADAPTIVE); - } - - public void testVp9AdaptiveWithSeeking() throws IOException { - if (Util.SDK_INT < 16 || shouldSkipAdaptiveTest(MimeTypes.VIDEO_VP9)) { - // Pass. - return; - } - String testName = "testVp9AdaptiveWithSeeking"; - testDashPlayback(getActivity(), testName, SEEKING_SCHEDULE, false, VORBIS_AUDIO_FRAME_COUNT, - VIDEO_FRAME_COUNT, VP9_MANIFEST, VORBIS_AUDIO_REPRESENTATION_ID, - ALLOW_ADDITIONAL_VIDEO_FORMATS, VP9_CDD_ADAPTIVE); - } - - public void testVp9AdaptiveWithRendererDisabling() throws IOException { - if (Util.SDK_INT < 16 || shouldSkipAdaptiveTest(MimeTypes.VIDEO_VP9)) { - // Pass. - return; - } - String testName = "testVp9AdaptiveWithRendererDisabling"; - testDashPlayback(getActivity(), testName, RENDERER_DISABLING_SCHEDULE, false, - VORBIS_AUDIO_FRAME_COUNT, VIDEO_FRAME_COUNT, VP9_MANIFEST, VORBIS_AUDIO_REPRESENTATION_ID, - ALLOW_ADDITIONAL_VIDEO_FORMATS, VP9_CDD_ADAPTIVE); - } - - // Internal. - - private void testDashPlayback(HostActivity activity, String testName, - int sourceAudioFrameCount, int sourceVideoFrameCount, String manifestFileName, - String audioFormat, boolean includeAdditionalVideoFormats, String... videoFormats) - throws IOException { - testDashPlayback(activity, testName, null, true, sourceAudioFrameCount, - sourceVideoFrameCount, manifestFileName, audioFormat, includeAdditionalVideoFormats, - videoFormats); - } - - private void testDashPlayback(HostActivity activity, String testName, - ActionSchedule actionSchedule, boolean fullPlaybackNoSeeking, int sourceAudioFrameCount, - int sourceVideoFrameCount, String manifestFileName, String audioFormat, - boolean includeAdditionalVideoFormats, String... videoFormats) throws IOException { - MediaPresentationDescription mpd = TestUtil.loadManifest(activity, - MANIFEST_URL_PREFIX + manifestFileName, new MediaPresentationDescriptionParser()); - MetricsLogger metricsLogger = MetricsLogger.Factory.createDefault(getInstrumentation(), TAG); - DashHostedTest test = new DashHostedTest(testName, mpd, metricsLogger, fullPlaybackNoSeeking, - sourceAudioFrameCount, sourceVideoFrameCount, audioFormat, includeAdditionalVideoFormats, - videoFormats); - if (actionSchedule != null) { - test.setSchedule(actionSchedule); - } - activity.runTest(test, mpd.duration + MAX_ADDITIONAL_TIME_MS); - } - - private boolean shouldSkipAdaptiveTest(String mimeType) throws IOException { - if (!MediaCodecUtil.getDecoderInfo(mimeType, false).adaptive) { - assertTrue(Util.SDK_INT < 21); - return true; - } - return false; - } - - @TargetApi(16) - private static class DashHostedTest extends ExoHostedTest { - - private static final int RENDERER_COUNT = 2; - private static final int VIDEO_RENDERER_INDEX = 0; - private static final int AUDIO_RENDERER_INDEX = 1; - - private static final int BUFFER_SEGMENT_SIZE = 64 * 1024; - private static final int VIDEO_BUFFER_SEGMENTS = 200; - private static final int AUDIO_BUFFER_SEGMENTS = 60; - - private static final String VIDEO_TAG = "Video"; - private static final String AUDIO_TAG = "Audio"; - private static final int VIDEO_EVENT_ID = 0; - private static final int AUDIO_EVENT_ID = 1; - - private final String testName; - private final MediaPresentationDescription mpd; - private final MetricsLogger metricsLogger; - private final boolean fullPlaybackNoSeeking; - private final int sourceAudioFrameCount; - private final int sourceVideoFrameCount; - private final boolean includeAdditionalVideoFormats; - private final String[] audioFormats; - private final String[] videoFormats; - - private CodecCounters videoCounters; - private CodecCounters audioCounters; - - /** - * @param testName The name of the test. - * @param mpd The manifest. - * @param metricsLogger Logger to log metrics from the test. - * @param fullPlaybackNoSeeking True if the test will play the entire source with no seeking. - * False otherwise. - * @param sourceAudioFrameCount The number of audio frames in the source. - * @param sourceVideoFrameCount The number of video frames in the source. - * @param audioFormat The audio format. - * @param includeAdditionalVideoFormats Whether to use video formats in addition to - * those listed in the videoFormats argument, if the device is capable of playing them. - * @param videoFormats The video formats. - */ - public DashHostedTest(String testName, MediaPresentationDescription mpd, - MetricsLogger metricsLogger, boolean fullPlaybackNoSeeking, int sourceAudioFrameCount, - int sourceVideoFrameCount, String audioFormat, boolean includeAdditionalVideoFormats, - String... videoFormats) { - super(RENDERER_COUNT); - this.testName = testName; - this.mpd = Assertions.checkNotNull(mpd); - this.metricsLogger = metricsLogger; - this.fullPlaybackNoSeeking = fullPlaybackNoSeeking; - this.sourceAudioFrameCount = sourceAudioFrameCount; - this.sourceVideoFrameCount = sourceVideoFrameCount; - this.audioFormats = new String[] {audioFormat}; - this.includeAdditionalVideoFormats = includeAdditionalVideoFormats; - this.videoFormats = videoFormats; - } - - @Override - public TrackRenderer[] buildRenderers(HostActivity host, ExoPlayer player, Surface surface) { - Handler handler = new Handler(); - LogcatLogger logger = new LogcatLogger(TAG, player); - LoadControl loadControl = new DefaultLoadControl(new DefaultAllocator(BUFFER_SEGMENT_SIZE)); - String userAgent = TestUtil.getUserAgent(host); - - // Build the video renderer. - DataSource videoDataSource = new DefaultUriDataSource(host, null, userAgent); - TrackSelector videoTrackSelector = new TrackSelector(AdaptationSet.TYPE_VIDEO, - includeAdditionalVideoFormats, videoFormats); - ChunkSource videoChunkSource = new DashChunkSource(mpd, videoTrackSelector, videoDataSource, - new FormatEvaluator.RandomEvaluator(0)); - ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl, - VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, handler, logger, VIDEO_EVENT_ID, - MIN_LOADABLE_RETRY_COUNT); - MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(host, - videoSampleSource, MediaCodecSelector.DEFAULT, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, - 0, handler, logger, 50); - videoCounters = videoRenderer.codecCounters; - player.sendMessage(videoRenderer, MediaCodecVideoTrackRenderer.MSG_SET_SURFACE, surface); - - // Build the audio renderer. - DataSource audioDataSource = new DefaultUriDataSource(host, null, userAgent); - TrackSelector audioTrackSelector = new TrackSelector(AdaptationSet.TYPE_AUDIO, false, - audioFormats); - ChunkSource audioChunkSource = new DashChunkSource(mpd, audioTrackSelector, audioDataSource, - null); - ChunkSampleSource audioSampleSource = new ChunkSampleSource(audioChunkSource, loadControl, - AUDIO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, handler, logger, AUDIO_EVENT_ID, - MIN_LOADABLE_RETRY_COUNT); - MediaCodecAudioTrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer( - audioSampleSource, MediaCodecSelector.DEFAULT, handler, logger); - audioCounters = audioRenderer.codecCounters; - - TrackRenderer[] renderers = new TrackRenderer[RENDERER_COUNT]; - renderers[VIDEO_RENDERER_INDEX] = videoRenderer; - renderers[AUDIO_RENDERER_INDEX] = audioRenderer; - return renderers; - } - - @Override - protected void assertPassed() { - if (fullPlaybackNoSeeking) { - // Audio is not adaptive and we didn't seek (which can re-instantiate the audio decoder - // in ExoPlayer), so the decoder output format should have changed exactly once. The output - // buffers should have changed 0 or 1 times. - CodecCountersUtil.assertOutputFormatChangedCount(AUDIO_TAG, audioCounters, 1); - CodecCountersUtil.assertOutputBuffersChangedLimit(AUDIO_TAG, audioCounters, 1); - - if (videoFormats != null && videoFormats.length == 1) { - // Video is not adaptive, so the decoder output format should have changed exactly once. - // The output buffers should have changed 0 or 1 times. - CodecCountersUtil.assertOutputFormatChangedCount(VIDEO_TAG, videoCounters, 1); - CodecCountersUtil.assertOutputBuffersChangedLimit(VIDEO_TAG, videoCounters, 1); - } - - // We shouldn't have skipped any output buffers. - CodecCountersUtil.assertSkippedOutputBufferCount(AUDIO_TAG, audioCounters, 0); - CodecCountersUtil.assertSkippedOutputBufferCount(VIDEO_TAG, videoCounters, 0); - - // We allow one fewer output buffer due to the way that MediaCodecTrackRenderer and the - // underlying decoders handle the end of stream. This should be tightened up in the future. - CodecCountersUtil.assertTotalOutputBufferCount(AUDIO_TAG, audioCounters, - sourceAudioFrameCount - 1, sourceAudioFrameCount); - CodecCountersUtil.assertTotalOutputBufferCount(VIDEO_TAG, videoCounters, - sourceVideoFrameCount - 1, sourceVideoFrameCount); - - // The total playing time should match the source duration. - long sourceDuration = mpd.duration; - long minAllowedActualPlayingTime = sourceDuration - MAX_PLAYING_TIME_DISCREPANCY_MS; - long maxAllowedActualPlayingTime = sourceDuration + MAX_PLAYING_TIME_DISCREPANCY_MS; - long actualPlayingTime = getTotalPlayingTimeMs(); - assertTrue("Total playing time: " + actualPlayingTime + ". Actual media duration: " - + sourceDuration, minAllowedActualPlayingTime <= actualPlayingTime - && actualPlayingTime <= maxAllowedActualPlayingTime); - } - - // Assert that the level of performance was acceptable. - // Assert that total dropped frames were within limit. - int droppedFrameLimit = (int) Math.ceil(MAX_DROPPED_VIDEO_FRAME_FRACTION - * CodecCountersUtil.getTotalOutputBuffers(videoCounters)); - CodecCountersUtil.assertDroppedOutputBufferLimit(VIDEO_TAG, videoCounters, droppedFrameLimit); - // Assert that consecutive dropped frames were within limit. - CodecCountersUtil.assertConsecutiveDroppedOutputBufferLimit(VIDEO_TAG, videoCounters, - MAX_CONSECUTIVE_DROPPED_VIDEO_FRAMES); - } - - @Override - protected void logMetrics() { - // Create Bundle of metrics from the test. - Bundle metrics = new Bundle(); - metrics.putString(MetricsLogger.KEY_TEST_NAME, testName); - metrics.putInt(MetricsLogger.KEY_FRAMES_DROPPED_COUNT, - videoCounters.droppedOutputBufferCount); - metrics.putInt(MetricsLogger.KEY_MAX_CONSECUTIVE_FRAMES_DROPPED_COUNT, - videoCounters.maxConsecutiveDroppedOutputBufferCount); - metrics.putInt(MetricsLogger.KEY_FRAMES_SKIPPED_COUNT, - videoCounters.skippedOutputBufferCount); - metrics.putInt(MetricsLogger.KEY_FRAMES_RENDERED_COUNT, - videoCounters.renderedOutputBufferCount); - - // Send metrics for logging. - metricsLogger.logMetrics(metrics); - } - - private static final class TrackSelector implements DashTrackSelector { - - private final int adaptationSetType; - private final String[] representationIds; - private final boolean includeAdditionalVideoRepresentations; - - private TrackSelector(int adaptationSetType, boolean includeAdditionalVideoRepresentations, - String[] representationIds) { - Assertions.checkState(!includeAdditionalVideoRepresentations - || adaptationSetType == AdaptationSet.TYPE_VIDEO); - this.adaptationSetType = adaptationSetType; - this.includeAdditionalVideoRepresentations = includeAdditionalVideoRepresentations; - this.representationIds = representationIds; - } - - @Override - public void selectTracks(MediaPresentationDescription manifest, int periodIndex, - Output output) throws IOException { - Period period = manifest.getPeriod(periodIndex); - int adaptationSetIndex = period.getAdaptationSetIndex(adaptationSetType); - AdaptationSet adaptationSet = period.adaptationSets.get(adaptationSetIndex); - int[] representationIndices = getRepresentationIndices(adaptationSet, representationIds, - includeAdditionalVideoRepresentations); - if (adaptationSetType == AdaptationSet.TYPE_VIDEO) { - output.adaptiveTrack(manifest, periodIndex, adaptationSetIndex, representationIndices); - } - for (int i = 0; i < representationIndices.length; i++) { - output.fixedTrack(manifest, periodIndex, adaptationSetIndex, representationIndices[i]); - } - } - - private static int[] getRepresentationIndices(AdaptationSet adaptationSet, - String[] representationIds, boolean includeAdditionalVideoRepresentations) - throws IOException { - List availableRepresentations = adaptationSet.representations; - List selectedRepresentationIndices = new ArrayList<>(); - - // Always select explicitly listed representations, failing if they're missing. - for (int i = 0; i < representationIds.length; i++) { - String representationId = representationIds[i]; - boolean foundIndex = false; - for (int j = 0; j < availableRepresentations.size() && !foundIndex; j++) { - if (availableRepresentations.get(j).format.id.equals(representationId)) { - selectedRepresentationIndices.add(j); - foundIndex = true; - } - } - if (!foundIndex) { - throw new IllegalStateException("Representation " + representationId + " not found."); - } - } - - // Select additional video representations, if supported by the device. - if (includeAdditionalVideoRepresentations) { - int[] supportedVideoRepresentationIndices = VideoFormatSelectorUtil.selectVideoFormats( - availableRepresentations, null, false, true, -1, -1); - for (int i = 0; i < supportedVideoRepresentationIndices.length; i++) { - int representationIndex = supportedVideoRepresentationIndices[i]; - if (!selectedRepresentationIndices.contains(representationIndex)) { - Log.d(TAG, "Adding video format: " + availableRepresentations.get(i).format.id); - selectedRepresentationIndices.add(representationIndex); - } - } - - } - - return Util.toArray(selectedRepresentationIndices); - } - } - } - -} - diff --git a/playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/util/Action.java b/playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/util/Action.java deleted file mode 100644 index 307380332f..0000000000 --- a/playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/util/Action.java +++ /dev/null @@ -1,147 +0,0 @@ -/* - * Copyright (C) 2014 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.exoplayer.playbacktests.util; - -import com.google.android.exoplayer.ExoPlayer; - -import android.util.Log; - -/** - * Base class for actions to perform during playback tests. - */ -public abstract class Action { - - private final String tag; - private final String description; - - /** - * @param tag A tag to use for logging. - * @param description A description to be logged when the action is executed. - */ - public Action(String tag, String description) { - this.tag = tag; - this.description = description; - } - - /** - * Executes the action. - * - * @param player An {@link ExoPlayer} on which the action is executed. - */ - public final void doAction(ExoPlayer player) { - Log.i(tag, description); - doActionImpl(player); - } - - /** - * Called by {@link #doAction(ExoPlayer)} do actually perform the action. - * - * @param player An {@link ExoPlayer} on which the action is executed. - */ - protected abstract void doActionImpl(ExoPlayer player); - - /** - * Calls {@link ExoPlayer#seekTo(long)}. - */ - public static final class Seek extends Action { - - private final long positionMs; - - /** - * @param tag A tag to use for logging. - * @param positionMs The seek position. - */ - public Seek(String tag, long positionMs) { - super(tag, "Seek:" + positionMs); - this.positionMs = positionMs; - } - - @Override - protected void doActionImpl(ExoPlayer player) { - player.seekTo(positionMs); - } - - } - - /** - * Calls {@link ExoPlayer#stop()}. - */ - public static final class Stop extends Action { - - /** - * @param tag A tag to use for logging. - */ - public Stop(String tag) { - super(tag, "Stop"); - } - - @Override - protected void doActionImpl(ExoPlayer player) { - player.stop(); - } - - } - - /** - * Calls {@link ExoPlayer#setPlayWhenReady(boolean)}. - */ - public static final class SetPlayWhenReady extends Action { - - private final boolean playWhenReady; - - /** - * @param tag A tag to use for logging. - * @param playWhenReady The value to pass. - */ - public SetPlayWhenReady(String tag, boolean playWhenReady) { - super(tag, playWhenReady ? "Play" : "Pause"); - this.playWhenReady = playWhenReady; - } - - @Override - protected void doActionImpl(ExoPlayer player) { - player.setPlayWhenReady(playWhenReady); - } - - } - - /** - * Calls {@link ExoPlayer#setSelectedTrack(int, int)}. - */ - public static final class SetSelectedTrack extends Action { - - private final int rendererIndex; - private final int trackIndex; - - /** - * @param tag A tag to use for logging. - * @param rendererIndex The index of the renderer. - * @param trackIndex The index of the track. - */ - public SetSelectedTrack(String tag, int rendererIndex, int trackIndex) { - super(tag, "SelectedTrack:" + rendererIndex + ":" + trackIndex); - this.rendererIndex = rendererIndex; - this.trackIndex = trackIndex; - } - - @Override - protected void doActionImpl(ExoPlayer player) { - player.setSelectedTrack(rendererIndex, trackIndex); - } - - } - -} diff --git a/playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/util/ActionSchedule.java b/playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/util/ActionSchedule.java deleted file mode 100644 index b78057aa39..0000000000 --- a/playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/util/ActionSchedule.java +++ /dev/null @@ -1,226 +0,0 @@ -/* - * Copyright (C) 2014 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.exoplayer.playbacktests.util; - -import com.google.android.exoplayer.ExoPlayer; -import com.google.android.exoplayer.playbacktests.util.Action.Seek; -import com.google.android.exoplayer.playbacktests.util.Action.SetPlayWhenReady; -import com.google.android.exoplayer.playbacktests.util.Action.SetSelectedTrack; -import com.google.android.exoplayer.playbacktests.util.Action.Stop; - -import android.os.Handler; - -/** - * Schedules a sequence of {@link Action}s for execution during a test. - */ -public final class ActionSchedule { - - private final ActionNode rootNode; - - /** - * @param rootNode The first node in the sequence. - */ - private ActionSchedule(ActionNode rootNode) { - this.rootNode = rootNode; - } - - /** - * Starts execution of the schedule. - * - * @param player The player to which each {@link Action} should be applied. - * @param mainHandler A handler associated with the main thread of the host activity. - */ - /* package */ void start(ExoPlayer player, Handler mainHandler) { - rootNode.schedule(player, mainHandler); - } - - /** - * A builder for {@link ActionSchedule} instances. - */ - public static final class Builder { - - private final String tag; - private final ActionNode rootNode; - private long currentDelayMs; - - private ActionNode previousNode; - - /** - * @param tag A tag to use for logging. - */ - public Builder(String tag) { - this.tag = tag; - rootNode = new ActionNode(new RootAction(tag), 0); - previousNode = rootNode; - } - - /** - * Schedules a delay between executing any previous actions and any subsequent ones. - * - * @param delayMs The delay in milliseconds. - * @return The builder, for convenience. - */ - public Builder delay(long delayMs) { - currentDelayMs += delayMs; - return this; - } - - /** - * Schedules an action to be executed. - * - * @param action The action to schedule. - * @return The builder, for convenience. - */ - public Builder apply(Action action) { - ActionNode next = new ActionNode(action, currentDelayMs); - previousNode.setNext(next); - previousNode = next; - currentDelayMs = 0; - return this; - } - - /** - * Schedules a seek action to be executed. - * - * @param positionMs The seek position. - * @return The builder, for convenience. - */ - public Builder seek(long positionMs) { - return apply(new Seek(tag, positionMs)); - } - - /** - * Schedules a stop action to be executed. - * - * @return The builder, for convenience. - */ - public Builder stop() { - return apply(new Stop(tag)); - } - - /** - * Schedules a play action to be executed. - * - * @return The builder, for convenience. - */ - public Builder play() { - return apply(new SetPlayWhenReady(tag, true)); - } - - /** - * Schedules a pause action to be executed. - * - * @return The builder, for convenience. - */ - public Builder pause() { - return apply(new SetPlayWhenReady(tag, false)); - } - - /** - * Schedules a renderer enable action to be executed. - * - * @return The builder, for convenience. - */ - public Builder enableRenderer(int index) { - return apply(new SetSelectedTrack(tag, index, ExoPlayer.TRACK_DEFAULT)); - } - - /** - * Schedules a renderer disable action to be executed. - * - * @return The builder, for convenience. - */ - public Builder disableRenderer(int index) { - return apply(new SetSelectedTrack(tag, index, ExoPlayer.TRACK_DISABLED)); - } - - public ActionSchedule build() { - return new ActionSchedule(rootNode); - } - - } - - /** - * Wraps an {@link Action}, allowing a delay and a next {@link Action} to be specified. - */ - private static final class ActionNode implements Runnable { - - private final Action action; - private final long delayMs; - - private ActionNode next; - - private ExoPlayer player; - private Handler mainHandler; - - /** - * @param action The wrapped action. - * @param delayMs The delay between the node being scheduled and the action being executed. - */ - public ActionNode(Action action, long delayMs) { - this.action = action; - this.delayMs = delayMs; - } - - /** - * Sets the next action. - * - * @param next The next {@link Action}. - */ - public void setNext(ActionNode next) { - this.next = next; - } - - /** - * Schedules {@link #action} to be executed after {@link #delayMs}. The {@link #next} node - * will be scheduled immediately after {@link #action} is executed. - * - * @param player The player to which each {@link Action} should be applied. - * @param mainHandler A handler associated with the main thread of the host activity. - */ - public void schedule(ExoPlayer player, Handler mainHandler) { - this.player = player; - this.mainHandler = mainHandler; - mainHandler.postDelayed(this, delayMs); - } - - @Override - public void run() { - action.doAction(player); - if (next != null) { - next.schedule(player, mainHandler); - } - } - - } - - /** - * A no-op root action. - */ - private static final class RootAction extends Action { - - public RootAction(String tag) { - super(tag, "Root"); - } - - @Override - protected void doActionImpl(ExoPlayer player) { - // Do nothing. - } - - } - -} diff --git a/playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/util/CodecCountersUtil.java b/playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/util/CodecCountersUtil.java deleted file mode 100644 index 6034068fcc..0000000000 --- a/playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/util/CodecCountersUtil.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright (C) 2014 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.exoplayer.playbacktests.util; - -import com.google.android.exoplayer.CodecCounters; - -import junit.framework.TestCase; - -/** - * Assertions for {@link CodecCounters}. - */ -public final class CodecCountersUtil { - - private CodecCountersUtil() {} - - /** - * Returns the sum of the skipped, dropped and rendered buffers. - * - * @param counters The counters for which the total should be calculated. - * @return The sum of the skipped, dropped and rendered buffers. - */ - public static int getTotalOutputBuffers(CodecCounters counters) { - return counters.skippedOutputBufferCount + counters.droppedOutputBufferCount - + counters.renderedOutputBufferCount; - } - - public static void assertOutputFormatChangedCount(String name, CodecCounters counters, - int expected) { - counters.ensureUpdated(); - int actual = counters.outputFormatChangedCount; - TestCase.assertEquals("Codec(" + name + ") output format changed " + actual + " times. " - + "Expected " + expected + " times.", expected, actual); - } - - public static void assertOutputBuffersChangedLimit(String name, CodecCounters counters, - int limit) { - counters.ensureUpdated(); - int actual = counters.outputBuffersChangedCount; - TestCase.assertTrue("Codec(" + name + ") output buffers changed " + actual + " times. " - + "Limit: " + limit + ".", actual <= limit); - } - - public static void assertSkippedOutputBufferCount(String name, CodecCounters counters, - int expected) { - counters.ensureUpdated(); - int actual = counters.skippedOutputBufferCount; - TestCase.assertEquals("Codec(" + name + ") skipped " + actual + " buffers. Expected " - + expected + ".", expected, actual); - } - - public static void assertTotalOutputBufferCount(String name, CodecCounters counters, - int minCount, int maxCount) { - counters.ensureUpdated(); - int actual = getTotalOutputBuffers(counters); - TestCase.assertTrue("Codec(" + name + ") output " + actual + " buffers. Expected in range [" - + minCount + ", " + maxCount + "].", minCount <= actual && actual <= maxCount); - } - - public static void assertDroppedOutputBufferLimit(String name, CodecCounters counters, - int limit) { - counters.ensureUpdated(); - int actual = counters.droppedOutputBufferCount; - TestCase.assertTrue("Codec(" + name + ") was late decoding: " + actual + " buffers. " - + "Limit: " + limit + ".", actual <= limit); - } - - public static void assertConsecutiveDroppedOutputBufferLimit(String name, CodecCounters counters, - int limit) { - counters.ensureUpdated(); - int actual = counters.maxConsecutiveDroppedOutputBufferCount; - TestCase.assertTrue("Codec(" + name + ") was late decoding: " + actual - + " buffers consecutively. " + "Limit: " + limit + ".", actual <= limit); - } - -} diff --git a/playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/util/ExoHostedTest.java b/playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/util/ExoHostedTest.java deleted file mode 100644 index 075a71d587..0000000000 --- a/playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/util/ExoHostedTest.java +++ /dev/null @@ -1,182 +0,0 @@ -/* - * Copyright (C) 2014 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.exoplayer.playbacktests.util; - -import com.google.android.exoplayer.ExoPlaybackException; -import com.google.android.exoplayer.ExoPlayer; -import com.google.android.exoplayer.TrackRenderer; -import com.google.android.exoplayer.audio.AudioTrack; -import com.google.android.exoplayer.playbacktests.util.HostActivity.HostedTest; - -import android.os.Handler; -import android.os.SystemClock; -import android.view.Surface; - -/** - * A {@link HostedTest} for {@link ExoPlayer} playback tests. - */ -public abstract class ExoHostedTest implements HostedTest, ExoPlayer.Listener { - - static { - // ExoPlayer's AudioTrack class is able to work around spurious timestamps reported by the - // platform (by ignoring them). Disable this workaround, since we're interested in testing - // that the underlying platform is behaving correctly. - AudioTrack.failOnSpuriousAudioTimestamp = true; - } - - private final int rendererCount; - private final boolean failOnPlayerError; - - private ActionSchedule pendingSchedule; - private Handler actionHandler; - private ExoPlayer player; - private ExoPlaybackException playerError; - private boolean playerWasPrepared; - private boolean playerFinished; - private boolean playing; - private long totalPlayingTimeMs; - private long lastPlayingStartTimeMs; - - /** - * Constructs a test that fails if a player error occurs. - * - * @param rendererCount The number of renderers that will be injected into the player. - */ - public ExoHostedTest(int rendererCount) { - this(rendererCount, true); - } - - /** - * @param rendererCount The number of renderers that will be injected into the player. - * @param failOnPlayerError True if a player error should be considered a test failure. False - * otherwise. - */ - public ExoHostedTest(int rendererCount, boolean failOnPlayerError) { - this.rendererCount = rendererCount; - this.failOnPlayerError = failOnPlayerError; - } - - /** - * Sets a schedule to be applied during the test. - * - * @param schedule The schedule. - */ - public final void setSchedule(ActionSchedule schedule) { - if (player == null) { - pendingSchedule = schedule; - } else { - schedule.start(player, actionHandler); - } - } - - // HostedTest implementation - - @Override - public final void initialize(HostActivity host, Surface surface) { - // Build the player. - player = ExoPlayer.Factory.newInstance(rendererCount); - player.addListener(this); - player.prepare(buildRenderers(host, player, surface)); - player.setPlayWhenReady(true); - actionHandler = new Handler(); - // Schedule any pending actions. - if (pendingSchedule != null) { - pendingSchedule.start(player, actionHandler); - pendingSchedule = null; - } - } - - @Override - public final void release() { - actionHandler.removeCallbacksAndMessages(null); - player.release(); - player = null; - } - - @Override - public final boolean isFinished() { - return playerFinished; - } - - @Override - public final void onFinished() { - if (failOnPlayerError && playerError != null) { - throw new Error(playerError); - } - logMetrics(); - assertPassed(); - } - - // ExoPlayer.Listener - - @Override - public final void onPlayerStateChanged(boolean playWhenReady, int playbackState) { - playerWasPrepared |= playbackState != ExoPlayer.STATE_IDLE; - if (playbackState == ExoPlayer.STATE_ENDED - || (playbackState == ExoPlayer.STATE_IDLE && playerWasPrepared)) { - playerFinished = true; - } - boolean playing = playWhenReady && playbackState == ExoPlayer.STATE_READY; - if (!this.playing && playing) { - lastPlayingStartTimeMs = SystemClock.elapsedRealtime(); - } else if (this.playing && !playing) { - totalPlayingTimeMs += SystemClock.elapsedRealtime() - lastPlayingStartTimeMs; - } - this.playing = playing; - } - - @Override - public final void onPlayerError(ExoPlaybackException error) { - playerWasPrepared = true; - playerError = error; - onPlayerErrorInternal(error); - } - - @Override - public final void onPlayWhenReadyCommitted() { - // Do nothing. - } - - // Internal logic - - @SuppressWarnings("unused") - protected abstract TrackRenderer[] buildRenderers(HostActivity host, ExoPlayer player, - Surface surface) throws IllegalStateException; - - @SuppressWarnings("unused") - protected void onPlayerErrorInternal(ExoPlaybackException error) { - // Do nothing. Interested subclasses may override. - } - - protected void assertPassed() { - // Do nothing. Subclasses may override to add additional assertions. - } - - protected void logMetrics() { - // Do nothing. Subclasses may override to log metrics. - } - - // Utility methods and actions for subclasses. - - protected final long getTotalPlayingTimeMs() { - return totalPlayingTimeMs; - } - - protected final ExoPlaybackException getError() { - return playerError; - } - -} diff --git a/playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/util/HostActivity.java b/playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/util/HostActivity.java deleted file mode 100644 index 73d30d99b3..0000000000 --- a/playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/util/HostActivity.java +++ /dev/null @@ -1,249 +0,0 @@ -/* - * Copyright (C) 2014 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.exoplayer.playbacktests.util; - -import static junit.framework.Assert.fail; - -import com.google.android.exoplayer.playbacktests.R; -import com.google.android.exoplayer.util.Assertions; -import com.google.android.exoplayer.util.Util; - -import android.annotation.SuppressLint; -import android.app.Activity; -import android.content.Context; -import android.net.wifi.WifiManager; -import android.net.wifi.WifiManager.WifiLock; -import android.os.Bundle; -import android.os.ConditionVariable; -import android.os.Handler; -import android.os.PowerManager; -import android.os.PowerManager.WakeLock; -import android.util.Log; -import android.view.Surface; -import android.view.SurfaceHolder; -import android.view.SurfaceView; -import android.view.Window; - -/** - * A host activity for performing playback tests. - */ -public final class HostActivity extends Activity implements SurfaceHolder.Callback { - - /** - * Interface for tests that run inside of a {@link HostActivity}. - */ - public interface HostedTest { - - /** - * Called once the activity has been resumed and its surface has been created. - *

- * Called on the main thread. - * - * @param host The host in which the test is being run. - * @param surface The created surface. - */ - void initialize(HostActivity host, Surface surface); - - /** - * Called when the test has finished, or if the activity is paused or its surface is destroyed. - *

- * Called on the main thread. - */ - void release(); - - /** - * Called periodically to check whether the test has finished. - *

- * Called on the main thread. - * - * @return True if the test has finished. False otherwise. - */ - boolean isFinished(); - - /** - * Called after the test is finished and has been released. Implementations may use this method - * to assert that test criteria were met. - *

- * Called on the test thread. - */ - void onFinished(); - - } - - private static final String TAG = "HostActivity"; - - private WakeLock wakeLock; - private WifiLock wifiLock; - - private SurfaceView surfaceView; - private Handler mainHandler; - private CheckFinishedRunnable checkFinishedRunnable; - - private HostedTest hostedTest; - private ConditionVariable hostedTestReleasedCondition; - private boolean hostedTestInitialized; - private boolean hostedTestFinished; - - /** - * Executes a {@link HostedTest} inside the host. - *

- * Must only be called once on each instance. Must be called from the test thread. - * - * @param hostedTest The test to execute. - * @param timeoutMs The number of milliseconds to wait for the test to finish. If the timeout - * is exceeded then the test will fail. - */ - public void runTest(final HostedTest hostedTest, long timeoutMs) { - Assertions.checkArgument(timeoutMs > 0); - Assertions.checkState(Thread.currentThread() != getMainLooper().getThread()); - runOnUiThread(new Runnable() { - @Override - public void run() { - Assertions.checkState(HostActivity.this.hostedTest == null); - HostActivity.this.hostedTest = Assertions.checkNotNull(hostedTest); - maybeInitializeHostedTest(); - } - }); - if (hostedTestReleasedCondition.block(timeoutMs)) { - if (hostedTestFinished) { - Log.d(TAG, "Test finished. Checking pass conditions."); - hostedTest.onFinished(); - Log.d(TAG, "Pass conditions checked."); - } else { - Log.e(TAG, "Test released before it finished. Activity may have been paused whilst test " - + "was in progress."); - fail(); - } - } else { - Log.e(TAG, "Test timed out after " + timeoutMs + " ms."); - fail(); - } - } - - // Activity lifecycle - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - requestWindowFeature(Window.FEATURE_NO_TITLE); - setContentView(R.layout.host_activity); - surfaceView = (SurfaceView) findViewById(R.id.surface_view); - surfaceView.getHolder().addCallback(this); - mainHandler = new Handler(); - hostedTestReleasedCondition = new ConditionVariable(); - checkFinishedRunnable = new CheckFinishedRunnable(); - } - - @Override - public void onStart() { - Context appContext = getApplicationContext(); - WifiManager wifiManager = (WifiManager) appContext.getSystemService(Context.WIFI_SERVICE); - wifiLock = wifiManager.createWifiLock(getWifiLockMode(), TAG); - wifiLock.acquire(); - PowerManager powerManager = (PowerManager) appContext.getSystemService(Context.POWER_SERVICE); - wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG); - wakeLock.acquire(); - super.onStart(); - } - - @Override - public void onResume() { - super.onResume(); - maybeInitializeHostedTest(); - } - - @Override - public void onPause() { - super.onPause(); - maybeReleaseHostedTest(); - } - - @Override - public void onStop() { - super.onStop(); - wakeLock.release(); - wakeLock = null; - wifiLock.release(); - wifiLock = null; - } - - // SurfaceHolder.Callback - - @Override - public void surfaceCreated(SurfaceHolder holder) { - maybeInitializeHostedTest(); - } - - @Override - public void surfaceDestroyed(SurfaceHolder holder) { - maybeReleaseHostedTest(); - } - - @Override - public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { - // Do nothing. - } - - // Internal logic - - private void maybeInitializeHostedTest() { - if (hostedTest == null || hostedTestInitialized) { - return; - } - Surface surface = surfaceView.getHolder().getSurface(); - if (surface != null && surface.isValid()) { - hostedTestInitialized = true; - Log.d(TAG, "Initializing test."); - hostedTest.initialize(this, surface); - checkFinishedRunnable.startChecking(); - } - } - - private void maybeReleaseHostedTest() { - if (hostedTest != null && hostedTestInitialized) { - hostedTest.release(); - hostedTest = null; - mainHandler.removeCallbacks(checkFinishedRunnable); - hostedTestReleasedCondition.open(); - } - } - - @SuppressLint("InlinedApi") - private static final int getWifiLockMode() { - return Util.SDK_INT < 12 ? WifiManager.WIFI_MODE_FULL : WifiManager.WIFI_MODE_FULL_HIGH_PERF; - } - - private final class CheckFinishedRunnable implements Runnable { - - private static final long CHECK_INTERVAL_MS = 1000; - - private void startChecking() { - mainHandler.post(this); - } - - @Override - public void run() { - if (hostedTest.isFinished()) { - hostedTestFinished = true; - finish(); - } else { - mainHandler.postDelayed(this, CHECK_INTERVAL_MS); - } - } - - } - -} diff --git a/playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/util/LogcatLogger.java b/playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/util/LogcatLogger.java deleted file mode 100644 index db33d400aa..0000000000 --- a/playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/util/LogcatLogger.java +++ /dev/null @@ -1,175 +0,0 @@ -/* - * Copyright (C) 2014 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.exoplayer.playbacktests.util; - -import com.google.android.exoplayer.ExoPlaybackException; -import com.google.android.exoplayer.ExoPlayer; -import com.google.android.exoplayer.MediaCodecAudioTrackRenderer; -import com.google.android.exoplayer.MediaCodecTrackRenderer.DecoderInitializationException; -import com.google.android.exoplayer.MediaCodecVideoTrackRenderer; -import com.google.android.exoplayer.audio.AudioTrack.InitializationException; -import com.google.android.exoplayer.audio.AudioTrack.WriteException; -import com.google.android.exoplayer.chunk.ChunkSampleSource; -import com.google.android.exoplayer.chunk.Format; -import com.google.android.exoplayer.hls.HlsSampleSource; - -import android.media.MediaCodec.CryptoException; -import android.util.Log; -import android.view.Surface; - -import java.io.IOException; -import java.text.NumberFormat; -import java.util.Locale; - -/** - * Logs information reported by an {@link ExoPlayer} instance and various player components. - */ -public final class LogcatLogger implements ExoPlayer.Listener, - MediaCodecVideoTrackRenderer.EventListener, MediaCodecAudioTrackRenderer.EventListener, - ChunkSampleSource.EventListener, HlsSampleSource.EventListener { - - private static final NumberFormat TIME_FORMAT; - static { - TIME_FORMAT = NumberFormat.getInstance(Locale.US); - TIME_FORMAT.setMinimumFractionDigits(2); - TIME_FORMAT.setMaximumFractionDigits(2); - } - - private final String tag; - private final ExoPlayer player; - - /** - * @param tag A tag to use for logging. - * @param player The player. - */ - public LogcatLogger(String tag, ExoPlayer player) { - this.tag = tag; - this.player = player; - player.addListener(this); - } - - // ExoPlayer.Listener. - - @Override - public final void onPlayerStateChanged(boolean playWhenReady, int playbackState) { - Log.i(tag, "Player state: " + getTimeString(player.getCurrentPosition()) + ", " - + playWhenReady + ", " + getStateString(playbackState)); - } - - @Override - public final void onPlayerError(ExoPlaybackException e) { - Log.e(tag, "Player failed", e); - } - - @Override - public void onPlayWhenReadyCommitted() {} - - // Component listeners. - - @Override - public void onDecoderInitializationError(DecoderInitializationException e) { - Log.e(tag, "Decoder initialization error", e); - } - - @Override - public void onCryptoError(CryptoException e) { - Log.e(tag, "Crypto error", e); - } - - @Override - public void onLoadError(int sourceId, IOException e) { - Log.e(tag, "Load error (" + sourceId + ")", e); - } - - @Override - public void onAudioTrackInitializationError(InitializationException e) { - Log.e(tag, "Audio track initialization error", e); - } - - @Override - public void onAudioTrackWriteError(WriteException e) { - Log.e(tag, "Audio track write error", e); - } - - @Override - public void onAudioTrackUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) { - Log.e(tag, "Audio track underrun (" + bufferSize + ", " + bufferSizeMs + ", " - + elapsedSinceLastFeedMs + ")"); - } - - @Override - public void onDroppedFrames(int count, long elapsed) { - Log.w(tag, "Dropped frames (" + count + ")"); - } - - @Override - public void onDecoderInitialized(String decoderName, long elapsedRealtimeMs, - long initializationDurationMs) { - Log.i(tag, "Initialized decoder: " + decoderName); - } - - @Override - public void onDownstreamFormatChanged(int sourceId, Format format, int trigger, - long mediaTimeMs) { - Log.i(tag, "Downstream format changed (" + sourceId + "): " + format.id); - } - - @Override - public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, - float pixelWidthHeightRatio) { - Log.i(tag, "Video size changed: " + width + "x" + height); - } - - @Override - public void onLoadStarted(int sourceId, long length, int type, int trigger, Format format, - long mediaStartTimeMs, long mediaEndTimeMs) {} - - @Override - public void onLoadCompleted(int sourceId, long bytesLoaded, int type, int trigger, - Format format, long mediaStartTimeMs, long mediaEndTimeMs, long elapsedRealtimeMs, - long loadDurationMs) {} - - @Override - public void onLoadCanceled(int sourceId, long bytesLoaded) {} - - @Override - public void onUpstreamDiscarded(int sourceId, long mediaStartTimeMs, long mediaEndTimeMs) {} - - @Override - public void onDrawnToSurface(Surface surface) {} - - private static String getStateString(int state) { - switch (state) { - case ExoPlayer.STATE_BUFFERING: - return "B"; - case ExoPlayer.STATE_ENDED: - return "E"; - case ExoPlayer.STATE_IDLE: - return "I"; - case ExoPlayer.STATE_PREPARING: - return "P"; - case ExoPlayer.STATE_READY: - return "R"; - default: - return "?"; - } - } - - private static String getTimeString(long timeMs) { - return TIME_FORMAT.format((timeMs) / 1000f); - } - -} diff --git a/playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/util/LogcatMetricsLogger.java b/playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/util/LogcatMetricsLogger.java deleted file mode 100644 index 07a9524d6c..0000000000 --- a/playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/util/LogcatMetricsLogger.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright (C) 2014 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.exoplayer.playbacktests.util; - -import android.os.Bundle; -import android.util.Log; - -/** - * Implementation of {@link MetricsLogger} that prints the metrics to logcat. - */ -public final class LogcatMetricsLogger implements MetricsLogger { - - private final String tag; - - public LogcatMetricsLogger(String tag) { - this.tag = tag; - } - - @Override - public void logMetrics(Bundle metrics) { - if (metrics != null) { - for (String key : metrics.keySet()) { - Log.v(tag, key + ": " + metrics.get(key).toString()); - } - } - } - -} diff --git a/playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/util/MetricsLogger.java b/playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/util/MetricsLogger.java deleted file mode 100644 index 9bbaf48388..0000000000 --- a/playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/util/MetricsLogger.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright (C) 2014 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.exoplayer.playbacktests.util; - -import android.app.Instrumentation; -import android.os.Bundle; - -/** - * Metric Logging interface for ExoPlayer playback tests. - */ -public interface MetricsLogger { - - String KEY_FRAMES_DROPPED_COUNT = "Frames Dropped (Count)"; - String KEY_FRAMES_RENDERED_COUNT = "Frames Rendered (Count)"; - String KEY_FRAMES_SKIPPED_COUNT = "Frames Skipped (Count)"; - String KEY_MAX_CONSECUTIVE_FRAMES_DROPPED_COUNT = "Maximum Consecutive Frames Dropped"; - String KEY_TEST_NAME = "Test Name"; - - /** - * Logs the metrics provided from a test. - * - * @param metrics The {@link Bundle} of metrics to be logged. - */ - void logMetrics(Bundle metrics); - - /** - * A factory for instantiating MetricsLogger instances. - */ - final class Factory { - - private Factory() {} - - /** - * Obtains a new instance of MetricsLogger. - * - * @param instrumentation The test instrumentation. - * @param tag The tag to be used for logcat logs. - */ - public static MetricsLogger createDefault(Instrumentation instrumentation, String tag) { - return new LogcatMetricsLogger(tag); - } - } - -} diff --git a/playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/util/TestUtil.java b/playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/util/TestUtil.java deleted file mode 100644 index b5027b9309..0000000000 --- a/playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/util/TestUtil.java +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright (C) 2014 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.exoplayer.playbacktests.util; - -import com.google.android.exoplayer.upstream.DefaultUriDataSource; -import com.google.android.exoplayer.upstream.UriLoadable; -import com.google.android.exoplayer.util.ManifestFetcher; -import com.google.android.exoplayer.util.ManifestFetcher.ManifestCallback; -import com.google.android.exoplayer.util.Util; - -import android.content.Context; -import android.os.ConditionVariable; - -import java.io.IOException; - -/** - * Utility methods for ExoPlayer playback tests. - */ -public final class TestUtil { - - private TestUtil() {} - - /** - * Gets a suitable user agent string for ExoPlayer playback tests. - * - * @param context A context. - * @return The user agent. - */ - public static String getUserAgent(Context context) { - return Util.getUserAgent(context, "ExoPlayerPlaybackTests"); - } - - /** - * Loads a manifest. - * - * @param context A context. - * @param url The manifest url. - * @param parser A suitable parser for the manifest. - * @return The parser manifest. - * @throws IOException If an error occurs loading the manifest. - */ - public static T loadManifest(Context context, String url, UriLoadable.Parser parser) - throws IOException { - String userAgent = getUserAgent(context); - DefaultUriDataSource manifestDataSource = new DefaultUriDataSource(context, userAgent); - ManifestFetcher manifestFetcher = new ManifestFetcher<>(url, manifestDataSource, parser); - SyncManifestCallback callback = new SyncManifestCallback<>(); - manifestFetcher.singleLoad(context.getMainLooper(), callback); - return callback.getResult(); - } - - /** - * A {@link ManifestCallback} that provides a blocking {@link #getResult()} method for retrieving - * the result. - * - * @param The type of the manifest. - */ - private static final class SyncManifestCallback implements ManifestCallback { - - private final ConditionVariable haveResultCondition; - - private T result; - private IOException error; - - public SyncManifestCallback() { - haveResultCondition = new ConditionVariable(); - } - - @Override - public void onSingleManifest(T manifest) { - result = manifest; - haveResultCondition.open(); - - } - @Override - public void onSingleManifestError(IOException e) { - error = e; - haveResultCondition.open(); - } - - /** - * Blocks for the result. - * - * @return The loaded manifest. - * @throws IOException If an error occurred loading the manifest. - */ - public T getResult() throws IOException { - haveResultCondition.block(); - if (error != null) { - throw error; - } - return result; - } - - } - -} diff --git a/playbacktests/src/main/res/layout/host_activity.xml b/playbacktests/src/main/res/layout/host_activity.xml deleted file mode 100644 index 75a88b823e..0000000000 --- a/playbacktests/src/main/res/layout/host_activity.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - diff --git a/settings.gradle b/settings.gradle index 8d0d2823bf..63dd803377 100644 --- a/settings.gradle +++ b/settings.gradle @@ -13,13 +13,3 @@ // limitations under the License. include ':library' include ':demo' -include ':demo-misc-vp9-opus-sw' -include ':playbacktests' -include ':extension-opus' -include ':extension-vp9' -include ':extension-okhttp' - -project(':demo-misc-vp9-opus-sw').projectDir = new File(settingsDir, 'demo_misc/vp9_opus_sw') -project(':extension-opus').projectDir = new File(settingsDir, 'extensions/opus') -project(':extension-vp9').projectDir = new File(settingsDir, 'extensions/vp9') -project(':extension-okhttp').projectDir = new File(settingsDir, 'extensions/okhttp')