Commit cdae9ac5 by olly Committed by Oliver Woodman

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
parent 61d581fe
Showing with 1041 additions and 8877 deletions
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.LIBRARIES"/>
<classpathentry kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.DEPENDENCIES"/>
<classpathentry combineaccessrules="false" kind="src" path="/ExoPlayerLib"/>
<classpathentry kind="src" path="java"/>
<classpathentry kind="src" path="gen"/>
<classpathentry kind="output" path="bin/classes"/>
</classpath>
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>ExoPlayerDemo</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>com.android.ide.eclipse.adt.ResourceManagerBuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>com.android.ide.eclipse.adt.PreCompilerBuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>com.android.ide.eclipse.adt.ApkBuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>com.android.ide.eclipse.adt.AndroidNature</nature>
<nature>org.eclipse.jdt.core.javanature</nature>
</natures>
<filteredResources>
<filter>
<id>1363908154650</id>
<name></name>
<type>22</type>
<matcher>
<id>org.eclipse.ui.ide.multiFilter</id>
<arguments>1.0-name-matches-false-false-BUILD</arguments>
</matcher>
</filter>
<filter>
<id>1363908154652</id>
<name></name>
<type>10</type>
<matcher>
<id>org.eclipse.ui.ide.multiFilter</id>
<arguments>1.0-name-matches-true-false-build</arguments>
</matcher>
</filter>
</filteredResources>
</projectDescription>
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
...@@ -23,12 +23,12 @@ import com.google.android.exoplayer.MediaCodecUtil.DecoderQueryException; ...@@ -23,12 +23,12 @@ import com.google.android.exoplayer.MediaCodecUtil.DecoderQueryException;
import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.audio.AudioCapabilities; import com.google.android.exoplayer.audio.AudioCapabilities;
import com.google.android.exoplayer.audio.AudioCapabilitiesReceiver; 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;
import com.google.android.exoplayer.demo.player.DemoPlayer.RendererBuilder; import com.google.android.exoplayer.demo.player.DemoPlayer.SourceBuilder;
import com.google.android.exoplayer.demo.player.ExtractorRendererBuilder; import com.google.android.exoplayer.demo.player.ExtractorSourceBuilder;
import com.google.android.exoplayer.demo.player.HlsRendererBuilder; import com.google.android.exoplayer.demo.player.HlsSourceBuilder;
import com.google.android.exoplayer.demo.player.SmoothStreamingRendererBuilder; import com.google.android.exoplayer.demo.player.SmoothStreamingSourceBuilder;
import com.google.android.exoplayer.drm.UnsupportedDrmException; import com.google.android.exoplayer.drm.UnsupportedDrmException;
import com.google.android.exoplayer.metadata.GeobMetadata; import com.google.android.exoplayer.metadata.GeobMetadata;
import com.google.android.exoplayer.metadata.PrivMetadata; import com.google.android.exoplayer.metadata.PrivMetadata;
...@@ -296,19 +296,19 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback, ...@@ -296,19 +296,19 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback,
// Internal methods // Internal methods
private RendererBuilder getRendererBuilder() { private SourceBuilder getSourceBuilder() {
String userAgent = Util.getUserAgent(this, "ExoPlayerDemo"); String userAgent = Util.getUserAgent(this, "ExoPlayerDemo");
switch (contentType) { switch (contentType) {
case Util.TYPE_SS: case Util.TYPE_SS:
return new SmoothStreamingRendererBuilder(this, userAgent, contentUri.toString(), return new SmoothStreamingSourceBuilder(this, userAgent, contentUri.toString(),
new SmoothStreamingTestMediaDrmCallback()); new SmoothStreamingTestMediaDrmCallback());
case Util.TYPE_DASH: case Util.TYPE_DASH:
return new DashRendererBuilder(this, userAgent, contentUri.toString(), return new DashSourceBuilder(this, userAgent, contentUri.toString(),
new WidevineTestMediaDrmCallback(contentId, provider)); new WidevineTestMediaDrmCallback(contentId, provider));
case Util.TYPE_HLS: case Util.TYPE_HLS:
return new HlsRendererBuilder(this, userAgent, contentUri.toString()); return new HlsSourceBuilder(this, userAgent, contentUri.toString());
case Util.TYPE_OTHER: case Util.TYPE_OTHER:
return new ExtractorRendererBuilder(this, userAgent, contentUri); return new ExtractorSourceBuilder(this, userAgent, contentUri);
default: default:
throw new IllegalStateException("Unsupported type: " + contentType); throw new IllegalStateException("Unsupported type: " + contentType);
} }
...@@ -316,7 +316,7 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback, ...@@ -316,7 +316,7 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback,
private void preparePlayer(boolean playWhenReady) { private void preparePlayer(boolean playWhenReady) {
if (player == null) { if (player == null) {
player = new DemoPlayer(getRendererBuilder()); player = new DemoPlayer(this, getSourceBuilder());
player.addListener(this); player.addListener(this);
player.setCaptionListener(this); player.setCaptionListener(this);
player.setMetadataListener(this); player.setMetadataListener(this);
......
...@@ -17,50 +17,38 @@ package com.google.android.exoplayer.demo.player; ...@@ -17,50 +17,38 @@ package com.google.android.exoplayer.demo.player;
import com.google.android.exoplayer.DefaultLoadControl; import com.google.android.exoplayer.DefaultLoadControl;
import com.google.android.exoplayer.LoadControl; import com.google.android.exoplayer.LoadControl;
import com.google.android.exoplayer.MediaCodecAudioTrackRenderer; import com.google.android.exoplayer.MultiSampleSource;
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.chunk.ChunkSampleSource; import com.google.android.exoplayer.chunk.ChunkSampleSource;
import com.google.android.exoplayer.chunk.ChunkSource; import com.google.android.exoplayer.chunk.ChunkSource;
import com.google.android.exoplayer.chunk.FormatEvaluator.AdaptiveEvaluator; import com.google.android.exoplayer.chunk.FormatEvaluator.AdaptiveEvaluator;
import com.google.android.exoplayer.dash.DashChunkSource; import com.google.android.exoplayer.dash.DashChunkSource;
import com.google.android.exoplayer.dash.DefaultDashTrackSelector; 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.MediaPresentationDescription;
import com.google.android.exoplayer.dash.mpd.MediaPresentationDescriptionParser; 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.UtcTimingElement;
import com.google.android.exoplayer.dash.mpd.UtcTimingElementResolver; import com.google.android.exoplayer.dash.mpd.UtcTimingElementResolver;
import com.google.android.exoplayer.dash.mpd.UtcTimingElementResolver.UtcTimingCallback; 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.MediaDrmCallback;
import com.google.android.exoplayer.drm.StreamingDrmSessionManager; import com.google.android.exoplayer.upstream.BandwidthMeter;
import com.google.android.exoplayer.drm.UnsupportedDrmException;
import com.google.android.exoplayer.text.TextTrackRenderer;
import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DefaultAllocator; 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.DefaultUriDataSource;
import com.google.android.exoplayer.upstream.UriDataSource; import com.google.android.exoplayer.upstream.UriDataSource;
import com.google.android.exoplayer.util.ManifestFetcher; import com.google.android.exoplayer.util.ManifestFetcher;
import com.google.android.exoplayer.util.Util;
import android.content.Context; import android.content.Context;
import android.media.AudioManager;
import android.media.MediaCodec;
import android.os.Handler; import android.os.Handler;
import android.util.Log; import android.util.Log;
import java.io.IOException; 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 BUFFER_SEGMENT_SIZE = 64 * 1024;
private static final int VIDEO_BUFFER_SEGMENTS = 200; private static final int VIDEO_BUFFER_SEGMENTS = 200;
...@@ -68,10 +56,6 @@ public class DashRendererBuilder implements RendererBuilder { ...@@ -68,10 +56,6 @@ public class DashRendererBuilder implements RendererBuilder {
private static final int TEXT_BUFFER_SEGMENTS = 2; private static final int TEXT_BUFFER_SEGMENTS = 2;
private static final int LIVE_EDGE_LATENCY_MS = 30000; 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 Context context;
private final String userAgent; private final String userAgent;
private final String url; private final String url;
...@@ -79,7 +63,7 @@ public class DashRendererBuilder implements RendererBuilder { ...@@ -79,7 +63,7 @@ public class DashRendererBuilder implements RendererBuilder {
private AsyncRendererBuilder currentAsyncBuilder; private AsyncRendererBuilder currentAsyncBuilder;
public DashRendererBuilder(Context context, String userAgent, String url, public DashSourceBuilder(Context context, String userAgent, String url,
MediaDrmCallback drmCallback) { MediaDrmCallback drmCallback) {
this.context = context; this.context = context;
this.userAgent = userAgent; this.userAgent = userAgent;
...@@ -155,7 +139,7 @@ public class DashRendererBuilder implements RendererBuilder { ...@@ -155,7 +139,7 @@ public class DashRendererBuilder implements RendererBuilder {
return; return;
} }
player.onRenderersError(e); player.onSourceBuilderError(e);
} }
@Override @Override
...@@ -180,11 +164,9 @@ public class DashRendererBuilder implements RendererBuilder { ...@@ -180,11 +164,9 @@ public class DashRendererBuilder implements RendererBuilder {
} }
private void buildRenderers() { private void buildRenderers() {
// TODO[REFACTOR]: Bring back DRM support.
/*
Period period = manifest.getPeriod(0); 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; boolean hasContentProtection = false;
for (int i = 0; i < period.adaptationSets.size(); i++) { for (int i = 0; i < period.adaptationSets.size(); i++) {
AdaptationSet adaptationSet = period.adaptationSets.get(i); AdaptationSet adaptationSet = period.adaptationSets.get(i);
...@@ -198,7 +180,7 @@ public class DashRendererBuilder implements RendererBuilder { ...@@ -198,7 +180,7 @@ public class DashRendererBuilder implements RendererBuilder {
StreamingDrmSessionManager drmSessionManager = null; StreamingDrmSessionManager drmSessionManager = null;
if (hasContentProtection) { if (hasContentProtection) {
if (Util.SDK_INT < 18) { if (Util.SDK_INT < 18) {
player.onRenderersError( player.onSourceBuilderError(
new UnsupportedDrmException(UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME)); new UnsupportedDrmException(UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME));
return; return;
} }
...@@ -207,23 +189,25 @@ public class DashRendererBuilder implements RendererBuilder { ...@@ -207,23 +189,25 @@ public class DashRendererBuilder implements RendererBuilder {
player.getPlaybackLooper(), drmCallback, null, player.getMainHandler(), player); player.getPlaybackLooper(), drmCallback, null, player.getMainHandler(), player);
filterHdContent = getWidevineSecurityLevel(drmSessionManager) != SECURITY_LEVEL_1; filterHdContent = getWidevineSecurityLevel(drmSessionManager) != SECURITY_LEVEL_1;
} catch (UnsupportedDrmException e) { } catch (UnsupportedDrmException e) {
player.onRenderersError(e); player.onSourceBuilderError(e);
return; return;
} }
} }
*/
Handler mainHandler = player.getMainHandler();
BandwidthMeter bandwidthMeter = player.getBandwidthMeter();
LoadControl loadControl = new DefaultLoadControl(new DefaultAllocator(BUFFER_SEGMENT_SIZE));
// Build the video renderer. // Build the video renderer.
DataSource videoDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent); DataSource videoDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent);
ChunkSource videoChunkSource = new DashChunkSource(manifestFetcher, ChunkSource videoChunkSource = new DashChunkSource(manifestFetcher,
DefaultDashTrackSelector.newVideoInstance(context, true, filterHdContent), DefaultDashTrackSelector.newVideoInstance(context, true, false),
videoDataSource, new AdaptiveEvaluator(bandwidthMeter), LIVE_EDGE_LATENCY_MS, videoDataSource, new AdaptiveEvaluator(bandwidthMeter), LIVE_EDGE_LATENCY_MS,
elapsedRealtimeOffset, mainHandler, player, DemoPlayer.TYPE_VIDEO); elapsedRealtimeOffset, mainHandler, player, DemoPlayer.TYPE_VIDEO);
ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl, ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl,
VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player, VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player,
DemoPlayer.TYPE_VIDEO); 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. // Build the audio renderer.
DataSource audioDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent); DataSource audioDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent);
...@@ -233,9 +217,6 @@ public class DashRendererBuilder implements RendererBuilder { ...@@ -233,9 +217,6 @@ public class DashRendererBuilder implements RendererBuilder {
ChunkSampleSource audioSampleSource = new ChunkSampleSource(audioChunkSource, loadControl, ChunkSampleSource audioSampleSource = new ChunkSampleSource(audioChunkSource, loadControl,
AUDIO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player, AUDIO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player,
DemoPlayer.TYPE_AUDIO); DemoPlayer.TYPE_AUDIO);
TrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer(audioSampleSource,
MediaCodecSelector.DEFAULT, drmSessionManager, true, mainHandler, player,
AudioCapabilities.getCapabilities(context), AudioManager.STREAM_MUSIC);
// Build the text renderer. // Build the text renderer.
DataSource textDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent); DataSource textDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent);
...@@ -245,23 +226,19 @@ public class DashRendererBuilder implements RendererBuilder { ...@@ -245,23 +226,19 @@ public class DashRendererBuilder implements RendererBuilder {
ChunkSampleSource textSampleSource = new ChunkSampleSource(textChunkSource, loadControl, ChunkSampleSource textSampleSource = new ChunkSampleSource(textChunkSource, loadControl,
TEXT_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player, TEXT_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player,
DemoPlayer.TYPE_TEXT); DemoPlayer.TYPE_TEXT);
TrackRenderer textRenderer = new TextTrackRenderer(textSampleSource, player,
mainHandler.getLooper());
// Invoke the callback. // Invoke the callback.
TrackRenderer[] renderers = new TrackRenderer[DemoPlayer.RENDERER_COUNT]; player.onSource(
renderers[DemoPlayer.TYPE_VIDEO] = videoRenderer; new MultiSampleSource(videoSampleSource, audioSampleSource, textSampleSource));
renderers[DemoPlayer.TYPE_AUDIO] = audioRenderer;
renderers[DemoPlayer.TYPE_TEXT] = textRenderer;
player.onRenderers(renderers, bandwidthMeter);
} }
/*
private static int getWidevineSecurityLevel(StreamingDrmSessionManager sessionManager) { private static int getWidevineSecurityLevel(StreamingDrmSessionManager sessionManager) {
String securityLevelProperty = sessionManager.getPropertyString("securityLevel"); String securityLevelProperty = sessionManager.getPropertyString("securityLevel");
return securityLevelProperty.equals("L1") ? SECURITY_LEVEL_1 : securityLevelProperty return securityLevelProperty.equals("L1") ? SECURITY_LEVEL_1 : securityLevelProperty
.equals("L3") ? SECURITY_LEVEL_3 : SECURITY_LEVEL_UNKNOWN; .equals("L3") ? SECURITY_LEVEL_3 : SECURITY_LEVEL_UNKNOWN;
} }
*/
} }
} }
...@@ -16,33 +16,39 @@ ...@@ -16,33 +16,39 @@
package com.google.android.exoplayer.demo.player; package com.google.android.exoplayer.demo.player;
import com.google.android.exoplayer.CodecCounters; import com.google.android.exoplayer.CodecCounters;
import com.google.android.exoplayer.DummyTrackRenderer;
import com.google.android.exoplayer.ExoPlaybackException; import com.google.android.exoplayer.ExoPlaybackException;
import com.google.android.exoplayer.ExoPlayer; import com.google.android.exoplayer.ExoPlayer;
import com.google.android.exoplayer.MediaCodecAudioTrackRenderer; 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.MediaCodecTrackRenderer.DecoderInitializationException;
import com.google.android.exoplayer.MediaCodecVideoTrackRenderer; import com.google.android.exoplayer.MediaCodecVideoTrackRenderer;
import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.SampleSource;
import com.google.android.exoplayer.TimeRange; import com.google.android.exoplayer.TimeRange;
import com.google.android.exoplayer.TrackRenderer; 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.audio.AudioTrack;
import com.google.android.exoplayer.chunk.ChunkSampleSource; import com.google.android.exoplayer.chunk.ChunkSampleSource;
import com.google.android.exoplayer.chunk.Format; import com.google.android.exoplayer.chunk.Format;
import com.google.android.exoplayer.dash.DashChunkSource; import com.google.android.exoplayer.dash.DashChunkSource;
import com.google.android.exoplayer.drm.StreamingDrmSessionManager; import com.google.android.exoplayer.drm.StreamingDrmSessionManager;
import com.google.android.exoplayer.hls.HlsSampleSource; 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.metadata.MetadataTrackRenderer.MetadataRenderer;
import com.google.android.exoplayer.text.Cue; import com.google.android.exoplayer.text.Cue;
import com.google.android.exoplayer.text.TextRenderer; 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.BandwidthMeter;
import com.google.android.exoplayer.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer.upstream.DefaultBandwidthMeter;
import com.google.android.exoplayer.util.DebugTextViewHelper; import com.google.android.exoplayer.util.DebugTextViewHelper;
import com.google.android.exoplayer.util.PlayerControl; 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.media.MediaCodec.CryptoException;
import android.os.Handler; import android.os.Handler;
import android.os.Looper;
import android.view.Surface; import android.view.Surface;
import java.io.IOException; import java.io.IOException;
...@@ -53,7 +59,7 @@ import java.util.concurrent.CopyOnWriteArrayList; ...@@ -53,7 +59,7 @@ import java.util.concurrent.CopyOnWriteArrayList;
/** /**
* A wrapper around {@link ExoPlayer} that provides a higher level interface. It can be prepared * 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). * SmoothStreaming and so on).
*/ */
public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventListener, public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventListener,
...@@ -65,20 +71,20 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi ...@@ -65,20 +71,20 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
/** /**
* Builds renderers for the player. * Builds renderers for the player.
*/ */
public interface RendererBuilder { public interface SourceBuilder {
/** /**
* Builds renderers for playback. * 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, * 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); void buildRenderers(DemoPlayer player);
/** /**
* Cancels the current build operation, if there is one. Else does nothing. * Cancels the current build operation, if there is one. Else does nothing.
* <p> * <p>
* A canceled build operation must not invoke {@link DemoPlayer#onRenderers} or * A canceled build operation must not invoke {@link DemoPlayer#onSource} or
* {@link DemoPlayer#onRenderersError} on the player, which may have been released. * {@link DemoPlayer#onSourceBuilderError} on the player, which may have been released.
*/ */
void cancel(); void cancel();
} }
...@@ -158,27 +164,26 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi ...@@ -158,27 +164,26 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
public static final int TYPE_TEXT = 2; public static final int TYPE_TEXT = 2;
public static final int TYPE_METADATA = 3; public static final int TYPE_METADATA = 3;
private static final int RENDERER_BUILDING_STATE_IDLE = 1; private static final int SOURCE_BUILDING_STATE_IDLE = 1;
private static final int RENDERER_BUILDING_STATE_BUILDING = 2; private static final int SOURCE_BUILDING_STATE_BUILDING = 2;
private static final int RENDERER_BUILDING_STATE_BUILT = 3; private static final int SOURCE_BUILDING_STATE_BUILT = 3;
private final RendererBuilder rendererBuilder;
private final ExoPlayer player; private final ExoPlayer player;
private final SourceBuilder sourceBuilder;
private final BandwidthMeter bandwidthMeter;
private final MediaCodecVideoTrackRenderer videoRenderer;
private final PlayerControl playerControl; private final PlayerControl playerControl;
private final Handler mainHandler; private final Handler mainHandler;
private final CopyOnWriteArrayList<Listener> listeners; private final CopyOnWriteArrayList<Listener> listeners;
private int rendererBuildingState; private int sourceBuildingState;
private int lastReportedPlaybackState; private int lastReportedPlaybackState;
private boolean lastReportedPlayWhenReady; private boolean lastReportedPlayWhenReady;
private Surface surface; private Surface surface;
private TrackRenderer videoRenderer;
private CodecCounters codecCounters;
private Format videoFormat; private Format videoFormat;
private int videoTrackToRestore; private int videoTrackToRestore;
private BandwidthMeter bandwidthMeter;
private boolean backgrounded; private boolean backgrounded;
private CaptionListener captionListener; private CaptionListener captionListener;
...@@ -186,16 +191,32 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi ...@@ -186,16 +191,32 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
private InternalErrorListener internalErrorListener; private InternalErrorListener internalErrorListener;
private InfoListener infoListener; private InfoListener infoListener;
public DemoPlayer(RendererBuilder rendererBuilder) { public DemoPlayer(Context context, SourceBuilder sourceBuilder) {
this.rendererBuilder = rendererBuilder; this.sourceBuilder = sourceBuilder;
player = ExoPlayer.Factory.newInstance(RENDERER_COUNT, 1000, 5000);
player.addListener(this);
playerControl = new PlayerControl(player);
mainHandler = new Handler(); mainHandler = new Handler();
bandwidthMeter = new DefaultBandwidthMeter();
listeners = new CopyOnWriteArrayList<>(); 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<Map<String, Object>> 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);
// Set initial state, with the text renderer initially disabled.
lastReportedPlaybackState = STATE_IDLE; lastReportedPlaybackState = STATE_IDLE;
rendererBuildingState = RENDERER_BUILDING_STATE_IDLE; sourceBuildingState = SOURCE_BUILDING_STATE_IDLE;
// Disable text initially.
player.setSelectedTrack(TYPE_TEXT, TRACK_DISABLED); player.setSelectedTrack(TYPE_TEXT, TRACK_DISABLED);
} }
...@@ -279,56 +300,38 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi ...@@ -279,56 +300,38 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
} }
public void prepare() { public void prepare() {
if (rendererBuildingState == RENDERER_BUILDING_STATE_BUILT) { if (sourceBuildingState == SOURCE_BUILDING_STATE_BUILT) {
player.stop(); player.stop();
} }
rendererBuilder.cancel(); sourceBuilder.cancel();
videoFormat = null; sourceBuildingState = SOURCE_BUILDING_STATE_BUILDING;
videoRenderer = null;
rendererBuildingState = RENDERER_BUILDING_STATE_BUILDING;
maybeReportPlayerState(); 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 * @param source The {@link SampleSource} to play.
* 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.
*/ */
/* package */ void onRenderers(TrackRenderer[] renderers, BandwidthMeter bandwidthMeter) { /* package */ void onSource(SampleSource source) {
for (int i = 0; i < RENDERER_COUNT; i++) { player.prepare(source);
if (renderers[i] == null) { sourceBuildingState = SOURCE_BUILDING_STATE_BUILT;
// 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;
} }
/** /**
* Invoked if a {@link RendererBuilder} encounters an error. * Invoked if a {@link SourceBuilder} encounters an error.
* *
* @param e Describes the error. * @param e Describes the error.
*/ */
/* package */ void onRenderersError(Exception e) { /* package */ void onSourceBuilderError(Exception e) {
if (internalErrorListener != null) { if (internalErrorListener != null) {
internalErrorListener.onRendererInitializationError(e); internalErrorListener.onRendererInitializationError(e);
} }
for (Listener listener : listeners) { for (Listener listener : listeners) {
listener.onError(e); listener.onError(e);
} }
rendererBuildingState = RENDERER_BUILDING_STATE_IDLE; sourceBuildingState = SOURCE_BUILDING_STATE_IDLE;
maybeReportPlayerState(); maybeReportPlayerState();
} }
...@@ -341,18 +344,18 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi ...@@ -341,18 +344,18 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
} }
public void release() { public void release() {
rendererBuilder.cancel(); sourceBuilder.cancel();
rendererBuildingState = RENDERER_BUILDING_STATE_IDLE; sourceBuildingState = SOURCE_BUILDING_STATE_IDLE;
surface = null; surface = null;
player.release(); player.release();
} }
public int getPlaybackState() { public int getPlaybackState() {
if (rendererBuildingState == RENDERER_BUILDING_STATE_BUILDING) { if (sourceBuildingState == SOURCE_BUILDING_STATE_BUILDING) {
return STATE_PREPARING; return STATE_PREPARING;
} }
int playerState = player.getPlaybackState(); 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 // This is an edge case where the renderers are built, but are still being passed to the
// player's playback thread. // player's playback thread.
return STATE_PREPARING; return STATE_PREPARING;
...@@ -372,7 +375,7 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi ...@@ -372,7 +375,7 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
@Override @Override
public CodecCounters getCodecCounters() { public CodecCounters getCodecCounters() {
return codecCounters; return videoRenderer.codecCounters;
} }
@Override @Override
...@@ -392,10 +395,6 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi ...@@ -392,10 +395,6 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
return player.getPlayWhenReady(); return player.getPlayWhenReady();
} }
/* package */ Looper getPlaybackLooper() {
return player.getPlaybackLooper();
}
/* package */ Handler getMainHandler() { /* package */ Handler getMainHandler() {
return mainHandler; return mainHandler;
} }
...@@ -407,7 +406,7 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi ...@@ -407,7 +406,7 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
@Override @Override
public void onPlayerError(ExoPlaybackException exception) { public void onPlayerError(ExoPlaybackException exception) {
rendererBuildingState = RENDERER_BUILDING_STATE_IDLE; sourceBuildingState = SOURCE_BUILDING_STATE_IDLE;
for (Listener listener : listeners) { for (Listener listener : listeners) {
listener.onError(exception); listener.onError(exception);
} }
...@@ -583,10 +582,6 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi ...@@ -583,10 +582,6 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
} }
private void pushSurface(boolean blockForSurfacePush) { private void pushSurface(boolean blockForSurfacePush) {
if (videoRenderer == null) {
return;
}
if (blockForSurfacePush) { if (blockForSurfacePush) {
player.blockingSendMessage( player.blockingSendMessage(
videoRenderer, MediaCodecVideoTrackRenderer.MSG_SET_SURFACE, surface); videoRenderer, MediaCodecVideoTrackRenderer.MSG_SET_SURFACE, surface);
......
...@@ -15,30 +15,21 @@ ...@@ -15,30 +15,21 @@
*/ */
package com.google.android.exoplayer.demo.player; package com.google.android.exoplayer.demo.player;
import com.google.android.exoplayer.MediaCodecAudioTrackRenderer; import com.google.android.exoplayer.demo.player.DemoPlayer.SourceBuilder;
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.Extractor;
import com.google.android.exoplayer.extractor.ExtractorSampleSource; 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.Allocator;
import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DefaultAllocator; 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.DefaultUriDataSource;
import android.content.Context; import android.content.Context;
import android.media.AudioManager;
import android.media.MediaCodec;
import android.net.Uri; import android.net.Uri;
/** /**
* A {@link RendererBuilder} for streams that can be read using an {@link Extractor}. * A {@link SourceBuilder} for streams that can be read using an {@link Extractor}.
*/ */
public class ExtractorRendererBuilder implements RendererBuilder { public class ExtractorSourceBuilder implements SourceBuilder {
private static final int BUFFER_SEGMENT_SIZE = 64 * 1024; private static final int BUFFER_SEGMENT_SIZE = 64 * 1024;
private static final int BUFFER_SEGMENT_COUNT = 256; private static final int BUFFER_SEGMENT_COUNT = 256;
...@@ -47,7 +38,7 @@ public class ExtractorRendererBuilder implements RendererBuilder { ...@@ -47,7 +38,7 @@ public class ExtractorRendererBuilder implements RendererBuilder {
private final String userAgent; private final String userAgent;
private final Uri uri; private final Uri uri;
public ExtractorRendererBuilder(Context context, String userAgent, Uri uri) { public ExtractorSourceBuilder(Context context, String userAgent, Uri uri) {
this.context = context; this.context = context;
this.userAgent = userAgent; this.userAgent = userAgent;
this.uri = uri; this.uri = uri;
...@@ -56,28 +47,11 @@ public class ExtractorRendererBuilder implements RendererBuilder { ...@@ -56,28 +47,11 @@ public class ExtractorRendererBuilder implements RendererBuilder {
@Override @Override
public void buildRenderers(DemoPlayer player) { public void buildRenderers(DemoPlayer player) {
Allocator allocator = new DefaultAllocator(BUFFER_SEGMENT_SIZE); Allocator allocator = new DefaultAllocator(BUFFER_SEGMENT_SIZE);
DataSource dataSource = new DefaultUriDataSource(context, player.getBandwidthMeter(),
// Build the video and audio renderers. userAgent);
DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(player.getMainHandler(), ExtractorSampleSource source = new ExtractorSampleSource(uri, dataSource, allocator,
null);
DataSource dataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent);
ExtractorSampleSource sampleSource = new ExtractorSampleSource(uri, dataSource, allocator,
BUFFER_SEGMENT_COUNT * BUFFER_SEGMENT_SIZE); BUFFER_SEGMENT_COUNT * BUFFER_SEGMENT_SIZE);
MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(context, player.onSource(source);
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 @Override
......
...@@ -17,46 +17,32 @@ package com.google.android.exoplayer.demo.player; ...@@ -17,46 +17,32 @@ package com.google.android.exoplayer.demo.player;
import com.google.android.exoplayer.DefaultLoadControl; import com.google.android.exoplayer.DefaultLoadControl;
import com.google.android.exoplayer.LoadControl; import com.google.android.exoplayer.LoadControl;
import com.google.android.exoplayer.MediaCodecAudioTrackRenderer; import com.google.android.exoplayer.demo.player.DemoPlayer.SourceBuilder;
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.hls.DefaultHlsTrackSelector; import com.google.android.exoplayer.hls.DefaultHlsTrackSelector;
import com.google.android.exoplayer.hls.HlsChunkSource; 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.HlsPlaylist;
import com.google.android.exoplayer.hls.HlsPlaylistParser; import com.google.android.exoplayer.hls.HlsPlaylistParser;
import com.google.android.exoplayer.hls.HlsSampleSource; import com.google.android.exoplayer.hls.HlsSampleSource;
import com.google.android.exoplayer.hls.PtsTimestampAdjusterProvider; import com.google.android.exoplayer.hls.PtsTimestampAdjusterProvider;
import com.google.android.exoplayer.metadata.Id3Parser; import com.google.android.exoplayer.upstream.BandwidthMeter;
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.DataSource; import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DefaultAllocator; 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.DefaultUriDataSource;
import com.google.android.exoplayer.util.ManifestFetcher; import com.google.android.exoplayer.util.ManifestFetcher;
import com.google.android.exoplayer.util.ManifestFetcher.ManifestCallback; import com.google.android.exoplayer.util.ManifestFetcher.ManifestCallback;
import android.content.Context; import android.content.Context;
import android.media.AudioManager;
import android.media.MediaCodec;
import android.os.Handler; import android.os.Handler;
import java.io.IOException; 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 BUFFER_SEGMENT_SIZE = 64 * 1024;
private static final int MAIN_BUFFER_SEGMENTS = 256; private static final int MAIN_BUFFER_SEGMENTS = 256;
private static final int TEXT_BUFFER_SEGMENTS = 2;
private final Context context; private final Context context;
private final String userAgent; private final String userAgent;
...@@ -64,7 +50,7 @@ public class HlsRendererBuilder implements RendererBuilder { ...@@ -64,7 +50,7 @@ public class HlsRendererBuilder implements RendererBuilder {
private AsyncRendererBuilder currentAsyncBuilder; private AsyncRendererBuilder currentAsyncBuilder;
public HlsRendererBuilder(Context context, String userAgent, String url) { public HlsSourceBuilder(Context context, String userAgent, String url) {
this.context = context; this.context = context;
this.userAgent = userAgent; this.userAgent = userAgent;
this.url = url; this.url = url;
...@@ -118,7 +104,7 @@ public class HlsRendererBuilder implements RendererBuilder { ...@@ -118,7 +104,7 @@ public class HlsRendererBuilder implements RendererBuilder {
return; return;
} }
player.onRenderersError(e); player.onSourceBuilderError(e);
} }
@Override @Override
...@@ -128,8 +114,8 @@ public class HlsRendererBuilder implements RendererBuilder { ...@@ -128,8 +114,8 @@ public class HlsRendererBuilder implements RendererBuilder {
} }
Handler mainHandler = player.getMainHandler(); Handler mainHandler = player.getMainHandler();
BandwidthMeter bandwidthMeter = player.getBandwidthMeter();
LoadControl loadControl = new DefaultLoadControl(new DefaultAllocator(BUFFER_SEGMENT_SIZE)); LoadControl loadControl = new DefaultLoadControl(new DefaultAllocator(BUFFER_SEGMENT_SIZE));
DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();
PtsTimestampAdjusterProvider timestampAdjusterProvider = new PtsTimestampAdjusterProvider(); PtsTimestampAdjusterProvider timestampAdjusterProvider = new PtsTimestampAdjusterProvider();
// Build the video/audio/metadata renderers. // Build the video/audio/metadata renderers.
...@@ -139,16 +125,10 @@ public class HlsRendererBuilder implements RendererBuilder { ...@@ -139,16 +125,10 @@ public class HlsRendererBuilder implements RendererBuilder {
timestampAdjusterProvider, HlsChunkSource.ADAPTIVE_MODE_SPLICE); timestampAdjusterProvider, HlsChunkSource.ADAPTIVE_MODE_SPLICE);
HlsSampleSource sampleSource = new HlsSampleSource(chunkSource, loadControl, HlsSampleSource sampleSource = new HlsSampleSource(chunkSource, loadControl,
MAIN_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player, DemoPlayer.TYPE_VIDEO); 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<Map<String, Object>> id3Renderer = new MetadataTrackRenderer<>(
sampleSource, new Id3Parser(), player, mainHandler.getLooper());
// TODO[REFACTOR]: Bring back caption support.
// Build the text renderer, preferring Webvtt where available. // Build the text renderer, preferring Webvtt where available.
/*
boolean preferWebvtt = false; boolean preferWebvtt = false;
if (manifest instanceof HlsMasterPlaylist) { if (manifest instanceof HlsMasterPlaylist) {
preferWebvtt = !((HlsMasterPlaylist) manifest).subtitles.isEmpty(); preferWebvtt = !((HlsMasterPlaylist) manifest).subtitles.isEmpty();
...@@ -156,7 +136,7 @@ public class HlsRendererBuilder implements RendererBuilder { ...@@ -156,7 +136,7 @@ public class HlsRendererBuilder implements RendererBuilder {
TrackRenderer textRenderer; TrackRenderer textRenderer;
if (preferWebvtt) { if (preferWebvtt) {
DataSource textDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent); 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, url, manifest, DefaultHlsTrackSelector.newVttInstance(), bandwidthMeter,
timestampAdjusterProvider, HlsChunkSource.ADAPTIVE_MODE_SPLICE); timestampAdjusterProvider, HlsChunkSource.ADAPTIVE_MODE_SPLICE);
HlsSampleSource textSampleSource = new HlsSampleSource(textChunkSource, loadControl, HlsSampleSource textSampleSource = new HlsSampleSource(textChunkSource, loadControl,
...@@ -165,13 +145,9 @@ public class HlsRendererBuilder implements RendererBuilder { ...@@ -165,13 +145,9 @@ public class HlsRendererBuilder implements RendererBuilder {
} else { } else {
textRenderer = new Eia608TrackRenderer(sampleSource, player, mainHandler.getLooper()); textRenderer = new Eia608TrackRenderer(sampleSource, player, mainHandler.getLooper());
} }
*/
TrackRenderer[] renderers = new TrackRenderer[DemoPlayer.RENDERER_COUNT]; player.onSource(sampleSource);
renderers[DemoPlayer.TYPE_VIDEO] = videoRenderer;
renderers[DemoPlayer.TYPE_AUDIO] = audioRenderer;
renderers[DemoPlayer.TYPE_METADATA] = id3Renderer;
renderers[DemoPlayer.TYPE_TEXT] = textRenderer;
player.onRenderers(renderers, bandwidthMeter);
} }
} }
......
...@@ -17,43 +17,33 @@ package com.google.android.exoplayer.demo.player; ...@@ -17,43 +17,33 @@ package com.google.android.exoplayer.demo.player;
import com.google.android.exoplayer.DefaultLoadControl; import com.google.android.exoplayer.DefaultLoadControl;
import com.google.android.exoplayer.LoadControl; import com.google.android.exoplayer.LoadControl;
import com.google.android.exoplayer.MediaCodecAudioTrackRenderer; import com.google.android.exoplayer.MultiSampleSource;
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.chunk.ChunkSampleSource; import com.google.android.exoplayer.chunk.ChunkSampleSource;
import com.google.android.exoplayer.chunk.ChunkSource; import com.google.android.exoplayer.chunk.ChunkSource;
import com.google.android.exoplayer.chunk.FormatEvaluator.AdaptiveEvaluator; import com.google.android.exoplayer.chunk.FormatEvaluator.AdaptiveEvaluator;
import com.google.android.exoplayer.demo.player.DemoPlayer.RendererBuilder; import com.google.android.exoplayer.demo.player.DemoPlayer.SourceBuilder;
import com.google.android.exoplayer.drm.DrmSessionManager;
import com.google.android.exoplayer.drm.MediaDrmCallback; 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.DefaultSmoothStreamingTrackSelector;
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingChunkSource; import com.google.android.exoplayer.smoothstreaming.SmoothStreamingChunkSource;
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest; import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest;
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifestParser; 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.DataSource;
import com.google.android.exoplayer.upstream.DefaultAllocator; 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.DefaultHttpDataSource;
import com.google.android.exoplayer.upstream.DefaultUriDataSource; import com.google.android.exoplayer.upstream.DefaultUriDataSource;
import com.google.android.exoplayer.util.ManifestFetcher; import com.google.android.exoplayer.util.ManifestFetcher;
import com.google.android.exoplayer.util.Util; import com.google.android.exoplayer.util.Util;
import android.content.Context; import android.content.Context;
import android.media.AudioManager;
import android.media.MediaCodec;
import android.os.Handler; import android.os.Handler;
import java.io.IOException; 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 BUFFER_SEGMENT_SIZE = 64 * 1024;
private static final int VIDEO_BUFFER_SEGMENTS = 200; private static final int VIDEO_BUFFER_SEGMENTS = 200;
...@@ -68,7 +58,7 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder { ...@@ -68,7 +58,7 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder {
private AsyncRendererBuilder currentAsyncBuilder; private AsyncRendererBuilder currentAsyncBuilder;
public SmoothStreamingRendererBuilder(Context context, String userAgent, String url, public SmoothStreamingSourceBuilder(Context context, String userAgent, String url,
MediaDrmCallback drmCallback) { MediaDrmCallback drmCallback) {
this.context = context; this.context = context;
this.userAgent = userAgent; this.userAgent = userAgent;
...@@ -126,7 +116,7 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder { ...@@ -126,7 +116,7 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder {
return; return;
} }
player.onRenderersError(exception); player.onSourceBuilderError(exception);
} }
@Override @Override
...@@ -135,15 +125,13 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder { ...@@ -135,15 +125,13 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder {
return; return;
} }
Handler mainHandler = player.getMainHandler(); // TODO[REFACTOR]: Bring back DRM support.
LoadControl loadControl = new DefaultLoadControl(new DefaultAllocator(BUFFER_SEGMENT_SIZE)); /*
DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(mainHandler, player);
// Check drm support if necessary. // Check drm support if necessary.
DrmSessionManager drmSessionManager = null; DrmSessionManager drmSessionManager = null;
if (manifest.protectionElement != null) { if (manifest.protectionElement != null) {
if (Util.SDK_INT < 18) { if (Util.SDK_INT < 18) {
player.onRenderersError( player.onSourceBuilderError(
new UnsupportedDrmException(UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME)); new UnsupportedDrmException(UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME));
return; return;
} }
...@@ -151,10 +139,15 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder { ...@@ -151,10 +139,15 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder {
drmSessionManager = new StreamingDrmSessionManager(manifest.protectionElement.uuid, drmSessionManager = new StreamingDrmSessionManager(manifest.protectionElement.uuid,
player.getPlaybackLooper(), drmCallback, null, player.getMainHandler(), player); player.getPlaybackLooper(), drmCallback, null, player.getMainHandler(), player);
} catch (UnsupportedDrmException e) { } catch (UnsupportedDrmException e) {
player.onRenderersError(e); player.onSourceBuilderError(e);
return; return;
} }
} }
*/
Handler mainHandler = player.getMainHandler();
BandwidthMeter bandwidthMeter = player.getBandwidthMeter();
LoadControl loadControl = new DefaultLoadControl(new DefaultAllocator(BUFFER_SEGMENT_SIZE));
// Build the video renderer. // Build the video renderer.
DataSource videoDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent); DataSource videoDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent);
...@@ -164,9 +157,6 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder { ...@@ -164,9 +157,6 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder {
ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl, ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl,
VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player, VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player,
DemoPlayer.TYPE_VIDEO); 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. // Build the audio renderer.
DataSource audioDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent); DataSource audioDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent);
...@@ -176,9 +166,6 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder { ...@@ -176,9 +166,6 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder {
ChunkSampleSource audioSampleSource = new ChunkSampleSource(audioChunkSource, loadControl, ChunkSampleSource audioSampleSource = new ChunkSampleSource(audioChunkSource, loadControl,
AUDIO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player, AUDIO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player,
DemoPlayer.TYPE_AUDIO); DemoPlayer.TYPE_AUDIO);
TrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer(audioSampleSource,
MediaCodecSelector.DEFAULT, drmSessionManager, true, mainHandler, player,
AudioCapabilities.getCapabilities(context), AudioManager.STREAM_MUSIC);
// Build the text renderer. // Build the text renderer.
DataSource textDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent); DataSource textDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent);
...@@ -188,15 +175,10 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder { ...@@ -188,15 +175,10 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder {
ChunkSampleSource textSampleSource = new ChunkSampleSource(textChunkSource, loadControl, ChunkSampleSource textSampleSource = new ChunkSampleSource(textChunkSource, loadControl,
TEXT_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player, TEXT_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player,
DemoPlayer.TYPE_TEXT); DemoPlayer.TYPE_TEXT);
TrackRenderer textRenderer = new TextTrackRenderer(textSampleSource, player,
mainHandler.getLooper());
// Invoke the callback. // Invoke the callback.
TrackRenderer[] renderers = new TrackRenderer[DemoPlayer.RENDERER_COUNT]; player.onSource(
renderers[DemoPlayer.TYPE_VIDEO] = videoRenderer; new MultiSampleSource(videoSampleSource, audioSampleSource, textSampleSource));
renderers[DemoPlayer.TYPE_AUDIO] = audioRenderer;
renderers[DemoPlayer.TYPE_TEXT] = textRenderer;
player.onRenderers(renderers, bandwidthMeter);
} }
} }
......
# 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.
# 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.
// 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')
}
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" path="gen"/>
<classpathentry kind="src" path="java"/>
<classpathentry kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.LIBRARIES"/>
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.DEPENDENCIES"/>
<classpathentry kind="output" path="bin/classes"/>
</classpath>
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>ExoPlayerDemoMisc-Vp9OpusSw</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>com.android.ide.eclipse.adt.ResourceManagerBuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>com.android.ide.eclipse.adt.PreCompilerBuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>com.android.ide.eclipse.adt.ApkBuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>com.android.ide.eclipse.adt.AndroidNature</nature>
<nature>org.eclipse.jdt.core.javanature</nature>
</natures>
</projectDescription>
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.google.android.exoplayer.demo.vp9opus"
android:versionCode="1503"
android:versionName="1.5.4"
android:theme="@style/RootTheme">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-feature android:glEsVersion="0x00020000"></uses-feature>
<uses-sdk android:minSdkVersion="9" android:targetSdkVersion="23"/>
<application
tools:ignore="UnusedAttribute"
android:label="@string/app_name"
android:largeHeap="true"
android:allowBackup="false"
android:icon="@drawable/ic_launcher">
<activity android:name="com.google.android.exoplayer.demo.vp9opus.SampleChooserActivity"
android:label="@string/app_name"
android:configChanges="keyboardHidden">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity android:name=".PlayerActivity"
android:configChanges="keyboardHidden|orientation|screenSize"
android:label="@string/app_name"
android:theme="@style/PlayerTheme"/>
</application>
</manifest>
/*
* 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<MediaPresentationDescription> {
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<MediaPresentationDescription> 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<Representation> 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);
}
}
/*
* 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());
}
}
/*
* 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<Object> {
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;
}
}
}
# 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
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/root"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:keepScreenOn="true">
<com.google.android.exoplayer.AspectRatioFrameLayout android:id="@+id/video_frame"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center">
<SurfaceView
android:id="@+id/surface_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center|center_vertical"/>
<com.google.android.exoplayer.ext.vp9.VpxVideoSurfaceView
android:id="@+id/vpx_surface_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center|center_vertical"/>
</com.google.android.exoplayer.AspectRatioFrameLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="#88000000">
<TextView
android:id="@+id/debug_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
</FrameLayout>
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/rowtext"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="25sp"/>
<?xml version="1.0" encoding="UTF-8"?>
<!-- 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.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ListView android:id="@+id/sample_list"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</LinearLayout>
<?xml version="1.0" encoding="UTF-8"?>
<!-- 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.
-->
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textStyle="bold"
android:textAllCaps="true"
android:textColor="@android:color/white"
android:textSize="14sp"
android:padding="8dp"
android:focusable="true"
android:background="#339999FF"/>
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android">
<style name="RootTheme" parent="android:Theme.Holo"/>
</resources>
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="app_name">WebM ExoPlayer Demo</string>
<string name="choose_file">Choose File</string>
<string name="play">Play</string>
<string name="current_path">
Path: <xliff:g id="path" example="/sdcard/test.webm">%1$s</xliff:g>
</string>
<string name="libvpx_version">
Libvpx: <xliff:g id="path">%1$s</xliff:g>
</string>
<string name="libopus_version">
Libopus: <xliff:g id="path">%1$s</xliff:g>
</string>
<string name="storage_permission_denied">Permission to access storage was denied</string>
</resources>
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android">
<style name="RootTheme" parent="android:Theme"/>
<style name="PlayerTheme" parent="@style/RootTheme">
<item name="android:windowNoTitle">true</item>
<item name="android:windowBackground">@android:color/black</item>
</style>
</resources>
# Extensions #
This folder contains optional ExoPlayer extensions.
# 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/
// 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'
}
}
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.LIBRARIES"/>
<classpathentry kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.DEPENDENCIES"/>
<classpathentry combineaccessrules="false" exported="true" kind="src" path="/ExoPlayerLib"/>
<classpathentry kind="src" path="java"/>
<classpathentry kind="src" path="gen"/>
<classpathentry kind="output" path="bin/classes"/>
</classpath>
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>ExoPlayerExt-OkHttp</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>com.android.ide.eclipse.adt.ResourceManagerBuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>com.android.ide.eclipse.adt.PreCompilerBuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>com.android.ide.eclipse.adt.ApkBuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>com.android.ide.eclipse.adt.AndroidNature</nature>
<nature>org.eclipse.jdt.core.javanature</nature>
</natures>
</projectDescription>
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.google.android.exoplayer.ext.okhttp">
<uses-sdk android:minSdkVersion="9" android:targetSdkVersion="23"/>
</manifest>
/*
* 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<byte[]> skipBufferReference = new AtomicReference<>();
private final OkHttpClient okHttpClient;
private final String userAgent;
private final Predicate<String> contentTypePredicate;
private final TransferListener listener;
private final CacheControl cacheControl;
private final HashMap<String, String> 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<String> 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<String> 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<String> 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<String, List<String>> 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<String, List<String>> 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}.
* <p>
* 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<String, String> 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.
* <p>
* 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}.
* <p>
* 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;
}
}
# 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
# 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 "<path to exoplayer checkout>"
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="<path to Android NDK>"
```
* 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
<!---
Work around to point to two different links for the same text.
-->
[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 := <arch>` (where `<arch>` 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=<arch>` (where `<arch>` 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`.
// 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')
}
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.LIBRARIES"/>
<classpathentry kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.DEPENDENCIES"/>
<classpathentry kind="src" path="gen"/>
<classpathentry kind="src" path="java"/>
<classpathentry kind="src" path="/ExoPlayerLib"/>
<classpathentry kind="output" path="bin/classes"/>
</classpath>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<?fileVersion 4.0.0?><cproject storage_type_id="org.eclipse.cdt.core.XmlProjectDescriptionStorage">
<storageModule moduleId="org.eclipse.cdt.core.settings">
<cconfiguration id="com.android.toolchain.gcc.423224913">
<storageModule buildSystemId="org.eclipse.cdt.managedbuilder.core.configurationDataProvider" id="com.android.toolchain.gcc.423224913" moduleId="org.eclipse.cdt.core.settings" name="Default">
<externalSettings/>
<extensions>
<extension id="org.eclipse.cdt.core.VCErrorParser" point="org.eclipse.cdt.core.ErrorParser"/>
<extension id="org.eclipse.cdt.core.GmakeErrorParser" point="org.eclipse.cdt.core.ErrorParser"/>
<extension id="org.eclipse.cdt.core.CWDLocator" point="org.eclipse.cdt.core.ErrorParser"/>
<extension id="org.eclipse.cdt.core.MakeErrorParser" point="org.eclipse.cdt.core.ErrorParser"/>
<extension id="org.eclipse.cdt.core.GCCErrorParser" point="org.eclipse.cdt.core.ErrorParser"/>
<extension id="org.eclipse.cdt.core.GASErrorParser" point="org.eclipse.cdt.core.ErrorParser"/>
<extension id="org.eclipse.cdt.core.GLDErrorParser" point="org.eclipse.cdt.core.ErrorParser"/>
<extension id="org.eclipse.cdt.core.ELF" point="org.eclipse.cdt.core.BinaryParser"/>
</extensions>
</storageModule>
<storageModule moduleId="cdtBuildSystem" version="4.0.0">
<configuration artifactName="${ProjName}" buildProperties="" description="" id="com.android.toolchain.gcc.423224913" name="Default" parent="org.eclipse.cdt.build.core.emptycfg">
<folderInfo id="com.android.toolchain.gcc.423224913.1376674556" name="/" resourcePath="">
<toolChain id="com.android.toolchain.gcc.1798416430" name="Android GCC" superClass="com.android.toolchain.gcc">
<targetPlatform binaryParser="org.eclipse.cdt.core.ELF" id="com.android.targetPlatform.1132129264" isAbstract="false" superClass="com.android.targetPlatform"/>
<builder buildPath="${workspace_loc:/ExoPlayerExt-Opus}/jni" id="com.android.builder.532503968" keepEnvironmentInBuildfile="false" managedBuildOn="false" name="Android Builder" superClass="com.android.builder">
<outputEntries>
<entry flags="VALUE_WORKSPACE_PATH|RESOLVED" kind="outputPath" name="obj"/>
<entry flags="VALUE_WORKSPACE_PATH|RESOLVED" kind="outputPath" name="libs"/>
</outputEntries>
</builder>
<tool id="com.android.gcc.compiler.906450637" name="Android GCC Compiler" superClass="com.android.gcc.compiler">
<inputType id="com.android.gcc.inputType.835889068" superClass="com.android.gcc.inputType"/>
</tool>
</toolChain>
</folderInfo>
<sourceEntries>
<entry flags="VALUE_WORKSPACE_PATH|RESOLVED" kind="sourcePath" name="jni"/>
</sourceEntries>
</configuration>
</storageModule>
<storageModule moduleId="org.eclipse.cdt.core.externalSettings"/>
</cconfiguration>
</storageModule>
<storageModule moduleId="cdtBuildSystem" version="4.0.0">
<project id="ExoPlayerExt-Opus.null.1840202624" name="ExoPlayerExt-Opus"/>
</storageModule>
<storageModule moduleId="org.eclipse.cdt.core.LanguageSettingsProviders"/>
<storageModule moduleId="scannerConfiguration">
<autodiscovery enabled="true" problemReportingEnabled="true" selectedProfileId=""/>
<scannerConfigBuildInfo instanceId="com.android.toolchain.gcc.423224913;com.android.toolchain.gcc.423224913.1376674556;com.android.gcc.compiler.906450637;com.android.gcc.inputType.835889068">
<autodiscovery enabled="true" problemReportingEnabled="true" selectedProfileId="com.android.AndroidPerProjectProfile"/>
</scannerConfigBuildInfo>
</storageModule>
<storageModule moduleId="refreshScope" versionNumber="2">
<configuration configurationName="Default">
<resource resourceType="PROJECT" workspacePath="/ExoPlayerExt-Opus"/>
</configuration>
</storageModule>
</cproject>
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>ExoPlayerExt-Opus</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.cdt.managedbuilder.core.genmakebuilder</name>
<triggers>clean,full,incremental,</triggers>
<arguments>
<dictionary>
<key>?children?</key>
<value>?name?=outputEntries\|?children?=?name?=entry\\\\\\\|\\\|?name?=entry\\\\\\\|\\\|\||</value>
</dictionary>
<dictionary>
<key>?name?</key>
<value></value>
</dictionary>
<dictionary>
<key>org.eclipse.cdt.make.core.append_environment</key>
<value>true</value>
</dictionary>
<dictionary>
<key>org.eclipse.cdt.make.core.buildArguments</key>
<value></value>
</dictionary>
<dictionary>
<key>org.eclipse.cdt.make.core.buildCommand</key>
<value>ndk-build</value>
</dictionary>
<dictionary>
<key>org.eclipse.cdt.make.core.cleanBuildTarget</key>
<value>clean</value>
</dictionary>
<dictionary>
<key>org.eclipse.cdt.make.core.contents</key>
<value>org.eclipse.cdt.make.core.activeConfigSettings</value>
</dictionary>
<dictionary>
<key>org.eclipse.cdt.make.core.enableAutoBuild</key>
<value>false</value>
</dictionary>
<dictionary>
<key>org.eclipse.cdt.make.core.enableCleanBuild</key>
<value>true</value>
</dictionary>
<dictionary>
<key>org.eclipse.cdt.make.core.enableFullBuild</key>
<value>true</value>
</dictionary>
<dictionary>
<key>org.eclipse.cdt.make.core.stopOnError</key>
<value>true</value>
</dictionary>
<dictionary>
<key>org.eclipse.cdt.make.core.useDefaultBuildCmd</key>
<value>true</value>
</dictionary>
</arguments>
</buildCommand>
<buildCommand>
<name>com.android.ide.eclipse.adt.ResourceManagerBuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>com.android.ide.eclipse.adt.PreCompilerBuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>com.android.ide.eclipse.adt.ApkBuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.cdt.managedbuilder.core.ScannerConfigBuilder</name>
<triggers>full,incremental,</triggers>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>com.android.ide.eclipse.adt.AndroidNature</nature>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.cdt.core.cnature</nature>
<nature>org.eclipse.cdt.core.ccnature</nature>
<nature>org.eclipse.cdt.managedbuilder.core.managedBuildNature</nature>
<nature>org.eclipse.cdt.managedbuilder.core.ScannerConfigNature</nature>
</natures>
</projectDescription>
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
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.google.android.exoplayer.ext.opus">
<uses-sdk android:minSdkVersion="9" android:targetSdkVersion="23"/>
</manifest>
/*
* 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<byte[]> 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);
}
});
}
}
}
/*
* 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);
}
/*
* 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);
}
}
/*
* 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<InputBuffer> dequeuedInputBuffers;
private final LinkedList<InputBuffer> queuedInputBuffers;
private final LinkedList<OutputBuffer> queuedOutputBuffers;
private final LinkedList<OutputBuffer> 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.
* <p>
* 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];
}
}
}
#
# 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)
#
# 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
#!/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."
#
# 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)
/*
* 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 <jni.h>
#include <android/log.h>
#include <cstdlib>
#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<void**>(&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<uint8_t*>(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<intptr_t>(decoder);
}
FUNC(jint, opusDecode, jlong jDecoder, jobject jInputBuffer, jint inputSize,
jobject jOutputBuffer, jint outputSize) {
OpusMSDecoder* decoder = reinterpret_cast<OpusMSDecoder*>(jDecoder);
const uint8_t* inputBuffer =
reinterpret_cast<const uint8_t*>(
env->GetDirectBufferAddress(jInputBuffer));
int16_t* outputBuffer = reinterpret_cast<int16_t*>(
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<OpusMSDecoder*>(jDecoder);
opus_multistream_decoder_destroy(decoder);
}
FUNC(void, opusReset, jlong jDecoder) {
OpusMSDecoder* decoder = reinterpret_cast<OpusMSDecoder*>(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));
}
# Proguard rules specific to the Opus extension.
# This prevents the names of native methods from being obfuscated.
-keepclasseswithmembernames class * {
native <methods>;
}
# 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
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.
# 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 "<path to exoplayer checkout>"
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="<path to Android NDK>"
```
* 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
<!---
Work around to point to two different links for the same text.
-->
[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 := <arch>` (where `<arch>` 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=<arch>` (where `<arch>` 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.
// 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')
}
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" path="gen"/>
<classpathentry kind="src" path="java"/>
<classpathentry kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.LIBRARIES"/>
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.DEPENDENCIES"/>
<classpathentry combineaccessrules="false" kind="src" path="/ExoPlayerExt-VP9"/>
<classpathentry kind="output" path="bin/classes"/>
</classpath>
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>ExoPlayerExt-VP9Tests</name>
<comment></comment>
<projects>
<project>ExoPlayerLib</project>
</projects>
<buildSpec>
<buildCommand>
<name>com.android.ide.eclipse.adt.ResourceManagerBuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>com.android.ide.eclipse.adt.PreCompilerBuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>com.android.ide.eclipse.adt.ApkBuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>com.android.ide.eclipse.adt.AndroidNature</nature>
<nature>org.eclipse.jdt.core.javanature</nature>
</natures>
<filteredResources>
<filter>
<id>0</id>
<name></name>
<type>14</type>
<matcher>
<id>org.eclipse.ui.ide.multiFilter</id>
<arguments>1.0-name-matches-true-false-BUILD</arguments>
</matcher>
</filter>
</filteredResources>
</projectDescription>
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.google.android.exoplayer.ext.vp9.test">
<uses-sdk android:minSdkVersion="9" android:targetSdkVersion="23"/>
<application android:debuggable="true"
android:allowBackup="false"
tools:ignore="MissingApplicationIcon,HardcodedDebugMode">
<uses-library android:name="android.test.runner"/>
</application>
<instrumentation
android:targetPackage="com.google.android.exoplayer.ext.vp9.test"
android:name="android.test.InstrumentationTestRunner"
tools:replace="android:targetPackage"/>
</manifest>
/*
* 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.ExoPlaybackException;
import com.google.android.exoplayer.ExoPlayer;
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.Util;
import android.content.Context;
import android.net.Uri;
import android.os.Looper;
import android.test.InstrumentationTestCase;
/**
* Playback tests using {@link LibvpxVideoTrackRenderer}.
*/
public class VpxPlaybackTest extends InstrumentationTestCase {
private static final String BEAR_URI = "asset:///bear-vp9.webm";
private static final String BEAR_ODD_DIMENSIONS_URI = "asset:///bear-vp9-odd-dimensions.webm";
private static final String INVALID_BITSTREAM_URI = "asset:///invalid-bitstream.webm";
public void testBasicPlayback() throws ExoPlaybackException {
playUri(BEAR_URI);
}
public void testOddDimensionsPlayback() throws ExoPlaybackException {
playUri(BEAR_ODD_DIMENSIONS_URI);
}
public void testInvalidBitstream() {
try {
playUri(INVALID_BITSTREAM_URI);
fail();
} catch (Exception e) {
assertNotNull(e.getCause());
assertTrue(e.getCause() instanceof VpxDecoderException);
}
}
private void playUri(String uri) throws ExoPlaybackException {
TestPlaybackThread thread = new TestPlaybackThread(Uri.parse(uri),
getInstrumentation().getContext());
thread.start();
try {
thread.join();
} catch (InterruptedException e) {
fail(); // Should never happen.
}
if (thread.playbackException != null) {
throw thread.playbackException;
}
}
private static class TestPlaybackThread extends Thread implements ExoPlayer.Listener {
private static final int BUFFER_SEGMENT_SIZE = 64 * 1024;
private static final int BUFFER_SEGMENT_COUNT = 16;
private final Context context;
private final Uri uri;
private ExoPlayer player;
private ExoPlaybackException playbackException;
public TestPlaybackThread(Uri uri, Context context) {
this.uri = uri;
this.context = context;
}
@Override
public void run() {
Looper.prepare();
player = ExoPlayer.Factory.newInstance(1);
player.addListener(this);
ExtractorSampleSource sampleSource = new ExtractorSampleSource(
uri, new DefaultUriDataSource(context, Util.getUserAgent(context, "ExoPlayerExtVP9Test")),
new DefaultAllocator(BUFFER_SEGMENT_SIZE), BUFFER_SEGMENT_SIZE * BUFFER_SEGMENT_COUNT,
new WebmExtractor());
LibvpxVideoTrackRenderer videoRenderer = new LibvpxVideoTrackRenderer(sampleSource, true);
player.sendMessage(videoRenderer, LibvpxVideoTrackRenderer.MSG_SET_OUTPUT_BUFFER_RENDERER,
new VpxVideoSurfaceView(context));
player.prepare(videoRenderer);
player.setPlayWhenReady(true);
Looper.loop();
}
@Override
public void onPlayWhenReadyCommitted () {
// Do nothing.
}
@Override
public void onPlayerError(ExoPlaybackException error) {
playbackException = error;
}
@Override
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
if (playbackState == ExoPlayer.STATE_ENDED
|| (playbackState == ExoPlayer.STATE_IDLE && playbackException != null)) {
releasePlayerAndQuitLooper();
}
}
private void releasePlayerAndQuitLooper() {
player.release();
Looper.myLooper().quit();
}
}
}
# 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
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.
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.LIBRARIES"/>
<classpathentry kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.DEPENDENCIES"/>
<classpathentry kind="src" path="java"/>
<classpathentry kind="src" path="gen"/>
<classpathentry kind="src" path="/ExoPlayerLib"/>
<classpathentry kind="output" path="bin/classes"/>
</classpath>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<?fileVersion 4.0.0?><cproject storage_type_id="org.eclipse.cdt.core.XmlProjectDescriptionStorage">
<storageModule moduleId="org.eclipse.cdt.core.settings">
<cconfiguration id="com.android.toolchain.gcc.367693784">
<storageModule buildSystemId="org.eclipse.cdt.managedbuilder.core.configurationDataProvider" id="com.android.toolchain.gcc.367693784" moduleId="org.eclipse.cdt.core.settings" name="Default">
<externalSettings/>
<extensions>
<extension id="org.eclipse.cdt.core.VCErrorParser" point="org.eclipse.cdt.core.ErrorParser"/>
<extension id="org.eclipse.cdt.core.GmakeErrorParser" point="org.eclipse.cdt.core.ErrorParser"/>
<extension id="org.eclipse.cdt.core.CWDLocator" point="org.eclipse.cdt.core.ErrorParser"/>
<extension id="org.eclipse.cdt.core.MakeErrorParser" point="org.eclipse.cdt.core.ErrorParser"/>
<extension id="org.eclipse.cdt.core.GCCErrorParser" point="org.eclipse.cdt.core.ErrorParser"/>
<extension id="org.eclipse.cdt.core.GASErrorParser" point="org.eclipse.cdt.core.ErrorParser"/>
<extension id="org.eclipse.cdt.core.GLDErrorParser" point="org.eclipse.cdt.core.ErrorParser"/>
<extension id="org.eclipse.cdt.core.ELF" point="org.eclipse.cdt.core.BinaryParser"/>
</extensions>
</storageModule>
<storageModule moduleId="cdtBuildSystem" version="4.0.0">
<configuration artifactName="${ProjName}" buildProperties="" description="" id="com.android.toolchain.gcc.367693784" name="Default" parent="org.eclipse.cdt.build.core.emptycfg">
<folderInfo id="com.android.toolchain.gcc.367693784.1582606005" name="/" resourcePath="">
<toolChain id="com.android.toolchain.gcc.2090539093" name="Android GCC" superClass="com.android.toolchain.gcc">
<targetPlatform binaryParser="org.eclipse.cdt.core.ELF" id="com.android.targetPlatform.1021581688" isAbstract="false" superClass="com.android.targetPlatform"/>
<builder buildPath="${workspace_loc:/ExoPlayerExt-VP9}/jni" id="com.android.builder.1955717109" keepEnvironmentInBuildfile="false" managedBuildOn="false" name="Android Builder" superClass="com.android.builder">
<outputEntries>
<entry flags="VALUE_WORKSPACE_PATH|RESOLVED" kind="outputPath" name="obj"/>
<entry flags="VALUE_WORKSPACE_PATH|RESOLVED" kind="outputPath" name="libs"/>
</outputEntries>
</builder>
<tool id="com.android.gcc.compiler.162335776" name="Android GCC Compiler" superClass="com.android.gcc.compiler">
<inputType id="com.android.gcc.inputType.78164988" superClass="com.android.gcc.inputType"/>
</tool>
</toolChain>
</folderInfo>
<sourceEntries>
<entry flags="VALUE_WORKSPACE_PATH|RESOLVED" kind="sourcePath" name="jni"/>
</sourceEntries>
</configuration>
</storageModule>
<storageModule moduleId="org.eclipse.cdt.core.externalSettings"/>
</cconfiguration>
</storageModule>
<storageModule moduleId="cdtBuildSystem" version="4.0.0">
<project id="ExoPlayerExt-VP9.null.410683598" name="ExoPlayerExt-VP9"/>
</storageModule>
<storageModule moduleId="org.eclipse.cdt.core.LanguageSettingsProviders"/>
<storageModule moduleId="scannerConfiguration">
<autodiscovery enabled="true" problemReportingEnabled="true" selectedProfileId=""/>
<scannerConfigBuildInfo instanceId="com.android.toolchain.gcc.367693784;com.android.toolchain.gcc.367693784.1582606005;com.android.gcc.compiler.162335776;com.android.gcc.inputType.78164988">
<autodiscovery enabled="true" problemReportingEnabled="true" selectedProfileId="com.android.AndroidPerProjectProfile"/>
</scannerConfigBuildInfo>
</storageModule>
<storageModule moduleId="refreshScope" versionNumber="2">
<configuration configurationName="Default">
<resource resourceType="PROJECT" workspacePath="/ExoPlayerExt-VP9"/>
</configuration>
</storageModule>
</cproject>
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>ExoPlayerExt-VP9</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.cdt.managedbuilder.core.genmakebuilder</name>
<triggers>clean,full,incremental,</triggers>
<arguments>
<dictionary>
<key>?children?</key>
<value>?name?=outputEntries\|?children?=?name?=entry\\\\\\\|\\\|?name?=entry\\\\\\\|\\\|\||</value>
</dictionary>
<dictionary>
<key>?name?</key>
<value></value>
</dictionary>
<dictionary>
<key>org.eclipse.cdt.make.core.append_environment</key>
<value>true</value>
</dictionary>
<dictionary>
<key>org.eclipse.cdt.make.core.buildArguments</key>
<value></value>
</dictionary>
<dictionary>
<key>org.eclipse.cdt.make.core.buildCommand</key>
<value>ndk-build</value>
</dictionary>
<dictionary>
<key>org.eclipse.cdt.make.core.cleanBuildTarget</key>
<value>clean</value>
</dictionary>
<dictionary>
<key>org.eclipse.cdt.make.core.contents</key>
<value>org.eclipse.cdt.make.core.activeConfigSettings</value>
</dictionary>
<dictionary>
<key>org.eclipse.cdt.make.core.enableAutoBuild</key>
<value>false</value>
</dictionary>
<dictionary>
<key>org.eclipse.cdt.make.core.enableCleanBuild</key>
<value>true</value>
</dictionary>
<dictionary>
<key>org.eclipse.cdt.make.core.enableFullBuild</key>
<value>true</value>
</dictionary>
<dictionary>
<key>org.eclipse.cdt.make.core.stopOnError</key>
<value>true</value>
</dictionary>
<dictionary>
<key>org.eclipse.cdt.make.core.useDefaultBuildCmd</key>
<value>true</value>
</dictionary>
</arguments>
</buildCommand>
<buildCommand>
<name>com.android.ide.eclipse.adt.ResourceManagerBuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>com.android.ide.eclipse.adt.PreCompilerBuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>com.android.ide.eclipse.adt.ApkBuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.cdt.managedbuilder.core.ScannerConfigBuilder</name>
<triggers>full,incremental,</triggers>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>com.android.ide.eclipse.adt.AndroidNature</nature>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.cdt.core.cnature</nature>
<nature>org.eclipse.cdt.core.ccnature</nature>
<nature>org.eclipse.cdt.managedbuilder.core.managedBuildNature</nature>
<nature>org.eclipse.cdt.managedbuilder.core.ScannerConfigNature</nature>
</natures>
</projectDescription>
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
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.google.android.exoplayer.ext.vp9">
<uses-sdk android:minSdkVersion="9" android:targetSdkVersion="23"/>
<uses-feature android:glEsVersion="0x00020000"/>
</manifest>
/*
* 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);
}
});
}
}
}
/*
* 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);
}
/*
* 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);
}
}
/*
* 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<VpxInputBuffer> queuedInputBuffers;
private final LinkedList<VpxOutputBuffer> 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.
* <p>
* 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);
}
}
}
/*
* 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;
}
}
/*
* 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);
}
/*
* 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<VpxOutputBuffer> 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;
}
}
/*
* 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();
}
}
#
# 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)
#
# 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
#!/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} <path_to_android_ndk>"
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."
#
# 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)
/*
* 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 <cpu-features.h>
#include <jni.h>
#include <android/log.h>
#include <algorithm>
#include <cstdlib>
#include <cstdio>
#include <new>
#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<void**>(&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<intptr_t>(context);
}
FUNC(jlong, vpxDecode, jlong jContext, jobject encoded, jint len) {
vpx_codec_ctx_t* const context = reinterpret_cast<vpx_codec_ctx_t*>(jContext);
const uint8_t* const buffer =
reinterpret_cast<const uint8_t*>(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<vpx_codec_ctx_t*>(jContext);
vpx_codec_destroy(context);
delete context;
return 0;
}
FUNC(jint, vpxGetFrame, jlong jContext, jobject jOutputBuffer) {
vpx_codec_ctx_t* const context = reinterpret_cast<vpx_codec_ctx_t*>(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<uint8_t*>(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<jbyte*>(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<vpx_codec_ctx_t*>(jContext);
return env->NewStringUTF(vpx_codec_error(context));
}
# Proguard rules specific to the VP9 extension.
# This prevents the names of native methods from being obfuscated.
-keepclasseswithmembernames class * {
native <methods>;
}
# Some members of this class are being accessed from native methods. Keep them unobfuscated.
-keep class com.google.android.exoplayer.ext.vp9.VpxDecoderWrapper$OutputBuffer {
*;
}
# 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
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.
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" path="gen"/>
<classpathentry kind="src" path="java"/>
<classpathentry kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.LIBRARIES"/>
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.DEPENDENCIES"/>
<classpathentry kind="output" path="bin/classes"/>
</classpath>
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>ExoPlayerLibTests</name>
<comment></comment>
<projects>
<project>ExoPlayerLib</project>
</projects>
<buildSpec>
<buildCommand>
<name>com.android.ide.eclipse.adt.ResourceManagerBuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>com.android.ide.eclipse.adt.PreCompilerBuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>com.android.ide.eclipse.adt.ApkBuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>com.android.ide.eclipse.adt.AndroidNature</nature>
<nature>org.eclipse.jdt.core.javanature</nature>
</natures>
<linkedResources>
<link>
<name>libs/dexmaker-1.2.jar</name>
<type>1</type>
<locationURI>$%7BPARENT-3-PROJECT_LOC%7D/third_party/dexmaker/dexmaker-1.2.jar</locationURI>
</link>
<link>
<name>libs/dexmaker-mockito-1.2.jar</name>
<type>1</type>
<locationURI>$%7BPARENT-3-PROJECT_LOC%7D/third_party/dexmaker/dexmaker-mockito-1.2.jar</locationURI>
</link>
<link>
<name>libs/mockito-all-1.9.5.jar</name>
<type>1</type>
<locationURI>$%7BPARENT-3-PROJECT_LOC%7D/third_party/mockito/mockito-all-1.9.5.jar</locationURI>
</link>
</linkedResources>
<filteredResources>
<filter>
<id>1425657306619</id>
<name></name>
<type>14</type>
<matcher>
<id>org.eclipse.ui.ide.multiFilter</id>
<arguments>1.0-name-matches-true-false-BUILD</arguments>
</matcher>
</filter>
</filteredResources>
</projectDescription>
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
This file is needed to make sure the libs directory is present.
...@@ -13,4 +13,4 @@ ...@@ -13,4 +13,4 @@
# Project target. # Project target.
target=android-23 target=android-23
android.library=false android.library=false
android.library.reference.1=../main android.library.reference.1=../experimental
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.LIBRARIES"/>
<classpathentry kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.DEPENDENCIES"/>
<classpathentry kind="src" path="java"/>
<classpathentry kind="src" path="gen"/>
<classpathentry kind="output" path="bin/classes"/>
</classpath>
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>ExoPlayerLib</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>com.android.ide.eclipse.adt.ResourceManagerBuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>com.android.ide.eclipse.adt.PreCompilerBuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>com.android.ide.eclipse.adt.ApkBuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>com.android.ide.eclipse.adt.AndroidNature</nature>
<nature>org.eclipse.jdt.core.javanature</nature>
</natures>
</projectDescription>
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
...@@ -27,15 +27,14 @@ import android.media.MediaExtractor; ...@@ -27,15 +27,14 @@ import android.media.MediaExtractor;
public final class C { 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; public static final long UNKNOWN_TIME_US = -1L;
/** /**
* Represents a microsecond duration whose exact value is unknown, but which should match the * Special microsecond constant representing the end of a source.
* longest of some other known durations.
*/ */
public static final long MATCH_LONGEST_US = -2L; public static final long END_OF_SOURCE_US = -2L;
/** /**
* The number of microseconds in one second. * The number of microseconds in one second.
......
...@@ -25,8 +25,8 @@ package com.google.android.exoplayer; ...@@ -25,8 +25,8 @@ package com.google.android.exoplayer;
public final class DummyTrackRenderer extends TrackRenderer { public final class DummyTrackRenderer extends TrackRenderer {
@Override @Override
protected boolean doPrepare(long positionUs) throws ExoPlaybackException { protected void doPrepare(SampleSource sampleSource) throws ExoPlaybackException {
return true; // Do nothing.
} }
@Override @Override
...@@ -50,11 +50,6 @@ public final class DummyTrackRenderer extends TrackRenderer { ...@@ -50,11 +50,6 @@ public final class DummyTrackRenderer extends TrackRenderer {
} }
@Override @Override
protected void seekTo(long positionUs) {
throw new IllegalStateException();
}
@Override
protected void doSomeWork(long positionUs, long elapsedRealtimeUs) { protected void doSomeWork(long positionUs, long elapsedRealtimeUs) {
throw new IllegalStateException(); throw new IllegalStateException();
} }
...@@ -64,14 +59,4 @@ public final class DummyTrackRenderer extends TrackRenderer { ...@@ -64,14 +59,4 @@ public final class DummyTrackRenderer extends TrackRenderer {
throw new IllegalStateException(); throw new IllegalStateException();
} }
@Override
protected long getDurationUs() {
throw new IllegalStateException();
}
@Override
protected long getBufferedPositionUs() {
throw new IllegalStateException();
}
} }
...@@ -85,7 +85,7 @@ import android.os.Looper; ...@@ -85,7 +85,7 @@ import android.os.Looper;
* *
* <p>The possible playback state transitions are shown below. Transitions can be triggered either * <p>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 * 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.</p> * {@link #prepare(SampleSource)}, {@link #stop()} or {@link #release()} being invoked.</p>
* <p align="center"><img src="../../../../../images/exoplayer_playbackstate.png" * <p align="center"><img src="../../../../../images/exoplayer_playbackstate.png"
* alt="ExoPlayer playback state transitions" * alt="ExoPlayer playback state transitions"
* border="0"/></p> * border="0"/></p>
...@@ -95,7 +95,7 @@ public interface ExoPlayer { ...@@ -95,7 +95,7 @@ public interface ExoPlayer {
/** /**
* A factory for instantiating ExoPlayer instances. * 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 * The default minimum duration of data that must be buffered for playback to start or resume
...@@ -117,16 +117,16 @@ public interface ExoPlayer { ...@@ -117,16 +117,16 @@ public interface ExoPlayer {
* <p> * <p>
* Must be invoked from a thread that has an associated {@link Looper}. * 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 * @param renderers The {@link TrackRenderer}s that will be used by the instance.
* {@link #prepare(TrackRenderer[])}.
* @param minBufferMs A minimum duration of data that must be buffered for playback to start * @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. * 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 * @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 * 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). * not due to a user action such as starting playback or seeking).
*/ */
public static ExoPlayer newInstance(int rendererCount, int minBufferMs, int minRebufferMs) { public static ExoPlayer newInstance(TrackRenderer[] renderers, int minBufferMs,
return new ExoPlayerImpl(rendererCount, minBufferMs, minRebufferMs); int minRebufferMs) {
return new ExoPlayerImpl(renderers, minBufferMs, minRebufferMs);
} }
/** /**
...@@ -134,11 +134,10 @@ public interface ExoPlayer { ...@@ -134,11 +134,10 @@ public interface ExoPlayer {
* <p> * <p>
* Must be invoked from a thread that has an associated {@link Looper}. * 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 * @param renderers The {@link TrackRenderer}s that will be used by the instance.
* {@link #prepare(TrackRenderer[])}.
*/ */
public static ExoPlayer newInstance(int rendererCount) { public static ExoPlayer newInstance(TrackRenderer... renderers) {
return new ExoPlayerImpl(rendererCount, DEFAULT_MIN_BUFFER_MS, DEFAULT_MIN_REBUFFER_MS); return new ExoPlayerImpl(renderers, DEFAULT_MIN_BUFFER_MS, DEFAULT_MIN_REBUFFER_MS);
} }
} }
...@@ -146,7 +145,7 @@ public interface ExoPlayer { ...@@ -146,7 +145,7 @@ public interface ExoPlayer {
/** /**
* Interface definition for a callback to be notified of changes in player state. * 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 * Invoked when the value returned from either {@link ExoPlayer#getPlayWhenReady()} or
* {@link ExoPlayer#getPlaybackState()} changes. * {@link ExoPlayer#getPlaybackState()} changes.
...@@ -183,7 +182,7 @@ public interface ExoPlayer { ...@@ -183,7 +182,7 @@ public interface ExoPlayer {
* Messages can be delivered to a component via {@link ExoPlayer#sendMessage} and * Messages can be delivered to a component via {@link ExoPlayer#sendMessage} and
* {@link ExoPlayer#blockingSendMessage}. * {@link ExoPlayer#blockingSendMessage}.
*/ */
public interface ExoPlayerComponent { interface ExoPlayerComponent {
/** /**
* Handles a message delivered to the component. Invoked on the playback thread. * Handles a message delivered to the component. Invoked on the playback thread.
...@@ -199,49 +198,49 @@ public interface ExoPlayer { ...@@ -199,49 +198,49 @@ public interface ExoPlayer {
/** /**
* The player is neither prepared or being prepared. * 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. * 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 * 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 * is {@link TrackRenderer} specific, but this state typically occurs when more data needs
* to be buffered for playback to start. * 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 * 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. * 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. * 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 * A value that can be passed as the second argument to {@link #setSelectedTrack(int, int)} to
* disable the renderer. * 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 * A value that can be passed as the second argument to {@link #setSelectedTrack(int, int)} to
* select the default track. * select the default track.
*/ */
public static final int TRACK_DEFAULT = 0; static final int TRACK_DEFAULT = 0;
/** /**
* Represents an unknown time or duration. * 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. * Gets the {@link Looper} associated with the playback thread.
* *
* @return 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 * Register a listener to receive events from the player. The listener's methods will be invoked
...@@ -249,29 +248,28 @@ public interface ExoPlayer { ...@@ -249,29 +248,28 @@ public interface ExoPlayer {
* *
* @param listener The listener to register. * @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. * Unregister a listener. The listener will no longer receive events from the player.
* *
* @param listener The listener to unregister. * @param listener The listener to unregister.
*/ */
public void removeListener(Listener listener); void removeListener(Listener listener);
/** /**
* Returns the current state of the player. * Returns the current state of the player.
* *
* @return One of the {@code STATE} constants defined in this interface. * @return One of the {@code STATE} constants defined in this interface.
*/ */
public int getPlaybackState(); int getPlaybackState();
/** /**
* Prepares the player for playback. * Prepares the player for playback.
* *
* @param renderers The {@link TrackRenderer}s to use. The number of renderers must match the * @param sampleSource The {@link SampleSource} to play.
* value that was passed to the {@link ExoPlayer.Factory#newInstance} method.
*/ */
public void prepare(TrackRenderer... renderers); void prepare(SampleSource sampleSource);
/** /**
* Returns the number of tracks exposed by the specified renderer. * Returns the number of tracks exposed by the specified renderer.
...@@ -279,7 +277,7 @@ public interface ExoPlayer { ...@@ -279,7 +277,7 @@ public interface ExoPlayer {
* @param rendererIndex The index of the renderer. * @param rendererIndex The index of the renderer.
* @return The number of tracks. * @return The number of tracks.
*/ */
public int getTrackCount(int rendererIndex); int getTrackCount(int rendererIndex);
/** /**
* Returns the format of a track. * Returns the format of a track.
...@@ -288,7 +286,7 @@ public interface ExoPlayer { ...@@ -288,7 +286,7 @@ public interface ExoPlayer {
* @param trackIndex The index of the track. * @param trackIndex The index of the track.
* @return The format 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. * Selects a track for the specified renderer.
...@@ -297,7 +295,7 @@ public interface ExoPlayer { ...@@ -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 * @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. * 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. * Returns the index of the currently selected track for the specified renderer.
...@@ -306,7 +304,7 @@ public interface ExoPlayer { ...@@ -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 * @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. * 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}. * Sets whether playback should proceed when {@link #getPlaybackState()} == {@link #STATE_READY}.
...@@ -315,14 +313,14 @@ public interface ExoPlayer { ...@@ -315,14 +313,14 @@ public interface ExoPlayer {
* *
* @param playWhenReady Whether playback should proceed when ready. * @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}. * Whether playback will proceed when {@link #getPlaybackState()} == {@link #STATE_READY}.
* *
* @return Whether playback will proceed when 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 * Whether the current value of {@link ExoPlayer#getPlayWhenReady()} has been reflected by the
...@@ -330,14 +328,14 @@ public interface ExoPlayer { ...@@ -330,14 +328,14 @@ public interface ExoPlayer {
* *
* @return True if the current value has been reflected. False otherwise. * @return True if the current value has been reflected. False otherwise.
*/ */
public boolean isPlayWhenReadyCommitted(); boolean isPlayWhenReadyCommitted();
/** /**
* Seeks to a position specified in milliseconds. * Seeks to a position specified in milliseconds.
* *
* @param positionMs The seek position. * @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 * Stops playback. Use {@code setPlayWhenReady(false)} rather than this method if the intention
...@@ -351,14 +349,14 @@ public interface ExoPlayer { ...@@ -351,14 +349,14 @@ public interface ExoPlayer {
* to play another video from its start, then {@code seekTo(0)} should be called after stopping * 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. * 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. * Releases the player. This method must be called when the player is no longer required.
* <p> * <p>
* The player must not be used after calling this method. * 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 * Sends a message to a specified component. The message is delivered to the component on the
...@@ -369,7 +367,7 @@ public interface ExoPlayer { ...@@ -369,7 +367,7 @@ public interface ExoPlayer {
* @param messageType An integer that can be used to identify the type of the message. * @param messageType An integer that can be used to identify the type of the message.
* @param message The message object. * @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 * Blocking variant of {@link #sendMessage(ExoPlayerComponent, int, Object)} that does not return
...@@ -379,7 +377,7 @@ public interface ExoPlayer { ...@@ -379,7 +377,7 @@ public interface ExoPlayer {
* @param messageType An integer that can be used to identify the type of the message. * @param messageType An integer that can be used to identify the type of the message.
* @param message The message object. * @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. * Gets the duration of the track in milliseconds.
...@@ -387,14 +385,14 @@ public interface ExoPlayer { ...@@ -387,14 +385,14 @@ public interface ExoPlayer {
* @return The duration of the track in milliseconds, or {@link ExoPlayer#UNKNOWN_TIME} if the * @return The duration of the track in milliseconds, or {@link ExoPlayer#UNKNOWN_TIME} if the
* duration is not known. * duration is not known.
*/ */
public long getDuration(); long getDuration();
/** /**
* Gets the current playback position in milliseconds. * Gets the current playback position in milliseconds.
* *
* @return 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. * Gets an estimate of the absolute position in milliseconds up to which data is buffered.
...@@ -402,7 +400,7 @@ public interface ExoPlayer { ...@@ -402,7 +400,7 @@ public interface ExoPlayer {
* @return An estimate of the absolute position in milliseconds up to which data is buffered, * @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. * 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. * Gets an estimate of the percentage into the media up to which data is buffered.
...@@ -410,6 +408,6 @@ public interface ExoPlayer { ...@@ -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 * @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. * duration of the media is not known or if no estimate is available.
*/ */
public int getBufferedPercentage(); int getBufferedPercentage();
} }
...@@ -15,6 +15,8 @@ ...@@ -15,6 +15,8 @@
*/ */
package com.google.android.exoplayer; package com.google.android.exoplayer;
import com.google.android.exoplayer.util.Assertions;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
...@@ -44,8 +46,7 @@ import java.util.concurrent.CopyOnWriteArraySet; ...@@ -44,8 +46,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
/** /**
* Constructs an instance. Must be invoked from a thread that has an associated {@link Looper}. * 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 * @param renderers The {@link TrackRenderer}s belonging to this instance.
* {@link #prepare(TrackRenderer[])}.
* @param minBufferMs A minimum duration of data that must be buffered for playback to start * @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. * 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 * @param minRebufferMs A minimum duration of data that must be buffered for playback to resume
...@@ -53,21 +54,23 @@ import java.util.concurrent.CopyOnWriteArraySet; ...@@ -53,21 +54,23 @@ import java.util.concurrent.CopyOnWriteArraySet;
* not due to a user action such as starting playback or seeking). * not due to a user action such as starting playback or seeking).
*/ */
@SuppressLint("HandlerLeak") @SuppressLint("HandlerLeak")
public ExoPlayerImpl(int rendererCount, int minBufferMs, int minRebufferMs) { public ExoPlayerImpl(TrackRenderer[] renderers, int minBufferMs, int minRebufferMs) {
Log.i(TAG, "Init " + ExoPlayerLibraryInfo.VERSION); Log.i(TAG, "Init " + ExoPlayerLibraryInfo.VERSION);
Assertions.checkNotNull(renderers);
Assertions.checkState(renderers.length > 0);
this.playWhenReady = false; this.playWhenReady = false;
this.playbackState = STATE_IDLE; this.playbackState = STATE_IDLE;
this.listeners = new CopyOnWriteArraySet<>(); this.listeners = new CopyOnWriteArraySet<>();
this.trackFormats = new MediaFormat[rendererCount][]; this.trackFormats = new MediaFormat[renderers.length][];
this.selectedTrackIndices = new int[rendererCount]; this.selectedTrackIndices = new int[renderers.length];
eventHandler = new Handler() { eventHandler = new Handler() {
@Override @Override
public void handleMessage(Message msg) { public void handleMessage(Message msg) {
ExoPlayerImpl.this.handleEvent(msg); ExoPlayerImpl.this.handleEvent(msg);
} }
}; };
internalPlayer = new ExoPlayerImplInternal(eventHandler, playWhenReady, selectedTrackIndices, internalPlayer = new ExoPlayerImplInternal(renderers, minBufferMs, minRebufferMs,
minBufferMs, minRebufferMs); playWhenReady, selectedTrackIndices, eventHandler);
} }
@Override @Override
...@@ -91,9 +94,9 @@ import java.util.concurrent.CopyOnWriteArraySet; ...@@ -91,9 +94,9 @@ import java.util.concurrent.CopyOnWriteArraySet;
} }
@Override @Override
public void prepare(TrackRenderer... renderers) { public void prepare(SampleSource source) {
Arrays.fill(trackFormats, null); Arrays.fill(trackFormats, null);
internalPlayer.prepare(renderers); internalPlayer.prepare(source);
} }
@Override @Override
......
...@@ -30,6 +30,7 @@ import android.os.SystemClock; ...@@ -30,6 +30,7 @@ import android.os.SystemClock;
import android.util.Log; import android.util.Log;
import android.util.Pair; import android.util.Pair;
import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
...@@ -38,6 +39,9 @@ import java.util.concurrent.atomic.AtomicInteger; ...@@ -38,6 +39,9 @@ import java.util.concurrent.atomic.AtomicInteger;
/** /**
* Implements the internal behavior of {@link ExoPlayerImpl}. * 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 { /* package */ final class ExoPlayerImplInternal implements Handler.Callback {
private static final String TAG = "ExoPlayerImplInternal"; private static final String TAG = "ExoPlayerImplInternal";
...@@ -63,21 +67,21 @@ import java.util.concurrent.atomic.AtomicInteger; ...@@ -63,21 +67,21 @@ import java.util.concurrent.atomic.AtomicInteger;
private static final int RENDERING_INTERVAL_MS = 10; private static final int RENDERING_INTERVAL_MS = 10;
private static final int IDLE_INTERVAL_MS = 1000; private static final int IDLE_INTERVAL_MS = 1000;
private final Handler handler; private final TrackRenderer[] renderers;
private final HandlerThread internalPlaybackThread; private final TrackRenderer rendererMediaClockSource;
private final Handler eventHandler; private final MediaClock rendererMediaClock;
private final StandaloneMediaClock standaloneMediaClock; private final StandaloneMediaClock standaloneMediaClock;
private final AtomicInteger pendingSeekCount; private final long minBufferUs;
private final long minRebufferUs;
private final List<TrackRenderer> enabledRenderers; private final List<TrackRenderer> enabledRenderers;
private final MediaFormat[][] trackFormats; private final MediaFormat[][] trackFormats;
private final int[] selectedTrackIndices; private final int[] selectedTrackIndices;
private final long minBufferUs; private final Handler handler;
private final long minRebufferUs; private final HandlerThread internalPlaybackThread;
private final Handler eventHandler;
private TrackRenderer[] renderers; private final AtomicInteger pendingSeekCount;
private TrackRenderer rendererMediaClockSource;
private MediaClock rendererMediaClock;
private SampleSource source;
private boolean released; private boolean released;
private boolean playWhenReady; private boolean playWhenReady;
private boolean rebuffering; private boolean rebuffering;
...@@ -91,16 +95,31 @@ import java.util.concurrent.atomic.AtomicInteger; ...@@ -91,16 +95,31 @@ import java.util.concurrent.atomic.AtomicInteger;
private volatile long positionUs; private volatile long positionUs;
private volatile long bufferedPositionUs; private volatile long bufferedPositionUs;
public ExoPlayerImplInternal(Handler eventHandler, boolean playWhenReady, public ExoPlayerImplInternal(TrackRenderer[] renderers, int minBufferMs, int minRebufferMs,
int[] selectedTrackIndices, int minBufferMs, int minRebufferMs) { boolean playWhenReady, int[] selectedTrackIndices, Handler eventHandler) {
this.eventHandler = eventHandler; this.renderers = renderers;
this.playWhenReady = playWhenReady;
this.minBufferUs = minBufferMs * 1000L; this.minBufferUs = minBufferMs * 1000L;
this.minRebufferUs = minRebufferMs * 1000L; this.minRebufferUs = minRebufferMs * 1000L;
this.playWhenReady = playWhenReady;
this.selectedTrackIndices = Arrays.copyOf(selectedTrackIndices, selectedTrackIndices.length); this.selectedTrackIndices = Arrays.copyOf(selectedTrackIndices, selectedTrackIndices.length);
this.eventHandler = eventHandler;
this.state = ExoPlayer.STATE_IDLE; this.state = ExoPlayer.STATE_IDLE;
this.durationUs = TrackRenderer.UNKNOWN_TIME_US; this.durationUs = C.UNKNOWN_TIME_US;
this.bufferedPositionUs = TrackRenderer.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(); standaloneMediaClock = new StandaloneMediaClock();
pendingSeekCount = new AtomicInteger(); pendingSeekCount = new AtomicInteger();
...@@ -123,17 +142,16 @@ import java.util.concurrent.atomic.AtomicInteger; ...@@ -123,17 +142,16 @@ import java.util.concurrent.atomic.AtomicInteger;
} }
public long getBufferedPosition() { public long getBufferedPosition() {
return bufferedPositionUs == TrackRenderer.UNKNOWN_TIME_US ? ExoPlayer.UNKNOWN_TIME return bufferedPositionUs == C.UNKNOWN_TIME_US ? ExoPlayer.UNKNOWN_TIME
: bufferedPositionUs / 1000; : bufferedPositionUs / 1000;
} }
public long getDuration() { public long getDuration() {
return durationUs == TrackRenderer.UNKNOWN_TIME_US ? ExoPlayer.UNKNOWN_TIME return durationUs == C.UNKNOWN_TIME_US ? ExoPlayer.UNKNOWN_TIME : durationUs / 1000;
: durationUs / 1000;
} }
public void prepare(TrackRenderer... renderers) { public void prepare(SampleSource sampleSource) {
handler.obtainMessage(MSG_PREPARE, renderers).sendToTarget(); handler.obtainMessage(MSG_PREPARE, sampleSource).sendToTarget();
} }
public void setPlayWhenReady(boolean playWhenReady) { public void setPlayWhenReady(boolean playWhenReady) {
...@@ -198,7 +216,7 @@ import java.util.concurrent.atomic.AtomicInteger; ...@@ -198,7 +216,7 @@ import java.util.concurrent.atomic.AtomicInteger;
try { try {
switch (msg.what) { switch (msg.what) {
case MSG_PREPARE: { case MSG_PREPARE: {
prepareInternal((TrackRenderer[]) msg.obj); prepareInternal((SampleSource) msg.obj);
return true; return true;
} }
case MSG_INCREMENTAL_PREPARE: { case MSG_INCREMENTAL_PREPARE: {
...@@ -241,6 +259,11 @@ import java.util.concurrent.atomic.AtomicInteger; ...@@ -241,6 +259,11 @@ import java.util.concurrent.atomic.AtomicInteger;
eventHandler.obtainMessage(MSG_ERROR, e).sendToTarget(); eventHandler.obtainMessage(MSG_ERROR, e).sendToTarget();
stopInternal(); stopInternal();
return true; 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) { } catch (RuntimeException e) {
Log.e(TAG, "Internal runtime error.", e); Log.e(TAG, "Internal runtime error.", e);
eventHandler.obtainMessage(MSG_ERROR, new ExoPlaybackException(e, true)).sendToTarget(); eventHandler.obtainMessage(MSG_ERROR, new ExoPlaybackException(e, true)).sendToTarget();
...@@ -256,47 +279,29 @@ import java.util.concurrent.atomic.AtomicInteger; ...@@ -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(); 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); setState(ExoPlayer.STATE_PREPARING);
this.source = sampleSource;
incrementalPrepareInternal(); incrementalPrepareInternal();
} }
private void incrementalPrepareInternal() throws ExoPlaybackException { private void incrementalPrepareInternal() throws ExoPlaybackException, IOException {
long operationStartTimeMs = SystemClock.elapsedRealtime(); long operationStartTimeMs = SystemClock.elapsedRealtime();
boolean prepared = true; if (!source.prepare(positionUs)) {
for (int rendererIndex = 0; rendererIndex < renderers.length; rendererIndex++) { // We're still waiting for the source to be prepared.
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.
scheduleNextOperation(MSG_INCREMENTAL_PREPARE, operationStartTimeMs, PREPARE_INTERVAL_MS); scheduleNextOperation(MSG_INCREMENTAL_PREPARE, operationStartTimeMs, PREPARE_INTERVAL_MS);
return; return;
} }
long durationUs = 0; this.durationUs = source.getDurationUs();
this.bufferedPositionUs = source.getBufferedPositionUs();
boolean allRenderersEnded = true; boolean allRenderersEnded = true;
boolean allRenderersReadyOrEnded = true; boolean allRenderersReadyOrEnded = true;
for (int rendererIndex = 0; rendererIndex < renderers.length; rendererIndex++) { for (int rendererIndex = 0; rendererIndex < renderers.length; rendererIndex++) {
TrackRenderer renderer = renderers[rendererIndex]; TrackRenderer renderer = renderers[rendererIndex];
renderer.prepare(source);
int rendererTrackCount = renderer.getTrackCount(); int rendererTrackCount = renderer.getTrackCount();
MediaFormat[] rendererTrackFormats = new MediaFormat[rendererTrackCount]; MediaFormat[] rendererTrackFormats = new MediaFormat[rendererTrackCount];
for (int trackIndex = 0; trackIndex < rendererTrackCount; trackIndex++) { for (int trackIndex = 0; trackIndex < rendererTrackCount; trackIndex++) {
...@@ -304,36 +309,22 @@ import java.util.concurrent.atomic.AtomicInteger; ...@@ -304,36 +309,22 @@ import java.util.concurrent.atomic.AtomicInteger;
} }
trackFormats[rendererIndex] = rendererTrackFormats; trackFormats[rendererIndex] = rendererTrackFormats;
if (rendererTrackCount > 0) { 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]; int trackIndex = selectedTrackIndices[rendererIndex];
if (0 <= trackIndex && trackIndex < rendererTrackFormats.length) { if (0 <= trackIndex && trackIndex < rendererTrackFormats.length) {
renderer.enable(trackIndex, positionUs, false); renderer.enable(trackIndex, positionUs, false);
enabledRenderers.add(renderer); enabledRenderers.add(renderer);
allRenderersEnded = allRenderersEnded && renderer.isEnded(); allRenderersEnded = allRenderersEnded && renderer.isEnded();
allRenderersReadyOrEnded = allRenderersReadyOrEnded && rendererReadyOrEnded(renderer); allRenderersReadyOrEnded = allRenderersReadyOrEnded && isReadyOrEnded(renderer);
} }
} }
} }
this.durationUs = durationUs;
if (allRenderersEnded if (allRenderersEnded && (durationUs == C.UNKNOWN_TIME_US || durationUs <= positionUs)) {
&& (durationUs == TrackRenderer.UNKNOWN_TIME_US || durationUs <= positionUs)) {
// We don't expect this case, but handle it anyway. // We don't expect this case, but handle it anyway.
state = ExoPlayer.STATE_ENDED; state = ExoPlayer.STATE_ENDED;
} else { } 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 // 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; ...@@ -347,26 +338,17 @@ import java.util.concurrent.atomic.AtomicInteger;
handler.sendEmptyMessage(MSG_DO_SOME_WORK); handler.sendEmptyMessage(MSG_DO_SOME_WORK);
} }
private boolean rendererReadyOrEnded(TrackRenderer renderer) { private boolean isReadyOrEnded(TrackRenderer renderer) {
if (renderer.isEnded()) { return renderer.isReady() || renderer.isEnded();
return true; }
}
if (!renderer.isReady()) { private boolean haveSufficientBuffer() {
return false;
}
if (state == ExoPlayer.STATE_READY) {
return true;
}
long rendererDurationUs = renderer.getDurationUs();
long rendererBufferedPositionUs = renderer.getBufferedPositionUs();
long minBufferDurationUs = rebuffering ? minRebufferUs : minBufferUs; long minBufferDurationUs = rebuffering ? minRebufferUs : minBufferUs;
return minBufferDurationUs <= 0 return minBufferDurationUs <= 0
|| rendererBufferedPositionUs == TrackRenderer.UNKNOWN_TIME_US || bufferedPositionUs == C.UNKNOWN_TIME_US
|| rendererBufferedPositionUs == TrackRenderer.END_OF_TRACK_US || bufferedPositionUs == C.END_OF_SOURCE_US
|| rendererBufferedPositionUs >= positionUs + minBufferDurationUs || bufferedPositionUs >= positionUs + minBufferDurationUs
|| (rendererDurationUs != TrackRenderer.UNKNOWN_TIME_US || (durationUs != C.UNKNOWN_TIME_US && bufferedPositionUs >= durationUs);
&& rendererDurationUs != TrackRenderer.MATCH_LONGEST_US
&& rendererBufferedPositionUs >= rendererDurationUs);
} }
private void setPlayWhenReadyInternal(boolean playWhenReady) throws ExoPlaybackException { private void setPlayWhenReadyInternal(boolean playWhenReady) throws ExoPlaybackException {
...@@ -415,14 +397,15 @@ import java.util.concurrent.atomic.AtomicInteger; ...@@ -415,14 +397,15 @@ import java.util.concurrent.atomic.AtomicInteger;
elapsedRealtimeUs = SystemClock.elapsedRealtime() * 1000; elapsedRealtimeUs = SystemClock.elapsedRealtime() * 1000;
} }
private void doSomeWork() throws ExoPlaybackException { private void doSomeWork() throws ExoPlaybackException, IOException {
TraceUtil.beginSection("doSomeWork"); TraceUtil.beginSection("doSomeWork");
long operationStartTimeMs = SystemClock.elapsedRealtime(); long operationStartTimeMs = SystemClock.elapsedRealtime();
long bufferedPositionUs = durationUs != TrackRenderer.UNKNOWN_TIME_US ? durationUs updatePositionUs();
: Long.MAX_VALUE; bufferedPositionUs = source.getBufferedPositionUs();
source.continueBuffering(positionUs);
boolean allRenderersEnded = true; boolean allRenderersEnded = true;
boolean allRenderersReadyOrEnded = true; boolean allRenderersReadyOrEnded = true;
updatePositionUs();
for (int i = 0; i < enabledRenderers.size(); i++) { for (int i = 0; i < enabledRenderers.size(); i++) {
TrackRenderer renderer = enabledRenderers.get(i); TrackRenderer renderer = enabledRenderers.get(i);
// TODO: Each renderer should return the maximum delay before which it wishes to be // TODO: Each renderer should return the maximum delay before which it wishes to be
...@@ -430,40 +413,20 @@ import java.util.concurrent.atomic.AtomicInteger; ...@@ -430,40 +413,20 @@ import java.util.concurrent.atomic.AtomicInteger;
// invocation of this method. // invocation of this method.
renderer.doSomeWork(positionUs, elapsedRealtimeUs); renderer.doSomeWork(positionUs, elapsedRealtimeUs);
allRenderersEnded = allRenderersEnded && renderer.isEnded(); allRenderersEnded = allRenderersEnded && renderer.isEnded();
// Determine whether the renderer is ready (or ended). If it's not, throw an error that's // 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. // preventing the renderer from making progress, if such an error exists.
boolean rendererReadyOrEnded = rendererReadyOrEnded(renderer); boolean rendererReadyOrEnded = isReadyOrEnded(renderer);
if (!rendererReadyOrEnded) { if (!rendererReadyOrEnded) {
renderer.maybeThrowError(); renderer.maybeThrowError();
} }
allRenderersReadyOrEnded = allRenderersReadyOrEnded && rendererReadyOrEnded; 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 if (allRenderersEnded && (durationUs == C.UNKNOWN_TIME_US || durationUs <= positionUs)) {
&& (durationUs == TrackRenderer.UNKNOWN_TIME_US || durationUs <= positionUs)) {
setState(ExoPlayer.STATE_ENDED); setState(ExoPlayer.STATE_ENDED);
stopRenderers(); stopRenderers();
} else if (state == ExoPlayer.STATE_BUFFERING && allRenderersReadyOrEnded) { } else if (state == ExoPlayer.STATE_BUFFERING && allRenderersReadyOrEnded
&& haveSufficientBuffer()) {
setState(ExoPlayer.STATE_READY); setState(ExoPlayer.STATE_READY);
if (playWhenReady) { if (playWhenReady) {
startRenderers(); startRenderers();
...@@ -512,9 +475,9 @@ import java.util.concurrent.atomic.AtomicInteger; ...@@ -512,9 +475,9 @@ import java.util.concurrent.atomic.AtomicInteger;
for (int i = 0; i < enabledRenderers.size(); i++) { for (int i = 0; i < enabledRenderers.size(); i++) {
TrackRenderer renderer = enabledRenderers.get(i); TrackRenderer renderer = enabledRenderers.get(i);
ensureStopped(renderer); ensureStopped(renderer);
renderer.seekTo(positionUs);
} }
setState(ExoPlayer.STATE_BUFFERING); setState(ExoPlayer.STATE_BUFFERING);
source.seekToUs(positionUs);
handler.sendEmptyMessage(MSG_DO_SOME_WORK); handler.sendEmptyMessage(MSG_DO_SOME_WORK);
} finally { } finally {
pendingSeekCount.decrementAndGet(); pendingSeekCount.decrementAndGet();
...@@ -544,21 +507,20 @@ import java.util.concurrent.atomic.AtomicInteger; ...@@ -544,21 +507,20 @@ import java.util.concurrent.atomic.AtomicInteger;
return; return;
} }
for (int i = 0; i < renderers.length; i++) { for (int i = 0; i < renderers.length; i++) {
TrackRenderer renderer = renderers[i]; unprepare(renderers[i]);
stopAndDisable(renderer);
release(renderer);
} }
renderers = null;
rendererMediaClock = null;
rendererMediaClockSource = null;
enabledRenderers.clear(); enabledRenderers.clear();
source = null;
} }
private void stopAndDisable(TrackRenderer renderer) { private void unprepare(TrackRenderer renderer) {
try { try {
ensureStopped(renderer); ensureStopped(renderer);
if (renderer.getState() == TrackRenderer.STATE_ENABLED) { if (renderer.getState() == TrackRenderer.STATE_ENABLED) {
renderer.disable(); renderer.disable();
renderer.unprepare();
} else if (renderer.getState() == TrackRenderer.STATE_PREPARED) {
renderer.unprepare();
} }
} catch (ExoPlaybackException e) { } catch (ExoPlaybackException e) {
// There's nothing we can do. // There's nothing we can do.
...@@ -569,18 +531,6 @@ import java.util.concurrent.atomic.AtomicInteger; ...@@ -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 <T> void sendMessageInternal(int what, Object obj) private <T> void sendMessageInternal(int what, Object obj)
throws ExoPlaybackException { throws ExoPlaybackException {
try { try {
...@@ -612,9 +562,7 @@ import java.util.concurrent.atomic.AtomicInteger; ...@@ -612,9 +562,7 @@ import java.util.concurrent.atomic.AtomicInteger;
TrackRenderer renderer = renderers[rendererIndex]; TrackRenderer renderer = renderers[rendererIndex];
int rendererState = renderer.getState(); int rendererState = renderer.getState();
if (rendererState == TrackRenderer.STATE_UNPREPARED if (rendererState == TrackRenderer.STATE_UNPREPARED || renderer.getTrackCount() == 0) {
|| rendererState == TrackRenderer.STATE_RELEASED
|| renderer.getTrackCount() == 0) {
return; return;
} }
......
...@@ -15,7 +15,6 @@ ...@@ -15,7 +15,6 @@
*/ */
package com.google.android.exoplayer; 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;
import com.google.android.exoplayer.drm.DrmInitData.SchemeInitData; import com.google.android.exoplayer.drm.DrmInitData.SchemeInitData;
import com.google.android.exoplayer.extractor.ExtractorSampleSource; import com.google.android.exoplayer.extractor.ExtractorSampleSource;
...@@ -54,7 +53,7 @@ import java.util.UUID; ...@@ -54,7 +53,7 @@ import java.util.UUID;
// through use of a background thread, or through changes to the framework's MediaExtractor API). // through use of a background thread, or through changes to the framework's MediaExtractor API).
@Deprecated @Deprecated
@TargetApi(16) @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; 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 ...@@ -72,13 +71,13 @@ public final class FrameworkSampleSource implements SampleSource, SampleSourceRe
private final long fileDescriptorOffset; private final long fileDescriptorOffset;
private final long fileDescriptorLength; private final long fileDescriptorLength;
private IOException preparationError;
private MediaExtractor extractor; private MediaExtractor extractor;
private MediaFormat[] trackFormats; private MediaFormat[] trackFormats;
private boolean prepared; private boolean prepared;
private int remainingReleaseCount; private long durationUs;
private int enabledTrackCount;
private int[] trackStates; private int[] trackStates;
private boolean[] pendingDiscontinuities; private boolean[] pendingResets;
private long lastSeekPositionUs; private long lastSeekPositionUs;
private long pendingSeekPositionUs; private long pendingSeekPositionUs;
...@@ -120,39 +119,39 @@ public final class FrameworkSampleSource implements SampleSource, SampleSourceRe ...@@ -120,39 +119,39 @@ public final class FrameworkSampleSource implements SampleSource, SampleSourceRe
} }
@Override @Override
public SampleSourceReader register() { public boolean prepare(long positionUs) throws IOException {
remainingReleaseCount++; if (prepared) {
return this; 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 @Override
public boolean prepare(long positionUs) { public boolean isPrepared() {
if (!prepared) { return prepared;
if (preparationError != null) { }
return false;
}
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()]; @Override
pendingDiscontinuities = new boolean[trackStates.length]; public long getDurationUs() {
trackFormats = new MediaFormat[trackStates.length]; return C.UNKNOWN_TIME_US;
for (int i = 0; i < trackStates.length; i++) {
trackFormats[i] = createMediaFormat(extractor.getTrackFormat(i));
}
prepared = true;
}
return true;
} }
@Override @Override
...@@ -168,45 +167,40 @@ public final class FrameworkSampleSource implements SampleSource, SampleSourceRe ...@@ -168,45 +167,40 @@ public final class FrameworkSampleSource implements SampleSource, SampleSourceRe
} }
@Override @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(prepared);
Assertions.checkState(trackStates[track] == TRACK_STATE_DISABLED); Assertions.checkState(trackStates[track] == TRACK_STATE_DISABLED);
enabledTrackCount++;
trackStates[track] = TRACK_STATE_ENABLED; trackStates[track] = TRACK_STATE_ENABLED;
extractor.selectTrack(track); extractor.selectTrack(track);
seekToUsInternal(positionUs, positionUs != 0); seekToUsInternal(positionUs, positionUs != 0);
return new TrackStreamImpl(track);
} }
@Override /* package */ long readReset(int track) {
public boolean continueBuffering(int track, long positionUs) { if (pendingResets[track]) {
// MediaExtractor takes care of buffering and blocks until it has samples, so we can always pendingResets[track] = false;
// 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;
return lastSeekPositionUs; return lastSeekPositionUs;
} }
return NO_DISCONTINUITY; return TrackStream.NO_RESET;
} }
@Override /* package */ int readData(int track, MediaFormatHolder formatHolder, SampleHolder sampleHolder) {
public int readData(int track, long positionUs, MediaFormatHolder formatHolder,
SampleHolder sampleHolder) {
Assertions.checkState(prepared); Assertions.checkState(prepared);
Assertions.checkState(trackStates[track] != TRACK_STATE_DISABLED); Assertions.checkState(trackStates[track] != TRACK_STATE_DISABLED);
if (pendingDiscontinuities[track]) { if (pendingResets[track]) {
return NOTHING_READ; return TrackStream.NOTHING_READ;
} }
if (trackStates[track] != TRACK_STATE_FORMAT_SENT) { if (trackStates[track] != TRACK_STATE_FORMAT_SENT) {
formatHolder.format = trackFormats[track]; formatHolder.format = trackFormats[track];
formatHolder.drmInitData = Util.SDK_INT >= 18 ? getDrmInitDataV18() : null; formatHolder.drmInitData = Util.SDK_INT >= 18 ? getDrmInitDataV18() : null;
trackStates[track] = TRACK_STATE_FORMAT_SENT; trackStates[track] = TRACK_STATE_FORMAT_SENT;
return FORMAT_READ; return TrackStream.FORMAT_READ;
} }
int extractorTrackIndex = extractor.getSampleTrackIndex(); int extractorTrackIndex = extractor.getSampleTrackIndex();
if (extractorTrackIndex == track) { if (extractorTrackIndex == track) {
...@@ -224,53 +218,53 @@ public final class FrameworkSampleSource implements SampleSource, SampleSourceRe ...@@ -224,53 +218,53 @@ public final class FrameworkSampleSource implements SampleSource, SampleSourceRe
} }
pendingSeekPositionUs = C.UNKNOWN_TIME_US; pendingSeekPositionUs = C.UNKNOWN_TIME_US;
extractor.advance(); extractor.advance();
return SAMPLE_READ; return TrackStream.SAMPLE_READ;
} else { } else {
return extractorTrackIndex < 0 ? END_OF_STREAM : NOTHING_READ; return extractorTrackIndex < 0 ? TrackStream.END_OF_STREAM : TrackStream.NOTHING_READ;
} }
} }
@Override /* package */ void disable(int track) {
public void disable(int track) {
Assertions.checkState(prepared); Assertions.checkState(prepared);
Assertions.checkState(trackStates[track] != TRACK_STATE_DISABLED); Assertions.checkState(trackStates[track] != TRACK_STATE_DISABLED);
extractor.unselectTrack(track); extractor.unselectTrack(track);
pendingDiscontinuities[track] = false;
trackStates[track] = TRACK_STATE_DISABLED; trackStates[track] = TRACK_STATE_DISABLED;
} pendingResets[track] = false;
enabledTrackCount--;
@Override
public void maybeThrowError() throws IOException {
if (preparationError != null) {
throw preparationError;
}
} }
@Override @Override
public void seekToUs(long positionUs) { public void seekToUs(long positionUs) {
Assertions.checkState(prepared); Assertions.checkState(prepared);
if (enabledTrackCount == 0) {
return;
}
seekToUsInternal(positionUs, false); seekToUsInternal(positionUs, false);
} }
@Override @Override
public long getBufferedPositionUs() { public long getBufferedPositionUs() {
Assertions.checkState(prepared); Assertions.checkState(prepared);
if (enabledTrackCount == 0) {
return C.END_OF_SOURCE_US;
}
long bufferedDurationUs = extractor.getCachedDuration(); long bufferedDurationUs = extractor.getCachedDuration();
if (bufferedDurationUs == -1) { if (bufferedDurationUs == -1) {
return TrackRenderer.UNKNOWN_TIME_US; return C.UNKNOWN_TIME_US;
} else {
long sampleTime = extractor.getSampleTime();
return sampleTime == -1 ? TrackRenderer.END_OF_TRACK_US : sampleTime + bufferedDurationUs;
} }
long sampleTime = extractor.getSampleTime();
return sampleTime == -1 ? C.END_OF_SOURCE_US : sampleTime + bufferedDurationUs;
} }
@Override @Override
public void release() { public void release() {
Assertions.checkState(remainingReleaseCount > 0); if (extractor != null) {
if (--remainingReleaseCount == 0 && extractor != null) {
extractor.release(); extractor.release();
extractor = null; extractor = null;
} }
prepared = false;
} }
@TargetApi(18) @TargetApi(18)
...@@ -297,7 +291,7 @@ public final class FrameworkSampleSource implements SampleSource, SampleSourceRe ...@@ -297,7 +291,7 @@ public final class FrameworkSampleSource implements SampleSource, SampleSourceRe
extractor.seekTo(positionUs, MediaExtractor.SEEK_TO_PREVIOUS_SYNC); extractor.seekTo(positionUs, MediaExtractor.SEEK_TO_PREVIOUS_SYNC);
for (int i = 0; i < trackStates.length; ++i) { for (int i = 0; i < trackStates.length; ++i) {
if (trackStates[i] != TRACK_STATE_DISABLED) { if (trackStates[i] != TRACK_STATE_DISABLED) {
pendingDiscontinuities[i] = true; pendingResets[i] = true;
} }
} }
} }
...@@ -341,4 +335,43 @@ public final class FrameworkSampleSource implements SampleSource, SampleSourceRe ...@@ -341,4 +335,43 @@ public final class FrameworkSampleSource implements SampleSource, SampleSourceRe
return format.containsKey(key) ? format.getInteger(key) : MediaFormat.NO_VALUE; 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);
}
}
} }
...@@ -99,15 +99,13 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer implem ...@@ -99,15 +99,13 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer implem
private long lastFeedElapsedRealtimeMs; private long lastFeedElapsedRealtimeMs;
/** /**
* @param source The upstream source from which the renderer obtains samples.
* @param mediaCodecSelector A decoder selector. * @param mediaCodecSelector A decoder selector.
*/ */
public MediaCodecAudioTrackRenderer(SampleSource source, MediaCodecSelector mediaCodecSelector) { public MediaCodecAudioTrackRenderer(MediaCodecSelector mediaCodecSelector) {
this(source, mediaCodecSelector, null, true); this(mediaCodecSelector, null, true);
} }
/** /**
* @param source The upstream source from which the renderer obtains samples.
* @param mediaCodecSelector A decoder selector. * @param mediaCodecSelector A decoder selector.
* @param drmSessionManager For use with encrypted content. May be null if support for encrypted * @param drmSessionManager For use with encrypted content. May be null if support for encrypted
* content is not required. * content is not required.
...@@ -117,25 +115,23 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer implem ...@@ -117,25 +115,23 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer implem
* permitted to play clear regions of encrypted media files before {@code drmSessionManager} * permitted to play clear regions of encrypted media files before {@code drmSessionManager}
* has obtained the keys necessary to decrypt encrypted regions of the media. * 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) { 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 mediaCodecSelector A decoder selector.
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
* null if delivery of events is not required. * 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 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, Handler eventHandler,
Handler eventHandler, EventListener eventListener) { EventListener eventListener) {
this(source, mediaCodecSelector, null, true, eventHandler, eventListener); this(mediaCodecSelector, null, true, eventHandler, eventListener);
} }
/** /**
* @param source The upstream source from which the renderer obtains samples.
* @param mediaCodecSelector A decoder selector. * @param mediaCodecSelector A decoder selector.
* @param drmSessionManager For use with encrypted content. May be null if support for encrypted * @param drmSessionManager For use with encrypted content. May be null if support for encrypted
* content is not required. * content is not required.
...@@ -148,15 +144,14 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer implem ...@@ -148,15 +144,14 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer implem
* null if delivery of events is not required. * 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 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, DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys,
Handler eventHandler, EventListener eventListener) { Handler eventHandler, EventListener eventListener) {
this(source, mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, eventHandler, this(mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, eventHandler,
eventListener, null, AudioManager.STREAM_MUSIC); eventListener, null, AudioManager.STREAM_MUSIC);
} }
/** /**
* @param source The upstream source from which the renderer obtains samples.
* @param mediaCodecSelector A decoder selector. * @param mediaCodecSelector A decoder selector.
* @param drmSessionManager For use with encrypted content. May be null if support for encrypted * @param drmSessionManager For use with encrypted content. May be null if support for encrypted
* content is not required. * content is not required.
...@@ -172,11 +167,11 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer implem ...@@ -172,11 +167,11 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer implem
* default capabilities (no encoded audio passthrough support) should be assumed. * default capabilities (no encoded audio passthrough support) should be assumed.
* @param streamType The type of audio stream for the {@link AudioTrack}. * @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, DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys,
Handler eventHandler, EventListener eventListener, AudioCapabilities audioCapabilities, Handler eventHandler, EventListener eventListener, AudioCapabilities audioCapabilities,
int streamType) { int streamType) {
super(source, mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, eventHandler, super(mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, eventHandler,
eventListener); eventListener);
this.eventListener = eventListener; this.eventListener = eventListener;
this.audioSessionId = AudioTrack.SESSION_ID_NOT_SET; this.audioSessionId = AudioTrack.SESSION_ID_NOT_SET;
...@@ -305,8 +300,8 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer implem ...@@ -305,8 +300,8 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer implem
} }
@Override @Override
protected void onDiscontinuity(long positionUs) throws ExoPlaybackException { protected void onReset(long positionUs) throws ExoPlaybackException {
super.onDiscontinuity(positionUs); super.onReset(positionUs);
audioTrack.reset(); audioTrack.reset();
currentPositionUs = positionUs; currentPositionUs = positionUs;
allowPositionDiscontinuity = true; allowPositionDiscontinuity = true;
......
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
package com.google.android.exoplayer; package com.google.android.exoplayer;
import com.google.android.exoplayer.MediaCodecUtil.DecoderQueryException; 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.DrmInitData;
import com.google.android.exoplayer.drm.DrmSessionManager; import com.google.android.exoplayer.drm.DrmSessionManager;
import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.Assertions;
...@@ -230,7 +231,6 @@ public abstract class MediaCodecTrackRenderer extends SampleSourceTrackRenderer ...@@ -230,7 +231,6 @@ public abstract class MediaCodecTrackRenderer extends SampleSourceTrackRenderer
private boolean waitingForFirstSyncFrame; private boolean waitingForFirstSyncFrame;
/** /**
* @param source The upstream source from which the renderer obtains samples.
* @param mediaCodecSelector A decoder selector. * @param mediaCodecSelector A decoder selector.
* @param drmSessionManager For use with encrypted media. May be null if support for encrypted * @param drmSessionManager For use with encrypted media. May be null if support for encrypted
* media is not required. * media is not required.
...@@ -243,10 +243,9 @@ public abstract class MediaCodecTrackRenderer extends SampleSourceTrackRenderer ...@@ -243,10 +243,9 @@ public abstract class MediaCodecTrackRenderer extends SampleSourceTrackRenderer
* null if delivery of events is not required. * 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 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, DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys,
Handler eventHandler, EventListener eventListener) { Handler eventHandler, EventListener eventListener) {
super(source);
Assertions.checkState(Util.SDK_INT >= 16); Assertions.checkState(Util.SDK_INT >= 16);
this.mediaCodecSelector = Assertions.checkNotNull(mediaCodecSelector); this.mediaCodecSelector = Assertions.checkNotNull(mediaCodecSelector);
this.drmSessionManager = drmSessionManager; this.drmSessionManager = drmSessionManager;
...@@ -451,7 +450,7 @@ public abstract class MediaCodecTrackRenderer extends SampleSourceTrackRenderer ...@@ -451,7 +450,7 @@ public abstract class MediaCodecTrackRenderer extends SampleSourceTrackRenderer
} }
@Override @Override
protected void onDiscontinuity(long positionUs) throws ExoPlaybackException { protected void onReset(long positionUs) throws ExoPlaybackException {
sourceState = SOURCE_STATE_NOT_READY; sourceState = SOURCE_STATE_NOT_READY;
inputStreamEnded = false; inputStreamEnded = false;
outputStreamEnded = false; outputStreamEnded = false;
...@@ -477,7 +476,7 @@ public abstract class MediaCodecTrackRenderer extends SampleSourceTrackRenderer ...@@ -477,7 +476,7 @@ public abstract class MediaCodecTrackRenderer extends SampleSourceTrackRenderer
? (sourceState == SOURCE_STATE_NOT_READY ? SOURCE_STATE_READY : sourceState) ? (sourceState == SOURCE_STATE_NOT_READY ? SOURCE_STATE_READY : sourceState)
: SOURCE_STATE_NOT_READY; : SOURCE_STATE_NOT_READY;
if (format == null) { if (format == null) {
readFormat(positionUs); readFormat();
} }
maybeInitCodec(); maybeInitCodec();
if (codec != null) { if (codec != null) {
...@@ -491,9 +490,9 @@ public abstract class MediaCodecTrackRenderer extends SampleSourceTrackRenderer ...@@ -491,9 +490,9 @@ public abstract class MediaCodecTrackRenderer extends SampleSourceTrackRenderer
codecCounters.ensureUpdated(); codecCounters.ensureUpdated();
} }
private void readFormat(long positionUs) throws ExoPlaybackException { private void readFormat() throws ExoPlaybackException {
int result = readSource(positionUs, formatHolder, null); int result = readSource(formatHolder, null);
if (result == SampleSource.FORMAT_READ) { if (result == TrackStream.FORMAT_READ) {
onInputFormatChanged(formatHolder); onInputFormatChanged(formatHolder);
} }
} }
...@@ -568,7 +567,7 @@ public abstract class MediaCodecTrackRenderer extends SampleSourceTrackRenderer ...@@ -568,7 +567,7 @@ public abstract class MediaCodecTrackRenderer extends SampleSourceTrackRenderer
int result; int result;
if (waitingForKeys) { if (waitingForKeys) {
// We've already read an encrypted sample into sampleHolder, and are waiting for keys. // We've already read an encrypted sample into sampleHolder, and are waiting for keys.
result = SampleSource.SAMPLE_READ; result = TrackStream.SAMPLE_READ;
} else { } else {
// For adaptive reconfiguration OMX decoders expect all reconfiguration data to be supplied // 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. // 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 ...@@ -579,16 +578,16 @@ public abstract class MediaCodecTrackRenderer extends SampleSourceTrackRenderer
} }
codecReconfigurationState = RECONFIGURATION_STATE_QUEUE_PENDING; codecReconfigurationState = RECONFIGURATION_STATE_QUEUE_PENDING;
} }
result = readSource(positionUs, formatHolder, sampleHolder); result = readSource(formatHolder, sampleHolder);
if (firstFeed && sourceState == SOURCE_STATE_READY && result == SampleSource.NOTHING_READ) { if (firstFeed && sourceState == SOURCE_STATE_READY && result == TrackStream.NOTHING_READ) {
sourceState = SOURCE_STATE_READY_READ_MAY_FAIL; sourceState = SOURCE_STATE_READY_READ_MAY_FAIL;
} }
} }
if (result == SampleSource.NOTHING_READ) { if (result == TrackStream.NOTHING_READ) {
return false; return false;
} }
if (result == SampleSource.FORMAT_READ) { if (result == TrackStream.FORMAT_READ) {
if (codecReconfigurationState == RECONFIGURATION_STATE_QUEUE_PENDING) { if (codecReconfigurationState == RECONFIGURATION_STATE_QUEUE_PENDING) {
// We received two formats in a row. Clear the current buffer of any reconfiguration data // We received two formats in a row. Clear the current buffer of any reconfiguration data
// associated with the first format. // associated with the first format.
...@@ -598,7 +597,7 @@ public abstract class MediaCodecTrackRenderer extends SampleSourceTrackRenderer ...@@ -598,7 +597,7 @@ public abstract class MediaCodecTrackRenderer extends SampleSourceTrackRenderer
onInputFormatChanged(formatHolder); onInputFormatChanged(formatHolder);
return true; return true;
} }
if (result == SampleSource.END_OF_STREAM) { if (result == TrackStream.END_OF_STREAM) {
if (codecReconfigurationState == RECONFIGURATION_STATE_QUEUE_PENDING) { if (codecReconfigurationState == RECONFIGURATION_STATE_QUEUE_PENDING) {
// We received a new format immediately before the end of the stream. We need to clear // 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 // the corresponding reconfiguration data from the current buffer, but re-write it into
......
...@@ -128,34 +128,30 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer { ...@@ -128,34 +128,30 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer {
/** /**
* @param context A context. * @param context A context.
* @param source The upstream source from which the renderer obtains samples.
* @param mediaCodecSelector A decoder selector. * @param mediaCodecSelector A decoder selector.
* @param videoScalingMode The scaling mode to pass to * @param videoScalingMode The scaling mode to pass to
* {@link MediaCodec#setVideoScalingMode(int)}. * {@link MediaCodec#setVideoScalingMode(int)}.
*/ */
public MediaCodecVideoTrackRenderer(Context context, SampleSource source, public MediaCodecVideoTrackRenderer(Context context, MediaCodecSelector mediaCodecSelector,
MediaCodecSelector mediaCodecSelector, int videoScalingMode) { int videoScalingMode) {
this(context, source, mediaCodecSelector, videoScalingMode, 0); this(context, mediaCodecSelector, videoScalingMode, 0);
} }
/** /**
* @param context A context. * @param context A context.
* @param source The upstream source from which the renderer obtains samples.
* @param mediaCodecSelector A decoder selector. * @param mediaCodecSelector A decoder selector.
* @param videoScalingMode The scaling mode to pass to * @param videoScalingMode The scaling mode to pass to
* {@link MediaCodec#setVideoScalingMode(int)}. * {@link MediaCodec#setVideoScalingMode(int)}.
* @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer * @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer
* can attempt to seamlessly join an ongoing playback. * can attempt to seamlessly join an ongoing playback.
*/ */
public MediaCodecVideoTrackRenderer(Context context, SampleSource source, public MediaCodecVideoTrackRenderer(Context context, MediaCodecSelector mediaCodecSelector,
MediaCodecSelector mediaCodecSelector, int videoScalingMode, long allowedJoiningTimeMs) { int videoScalingMode, long allowedJoiningTimeMs) {
this(context, source, mediaCodecSelector, videoScalingMode, allowedJoiningTimeMs, null, null, this(context, mediaCodecSelector, videoScalingMode, allowedJoiningTimeMs, null, null, -1);
-1);
} }
/** /**
* @param context A context. * @param context A context.
* @param source The upstream source from which the renderer obtains samples.
* @param mediaCodecSelector A decoder selector. * @param mediaCodecSelector A decoder selector.
* @param videoScalingMode The scaling mode to pass to * @param videoScalingMode The scaling mode to pass to
* {@link MediaCodec#setVideoScalingMode(int)}. * {@link MediaCodec#setVideoScalingMode(int)}.
...@@ -167,16 +163,15 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer { ...@@ -167,16 +163,15 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer {
* @param maxDroppedFrameCountToNotify The maximum number of frames that can be dropped between * @param maxDroppedFrameCountToNotify The maximum number of frames that can be dropped between
* invocations of {@link EventListener#onDroppedFrames(int, long)}. * invocations of {@link EventListener#onDroppedFrames(int, long)}.
*/ */
public MediaCodecVideoTrackRenderer(Context context, SampleSource source, public MediaCodecVideoTrackRenderer(Context context, MediaCodecSelector mediaCodecSelector,
MediaCodecSelector mediaCodecSelector, int videoScalingMode, long allowedJoiningTimeMs, int videoScalingMode, long allowedJoiningTimeMs, Handler eventHandler,
Handler eventHandler, EventListener eventListener, int maxDroppedFrameCountToNotify) { EventListener eventListener, int maxDroppedFrameCountToNotify) {
this(context, source, mediaCodecSelector, videoScalingMode, allowedJoiningTimeMs, null, false, this(context, mediaCodecSelector, videoScalingMode, allowedJoiningTimeMs, null, false,
eventHandler, eventListener, maxDroppedFrameCountToNotify); eventHandler, eventListener, maxDroppedFrameCountToNotify);
} }
/** /**
* @param context A context. * @param context A context.
* @param source The upstream source from which the renderer obtains samples.
* @param mediaCodecSelector A decoder selector. * @param mediaCodecSelector A decoder selector.
* @param videoScalingMode The scaling mode to pass to * @param videoScalingMode The scaling mode to pass to
* {@link MediaCodec#setVideoScalingMode(int)}. * {@link MediaCodec#setVideoScalingMode(int)}.
...@@ -195,11 +190,11 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer { ...@@ -195,11 +190,11 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer {
* @param maxDroppedFrameCountToNotify The maximum number of frames that can be dropped between * @param maxDroppedFrameCountToNotify The maximum number of frames that can be dropped between
* invocations of {@link EventListener#onDroppedFrames(int, long)}. * invocations of {@link EventListener#onDroppedFrames(int, long)}.
*/ */
public MediaCodecVideoTrackRenderer(Context context, SampleSource source, public MediaCodecVideoTrackRenderer(Context context, MediaCodecSelector mediaCodecSelector,
MediaCodecSelector mediaCodecSelector, int videoScalingMode, long allowedJoiningTimeMs, int videoScalingMode, long allowedJoiningTimeMs, DrmSessionManager drmSessionManager,
DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys, boolean playClearSamplesWithoutKeys, Handler eventHandler, EventListener eventListener,
Handler eventHandler, EventListener eventListener, int maxDroppedFrameCountToNotify) { int maxDroppedFrameCountToNotify) {
super(source, mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, eventHandler, super(mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, eventHandler,
eventListener); eventListener);
this.frameReleaseTimeHelper = new VideoFrameReleaseTimeHelper(context); this.frameReleaseTimeHelper = new VideoFrameReleaseTimeHelper(context);
this.videoScalingMode = videoScalingMode; this.videoScalingMode = videoScalingMode;
...@@ -235,8 +230,8 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer { ...@@ -235,8 +230,8 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer {
} }
@Override @Override
protected void onDiscontinuity(long positionUs) throws ExoPlaybackException { protected void onReset(long positionUs) throws ExoPlaybackException {
super.onDiscontinuity(positionUs); super.onReset(positionUs);
renderedFirstFrame = false; renderedFirstFrame = false;
consecutiveDroppedFrameCount = 0; consecutiveDroppedFrameCount = 0;
joiningDeadlineUs = -1; joiningDeadlineUs = -1;
......
...@@ -58,9 +58,7 @@ public final class MediaFormat { ...@@ -58,9 +58,7 @@ public final class MediaFormat {
*/ */
public final int maxInputSize; public final int maxInputSize;
/** /**
* The duration in microseconds, or {@link C#UNKNOWN_TIME_US} if the duration is unknown, or * The duration in microseconds, or {@link C#UNKNOWN_TIME_US} if the duration is unknown.
* {@link C#MATCH_LONGEST_US} if the duration should match the duration of the longest track whose
* duration is known.
*/ */
public final long durationUs; public final long durationUs;
/** /**
......
/*
* 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;
}
}
...@@ -18,196 +18,189 @@ package com.google.android.exoplayer; ...@@ -18,196 +18,189 @@ package com.google.android.exoplayer;
import java.io.IOException; import java.io.IOException;
/** /**
* A source of media samples. * A source of media.
* <p> * <p>
* A {@link SampleSource} may expose one or multiple tracks. The number of tracks and each track's * 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 * media format can be queried using {@link #getTrackCount()} and {@link #getFormat(int)}
* {@link SampleSourceReader#getFormat(int)} respectively. * respectively.
*/ */
public interface SampleSource { public interface SampleSource {
/** /**
* The end of stream has been reached. * Prepares the source.
* <p>
* 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.
*
* @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 static final int END_OF_STREAM = -1; boolean prepare(long positionUs) throws IOException;
/** /**
* Neither a sample nor a format was read in full. This may be because insufficient data is * Returns whether the source is prepared.
* 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 * @return True if the source is prepared. False otherwise.
* track than the one for which data was requested.
*/ */
public static final int NOTHING_READ = -2; boolean isPrepared();
/** /**
* A sample was read. * Returns the duration of the source.
* <p>
* 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.
*/ */
public static final int SAMPLE_READ = -3; long getDurationUs();
/** /**
* A format was read. * Returns the number of tracks exposed by the source.
* <p>
* This method should only be called after the source has been prepared.
*
* @return The number of tracks.
*/ */
public static final int FORMAT_READ = -4; int getTrackCount();
/** /**
* Returned from {@link SampleSourceReader#readDiscontinuity(int)} to indicate no discontinuity. * Returns the format of the specified track.
* <p>
* 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)}.
* <p>
* 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 static final long NO_DISCONTINUITY = Long.MIN_VALUE; MediaFormat getFormat(int track);
/** /**
* A consumer of samples should call this method to register themselves and gain access to the * Indicates to the source that it should continue buffering data for its enabled tracks.
* source through the returned {@link SampleSourceReader}.
* <p> * <p>
* {@link SampleSourceReader#release()} should be called on the returned object when access is no * This method should only be called after the source has been prepared.
* longer required.
* *
* @return A {@link SampleSourceReader} that provides access to the source. * @param positionUs The current playback position.
*/ */
public SampleSourceReader register(); void continueBuffering(long positionUs);
/** /**
* An interface providing read access to a {@link SampleSource}. * Returns an estimate of the position up to which data is buffered for the enabled tracks.
* <p>
* 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.
*/ */
public interface SampleSourceReader { long getBufferedPositionUs();
/** /**
* If the source is currently having difficulty preparing or loading samples, then this method * Seeks to the specified time in microseconds.
* throws the underlying error. Otherwise does nothing. * <p>
* * This method should only be called after the source has been prepared.
* @throws IOException The underlying error. *
*/ * @param positionUs The seek position in microseconds.
public void maybeThrowError() throws IOException; */
void seekToUs(long positionUs);
/** /**
* Prepares the source. * Enables the specified track. Returning a {@link TrackStream} from which the track's data can
* <p> * be read.
* Preparation may require reading from the data source (e.g. to determine the available tracks * <p>
* and formats). If insufficient data is available then the call will return {@code false} * This method should only be called after the source has been prepared, and when the specified
* rather than block. The method can be called repeatedly until the return value indicates * track is disabled.
* success. *
* * @param track The track to enable.
* @param positionUs The player's current playback position. * @param positionUs The current playback position in microseconds.
* @return True if the source was prepared, false otherwise. * @return A {@link TrackStream} from which the enabled track's data can be read.
*/ */
public boolean prepare(long positionUs); TrackStream enable(int track, long positionUs);
/**
* Releases the source.
* <p>
* 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 {
/** /**
* Returns the number of tracks exposed by the source. * The end of stream has been reached.
* <p>
* This method should only be called after the source has been prepared.
*
* @return The number of tracks.
*/ */
public int getTrackCount(); static final int END_OF_STREAM = -1;
/** /**
* Returns the format of the specified track. * Nothing was read.
* <p>
* 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)}.
* <p>
* 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); static final int NOTHING_READ = -2;
/** /**
* Enable the specified track. This allows the track's format and samples to be read from * A sample was read.
* {@link #readData(int, long, MediaFormatHolder, SampleHolder)}.
* <p>
* 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); static final int SAMPLE_READ = -3;
/** /**
* Indicates to the source that it should still be buffering data for the specified track. * A format was read.
* <p>
* 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); static final int FORMAT_READ = -4;
/** /**
* Attempts to read a pending discontinuity from the source. * Returned from {@link #readReset()} to indicate no reset is required.
* <p>
* 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); static final long NO_RESET = Long.MIN_VALUE;
/** /**
* Attempts to read a sample or a new format from the source. * Returns whether data is available to be read.
* <p>
* This method should only be called when the specified track is enabled.
* <p>
* 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.
* <p> * <p>
* This method will always return {@link #NOTHING_READ} in the case that there's a pending * Note: If the stream has ended then {@link #END_OF_STREAM} can always be read from
* discontinuity to be read from {@link #readDiscontinuity(int)} for the specified track. * {@link #readData(MediaFormatHolder, SampleHolder)}. Hence an ended stream is always ready.
* *
* @param track The track from which to read. * @return True if data is available to be read. False otherwise.
* @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 #SAMPLE_READ}, {@link #FORMAT_READ},
* {@link #NOTHING_READ} or {@link #END_OF_STREAM}.
*/ */
public int readData(int track, long positionUs, MediaFormatHolder formatHolder, boolean isReady();
SampleHolder sampleHolder);
/** /**
* Seeks to the specified time in microseconds. * If there's an underlying error preventing data from being read, it's thrown by this method.
* <p> * If not, this method does nothing.
* This method should only be called when at least one track is enabled.
* *
* @param positionUs The seek position in microseconds. * @throws IOException The underlying error.
*/ */
public void seekToUs(long positionUs); void maybeThrowError() throws IOException;
/** /**
* Returns an estimate of the position up to which data is buffered. * Attempts to read a pending reset.
* <p>
* 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, * @return If a reset was read then the position after the reset. Else {@link #NO_RESET}.
* 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(); long readReset();
/** /**
* Disable the specified track. * Attempts to read the next format or sample.
* <p> * <p>
* This method should only be called when the specified track is enabled. * This method will always return {@link #NOTHING_READ} in the case that there's a pending
* discontinuity to be read from {@link #readReset} for the specified track.
* *
* @param track The track to disable. * @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 #END_OF_STREAM}, {@link #NOTHING_READ},
* {@link #FORMAT_READ} or {@link #SAMPLE_READ}.
*/ */
public void disable(int track); int readData(MediaFormatHolder formatHolder, SampleHolder sampleHolder);
/** /**
* Releases the {@link SampleSourceReader}. * Disables the track.
* <p>
* This method should be called when access to the {@link SampleSource} is no longer required.
*/ */
public void release(); void disable();
} }
......
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
package com.google.android.exoplayer; package com.google.android.exoplayer;
import com.google.android.exoplayer.MediaCodecUtil.DecoderQueryException; 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.io.IOException;
import java.util.Arrays; import java.util.Arrays;
...@@ -27,167 +27,78 @@ import java.util.Arrays; ...@@ -27,167 +27,78 @@ import java.util.Arrays;
*/ */
public abstract class SampleSourceTrackRenderer extends TrackRenderer { public abstract class SampleSourceTrackRenderer extends TrackRenderer {
private final SampleSourceReader[] sources; private SampleSource source;
private TrackStream trackStream;
private int[] handledSourceIndices; private int[] handledTrackIndices;
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();
}
}
@Override @Override
protected final boolean doPrepare(long positionUs) throws ExoPlaybackException { protected final void doPrepare(SampleSource source) throws ExoPlaybackException {
boolean allSourcesPrepared = true; int sourceTrackCount = source.getTrackCount();
for (int i = 0; i < sources.length; i++) { int[] handledTrackIndices = new int[sourceTrackCount];
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;
int handledTrackCount = 0; int handledTrackCount = 0;
int[] handledSourceIndices = new int[totalSourceTrackCount]; for (int trackIndex = 0; trackIndex < sourceTrackCount; trackIndex++) {
int[] handledTrackIndices = new int[totalSourceTrackCount]; MediaFormat format = source.getFormat(trackIndex);
int sourceCount = sources.length; boolean handlesTrack;
for (int sourceIndex = 0; sourceIndex < sourceCount; sourceIndex++) { try {
SampleSourceReader source = sources[sourceIndex]; handlesTrack = handlesTrack(format);
int sourceTrackCount = source.getTrackCount(); } catch (DecoderQueryException e) {
for (int trackIndex = 0; trackIndex < sourceTrackCount; trackIndex++) { throw new ExoPlaybackException(e);
MediaFormat format = source.getFormat(trackIndex); }
boolean handlesTrack; if (handlesTrack) {
try { handledTrackIndices[handledTrackCount] = trackIndex;
handlesTrack = handlesTrack(format); handledTrackCount++;
} 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);
}
}
}
} }
} }
this.durationUs = durationUs; this.source = source;
this.handledSourceIndices = Arrays.copyOf(handledSourceIndices, handledTrackCount); this.handledTrackIndices = Arrays.copyOf(handledTrackIndices, handledTrackCount);
this.handledSourceTrackIndices = Arrays.copyOf(handledTrackIndices, handledTrackCount);
return true;
} }
@Override @Override
protected void onEnabled(int track, long positionUs, boolean joining) protected void onEnabled(int track, long positionUs, boolean joining)
throws ExoPlaybackException { throws ExoPlaybackException {
positionUs = shiftInputPosition(positionUs); trackStream = source.enable(handledTrackIndices[track], positionUs);
enabledSource = sources[handledSourceIndices[track]]; onReset(positionUs);
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);
} }
@Override @Override
protected final void doSomeWork(long positionUs, long elapsedRealtimeUs) protected final void doSomeWork(long positionUs, long elapsedRealtimeUs)
throws ExoPlaybackException { throws ExoPlaybackException {
positionUs = shiftInputPosition(positionUs); // TODO[REFACTOR]: Consider splitting reading of resets into a separate method?
boolean sourceIsReady = enabledSource.continueBuffering(enabledSourceTrackIndex, positionUs); long resetPositionUs = trackStream.readReset();
positionUs = checkForDiscontinuity(positionUs); if (resetPositionUs != TrackStream.NO_RESET) {
doSomeWork(positionUs, elapsedRealtimeUs, sourceIsReady); onReset(resetPositionUs);
} return;
}
@Override doSomeWork(positionUs, elapsedRealtimeUs, trackStream.isReady());
protected long getBufferedPositionUs() {
return enabledSource.getBufferedPositionUs();
}
@Override
protected long getDurationUs() {
return durationUs;
} }
@Override @Override
protected void maybeThrowError() throws ExoPlaybackException { protected void maybeThrowError() throws IOException {
if (enabledSource != null) { if (source != null) {
maybeThrowError(enabledSource); trackStream.maybeThrowError();
} else {
int sourceCount = sources.length;
for (int i = 0; i < sourceCount; i++) {
maybeThrowError(sources[i]);
}
} }
} }
@Override @Override
protected void onDisabled() throws ExoPlaybackException { protected void onDisabled() throws ExoPlaybackException {
enabledSource.disable(enabledSourceTrackIndex); trackStream.disable();
enabledSource = null; trackStream = null;
} }
@Override @Override
protected void onReleased() throws ExoPlaybackException { protected void onUnprepared() {
int sourceCount = sources.length; source = null;
for (int i = 0; i < sourceCount; i++) { handledTrackIndices = null;
sources[i].release();
}
} }
@Override @Override
protected final int getTrackCount() { protected final int getTrackCount() {
return handledSourceTrackIndices.length; return handledTrackIndices.length;
} }
@Override @Override
protected final MediaFormat getFormat(int track) { protected final MediaFormat getFormat(int track) {
SampleSourceReader source = sources[handledSourceIndices[track]]; return source.getFormat(handledTrackIndices[track]);
return source.getFormat(handledSourceTrackIndices[track]);
}
/**
* Shifts positions passed to {@link #onEnabled(int, long, boolean)}, {@link #seekTo(long)} and
* {@link #doSomeWork(long, long)}.
* <p>
* 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;
} }
// Methods to be called by subclasses. // Methods to be called by subclasses.
...@@ -195,18 +106,16 @@ public abstract class SampleSourceTrackRenderer extends TrackRenderer { ...@@ -195,18 +106,16 @@ public abstract class SampleSourceTrackRenderer extends TrackRenderer {
/** /**
* Reads from the enabled upstream source. * 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 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. * @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} * If the caller requires the sample data then it must ensure that {@link SampleHolder#data}
* references a valid output buffer. * references a valid output buffer.
* @return The result, which can be {@link SampleSource#SAMPLE_READ}, * @return The result, which can be {@link TrackStream#SAMPLE_READ},
* {@link SampleSource#FORMAT_READ}, {@link SampleSource#NOTHING_READ} or * {@link TrackStream#FORMAT_READ}, {@link TrackStream#NOTHING_READ} or
* {@link SampleSource#END_OF_STREAM}. * {@link TrackStream#END_OF_STREAM}.
*/ */
protected final int readSource(long positionUs, MediaFormatHolder formatHolder, protected final int readSource(MediaFormatHolder formatHolder, SampleHolder sampleHolder) {
SampleHolder sampleHolder) { return trackStream.readData(formatHolder, sampleHolder);
return enabledSource.readData(enabledSourceTrackIndex, positionUs, formatHolder, sampleHolder);
} }
// Abstract methods. // Abstract methods.
...@@ -221,14 +130,12 @@ public abstract class SampleSourceTrackRenderer extends TrackRenderer { ...@@ -221,14 +130,12 @@ public abstract class SampleSourceTrackRenderer extends TrackRenderer {
protected abstract boolean handlesTrack(MediaFormat mediaFormat) throws DecoderQueryException; protected abstract boolean handlesTrack(MediaFormat mediaFormat) throws DecoderQueryException;
/** /**
* Invoked when a discontinuity is encountered. Also invoked when the renderer is enabled, for * Invoked when a reset is encountered. Also invoked when the renderer is enabled.
* convenience.
* *
* @param positionUs The playback position after the discontinuity, or the position at which * @param positionUs The playback position in microseconds.
* the renderer is being enabled. * @throws ExoPlaybackException If an error occurs handling the reset.
* @throws ExoPlaybackException If an error occurs handling the discontinuity.
*/ */
protected abstract void onDiscontinuity(long positionUs) throws ExoPlaybackException; protected abstract void onReset(long positionUs) throws ExoPlaybackException;
/** /**
* Called by {@link #doSomeWork(long, long)}. * Called by {@link #doSomeWork(long, long)}.
...@@ -237,31 +144,11 @@ public abstract class SampleSourceTrackRenderer extends TrackRenderer { ...@@ -237,31 +144,11 @@ public abstract class SampleSourceTrackRenderer extends TrackRenderer {
* current iteration of the rendering loop. * current iteration of the rendering loop.
* @param elapsedRealtimeUs {@link android.os.SystemClock#elapsedRealtime()} in microseconds, * @param elapsedRealtimeUs {@link android.os.SystemClock#elapsedRealtime()} in microseconds,
* measured at the start of the current iteration of the rendering loop. * measured at the start of the current iteration of the rendering loop.
* @param sourceIsReady The result of the most recent call to * @param sourceIsReady The result of the most recent call to {@link TrackStream#isReady()}.
* {@link SampleSourceReader#continueBuffering(int, long)}.
* @throws ExoPlaybackException If an error occurs. * @throws ExoPlaybackException If an error occurs.
* @throws ExoPlaybackException * @throws ExoPlaybackException
*/ */
protected abstract void doSomeWork(long positionUs, long elapsedRealtimeUs, boolean sourceIsReady) protected abstract void doSomeWork(long positionUs, long elapsedRealtimeUs, boolean sourceIsReady)
throws ExoPlaybackException; 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);
}
}
} }
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
*/ */
package com.google.android.exoplayer; 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.DataSource;
import com.google.android.exoplayer.upstream.DataSpec; import com.google.android.exoplayer.upstream.DataSpec;
import com.google.android.exoplayer.upstream.Loader; import com.google.android.exoplayer.upstream.Loader;
...@@ -31,7 +31,7 @@ import java.util.Arrays; ...@@ -31,7 +31,7 @@ import java.util.Arrays;
/** /**
* A {@link SampleSource} that loads the data at a given {@link Uri} as a single sample. * 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 { Loadable {
/** /**
...@@ -77,8 +77,10 @@ public final class SingleSampleSource implements SampleSource, SampleSourceReade ...@@ -77,8 +77,10 @@ public final class SingleSampleSource implements SampleSource, SampleSourceReade
} }
@Override @Override
public SampleSourceReader register() { public void maybeThrowError() throws IOException {
return this; if (currentLoadableException != null && currentLoadableExceptionCount > minLoadableRetryCount) {
throw currentLoadableException;
}
} }
@Override @Override
...@@ -90,6 +92,16 @@ public final class SingleSampleSource implements SampleSource, SampleSourceReade ...@@ -90,6 +92,16 @@ public final class SingleSampleSource implements SampleSource, SampleSourceReade
} }
@Override @Override
public boolean isPrepared() {
return loader != null;
}
@Override
public long getDurationUs() {
return format.durationUs;
}
@Override
public int getTrackCount() { public int getTrackCount() {
return 1; return 1;
} }
...@@ -100,33 +112,30 @@ public final class SingleSampleSource implements SampleSource, SampleSourceReade ...@@ -100,33 +112,30 @@ public final class SingleSampleSource implements SampleSource, SampleSourceReade
} }
@Override @Override
public void enable(int track, long positionUs) { public TrackStream enable(int track, long positionUs) {
state = STATE_SEND_FORMAT; state = STATE_SEND_FORMAT;
clearCurrentLoadableException(); clearCurrentLoadableException();
maybeStartLoading(); maybeStartLoading();
return this;
} }
@Override @Override
public boolean continueBuffering(int track, long positionUs) { public void continueBuffering(long positionUs) {
maybeStartLoading(); maybeStartLoading();
return loadingFinished;
} }
@Override @Override
public void maybeThrowError() throws IOException { public boolean isReady() {
if (currentLoadableException != null && currentLoadableExceptionCount > minLoadableRetryCount) { return loadingFinished;
throw currentLoadableException;
}
} }
@Override @Override
public long readDiscontinuity(int track) { public long readReset() {
return NO_DISCONTINUITY; return TrackStream.NO_RESET;
} }
@Override @Override
public int readData(int track, long positionUs, MediaFormatHolder formatHolder, public int readData(MediaFormatHolder formatHolder, SampleHolder sampleHolder) {
SampleHolder sampleHolder) {
if (state == STATE_END_OF_STREAM) { if (state == STATE_END_OF_STREAM) {
return END_OF_STREAM; return END_OF_STREAM;
} else if (state == STATE_SEND_FORMAT) { } else if (state == STATE_SEND_FORMAT) {
...@@ -158,11 +167,11 @@ public final class SingleSampleSource implements SampleSource, SampleSourceReade ...@@ -158,11 +167,11 @@ public final class SingleSampleSource implements SampleSource, SampleSourceReade
@Override @Override
public long getBufferedPositionUs() { 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 @Override
public void disable(int track) { public void disable() {
state = STATE_END_OF_STREAM; state = STATE_END_OF_STREAM;
} }
......
...@@ -18,6 +18,8 @@ package com.google.android.exoplayer; ...@@ -18,6 +18,8 @@ package com.google.android.exoplayer;
import com.google.android.exoplayer.ExoPlayer.ExoPlayerComponent; import com.google.android.exoplayer.ExoPlayer.ExoPlayerComponent;
import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.Assertions;
import java.io.IOException;
/** /**
* Renders a single component of media. * Renders a single component of media.
* *
...@@ -32,24 +34,6 @@ import com.google.android.exoplayer.util.Assertions; ...@@ -32,24 +34,6 @@ import com.google.android.exoplayer.util.Assertions;
public abstract class TrackRenderer implements ExoPlayerComponent { 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. * The renderer has not yet been prepared.
*/ */
protected static final int STATE_UNPREPARED = 0; protected static final int STATE_UNPREPARED = 0;
...@@ -98,37 +82,30 @@ public abstract class TrackRenderer implements ExoPlayerComponent { ...@@ -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 * Prepares the renderer to read from the provided {@link SampleSource}.
* more than once in order to transition the renderer into the prepared state. * <p>
* The {@link SampleSource} must itself be prepared before it is passed to this method.
* *
* @param positionUs The player's current playback position. * @param sampleSource The {@link SampleSource} from which to read.
* @return The current state (one of the STATE_* constants), for convenience.
* @throws ExoPlaybackException If an error occurs. * @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); Assertions.checkState(state == STATE_UNPREPARED);
state = doPrepare(positionUs) ? STATE_PREPARED : STATE_UNPREPARED; Assertions.checkState(sampleSource.isPrepared());
return state; doPrepare(sampleSource);
state = STATE_PREPARED;
} }
/** /**
* Invoked to make progress when the renderer is in the {@link #STATE_UNPREPARED} state. This * Called when the renderer is prepared.
* method will be called repeatedly until {@code true} is returned.
* <p>
* This method should return quickly, and should not block if the renderer is currently unable to
* make any useful progress.
* *
* @param positionUs The player's current playback position. * @param sampleSource The {@link SampleSource} from which to read.
* @return True if the renderer is now prepared. False otherwise.
* @throws ExoPlaybackException If an error occurs. * @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. * Returns the number of tracks exposed by the renderer.
* <p>
* 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. * @return The number of tracks.
*/ */
...@@ -136,9 +113,6 @@ public abstract class TrackRenderer implements ExoPlayerComponent { ...@@ -136,9 +113,6 @@ public abstract class TrackRenderer implements ExoPlayerComponent {
/** /**
* Returns the format of the specified track. * Returns the format of the specified track.
* <p>
* 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. * @param track The track index.
* @return The format of the specified track. * @return The format of the specified track.
...@@ -243,26 +217,24 @@ public abstract class TrackRenderer implements ExoPlayerComponent { ...@@ -243,26 +217,24 @@ public abstract class TrackRenderer implements ExoPlayerComponent {
} }
/** /**
* Releases the renderer. * Unprepares the renderer.
* *
* @throws ExoPlaybackException If an error occurs. * @throws ExoPlaybackException If an error occurs.
*/ */
/* package */ final void release() throws ExoPlaybackException { /* package */ final void unprepare() throws ExoPlaybackException {
Assertions.checkState(state != STATE_ENABLED Assertions.checkState(state == STATE_PREPARED);
&& state != STATE_STARTED state = STATE_UNPREPARED;
&& state != STATE_RELEASED); onUnprepared();
state = STATE_RELEASED;
onReleased();
} }
/** /**
* Called when the renderer is released. * Called when the renderer is unprepared.
* <p> * <p>
* The default implementation is a no-op. * The default implementation is a no-op.
* *
* @throws ExoPlaybackException If an error occurs. * @throws ExoPlaybackException If an error occurs.
*/ */
protected void onReleased() throws ExoPlaybackException { protected void onUnprepared() throws ExoPlaybackException {
// Do nothing. // Do nothing.
} }
...@@ -322,46 +294,14 @@ public abstract class TrackRenderer implements ExoPlayerComponent { ...@@ -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 * Throws an error that's preventing the renderer from making progress or buffering more data at
* this point in time. * 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.
* <p>
* 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.
* <p>
* 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.
* <p> * <p>
* This method may be called when the renderer is in the following states: * This method may be called when the renderer is in the following states:
* {@link #STATE_ENABLED} * {@link #STATE_ENABLED}
* *
* @param positionUs The desired playback position in microseconds. * @throws IOException An error that's preventing the renderer from making progress or buffering
* @throws ExoPlaybackException If an error occurs. * more data.
*/ */
protected abstract void seekTo(long positionUs) throws ExoPlaybackException; protected abstract void maybeThrowError() throws IOException;
@Override @Override
public void handleMessage(int what, Object object) throws ExoPlaybackException { public void handleMessage(int what, Object object) throws ExoPlaybackException {
......
...@@ -17,7 +17,7 @@ package com.google.android.exoplayer.chunk; ...@@ -17,7 +17,7 @@ package com.google.android.exoplayer.chunk;
import com.google.android.exoplayer.C; import com.google.android.exoplayer.C;
import com.google.android.exoplayer.SampleSource; import com.google.android.exoplayer.SampleSource;
import com.google.android.exoplayer.SampleSource.SampleSourceReader; import com.google.android.exoplayer.SampleSource.TrackStream;
import java.io.IOException; import java.io.IOException;
...@@ -91,7 +91,7 @@ public interface BaseChunkSampleSourceEventListener { ...@@ -91,7 +91,7 @@ public interface BaseChunkSampleSourceEventListener {
/** /**
* Invoked when the downstream format changes (i.e. when the format being supplied to the * 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 sourceId The id of the reporting {@link SampleSource}.
* @param format The format. * @param format The format.
......
...@@ -21,8 +21,7 @@ import com.google.android.exoplayer.MediaFormat; ...@@ -21,8 +21,7 @@ import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.MediaFormatHolder; import com.google.android.exoplayer.MediaFormatHolder;
import com.google.android.exoplayer.SampleHolder; import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.SampleSource; import com.google.android.exoplayer.SampleSource;
import com.google.android.exoplayer.SampleSource.SampleSourceReader; import com.google.android.exoplayer.SampleSource.TrackStream;
import com.google.android.exoplayer.TrackRenderer;
import com.google.android.exoplayer.extractor.DefaultTrackOutput; import com.google.android.exoplayer.extractor.DefaultTrackOutput;
import com.google.android.exoplayer.upstream.Loader; import com.google.android.exoplayer.upstream.Loader;
import com.google.android.exoplayer.upstream.Loader.Loadable; import com.google.android.exoplayer.upstream.Loader.Loadable;
...@@ -40,7 +39,7 @@ import java.util.List; ...@@ -40,7 +39,7 @@ import java.util.List;
* A {@link SampleSource} that loads media in {@link Chunk}s, which are themselves obtained from a * A {@link SampleSource} that loads media in {@link Chunk}s, which are themselves obtained from a
* {@link ChunkSource}. * {@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. * Interface definition for a callback to be notified of {@link ChunkSampleSource} events.
...@@ -53,9 +52,8 @@ public class ChunkSampleSource implements SampleSource, SampleSourceReader, Load ...@@ -53,9 +52,8 @@ public class ChunkSampleSource implements SampleSource, SampleSourceReader, Load
public static final int DEFAULT_MIN_LOADABLE_RETRY_COUNT = 3; public static final int DEFAULT_MIN_LOADABLE_RETRY_COUNT = 3;
private static final int STATE_IDLE = 0; private static final int STATE_IDLE = 0;
private static final int STATE_INITIALIZED = 1; private static final int STATE_PREPARED = 1;
private static final int STATE_PREPARED = 2; private static final int STATE_ENABLED = 2;
private static final int STATE_ENABLED = 3;
private static final long NO_RESET_PENDING = Long.MIN_VALUE; private static final long NO_RESET_PENDING = Long.MIN_VALUE;
...@@ -76,8 +74,9 @@ public class ChunkSampleSource implements SampleSource, SampleSourceReader, Load ...@@ -76,8 +74,9 @@ public class ChunkSampleSource implements SampleSource, SampleSourceReader, Load
private long lastSeekPositionUs; private long lastSeekPositionUs;
private long pendingResetPositionUs; private long pendingResetPositionUs;
private long lastPerformedBufferOperation; private long lastPerformedBufferOperation;
private boolean pendingDiscontinuity; private boolean pendingReset;
private long durationUs;
private Loader loader; private Loader loader;
private boolean loadingFinished; private boolean loadingFinished;
private IOException currentLoadableException; private IOException currentLoadableException;
...@@ -145,41 +144,47 @@ public class ChunkSampleSource implements SampleSource, SampleSourceReader, Load ...@@ -145,41 +144,47 @@ public class ChunkSampleSource implements SampleSource, SampleSourceReader, Load
} }
@Override @Override
public SampleSourceReader register() { public boolean prepare(long positionUs) throws IOException {
Assertions.checkState(state == STATE_IDLE); if (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) {
return true; return true;
} else if (!chunkSource.prepare()) { }
if (!chunkSource.prepare()) {
maybeThrowError();
return false; return false;
} }
durationUs = C.UNKNOWN_TIME_US;
if (chunkSource.getTrackCount() > 0) { if (chunkSource.getTrackCount() > 0) {
loader = new Loader("Loader:" + chunkSource.getFormat(0).mimeType); loader = new Loader("Loader:" + chunkSource.getFormat(0).mimeType);
durationUs = chunkSource.getFormat(0).durationUs;
} }
state = STATE_PREPARED; state = STATE_PREPARED;
return true; return true;
} }
@Override @Override
public boolean isPrepared() {
return state != STATE_IDLE;
}
@Override
public long getDurationUs() {
return durationUs;
}
@Override
public int getTrackCount() { public int getTrackCount() {
Assertions.checkState(state == STATE_PREPARED || state == STATE_ENABLED); Assertions.checkState(state != STATE_IDLE);
return chunkSource.getTrackCount(); return chunkSource.getTrackCount();
} }
@Override @Override
public MediaFormat getFormat(int track) { public MediaFormat getFormat(int track) {
Assertions.checkState(state == STATE_PREPARED || state == STATE_ENABLED); Assertions.checkState(state != STATE_IDLE);
return chunkSource.getFormat(track); return chunkSource.getFormat(track);
} }
@Override @Override
public void enable(int track, long positionUs) { public TrackStream enable(int track, long positionUs) {
Assertions.checkState(state == STATE_PREPARED); Assertions.checkState(state == STATE_PREPARED);
Assertions.checkState(enabledTrackCount++ == 0); Assertions.checkState(enabledTrackCount++ == 0);
state = STATE_ENABLED; state = STATE_ENABLED;
...@@ -189,12 +194,13 @@ public class ChunkSampleSource implements SampleSource, SampleSourceReader, Load ...@@ -189,12 +194,13 @@ public class ChunkSampleSource implements SampleSource, SampleSourceReader, Load
downstreamMediaFormat = null; downstreamMediaFormat = null;
downstreamPositionUs = positionUs; downstreamPositionUs = positionUs;
lastSeekPositionUs = positionUs; lastSeekPositionUs = positionUs;
pendingDiscontinuity = false; pendingReset = false;
restartFrom(positionUs); restartFrom(positionUs);
return this;
} }
@Override @Override
public void disable(int track) { public void disable() {
Assertions.checkState(state == STATE_ENABLED); Assertions.checkState(state == STATE_ENABLED);
Assertions.checkState(--enabledTrackCount == 0); Assertions.checkState(--enabledTrackCount == 0);
state = STATE_PREPARED; state = STATE_PREPARED;
...@@ -214,30 +220,35 @@ public class ChunkSampleSource implements SampleSource, SampleSourceReader, Load ...@@ -214,30 +220,35 @@ public class ChunkSampleSource implements SampleSource, SampleSourceReader, Load
} }
@Override @Override
public boolean continueBuffering(int track, long positionUs) { public void continueBuffering(long positionUs) {
Assertions.checkState(state == STATE_ENABLED); Assertions.checkState(state != STATE_IDLE);
if (state == STATE_PREPARED) {
return;
}
downstreamPositionUs = positionUs; downstreamPositionUs = positionUs;
chunkSource.continueBuffering(positionUs); chunkSource.continueBuffering(positionUs);
updateLoadControl(); updateLoadControl();
}
@Override
public boolean isReady() {
Assertions.checkState(state == STATE_ENABLED);
return loadingFinished || !sampleQueue.isEmpty(); return loadingFinished || !sampleQueue.isEmpty();
} }
@Override @Override
public long readDiscontinuity(int track) { public long readReset() {
if (pendingDiscontinuity) { if (pendingReset) {
pendingDiscontinuity = false; pendingReset = false;
return lastSeekPositionUs; return lastSeekPositionUs;
} }
return NO_DISCONTINUITY; return TrackStream.NO_RESET;
} }
@Override @Override
public int readData(int track, long positionUs, MediaFormatHolder formatHolder, public int readData(MediaFormatHolder formatHolder, SampleHolder sampleHolder) {
SampleHolder sampleHolder) {
Assertions.checkState(state == STATE_ENABLED); Assertions.checkState(state == STATE_ENABLED);
downstreamPositionUs = positionUs; if (pendingReset || isPendingReset()) {
if (pendingDiscontinuity || isPendingReset()) {
return NOTHING_READ; return NOTHING_READ;
} }
...@@ -284,15 +295,12 @@ public class ChunkSampleSource implements SampleSource, SampleSourceReader, Load ...@@ -284,15 +295,12 @@ public class ChunkSampleSource implements SampleSource, SampleSourceReader, Load
@Override @Override
public void seekToUs(long positionUs) { public void seekToUs(long positionUs) {
Assertions.checkState(state == STATE_ENABLED); Assertions.checkState(state != STATE_IDLE);
if (state == STATE_PREPARED) {
long currentPositionUs = isPendingReset() ? pendingResetPositionUs : downstreamPositionUs;
downstreamPositionUs = positionUs;
lastSeekPositionUs = positionUs;
if (currentPositionUs == positionUs) {
return; return;
} }
downstreamPositionUs = positionUs;
lastSeekPositionUs = positionUs;
// If we're not pending a reset, see if we can seek within the sample queue. // If we're not pending a reset, see if we can seek within the sample queue.
boolean seekInsideBuffer = !isPendingReset() && sampleQueue.skipToKeyframeBefore(positionUs); boolean seekInsideBuffer = !isPendingReset() && sampleQueue.skipToKeyframeBefore(positionUs);
if (seekInsideBuffer) { if (seekInsideBuffer) {
...@@ -307,7 +315,7 @@ public class ChunkSampleSource implements SampleSource, SampleSourceReader, Load ...@@ -307,7 +315,7 @@ public class ChunkSampleSource implements SampleSource, SampleSourceReader, Load
restartFrom(positionUs); restartFrom(positionUs);
} }
// Either way, we need to send a discontinuity to the downstream components. // Either way, we need to send a discontinuity to the downstream components.
pendingDiscontinuity = true; pendingReset = true;
} }
@Override @Override
...@@ -321,11 +329,11 @@ public class ChunkSampleSource implements SampleSource, SampleSourceReader, Load ...@@ -321,11 +329,11 @@ public class ChunkSampleSource implements SampleSource, SampleSourceReader, Load
@Override @Override
public long getBufferedPositionUs() { public long getBufferedPositionUs() {
Assertions.checkState(state == STATE_ENABLED); Assertions.checkState(state != STATE_IDLE);
if (isPendingReset()) { if (state != STATE_ENABLED || loadingFinished) {
return C.END_OF_SOURCE_US;
} else if (isPendingReset()) {
return pendingResetPositionUs; return pendingResetPositionUs;
} else if (loadingFinished) {
return TrackRenderer.END_OF_TRACK_US;
} else { } else {
long largestParsedTimestampUs = sampleQueue.getLargestParsedTimestampUs(); long largestParsedTimestampUs = sampleQueue.getLargestParsedTimestampUs();
return largestParsedTimestampUs == Long.MIN_VALUE ? downstreamPositionUs return largestParsedTimestampUs == Long.MIN_VALUE ? downstreamPositionUs
......
...@@ -41,8 +41,7 @@ public final class SingleSampleChunkSource implements ChunkSource { ...@@ -41,8 +41,7 @@ public final class SingleSampleChunkSource implements ChunkSource {
* @param dataSpec Defines the location of the sample. * @param dataSpec Defines the location of the sample.
* @param format The format 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 * @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 * the duration is unknown.
* duration of the longest track whose duration is known.
* @param mediaFormat The sample media format. May be null. * @param mediaFormat The sample media format. May be null.
*/ */
public SingleSampleChunkSource(DataSource dataSource, DataSpec dataSpec, Format format, public SingleSampleChunkSource(DataSource dataSource, DataSpec dataSpec, Format format,
......
...@@ -21,8 +21,6 @@ import com.google.android.exoplayer.MediaFormatHolder; ...@@ -21,8 +21,6 @@ import com.google.android.exoplayer.MediaFormatHolder;
import com.google.android.exoplayer.ParserException; import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.SampleHolder; import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.SampleSource; 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.drm.DrmInitData;
import com.google.android.exoplayer.upstream.Allocator; import com.google.android.exoplayer.upstream.Allocator;
import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DataSource;
...@@ -39,6 +37,7 @@ import android.util.SparseArray; ...@@ -39,6 +37,7 @@ import android.util.SparseArray;
import java.io.EOFException; import java.io.EOFException;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.List; import java.util.List;
/** /**
...@@ -66,8 +65,7 @@ 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} * constructor. When reading a new stream, the first {@link Extractor} that returns {@code true}
* from {@link Extractor#sniff(ExtractorInput)} will be used. * from {@link Extractor#sniff(ExtractorInput)} will be used.
*/ */
public final class ExtractorSampleSource implements SampleSource, SampleSourceReader, public final class ExtractorSampleSource implements SampleSource, ExtractorOutput, Loader.Callback {
ExtractorOutput, Loader.Callback {
/** /**
* Thrown if the input format could not recognized. * Thrown if the input format could not recognized.
...@@ -177,12 +175,11 @@ public final class ExtractorSampleSource implements SampleSource, SampleSourceRe ...@@ -177,12 +175,11 @@ public final class ExtractorSampleSource implements SampleSource, SampleSourceRe
private boolean prepared; private boolean prepared;
private int enabledTrackCount; private int enabledTrackCount;
private MediaFormat[] mediaFormats; private MediaFormat[] mediaFormats;
private long maxTrackDurationUs; private long durationUs;
private boolean[] pendingMediaFormat; private boolean[] pendingMediaFormat;
private boolean[] pendingDiscontinuities; private boolean[] pendingResets;
private boolean[] trackEnabledStates; private boolean[] trackEnabledStates;
private int remainingReleaseCount;
private long downstreamPositionUs; private long downstreamPositionUs;
private long lastSeekPositionUs; private long lastSeekPositionUs;
private long pendingResetPositionUs; private long pendingResetPositionUs;
...@@ -253,12 +250,6 @@ public final class ExtractorSampleSource implements SampleSource, SampleSourceRe ...@@ -253,12 +250,6 @@ public final class ExtractorSampleSource implements SampleSource, SampleSourceRe
} }
@Override @Override
public SampleSourceReader register() {
remainingReleaseCount++;
return this;
}
@Override
public boolean prepare(long positionUs) { public boolean prepare(long positionUs) {
if (prepared) { if (prepared) {
return true; return true;
...@@ -272,15 +263,15 @@ public final class ExtractorSampleSource implements SampleSource, SampleSourceRe ...@@ -272,15 +263,15 @@ public final class ExtractorSampleSource implements SampleSource, SampleSourceRe
if (seekMap != null && tracksBuilt && haveFormatsForAllTracks()) { if (seekMap != null && tracksBuilt && haveFormatsForAllTracks()) {
int trackCount = sampleQueues.size(); int trackCount = sampleQueues.size();
trackEnabledStates = new boolean[trackCount]; trackEnabledStates = new boolean[trackCount];
pendingDiscontinuities = new boolean[trackCount]; pendingResets = new boolean[trackCount];
pendingMediaFormat = new boolean[trackCount]; pendingMediaFormat = new boolean[trackCount];
mediaFormats = new MediaFormat[trackCount]; mediaFormats = new MediaFormat[trackCount];
maxTrackDurationUs = C.UNKNOWN_TIME_US; durationUs = C.UNKNOWN_TIME_US;
for (int i = 0; i < trackCount; i++) { for (int i = 0; i < trackCount; i++) {
MediaFormat format = sampleQueues.valueAt(i).getFormat(); MediaFormat format = sampleQueues.valueAt(i).getFormat();
mediaFormats[i] = format; mediaFormats[i] = format;
if (format.durationUs != C.UNKNOWN_TIME_US && format.durationUs > maxTrackDurationUs) { if (format.durationUs > durationUs) {
maxTrackDurationUs = format.durationUs; durationUs = format.durationUs;
} }
} }
prepared = true; prepared = true;
...@@ -291,6 +282,16 @@ public final class ExtractorSampleSource implements SampleSource, SampleSourceRe ...@@ -291,6 +282,16 @@ public final class ExtractorSampleSource implements SampleSource, SampleSourceRe
} }
@Override @Override
public boolean isPrepared() {
return prepared;
}
@Override
public long getDurationUs() {
return durationUs;
}
@Override
public int getTrackCount() { public int getTrackCount() {
return sampleQueues.size(); return sampleQueues.size();
} }
...@@ -302,13 +303,13 @@ public final class ExtractorSampleSource implements SampleSource, SampleSourceRe ...@@ -302,13 +303,13 @@ public final class ExtractorSampleSource implements SampleSource, SampleSourceRe
} }
@Override @Override
public void enable(int track, long positionUs) { public TrackStream enable(int track, long positionUs) {
Assertions.checkState(prepared); Assertions.checkState(prepared);
Assertions.checkState(!trackEnabledStates[track]); Assertions.checkState(!trackEnabledStates[track]);
enabledTrackCount++; enabledTrackCount++;
trackEnabledStates[track] = true; trackEnabledStates[track] = true;
pendingMediaFormat[track] = true; pendingMediaFormat[track] = true;
pendingDiscontinuities[track] = false; pendingResets[track] = false;
if (enabledTrackCount == 1) { if (enabledTrackCount == 1) {
// Treat all enables in non-seekable media as being from t=0. // Treat all enables in non-seekable media as being from t=0.
positionUs = !seekMap.isSeekable() ? 0 : positionUs; positionUs = !seekMap.isSeekable() ? 0 : positionUs;
...@@ -316,10 +317,10 @@ public final class ExtractorSampleSource implements SampleSource, SampleSourceRe ...@@ -316,10 +317,10 @@ public final class ExtractorSampleSource implements SampleSource, SampleSourceRe
lastSeekPositionUs = positionUs; lastSeekPositionUs = positionUs;
restartFrom(positionUs); restartFrom(positionUs);
} }
return new TrackStreamImpl(track);
} }
@Override /* package */ void disable(int track) {
public void disable(int track) {
Assertions.checkState(prepared); Assertions.checkState(prepared);
Assertions.checkState(trackEnabledStates[track]); Assertions.checkState(trackEnabledStates[track]);
enabledTrackCount--; enabledTrackCount--;
...@@ -336,37 +337,37 @@ public final class ExtractorSampleSource implements SampleSource, SampleSourceRe ...@@ -336,37 +337,37 @@ public final class ExtractorSampleSource implements SampleSource, SampleSourceRe
} }
@Override @Override
public boolean continueBuffering(int track, long playbackPositionUs) { public void continueBuffering(long playbackPositionUs) {
Assertions.checkState(prepared); Assertions.checkState(prepared);
Assertions.checkState(trackEnabledStates[track]); if (enabledTrackCount == 0) {
return;
}
downstreamPositionUs = playbackPositionUs; downstreamPositionUs = playbackPositionUs;
discardSamplesForDisabledTracks(downstreamPositionUs); discardSamplesForDisabledTracks(downstreamPositionUs);
if (loadingFinished) { if (loadingFinished) {
return true; return;
} }
maybeStartLoading(); maybeStartLoading();
if (isPendingReset()) { }
return false;
} /* package */ boolean isReady(int track) {
Assertions.checkState(prepared);
Assertions.checkState(trackEnabledStates[track]);
return !sampleQueues.valueAt(track).isEmpty(); return !sampleQueues.valueAt(track).isEmpty();
} }
@Override /* package */ long readReset(int track) {
public long readDiscontinuity(int track) { if (pendingResets[track]) {
if (pendingDiscontinuities[track]) { pendingResets[track] = false;
pendingDiscontinuities[track] = false;
return lastSeekPositionUs; return lastSeekPositionUs;
} }
return NO_DISCONTINUITY; return TrackStream.NO_RESET;
} }
@Override /* package */ int readData(int track, MediaFormatHolder formatHolder, SampleHolder sampleHolder) {
public int readData(int track, long playbackPositionUs, MediaFormatHolder formatHolder, if (pendingResets[track] || isPendingReset()) {
SampleHolder sampleHolder) { return TrackStream.NOTHING_READ;
downstreamPositionUs = playbackPositionUs;
if (pendingDiscontinuities[track] || isPendingReset()) {
return NOTHING_READ;
} }
InternalTrackOutput sampleQueue = sampleQueues.valueAt(track); InternalTrackOutput sampleQueue = sampleQueues.valueAt(track);
...@@ -374,7 +375,7 @@ public final class ExtractorSampleSource implements SampleSource, SampleSourceRe ...@@ -374,7 +375,7 @@ public final class ExtractorSampleSource implements SampleSource, SampleSourceRe
formatHolder.format = sampleQueue.getFormat(); formatHolder.format = sampleQueue.getFormat();
formatHolder.drmInitData = drmInitData; formatHolder.drmInitData = drmInitData;
pendingMediaFormat[track] = false; pendingMediaFormat[track] = false;
return FORMAT_READ; return TrackStream.FORMAT_READ;
} }
if (sampleQueue.getSample(sampleHolder)) { if (sampleQueue.getSample(sampleHolder)) {
...@@ -386,18 +387,17 @@ public final class ExtractorSampleSource implements SampleSource, SampleSourceRe ...@@ -386,18 +387,17 @@ public final class ExtractorSampleSource implements SampleSource, SampleSourceRe
havePendingNextSampleUs = false; havePendingNextSampleUs = false;
} }
sampleHolder.timeUs += sampleTimeOffsetUs; sampleHolder.timeUs += sampleTimeOffsetUs;
return SAMPLE_READ; return TrackStream.SAMPLE_READ;
} }
if (loadingFinished) { if (loadingFinished) {
return END_OF_STREAM; return TrackStream.END_OF_STREAM;
} }
return NOTHING_READ; return TrackStream.NOTHING_READ;
} }
@Override /* package */ void maybeThrowError() throws IOException {
public void maybeThrowError() throws IOException {
if (currentLoadableException == null) { if (currentLoadableException == null) {
return; return;
} }
...@@ -420,38 +420,30 @@ public final class ExtractorSampleSource implements SampleSource, SampleSourceRe ...@@ -420,38 +420,30 @@ public final class ExtractorSampleSource implements SampleSource, SampleSourceRe
@Override @Override
public void seekToUs(long positionUs) { public void seekToUs(long positionUs) {
Assertions.checkState(prepared); Assertions.checkState(prepared);
Assertions.checkState(enabledTrackCount > 0); if (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) {
return; 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. // If we're not pending a reset, see if we can seek within the sample queues.
boolean seekInsideBuffer = !isPendingReset(); boolean seekInsideBuffer = !isPendingReset();
for (int i = 0; seekInsideBuffer && i < sampleQueues.size(); i++) { for (int i = 0; seekInsideBuffer && i < sampleQueues.size(); i++) {
seekInsideBuffer &= sampleQueues.valueAt(i).skipToKeyframeBefore(positionUs); seekInsideBuffer &= sampleQueues.valueAt(i).skipToKeyframeBefore(positionUs);
} }
// If we failed to seek within the sample queues, we need to restart. // If we failed to seek within the sample queues, we need to restart.
if (!seekInsideBuffer) { if (!seekInsideBuffer) {
restartFrom(positionUs); restartFrom(positionUs);
} }
// Either way, we need to send discontinuities to the downstream components. // Either way, we need to send discontinuities to the downstream components.
for (int i = 0; i < pendingDiscontinuities.length; i++) { Arrays.fill(pendingResets, true);
pendingDiscontinuities[i] = true;
}
} }
@Override @Override
public long getBufferedPositionUs() { public long getBufferedPositionUs() {
if (loadingFinished) { if (enabledTrackCount == 0 || loadingFinished) {
return TrackRenderer.END_OF_TRACK_US; return C.END_OF_SOURCE_US;
} else if (isPendingReset()) { } else if (isPendingReset()) {
return pendingResetPositionUs; return pendingResetPositionUs;
} else { } else {
...@@ -467,11 +459,11 @@ public final class ExtractorSampleSource implements SampleSource, SampleSourceRe ...@@ -467,11 +459,11 @@ public final class ExtractorSampleSource implements SampleSource, SampleSourceRe
@Override @Override
public void release() { public void release() {
Assertions.checkState(remainingReleaseCount > 0); if (loader != null) {
if (--remainingReleaseCount == 0 && loader != null) {
loader.release(); loader.release();
loader = null; loader = null;
} }
prepared = false;
} }
// Loader.Callback implementation. // Loader.Callback implementation.
...@@ -561,7 +553,7 @@ public final class ExtractorSampleSource implements SampleSource, SampleSourceRe ...@@ -561,7 +553,7 @@ public final class ExtractorSampleSource implements SampleSource, SampleSourceRe
sampleQueues.valueAt(i).clear(); sampleQueues.valueAt(i).clear();
} }
loadable = createLoadableFromStart(); 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 // 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 // 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 // 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 ...@@ -594,7 +586,7 @@ public final class ExtractorSampleSource implements SampleSource, SampleSourceRe
loadable = createLoadableFromStart(); loadable = createLoadableFromStart();
} else { } else {
Assertions.checkState(isPendingReset()); Assertions.checkState(isPendingReset());
if (maxTrackDurationUs != C.UNKNOWN_TIME_US && pendingResetPositionUs >= maxTrackDurationUs) { if (durationUs != C.UNKNOWN_TIME_US && pendingResetPositionUs >= durationUs) {
loadingFinished = true; loadingFinished = true;
pendingResetPositionUs = NO_RESET_PENDING; pendingResetPositionUs = NO_RESET_PENDING;
return; return;
...@@ -654,6 +646,41 @@ public final class ExtractorSampleSource implements SampleSource, SampleSourceRe ...@@ -654,6 +646,41 @@ public final class ExtractorSampleSource implements SampleSource, SampleSourceRe
return Math.min((errorCount - 1) * 1000, 5000); 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 * Extension of {@link DefaultTrackOutput} that increments a shared counter of the total number
* of extracted samples. * of extracted samples.
......
...@@ -21,8 +21,6 @@ import com.google.android.exoplayer.MediaFormat; ...@@ -21,8 +21,6 @@ import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.MediaFormatHolder; import com.google.android.exoplayer.MediaFormatHolder;
import com.google.android.exoplayer.SampleHolder; import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.SampleSource; 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.BaseChunkSampleSourceEventListener;
import com.google.android.exoplayer.chunk.Chunk; import com.google.android.exoplayer.chunk.Chunk;
import com.google.android.exoplayer.chunk.ChunkOperationHolder; import com.google.android.exoplayer.chunk.ChunkOperationHolder;
...@@ -42,7 +40,7 @@ import java.util.LinkedList; ...@@ -42,7 +40,7 @@ import java.util.LinkedList;
/** /**
* A {@link SampleSource} for HLS streams. * 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. * Interface definition for a callback to be notified of {@link HlsSampleSource} events.
...@@ -72,7 +70,6 @@ public final class HlsSampleSource implements SampleSource, SampleSourceReader, ...@@ -72,7 +70,6 @@ public final class HlsSampleSource implements SampleSource, SampleSourceReader,
private final Handler eventHandler; private final Handler eventHandler;
private final EventListener eventListener; private final EventListener eventListener;
private int remainingReleaseCount;
private boolean prepared; private boolean prepared;
private boolean loadControlRegistered; private boolean loadControlRegistered;
private int trackCount; private int trackCount;
...@@ -84,7 +81,7 @@ public final class HlsSampleSource implements SampleSource, SampleSourceReader, ...@@ -84,7 +81,7 @@ public final class HlsSampleSource implements SampleSource, SampleSourceReader,
// Indexed by track (as exposed by this source). // Indexed by track (as exposed by this source).
private MediaFormat[] trackFormats; private MediaFormat[] trackFormats;
private boolean[] trackEnabledStates; private boolean[] trackEnabledStates;
private boolean[] pendingDiscontinuities; private boolean[] pendingResets;
private MediaFormat[] downstreamMediaFormats; private MediaFormat[] downstreamMediaFormats;
// Maps track index (as exposed by this source) to the corresponding chunk source track index for // Maps track index (as exposed by this source) to the corresponding chunk source track index for
// primary tracks, or to -1 otherwise. // primary tracks, or to -1 otherwise.
...@@ -137,16 +134,11 @@ public final class HlsSampleSource implements SampleSource, SampleSourceReader, ...@@ -137,16 +134,11 @@ public final class HlsSampleSource implements SampleSource, SampleSourceReader,
} }
@Override @Override
public SampleSourceReader register() { public boolean prepare(long positionUs) throws IOException {
remainingReleaseCount++;
return this;
}
@Override
public boolean prepare(long positionUs) {
if (prepared) { if (prepared) {
return true; return true;
} else if (!chunkSource.prepare()) { } else if (!chunkSource.prepare()) {
maybeThrowError();
return false; return false;
} }
if (!extractors.isEmpty()) { if (!extractors.isEmpty()) {
...@@ -179,10 +171,21 @@ public final class HlsSampleSource implements SampleSource, SampleSourceReader, ...@@ -179,10 +171,21 @@ public final class HlsSampleSource implements SampleSource, SampleSourceReader,
downstreamPositionUs = positionUs; downstreamPositionUs = positionUs;
} }
maybeStartLoading(); maybeStartLoading();
maybeThrowError();
return false; return false;
} }
@Override @Override
public boolean isPrepared() {
return prepared;
}
@Override
public long getDurationUs() {
return chunkSource.getDurationUs();
}
@Override
public int getTrackCount() { public int getTrackCount() {
Assertions.checkState(prepared); Assertions.checkState(prepared);
return trackCount; return trackCount;
...@@ -195,11 +198,11 @@ public final class HlsSampleSource implements SampleSource, SampleSourceReader, ...@@ -195,11 +198,11 @@ public final class HlsSampleSource implements SampleSource, SampleSourceReader,
} }
@Override @Override
public void enable(int track, long positionUs) { public TrackStream enable(int track, long positionUs) {
Assertions.checkState(prepared); Assertions.checkState(prepared);
setTrackEnabledState(track, true); setTrackEnabledState(track, true);
downstreamMediaFormats[track] = null; downstreamMediaFormats[track] = null;
pendingDiscontinuities[track] = false; pendingResets[track] = false;
downstreamFormat = null; downstreamFormat = null;
boolean wasLoadControlRegistered = loadControlRegistered; boolean wasLoadControlRegistered = loadControlRegistered;
if (!loadControlRegistered) { if (!loadControlRegistered) {
...@@ -216,9 +219,7 @@ public final class HlsSampleSource implements SampleSource, SampleSourceReader, ...@@ -216,9 +219,7 @@ public final class HlsSampleSource implements SampleSource, SampleSourceReader,
// renderers receive a discontinuity event. // renderers receive a discontinuity event.
chunkSource.selectTrack(chunkSourceTrack); chunkSource.selectTrack(chunkSourceTrack);
seekToInternal(positionUs); seekToInternal(positionUs);
return; } else if (enabledTrackCount == 1) {
}
if (enabledTrackCount == 1) {
lastSeekPositionUs = positionUs; lastSeekPositionUs = positionUs;
if (wasLoadControlRegistered && downstreamPositionUs == positionUs) { if (wasLoadControlRegistered && downstreamPositionUs == positionUs) {
// TODO: Address [Internal: b/21743989] to remove the need for this kind of hack. // 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, ...@@ -231,10 +232,10 @@ public final class HlsSampleSource implements SampleSource, SampleSourceReader,
restartFrom(positionUs); restartFrom(positionUs);
} }
} }
return new TrackStreamImpl(track);
} }
@Override /* package */ void disable(int track) {
public void disable(int track) {
Assertions.checkState(prepared); Assertions.checkState(prepared);
setTrackEnabledState(track, false); setTrackEnabledState(track, false);
if (enabledTrackCount == 0) { if (enabledTrackCount == 0) {
...@@ -254,14 +255,20 @@ public final class HlsSampleSource implements SampleSource, SampleSourceReader, ...@@ -254,14 +255,20 @@ public final class HlsSampleSource implements SampleSource, SampleSourceReader,
} }
@Override @Override
public boolean continueBuffering(int track, long playbackPositionUs) { public void continueBuffering(long playbackPositionUs) {
Assertions.checkState(prepared); Assertions.checkState(prepared);
Assertions.checkState(trackEnabledStates[track]); if (enabledTrackCount == 0) {
return;
}
downstreamPositionUs = playbackPositionUs; downstreamPositionUs = playbackPositionUs;
if (!extractors.isEmpty()) { if (!extractors.isEmpty()) {
discardSamplesForDisabledTracks(getCurrentExtractor(), downstreamPositionUs); discardSamplesForDisabledTracks(getCurrentExtractor(), downstreamPositionUs);
} }
maybeStartLoading(); maybeStartLoading();
}
/* package */ boolean isReady(int track) {
Assertions.checkState(trackEnabledStates[track]);
if (loadingFinished) { if (loadingFinished) {
return true; return true;
} }
...@@ -281,28 +288,24 @@ public final class HlsSampleSource implements SampleSource, SampleSourceReader, ...@@ -281,28 +288,24 @@ public final class HlsSampleSource implements SampleSource, SampleSourceReader,
return false; return false;
} }
@Override /* package */ long readReset(int track) {
public long readDiscontinuity(int track) { if (pendingResets[track]) {
if (pendingDiscontinuities[track]) { pendingResets[track] = false;
pendingDiscontinuities[track] = false;
return lastSeekPositionUs; return lastSeekPositionUs;
} }
return NO_DISCONTINUITY; return TrackStream.NO_RESET;
} }
@Override /* package */ int readData(int track, MediaFormatHolder formatHolder, SampleHolder sampleHolder) {
public int readData(int track, long playbackPositionUs, MediaFormatHolder formatHolder,
SampleHolder sampleHolder) {
Assertions.checkState(prepared); Assertions.checkState(prepared);
downstreamPositionUs = playbackPositionUs;
if (pendingDiscontinuities[track] || isPendingReset()) { if (pendingResets[track] || isPendingReset()) {
return NOTHING_READ; return TrackStream.NOTHING_READ;
} }
HlsExtractorWrapper extractor = getCurrentExtractor(); HlsExtractorWrapper extractor = getCurrentExtractor();
if (!extractor.isPrepared()) { if (!extractor.isPrepared()) {
return NOTHING_READ; return TrackStream.NOTHING_READ;
} }
if (downstreamFormat == null || !downstreamFormat.equals(extractor.format)) { if (downstreamFormat == null || !downstreamFormat.equals(extractor.format)) {
...@@ -324,7 +327,7 @@ public final class HlsSampleSource implements SampleSource, SampleSourceReader, ...@@ -324,7 +327,7 @@ public final class HlsSampleSource implements SampleSource, SampleSourceReader,
// next one for the current read. // next one for the current read.
extractor = extractors.get(++extractorIndex); extractor = extractors.get(++extractorIndex);
if (!extractor.isPrepared()) { if (!extractor.isPrepared()) {
return NOTHING_READ; return TrackStream.NOTHING_READ;
} }
} }
...@@ -332,24 +335,23 @@ public final class HlsSampleSource implements SampleSource, SampleSourceReader, ...@@ -332,24 +335,23 @@ public final class HlsSampleSource implements SampleSource, SampleSourceReader,
if (mediaFormat != null && !mediaFormat.equals(downstreamMediaFormats[track])) { if (mediaFormat != null && !mediaFormat.equals(downstreamMediaFormats[track])) {
formatHolder.format = mediaFormat; formatHolder.format = mediaFormat;
downstreamMediaFormats[track] = mediaFormat; downstreamMediaFormats[track] = mediaFormat;
return FORMAT_READ; return TrackStream.FORMAT_READ;
} }
if (extractor.getSample(extractorTrack, sampleHolder)) { if (extractor.getSample(extractorTrack, sampleHolder)) {
boolean decodeOnly = sampleHolder.timeUs < lastSeekPositionUs; boolean decodeOnly = sampleHolder.timeUs < lastSeekPositionUs;
sampleHolder.flags |= decodeOnly ? C.SAMPLE_FLAG_DECODE_ONLY : 0; sampleHolder.flags |= decodeOnly ? C.SAMPLE_FLAG_DECODE_ONLY : 0;
return SAMPLE_READ; return TrackStream.SAMPLE_READ;
} }
if (loadingFinished) { if (loadingFinished) {
return END_OF_STREAM; return TrackStream.END_OF_STREAM;
} }
return NOTHING_READ; return TrackStream.NOTHING_READ;
} }
@Override /* package */ void maybeThrowError() throws IOException {
public void maybeThrowError() throws IOException {
if (currentLoadableException != null && currentLoadableExceptionCount > minLoadableRetryCount) { if (currentLoadableException != null && currentLoadableExceptionCount > minLoadableRetryCount) {
throw currentLoadableException; throw currentLoadableException;
} else if (currentLoadable == null) { } else if (currentLoadable == null) {
...@@ -360,29 +362,21 @@ public final class HlsSampleSource implements SampleSource, SampleSourceReader, ...@@ -360,29 +362,21 @@ public final class HlsSampleSource implements SampleSource, SampleSourceReader,
@Override @Override
public void seekToUs(long positionUs) { public void seekToUs(long positionUs) {
Assertions.checkState(prepared); Assertions.checkState(prepared);
Assertions.checkState(enabledTrackCount > 0); if (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) {
return; return;
} }
seekToInternal(chunkSource.isLive() ? 0 : positionUs);
seekToInternal(positionUs);
} }
@Override @Override
public long getBufferedPositionUs() { public long getBufferedPositionUs() {
Assertions.checkState(prepared); Assertions.checkState(prepared);
Assertions.checkState(enabledTrackCount > 0); if (enabledTrackCount == 0) {
if (isPendingReset()) { return C.END_OF_SOURCE_US;
} else if (isPendingReset()) {
return pendingResetPositionUs; return pendingResetPositionUs;
} else if (loadingFinished) { } else if (loadingFinished) {
return TrackRenderer.END_OF_TRACK_US; return C.END_OF_SOURCE_US;
} else { } else {
long largestParsedTimestampUs = extractors.getLast().getLargestParsedTimestampUs(); long largestParsedTimestampUs = extractors.getLast().getLargestParsedTimestampUs();
if (extractors.size() > 1) { if (extractors.size() > 1) {
...@@ -398,8 +392,7 @@ public final class HlsSampleSource implements SampleSource, SampleSourceReader, ...@@ -398,8 +392,7 @@ public final class HlsSampleSource implements SampleSource, SampleSourceReader,
@Override @Override
public void release() { public void release() {
Assertions.checkState(remainingReleaseCount > 0); if (loader != null) {
if (--remainingReleaseCount == 0 && loader != null) {
if (loadControlRegistered) { if (loadControlRegistered) {
loadControl.unregister(this); loadControl.unregister(this);
loadControlRegistered = false; loadControlRegistered = false;
...@@ -407,6 +400,7 @@ public final class HlsSampleSource implements SampleSource, SampleSourceReader, ...@@ -407,6 +400,7 @@ public final class HlsSampleSource implements SampleSource, SampleSourceReader,
loader.release(); loader.release();
loader = null; loader = null;
} }
prepared = false;
} }
// Loader.Callback implementation. // Loader.Callback implementation.
...@@ -531,7 +525,7 @@ public final class HlsSampleSource implements SampleSource, SampleSourceReader, ...@@ -531,7 +525,7 @@ public final class HlsSampleSource implements SampleSource, SampleSourceReader,
// Instantiate the necessary internal data-structures. // Instantiate the necessary internal data-structures.
trackFormats = new MediaFormat[trackCount]; trackFormats = new MediaFormat[trackCount];
trackEnabledStates = new boolean[trackCount]; trackEnabledStates = new boolean[trackCount];
pendingDiscontinuities = new boolean[trackCount]; pendingResets = new boolean[trackCount];
downstreamMediaFormats = new MediaFormat[trackCount]; downstreamMediaFormats = new MediaFormat[trackCount];
chunkSourceTrackIndices = new int[trackCount]; chunkSourceTrackIndices = new int[trackCount];
extractorTrackIndices = new int[trackCount]; extractorTrackIndices = new int[trackCount];
...@@ -596,7 +590,7 @@ public final class HlsSampleSource implements SampleSource, SampleSourceReader, ...@@ -596,7 +590,7 @@ public final class HlsSampleSource implements SampleSource, SampleSourceReader,
private void seekToInternal(long positionUs) { private void seekToInternal(long positionUs) {
lastSeekPositionUs = positionUs; lastSeekPositionUs = positionUs;
downstreamPositionUs = positionUs; downstreamPositionUs = positionUs;
Arrays.fill(pendingDiscontinuities, true); Arrays.fill(pendingResets, true);
chunkSource.seek(); chunkSource.seek();
restartFrom(positionUs); restartFrom(positionUs);
} }
...@@ -823,4 +817,39 @@ public final class HlsSampleSource implements SampleSource, SampleSourceReader, ...@@ -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);
}
}
} }
...@@ -19,7 +19,7 @@ import com.google.android.exoplayer.ExoPlaybackException; ...@@ -19,7 +19,7 @@ import com.google.android.exoplayer.ExoPlaybackException;
import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.MediaFormatHolder; import com.google.android.exoplayer.MediaFormatHolder;
import com.google.android.exoplayer.SampleHolder; 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.SampleSourceTrackRenderer;
import com.google.android.exoplayer.TrackRenderer; import com.google.android.exoplayer.TrackRenderer;
import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.Assertions;
...@@ -67,7 +67,6 @@ public final class MetadataTrackRenderer<T> extends SampleSourceTrackRenderer im ...@@ -67,7 +67,6 @@ public final class MetadataTrackRenderer<T> extends SampleSourceTrackRenderer im
private T pendingMetadata; private T pendingMetadata;
/** /**
* @param source A source from which samples containing metadata can be read.
* @param metadataParser A parser for parsing the metadata. * @param metadataParser A parser for parsing the metadata.
* @param metadataRenderer The metadata renderer to receive the parsed metadata. * @param metadataRenderer The metadata renderer to receive the parsed metadata.
* @param metadataRendererLooper The looper associated with the thread on which metadataRenderer * @param metadataRendererLooper The looper associated with the thread on which metadataRenderer
...@@ -76,9 +75,8 @@ public final class MetadataTrackRenderer<T> extends SampleSourceTrackRenderer im ...@@ -76,9 +75,8 @@ public final class MetadataTrackRenderer<T> extends SampleSourceTrackRenderer im
* obtained using {@link android.app.Activity#getMainLooper()}. Null may be passed if the * 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. * renderer should be invoked directly on the player's internal rendering thread.
*/ */
public MetadataTrackRenderer(SampleSource source, MetadataParser<T> metadataParser, public MetadataTrackRenderer(MetadataParser<T> metadataParser,
MetadataRenderer<T> metadataRenderer, Looper metadataRendererLooper) { MetadataRenderer<T> metadataRenderer, Looper metadataRendererLooper) {
super(source);
this.metadataParser = Assertions.checkNotNull(metadataParser); this.metadataParser = Assertions.checkNotNull(metadataParser);
this.metadataRenderer = Assertions.checkNotNull(metadataRenderer); this.metadataRenderer = Assertions.checkNotNull(metadataRenderer);
this.metadataHandler = metadataRendererLooper == null ? null this.metadataHandler = metadataRendererLooper == null ? null
...@@ -93,7 +91,7 @@ public final class MetadataTrackRenderer<T> extends SampleSourceTrackRenderer im ...@@ -93,7 +91,7 @@ public final class MetadataTrackRenderer<T> extends SampleSourceTrackRenderer im
} }
@Override @Override
protected void onDiscontinuity(long positionUs) { protected void onReset(long positionUs) {
pendingMetadata = null; pendingMetadata = null;
inputStreamEnded = false; inputStreamEnded = false;
} }
...@@ -103,15 +101,15 @@ public final class MetadataTrackRenderer<T> extends SampleSourceTrackRenderer im ...@@ -103,15 +101,15 @@ public final class MetadataTrackRenderer<T> extends SampleSourceTrackRenderer im
throws ExoPlaybackException { throws ExoPlaybackException {
if (!inputStreamEnded && pendingMetadata == null) { if (!inputStreamEnded && pendingMetadata == null) {
sampleHolder.clearData(); sampleHolder.clearData();
int result = readSource(positionUs, formatHolder, sampleHolder); int result = readSource(formatHolder, sampleHolder);
if (result == SampleSource.SAMPLE_READ) { if (result == TrackStream.SAMPLE_READ) {
pendingMetadataTimestamp = sampleHolder.timeUs; pendingMetadataTimestamp = sampleHolder.timeUs;
try { try {
pendingMetadata = metadataParser.parse(sampleHolder.data.array(), sampleHolder.size); pendingMetadata = metadataParser.parse(sampleHolder.data.array(), sampleHolder.size);
} catch (IOException e) { } catch (IOException e) {
throw new ExoPlaybackException(e); throw new ExoPlaybackException(e);
} }
} else if (result == SampleSource.END_OF_STREAM) { } else if (result == TrackStream.END_OF_STREAM) {
inputStreamEnded = true; inputStreamEnded = true;
} }
} }
...@@ -129,11 +127,6 @@ public final class MetadataTrackRenderer<T> extends SampleSourceTrackRenderer im ...@@ -129,11 +127,6 @@ public final class MetadataTrackRenderer<T> extends SampleSourceTrackRenderer im
} }
@Override @Override
protected long getBufferedPositionUs() {
return TrackRenderer.END_OF_TRACK_US;
}
@Override
protected boolean isEnded() { protected boolean isEnded() {
return inputStreamEnded; return inputStreamEnded;
} }
......
...@@ -19,7 +19,7 @@ import com.google.android.exoplayer.ExoPlaybackException; ...@@ -19,7 +19,7 @@ import com.google.android.exoplayer.ExoPlaybackException;
import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.MediaFormatHolder; import com.google.android.exoplayer.MediaFormatHolder;
import com.google.android.exoplayer.SampleHolder; 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.SampleSourceTrackRenderer;
import com.google.android.exoplayer.TrackRenderer; import com.google.android.exoplayer.TrackRenderer;
import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.Assertions;
...@@ -124,7 +124,6 @@ public final class TextTrackRenderer extends SampleSourceTrackRenderer implement ...@@ -124,7 +124,6 @@ public final class TextTrackRenderer extends SampleSourceTrackRenderer implement
private int nextSubtitleEventIndex; private int nextSubtitleEventIndex;
/** /**
* @param source A source from which samples containing subtitle data can be read.
* @param textRenderer The text renderer. * @param textRenderer The text renderer.
* @param textRendererLooper The looper associated with the thread on which textRenderer should be * @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 * 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 ...@@ -134,25 +133,8 @@ public final class TextTrackRenderer extends SampleSourceTrackRenderer implement
* @param subtitleParsers {@link SubtitleParser}s to parse text samples, in order of decreasing * @param subtitleParsers {@link SubtitleParser}s to parse text samples, in order of decreasing
* priority. If omitted, the default parsers will be used. * priority. If omitted, the default parsers will be used.
*/ */
public TextTrackRenderer(SampleSource source, TextRenderer textRenderer, public TextTrackRenderer(TextRenderer textRenderer, Looper textRendererLooper,
Looper textRendererLooper, SubtitleParser... subtitleParsers) { 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);
this.textRenderer = Assertions.checkNotNull(textRenderer); this.textRenderer = Assertions.checkNotNull(textRenderer);
this.textRendererHandler = textRendererLooper == null ? null this.textRendererHandler = textRendererLooper == null ? null
: new Handler(textRendererLooper, this); : new Handler(textRendererLooper, this);
...@@ -188,7 +170,7 @@ public final class TextTrackRenderer extends SampleSourceTrackRenderer implement ...@@ -188,7 +170,7 @@ public final class TextTrackRenderer extends SampleSourceTrackRenderer implement
} }
@Override @Override
protected void onDiscontinuity(long positionUs) { protected void onReset(long positionUs) {
inputStreamEnded = false; inputStreamEnded = false;
subtitle = null; subtitle = null;
nextSubtitle = null; nextSubtitle = null;
...@@ -243,12 +225,12 @@ public final class TextTrackRenderer extends SampleSourceTrackRenderer implement ...@@ -243,12 +225,12 @@ public final class TextTrackRenderer extends SampleSourceTrackRenderer implement
// Try and read the next subtitle from the source. // Try and read the next subtitle from the source.
SampleHolder sampleHolder = parserHelper.getSampleHolder(); SampleHolder sampleHolder = parserHelper.getSampleHolder();
sampleHolder.clearData(); sampleHolder.clearData();
int result = readSource(positionUs, formatHolder, sampleHolder); int result = readSource(formatHolder, sampleHolder);
if (result == SampleSource.FORMAT_READ) { if (result == TrackStream.FORMAT_READ) {
parserHelper.setFormat(formatHolder.format); parserHelper.setFormat(formatHolder.format);
} else if (result == SampleSource.SAMPLE_READ) { } else if (result == TrackStream.SAMPLE_READ) {
parserHelper.startParseOperation(); parserHelper.startParseOperation();
} else if (result == SampleSource.END_OF_STREAM) { } else if (result == TrackStream.END_OF_STREAM) {
inputStreamEnded = true; inputStreamEnded = true;
} }
} }
...@@ -266,12 +248,6 @@ public final class TextTrackRenderer extends SampleSourceTrackRenderer implement ...@@ -266,12 +248,6 @@ public final class TextTrackRenderer extends SampleSourceTrackRenderer implement
} }
@Override @Override
protected long getBufferedPositionUs() {
// Don't block playback whilst subtitles are loading.
return END_OF_TRACK_US;
}
@Override
protected boolean isEnded() { protected boolean isEnded() {
return inputStreamEnded && (subtitle == null || getNextEventTime() == Long.MAX_VALUE); return inputStreamEnded && (subtitle == null || getNextEventTime() == Long.MAX_VALUE);
} }
......
...@@ -20,7 +20,7 @@ import com.google.android.exoplayer.ExoPlaybackException; ...@@ -20,7 +20,7 @@ import com.google.android.exoplayer.ExoPlaybackException;
import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.MediaFormatHolder; import com.google.android.exoplayer.MediaFormatHolder;
import com.google.android.exoplayer.SampleHolder; 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.SampleSourceTrackRenderer;
import com.google.android.exoplayer.TrackRenderer; import com.google.android.exoplayer.TrackRenderer;
import com.google.android.exoplayer.text.Cue; import com.google.android.exoplayer.text.Cue;
...@@ -68,7 +68,6 @@ public final class Eia608TrackRenderer extends SampleSourceTrackRenderer impleme ...@@ -68,7 +68,6 @@ public final class Eia608TrackRenderer extends SampleSourceTrackRenderer impleme
private String lastRenderedCaption; private String lastRenderedCaption;
/** /**
* @param source A source from which samples containing EIA-608 closed captions can be read.
* @param textRenderer The text renderer. * @param textRenderer The text renderer.
* @param textRendererLooper The looper associated with the thread on which textRenderer should be * @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 * 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 ...@@ -76,9 +75,7 @@ public final class Eia608TrackRenderer extends SampleSourceTrackRenderer impleme
* obtained using {@link android.app.Activity#getMainLooper()}. Null may be passed if the * 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. * renderer should be invoked directly on the player's internal rendering thread.
*/ */
public Eia608TrackRenderer(SampleSource source, TextRenderer textRenderer, public Eia608TrackRenderer(TextRenderer textRenderer, Looper textRendererLooper) {
Looper textRendererLooper) {
super(source);
this.textRenderer = Assertions.checkNotNull(textRenderer); this.textRenderer = Assertions.checkNotNull(textRenderer);
textRendererHandler = textRendererLooper == null ? null : new Handler(textRendererLooper, this); textRendererHandler = textRendererLooper == null ? null : new Handler(textRendererLooper, this);
eia608Parser = new Eia608Parser(); eia608Parser = new Eia608Parser();
...@@ -100,7 +97,7 @@ public final class Eia608TrackRenderer extends SampleSourceTrackRenderer impleme ...@@ -100,7 +97,7 @@ public final class Eia608TrackRenderer extends SampleSourceTrackRenderer impleme
} }
@Override @Override
protected void onDiscontinuity(long positionUs) { protected void onReset(long positionUs) {
inputStreamEnded = false; inputStreamEnded = false;
pendingCaptionLists.clear(); pendingCaptionLists.clear();
clearPendingSample(); clearPendingSample();
...@@ -116,12 +113,12 @@ public final class Eia608TrackRenderer extends SampleSourceTrackRenderer impleme ...@@ -116,12 +113,12 @@ public final class Eia608TrackRenderer extends SampleSourceTrackRenderer impleme
maybeParsePendingSample(positionUs); maybeParsePendingSample(positionUs);
} }
int result = inputStreamEnded ? SampleSource.END_OF_STREAM : SampleSource.SAMPLE_READ; int result = inputStreamEnded ? TrackStream.END_OF_STREAM : TrackStream.SAMPLE_READ;
while (!isSamplePending() && result == SampleSource.SAMPLE_READ) { while (!isSamplePending() && result == TrackStream.SAMPLE_READ) {
result = readSource(positionUs, formatHolder, sampleHolder); result = readSource(formatHolder, sampleHolder);
if (result == SampleSource.SAMPLE_READ) { if (result == TrackStream.SAMPLE_READ) {
maybeParsePendingSample(positionUs); maybeParsePendingSample(positionUs);
} else if (result == SampleSource.END_OF_STREAM) { } else if (result == TrackStream.END_OF_STREAM) {
inputStreamEnded = true; inputStreamEnded = true;
} }
} }
...@@ -142,11 +139,6 @@ public final class Eia608TrackRenderer extends SampleSourceTrackRenderer impleme ...@@ -142,11 +139,6 @@ public final class Eia608TrackRenderer extends SampleSourceTrackRenderer impleme
} }
@Override @Override
protected long getBufferedPositionUs() {
return TrackRenderer.END_OF_TRACK_US;
}
@Override
protected boolean isEnded() { protected boolean isEnded() {
return inputStreamEnded; return inputStreamEnded;
} }
......
// 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')
}
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.LIBRARIES"/>
<classpathentry kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.DEPENDENCIES"/>
<classpathentry combineaccessrules="false" kind="src" path="/ExoPlayerLib"/>
<classpathentry kind="src" path="java"/>
<classpathentry kind="src" path="gen"/>
<classpathentry kind="output" path="bin/classes"/>
</classpath>
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>ExoPlayerPlaybackTests</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>com.android.ide.eclipse.adt.ResourceManagerBuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>com.android.ide.eclipse.adt.PreCompilerBuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>com.android.ide.eclipse.adt.ApkBuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>com.android.ide.eclipse.adt.AndroidNature</nature>
<nature>org.eclipse.jdt.core.javanature</nature>
</natures>
<filteredResources>
<filter>
<id>1363908154650</id>
<name></name>
<type>22</type>
<matcher>
<id>org.eclipse.ui.ide.multiFilter</id>
<arguments>1.0-name-matches-false-false-BUILD</arguments>
</matcher>
</filter>
<filter>
<id>1363908154652</id>
<name></name>
<type>10</type>
<matcher>
<id>org.eclipse.ui.ide.multiFilter</id>
<arguments>1.0-name-matches-true-false-build</arguments>
</matcher>
</filter>
</filteredResources>
</projectDescription>
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.google.android.exoplayer.playbacktests"
android:versionCode="1503"
android:versionName="1.5.4">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-sdk android:minSdkVersion="9" android:targetSdkVersion="23"/>
<application android:debuggable="true"
android:allowBackup="false"
tools:ignore="MissingApplicationIcon,HardcodedDebugMode">
<uses-library android:name="android.test.runner"/>
<activity android:name="com.google.android.exoplayer.playbacktests.util.HostActivity"
android:configChanges="keyboardHidden|orientation|screenSize"
android:label="ExoPlayerTest"/>
</application>
<instrumentation
android:targetPackage="com.google.android.exoplayer.playbacktests"
android:name="android.test.InstrumentationTestRunner"/>
</manifest>
/*
* 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<HostActivity> {
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<Representation> availableRepresentations = adaptationSet.representations;
List<Integer> 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);
}
}
}
}
/*
* 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);
}
}
}
/*
* 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.
}
}
}
/*
* 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);
}
}
/*
* 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;
}
}
/*
* 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.
* <p>
* 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.
* <p>
* Called on the main thread.
*/
void release();
/**
* Called periodically to check whether the test has finished.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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);
}
}
}
}
/*
* 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);
}
}
/*
* 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());
}
}
}
}
/*
* 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);
}
}
}
/*
* 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> T loadManifest(Context context, String url, UriLoadable.Parser<T> parser)
throws IOException {
String userAgent = getUserAgent(context);
DefaultUriDataSource manifestDataSource = new DefaultUriDataSource(context, userAgent);
ManifestFetcher<T> manifestFetcher = new ManifestFetcher<>(url, manifestDataSource, parser);
SyncManifestCallback<T> 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 <T> The type of the manifest.
*/
private static final class SyncManifestCallback<T> implements ManifestCallback<T> {
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;
}
}
}
<?xml version="1.0" encoding="UTF-8"?>
<!-- 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.
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/root"
android:focusable="true"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:keepScreenOn="true">
<SurfaceView android:id="@+id/surface_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"/>
</FrameLayout>
...@@ -13,13 +13,3 @@ ...@@ -13,13 +13,3 @@
// limitations under the License. // limitations under the License.
include ':library' include ':library'
include ':demo' 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')
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment