Merge branch 'dev' into dev-hls

This commit is contained in:
Oliver Woodman 2014-12-03 18:48:20 +00:00
commit 511dd9435a
25 changed files with 743 additions and 225 deletions

View file

@ -47,7 +47,7 @@ public class DemoUtil {
public static final String CONTENT_TYPE_EXTRA = "content_type";
public static final String CONTENT_ID_EXTRA = "content_id";
public static final int TYPE_DASH_VOD = 0;
public static final int TYPE_DASH = 0;
public static final int TYPE_SS = 1;
public static final int TYPE_OTHER = 2;
public static final int TYPE_DASH_LIVE = 3;

View file

@ -46,13 +46,13 @@ package com.google.android.exoplayer.demo;
"http://www.youtube.com/api/manifest/dash/id/bf5bb2419360daf1/source/youtube?"
+ "as=fmp4_audio_clear,fmp4_sd_hd_clear&sparams=ip,ipbits,expire,as&ip=0.0.0.0&"
+ "ipbits=0&expire=19000000000&signature=255F6B3C07C753C88708C07EA31B7A1A10703C8D."
+ "2D6A28B21F921D0B245CDCF36F7EB54A2B5ABFC2&key=ik0", DemoUtil.TYPE_DASH_VOD, false,
+ "2D6A28B21F921D0B245CDCF36F7EB54A2B5ABFC2&key=ik0", DemoUtil.TYPE_DASH, false,
false),
new Sample("Google Play (DASH)", "3aa39fa2cc27967f",
"http://www.youtube.com/api/manifest/dash/id/3aa39fa2cc27967f/source/youtube?"
+ "as=fmp4_audio_clear,fmp4_sd_hd_clear&sparams=ip,ipbits,expire,as&ip=0.0.0.0&ipbits=0&"
+ "expire=19000000000&signature=7181C59D0252B285D593E1B61D985D5B7C98DE2A."
+ "5B445837F55A40E0F28AACAA047982E372D177E2&key=ik0", DemoUtil.TYPE_DASH_VOD, false,
+ "5B445837F55A40E0F28AACAA047982E372D177E2&key=ik0", DemoUtil.TYPE_DASH, false,
false),
new Sample("Super speed (SmoothStreaming)", "uid:ss:superspeed",
"http://playready.directtaps.net/smoothstreaming/SSWSS720H264/SuperSpeedway_720.ism",
@ -69,13 +69,13 @@ package com.google.android.exoplayer.demo;
"http://www.youtube.com/api/manifest/dash/id/bf5bb2419360daf1/source/youtube?"
+ "as=fmp4_audio_clear,fmp4_sd_hd_clear&sparams=ip,ipbits,expire,as&ip=0.0.0.0&"
+ "ipbits=0&expire=19000000000&signature=255F6B3C07C753C88708C07EA31B7A1A10703C8D."
+ "2D6A28B21F921D0B245CDCF36F7EB54A2B5ABFC2&key=ik0", DemoUtil.TYPE_DASH_VOD, false,
+ "2D6A28B21F921D0B245CDCF36F7EB54A2B5ABFC2&key=ik0", DemoUtil.TYPE_DASH, false,
true),
new Sample("Google Play", "3aa39fa2cc27967f",
"http://www.youtube.com/api/manifest/dash/id/3aa39fa2cc27967f/source/youtube?"
+ "as=fmp4_audio_clear,fmp4_sd_hd_clear&sparams=ip,ipbits,expire,as&ip=0.0.0.0&ipbits=0&"
+ "expire=19000000000&signature=7181C59D0252B285D593E1B61D985D5B7C98DE2A."
+ "5B445837F55A40E0F28AACAA047982E372D177E2&key=ik0", DemoUtil.TYPE_DASH_VOD, false,
+ "5B445837F55A40E0F28AACAA047982E372D177E2&key=ik0", DemoUtil.TYPE_DASH, false,
true),
};
@ -84,12 +84,12 @@ package com.google.android.exoplayer.demo;
"http://www.youtube.com/api/manifest/dash/id/bf5bb2419360daf1/source/youtube?"
+ "as=fmp4_audio_clear,webm2_sd_hd_clear&sparams=ip,ipbits,expire,as&ip=0.0.0.0&ipbits=0&"
+ "expire=19000000000&signature=A3EC7EE53ABE601B357F7CAB8B54AD0702CA85A7."
+ "446E9C38E47E3EDAF39E0163C390FF83A7944918&key=ik0", DemoUtil.TYPE_DASH_VOD, false, true),
+ "446E9C38E47E3EDAF39E0163C390FF83A7944918&key=ik0", DemoUtil.TYPE_DASH, false, true),
new Sample("Google Play", "3aa39fa2cc27967f",
"http://www.youtube.com/api/manifest/dash/id/3aa39fa2cc27967f/source/youtube?"
+ "as=fmp4_audio_clear,webm2_sd_hd_clear&sparams=ip,ipbits,expire,as&ip=0.0.0.0&ipbits=0&"
+ "expire=19000000000&signature=B752B262C6D7262EC4E4EB67901E5D8F7058A81D."
+ "C0358CE1E335417D9A8D88FF192F0D5D8F6DA1B6&key=ik0", DemoUtil.TYPE_DASH_VOD, false, true),
+ "C0358CE1E335417D9A8D88FF192F0D5D8F6DA1B6&key=ik0", DemoUtil.TYPE_DASH, false, true),
};
public static final Sample[] SMOOTHSTREAMING = new Sample[] {
@ -106,32 +106,32 @@ package com.google.android.exoplayer.demo;
"http://www.youtube.com/api/manifest/dash/id/d286538032258a1c/source/youtube?"
+ "as=fmp4_audio_cenc,fmp4_sd_hd_cenc&sparams=ip,ipbits,expire,as&ip=0.0.0.0&ipbits=0"
+ "&expire=19000000000&signature=41EA40A027A125A16292E0A5E3277A3B5FA9B938."
+ "0BB075C396FFDDC97E526E8F77DC26FF9667D0D6&key=ik0", DemoUtil.TYPE_DASH_VOD, true, true),
+ "0BB075C396FFDDC97E526E8F77DC26FF9667D0D6&key=ik0", DemoUtil.TYPE_DASH, true, true),
new Sample("WV: HDCP not required", "48fcc369939ac96c",
"http://www.youtube.com/api/manifest/dash/id/48fcc369939ac96c/source/youtube?"
+ "as=fmp4_audio_cenc,fmp4_sd_hd_cenc&sparams=ip,ipbits,expire,as&ip=0.0.0.0&ipbits=0"
+ "&expire=19000000000&signature=315911BDCEED0FB0C763455BDCC97449DAAFA9E8."
+ "5B41E2EB411F797097A359D6671D2CDE26272373&key=ik0", DemoUtil.TYPE_DASH_VOD, true, true),
+ "5B41E2EB411F797097A359D6671D2CDE26272373&key=ik0", DemoUtil.TYPE_DASH, true, true),
new Sample("WV: HDCP required", "e06c39f1151da3df",
"http://www.youtube.com/api/manifest/dash/id/e06c39f1151da3df/source/youtube?"
+ "as=fmp4_audio_cenc,fmp4_sd_hd_cenc&sparams=ip,ipbits,expire,as&ip=0.0.0.0&ipbits=0"
+ "&expire=19000000000&signature=A47A1E13E7243BD567601A75F79B34644D0DC592."
+ "B09589A34FA23527EFC1552907754BB8033870BD&key=ik0", DemoUtil.TYPE_DASH_VOD, true, true),
+ "B09589A34FA23527EFC1552907754BB8033870BD&key=ik0", DemoUtil.TYPE_DASH, true, true),
new Sample("WV: Secure video path required", "0894c7c8719b28a0",
"http://www.youtube.com/api/manifest/dash/id/0894c7c8719b28a0/source/youtube?"
+ "as=fmp4_audio_cenc,fmp4_sd_hd_cenc&sparams=ip,ipbits,expire,as&ip=0.0.0.0&ipbits=0"
+ "&expire=19000000000&signature=2847EE498970F6B45176766CD2802FEB4D4CB7B2."
+ "A1CA51EC40A1C1039BA800C41500DD448C03EEDA&key=ik0", DemoUtil.TYPE_DASH_VOD, true, true),
+ "A1CA51EC40A1C1039BA800C41500DD448C03EEDA&key=ik0", DemoUtil.TYPE_DASH, true, true),
new Sample("WV: HDCP + secure video path required", "efd045b1eb61888a",
"http://www.youtube.com/api/manifest/dash/id/efd045b1eb61888a/source/youtube?"
+ "as=fmp4_audio_cenc,fmp4_sd_hd_cenc&sparams=ip,ipbits,expire,as&ip=0.0.0.0&ipbits=0"
+ "&expire=19000000000&signature=61611F115EEEC7BADE5536827343FFFE2D83D14F."
+ "2FDF4BFA502FB5865C5C86401314BDDEA4799BD0&key=ik0", DemoUtil.TYPE_DASH_VOD, true, true),
+ "2FDF4BFA502FB5865C5C86401314BDDEA4799BD0&key=ik0", DemoUtil.TYPE_DASH, true, true),
new Sample("WV: 30s license duration", "f9a34cab7b05881a",
"http://www.youtube.com/api/manifest/dash/id/f9a34cab7b05881a/source/youtube?"
+ "as=fmp4_audio_cenc,fmp4_sd_hd_cenc&sparams=ip,ipbits,expire,as&ip=0.0.0.0&ipbits=0"
+ "&expire=19000000000&signature=88DC53943385CED8CF9F37ADD9E9843E3BF621E6."
+ "22727BB612D24AA4FACE4EF62726F9461A9BF57A&key=ik0", DemoUtil.TYPE_DASH_VOD, true, true),
+ "22727BB612D24AA4FACE4EF62726F9461A9BF57A&key=ik0", DemoUtil.TYPE_DASH, true, true),
};
public static final Sample[] HLS = new Sample[] {

View file

@ -19,7 +19,7 @@ import com.google.android.exoplayer.ExoPlayer;
import com.google.android.exoplayer.VideoSurfaceView;
import com.google.android.exoplayer.demo.DemoUtil;
import com.google.android.exoplayer.demo.R;
import com.google.android.exoplayer.demo.full.player.DashVodRendererBuilder;
import com.google.android.exoplayer.demo.full.player.DashRendererBuilder;
import com.google.android.exoplayer.demo.full.player.DefaultRendererBuilder;
import com.google.android.exoplayer.demo.full.player.DemoPlayer;
import com.google.android.exoplayer.demo.full.player.DemoPlayer.RendererBuilder;
@ -179,8 +179,8 @@ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callba
case DemoUtil.TYPE_SS:
return new SmoothStreamingRendererBuilder(userAgent, contentUri.toString(), contentId,
new SmoothStreamingTestMediaDrmCallback(), debugTextView);
case DemoUtil.TYPE_DASH_VOD:
return new DashVodRendererBuilder(userAgent, contentUri.toString(), contentId,
case DemoUtil.TYPE_DASH:
return new DashRendererBuilder(userAgent, contentUri.toString(), contentId,
new WidevineTestMediaDrmCallback(contentId), debugTextView);
case DemoUtil.TYPE_HLS:
return new HlsRendererBuilder(userAgent, contentUri.toString(), contentId);

View file

@ -40,6 +40,8 @@ import com.google.android.exoplayer.demo.full.player.DemoPlayer.RendererBuilderC
import com.google.android.exoplayer.drm.DrmSessionManager;
import com.google.android.exoplayer.drm.MediaDrmCallback;
import com.google.android.exoplayer.drm.StreamingDrmSessionManager;
import com.google.android.exoplayer.text.TextTrackRenderer;
import com.google.android.exoplayer.text.webvtt.WebvttParser;
import com.google.android.exoplayer.upstream.BufferPool;
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DefaultBandwidthMeter;
@ -58,16 +60,19 @@ import android.widget.TextView;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* A {@link RendererBuilder} for DASH VOD.
* A {@link RendererBuilder} for DASH.
*/
public class DashVodRendererBuilder implements RendererBuilder,
public class DashRendererBuilder implements RendererBuilder,
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 static final int TEXT_BUFFER_SEGMENTS = 2;
private static final int LIVE_EDGE_LATENCY_MS = 30000;
private static final int SECURITY_LEVEL_UNKNOWN = -1;
private static final int SECURITY_LEVEL_1 = 1;
@ -81,8 +86,9 @@ public class DashVodRendererBuilder implements RendererBuilder,
private DemoPlayer player;
private RendererBuilderCallback callback;
private ManifestFetcher<MediaPresentationDescription> manifestFetcher;
public DashVodRendererBuilder(String userAgent, String url, String contentId,
public DashRendererBuilder(String userAgent, String url, String contentId,
MediaDrmCallback drmCallback, TextView debugTextView) {
this.userAgent = userAgent;
this.url = url;
@ -96,8 +102,8 @@ public class DashVodRendererBuilder implements RendererBuilder,
this.player = player;
this.callback = callback;
MediaPresentationDescriptionParser parser = new MediaPresentationDescriptionParser();
ManifestFetcher<MediaPresentationDescription> manifestFetcher =
new ManifestFetcher<MediaPresentationDescription>(parser, contentId, url, userAgent);
manifestFetcher = new ManifestFetcher<MediaPresentationDescription>(parser, contentId, url,
userAgent);
manifestFetcher.singleLoad(player.getMainHandler().getLooper(), this);
}
@ -108,38 +114,17 @@ public class DashVodRendererBuilder implements RendererBuilder,
@Override
public void onManifest(String contentId, MediaPresentationDescription manifest) {
Period period = manifest.periods.get(0);
Handler mainHandler = player.getMainHandler();
LoadControl loadControl = new DefaultLoadControl(new BufferPool(BUFFER_SEGMENT_SIZE));
DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(mainHandler, player);
// Obtain Representations for playback.
int maxDecodableFrameSize = MediaCodecUtil.maxH264DecodableFrameSize();
ArrayList<Representation> audioRepresentationsList = new ArrayList<Representation>();
ArrayList<Representation> videoRepresentationsList = new ArrayList<Representation>();
Period period = manifest.periods.get(0);
boolean hasContentProtection = false;
for (int i = 0; i < period.adaptationSets.size(); i++) {
AdaptationSet adaptationSet = period.adaptationSets.get(i);
hasContentProtection |= adaptationSet.hasContentProtection();
int adaptationSetType = adaptationSet.type;
for (int j = 0; j < adaptationSet.representations.size(); j++) {
Representation representation = adaptationSet.representations.get(j);
if (adaptationSetType == AdaptationSet.TYPE_AUDIO) {
audioRepresentationsList.add(representation);
} else if (adaptationSetType == AdaptationSet.TYPE_VIDEO) {
Format format = representation.format;
if (format.width * format.height <= maxDecodableFrameSize) {
videoRepresentationsList.add(representation);
} else {
// The device isn't capable of playing this stream.
}
}
}
}
Representation[] videoRepresentations = new Representation[videoRepresentationsList.size()];
videoRepresentationsList.toArray(videoRepresentations);
int videoAdaptationSetIndex = period.getAdaptationSetIndex(AdaptationSet.TYPE_VIDEO);
AdaptationSet videoAdaptationSet = period.adaptationSets.get(videoAdaptationSetIndex);
// Check drm support if necessary.
boolean hasContentProtection = videoAdaptationSet.hasContentProtection();
boolean filterHdContent = false;
DrmSessionManager drmSessionManager = null;
if (hasContentProtection) {
if (Util.SDK_INT < 18) {
@ -151,55 +136,81 @@ public class DashVodRendererBuilder implements RendererBuilder,
Pair<DrmSessionManager, Boolean> drmSessionManagerData =
V18Compat.getDrmSessionManagerData(player, drmCallback);
drmSessionManager = drmSessionManagerData.first;
if (!drmSessionManagerData.second) {
// HD streams require L1 security.
videoRepresentations = getSdRepresentations(videoRepresentations);
}
// HD streams require L1 security.
filterHdContent = !drmSessionManagerData.second;
} catch (Exception e) {
callback.onRenderersError(e);
return;
}
}
// Build the video renderer.
DataSource videoDataSource = new UriDataSource(userAgent, bandwidthMeter);
ChunkSource videoChunkSource;
String mimeType = videoRepresentations[0].format.mimeType;
if (mimeType.equals(MimeTypes.VIDEO_MP4) || mimeType.equals(MimeTypes.VIDEO_WEBM)) {
videoChunkSource = new DashChunkSource(videoDataSource,
new AdaptiveEvaluator(bandwidthMeter), videoRepresentations);
} else {
throw new IllegalStateException("Unexpected mime type: " + mimeType);
// Determine which video representations we should use for playback.
int maxDecodableFrameSize = MediaCodecUtil.maxH264DecodableFrameSize();
List<Representation> videoRepresentations = videoAdaptationSet.representations;
ArrayList<Integer> videoRepresentationIndexList = new ArrayList<Integer>();
for (int i = 0; i < videoRepresentations.size(); i++) {
Format format = videoRepresentations.get(i).format;
if (filterHdContent && (format.width >= 1280 || format.height >= 720)) {
// Filtering HD content
} else if (format.width * format.height > maxDecodableFrameSize) {
// Filtering stream that device cannot play
} else if (!format.mimeType.equals(MimeTypes.VIDEO_MP4)
&& !format.mimeType.equals(MimeTypes.VIDEO_WEBM)) {
// Filtering unsupported mime type
} else {
videoRepresentationIndexList.add(i);
}
}
// Build the video renderer.
final MediaCodecVideoTrackRenderer videoRenderer;
final TrackRenderer debugRenderer;
if (videoRepresentationIndexList.isEmpty()) {
videoRenderer = null;
debugRenderer = null;
} else {
int[] videoRepresentationIndices = Util.toArray(videoRepresentationIndexList);
DataSource videoDataSource = new UriDataSource(userAgent, bandwidthMeter);
ChunkSource videoChunkSource = new DashChunkSource(manifestFetcher, videoAdaptationSetIndex,
videoRepresentationIndices, videoDataSource, new AdaptiveEvaluator(bandwidthMeter),
LIVE_EDGE_LATENCY_MS);
ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl,
VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true, mainHandler, player,
DemoPlayer.TYPE_VIDEO);
videoRenderer = new MediaCodecVideoTrackRenderer(videoSampleSource, drmSessionManager, true,
MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 5000, null, mainHandler, player, 50);
debugRenderer = debugTextView != null
? new DebugTrackRenderer(debugTextView, videoRenderer, videoSampleSource) : null;
}
// Build the audio chunk sources.
int audioAdaptationSetIndex = period.getAdaptationSetIndex(AdaptationSet.TYPE_AUDIO);
AdaptationSet audioAdaptationSet = period.adaptationSets.get(audioAdaptationSetIndex);
DataSource audioDataSource = new UriDataSource(userAgent, bandwidthMeter);
FormatEvaluator audioEvaluator = new FormatEvaluator.FixedEvaluator();
List<ChunkSource> audioChunkSourceList = new ArrayList<ChunkSource>();
List<String> audioTrackNameList = new ArrayList<String>();
List<Representation> audioRepresentations = audioAdaptationSet.representations;
for (int i = 0; i < audioRepresentations.size(); i++) {
Format format = audioRepresentations.get(i).format;
audioTrackNameList.add(format.id + " (" + format.numChannels + "ch, " +
format.audioSamplingRate + "Hz)");
audioChunkSourceList.add(new DashChunkSource(manifestFetcher, audioAdaptationSetIndex,
new int[] {i}, audioDataSource, audioEvaluator, LIVE_EDGE_LATENCY_MS));
}
ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl,
VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true, mainHandler, player,
DemoPlayer.TYPE_VIDEO);
MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(videoSampleSource,
drmSessionManager, true, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 5000, null,
mainHandler, player, 50);
// Build the audio renderer.
final String[] audioTrackNames;
final MultiTrackChunkSource audioChunkSource;
final TrackRenderer audioRenderer;
if (audioRepresentationsList.isEmpty()) {
if (audioChunkSourceList.isEmpty()) {
audioTrackNames = null;
audioChunkSource = null;
audioRenderer = null;
} else {
DataSource audioDataSource = new UriDataSource(userAgent, bandwidthMeter);
audioTrackNames = new String[audioRepresentationsList.size()];
ChunkSource[] audioChunkSources = new ChunkSource[audioRepresentationsList.size()];
FormatEvaluator audioEvaluator = new FormatEvaluator.FixedEvaluator();
for (int i = 0; i < audioRepresentationsList.size(); i++) {
Representation representation = audioRepresentationsList.get(i);
Format format = representation.format;
audioTrackNames[i] = format.id + " (" + format.numChannels + "ch, " +
format.audioSamplingRate + "Hz)";
audioChunkSources[i] = new DashChunkSource(audioDataSource,
audioEvaluator, representation);
}
audioChunkSource = new MultiTrackChunkSource(audioChunkSources);
audioTrackNames = new String[audioTrackNameList.size()];
audioTrackNameList.toArray(audioTrackNames);
audioChunkSource = new MultiTrackChunkSource(audioChunkSourceList);
SampleSource audioSampleSource = new ChunkSampleSource(audioChunkSource, loadControl,
AUDIO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true, mainHandler, player,
DemoPlayer.TYPE_AUDIO);
@ -207,37 +218,61 @@ public class DashVodRendererBuilder implements RendererBuilder,
mainHandler, player);
}
// Build the debug renderer.
TrackRenderer debugRenderer = debugTextView != null
? new DebugTrackRenderer(debugTextView, videoRenderer, videoSampleSource) : null;
// Build the text chunk sources.
DataSource textDataSource = new UriDataSource(userAgent, bandwidthMeter);
FormatEvaluator textEvaluator = new FormatEvaluator.FixedEvaluator();
List<ChunkSource> textChunkSourceList = new ArrayList<ChunkSource>();
List<String> textTrackNameList = new ArrayList<String>();
for (int i = 0; i < period.adaptationSets.size(); i++) {
AdaptationSet adaptationSet = period.adaptationSets.get(i);
if (adaptationSet.type == AdaptationSet.TYPE_TEXT) {
List<Representation> representations = adaptationSet.representations;
for (int j = 0; j < representations.size(); j++) {
Representation representation = representations.get(j);
textTrackNameList.add(representation.format.id);
textChunkSourceList.add(new DashChunkSource(manifestFetcher, i, new int[] {j},
textDataSource, textEvaluator, LIVE_EDGE_LATENCY_MS));
}
}
}
// Build the text renderers
final String[] textTrackNames;
final MultiTrackChunkSource textChunkSource;
final TrackRenderer textRenderer;
if (textChunkSourceList.isEmpty()) {
textTrackNames = null;
textChunkSource = null;
textRenderer = null;
} else {
textTrackNames = new String[textTrackNameList.size()];
textTrackNameList.toArray(textTrackNames);
textChunkSource = new MultiTrackChunkSource(textChunkSourceList);
SampleSource textSampleSource = new ChunkSampleSource(textChunkSource, loadControl,
TEXT_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true, mainHandler, player,
DemoPlayer.TYPE_TEXT);
textRenderer = new TextTrackRenderer(textSampleSource, new WebvttParser(), player,
mainHandler.getLooper());
}
// Invoke the callback.
String[][] trackNames = new String[DemoPlayer.RENDERER_COUNT][];
trackNames[DemoPlayer.TYPE_AUDIO] = audioTrackNames;
trackNames[DemoPlayer.TYPE_TEXT] = textTrackNames;
MultiTrackChunkSource[] multiTrackChunkSources =
new MultiTrackChunkSource[DemoPlayer.RENDERER_COUNT];
multiTrackChunkSources[DemoPlayer.TYPE_AUDIO] = audioChunkSource;
multiTrackChunkSources[DemoPlayer.TYPE_TEXT] = textChunkSource;
TrackRenderer[] renderers = new TrackRenderer[DemoPlayer.RENDERER_COUNT];
renderers[DemoPlayer.TYPE_VIDEO] = videoRenderer;
renderers[DemoPlayer.TYPE_AUDIO] = audioRenderer;
renderers[DemoPlayer.TYPE_TEXT] = textRenderer;
renderers[DemoPlayer.TYPE_DEBUG] = debugRenderer;
callback.onRenderers(trackNames, multiTrackChunkSources, renderers);
}
private Representation[] getSdRepresentations(Representation[] representations) {
ArrayList<Representation> sdRepresentations = new ArrayList<Representation>();
for (int i = 0; i < representations.length; i++) {
if (representations[i].format.height < 720 && representations[i].format.width < 1280) {
sdRepresentations.add(representations[i]);
}
}
Representation[] sdRepresentationArray = new Representation[sdRepresentations.size()];
sdRepresentations.toArray(sdRepresentationArray);
return sdRepresentationArray;
}
@TargetApi(18)
private static class V18Compat {

View file

@ -64,7 +64,7 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder,
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 int TTML_BUFFER_SEGMENTS = 2;
private static final int TEXT_BUFFER_SEGMENTS = 2;
private static final int LIVE_EDGE_LATENCY_MS = 30000;
private final String userAgent;
@ -149,10 +149,7 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder,
}
}
}
int[] videoTrackIndices = new int[videoTrackIndexList.size()];
for (int i = 0; i < videoTrackIndexList.size(); i++) {
videoTrackIndices[i] = videoTrackIndexList.get(i);
}
int[] videoTrackIndices = Util.toArray(videoTrackIndexList);
// Build the video renderer.
DataSource videoDataSource = new UriDataSource(userAgent, bandwidthMeter);
@ -221,7 +218,7 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder,
}
textChunkSource = new MultiTrackChunkSource(textChunkSources);
ChunkSampleSource ttmlSampleSource = new ChunkSampleSource(textChunkSource, loadControl,
TTML_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true, mainHandler, player,
TEXT_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true, mainHandler, player,
DemoPlayer.TYPE_TEXT);
textRenderer = new TextTrackRenderer(ttmlSampleSource, new TtmlParser(), player,
mainHandler.getLooper());

View file

@ -40,22 +40,26 @@ import com.google.android.exoplayer.upstream.DefaultBandwidthMeter;
import com.google.android.exoplayer.upstream.UriDataSource;
import com.google.android.exoplayer.util.ManifestFetcher;
import com.google.android.exoplayer.util.ManifestFetcher.ManifestCallback;
import com.google.android.exoplayer.util.MimeTypes;
import com.google.android.exoplayer.util.Util;
import android.media.MediaCodec;
import android.os.Handler;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* A {@link RendererBuilder} for DASH VOD.
* A {@link RendererBuilder} for DASH.
*/
/* package */ class DashVodRendererBuilder implements RendererBuilder,
/* package */ class DashRendererBuilder implements RendererBuilder,
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 static final int LIVE_EDGE_LATENCY_MS = 30000;
private final SimplePlayerActivity playerActivity;
private final String userAgent;
@ -63,8 +67,9 @@ import java.util.ArrayList;
private final String contentId;
private RendererBuilderCallback callback;
private ManifestFetcher<MediaPresentationDescription> manifestFetcher;
public DashVodRendererBuilder(SimplePlayerActivity playerActivity, String userAgent, String url,
public DashRendererBuilder(SimplePlayerActivity playerActivity, String userAgent, String url,
String contentId) {
this.playerActivity = playerActivity;
this.userAgent = userAgent;
@ -76,8 +81,8 @@ import java.util.ArrayList;
public void buildRenderers(RendererBuilderCallback callback) {
this.callback = callback;
MediaPresentationDescriptionParser parser = new MediaPresentationDescriptionParser();
ManifestFetcher<MediaPresentationDescription> manifestFetcher =
new ManifestFetcher<MediaPresentationDescription>(parser, contentId, url, userAgent);
manifestFetcher = new ManifestFetcher<MediaPresentationDescription>(parser, contentId, url,
userAgent);
manifestFetcher.singleLoad(playerActivity.getMainLooper(), this);
}
@ -88,48 +93,50 @@ import java.util.ArrayList;
@Override
public void onManifest(String contentId, MediaPresentationDescription manifest) {
Period period = manifest.periods.get(0);
Handler mainHandler = playerActivity.getMainHandler();
LoadControl loadControl = new DefaultLoadControl(new BufferPool(BUFFER_SEGMENT_SIZE));
DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();
// Obtain Representations for playback.
// Determine which video representations we should use for playback.
int maxDecodableFrameSize = MediaCodecUtil.maxH264DecodableFrameSize();
Representation audioRepresentation = null;
ArrayList<Representation> videoRepresentationsList = new ArrayList<Representation>();
Period period = manifest.periods.get(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);
if (audioRepresentation == null && adaptationSetType == AdaptationSet.TYPE_AUDIO) {
audioRepresentation = representation;
} else if (adaptationSetType == AdaptationSet.TYPE_VIDEO) {
Format format = representation.format;
if (format.width * format.height <= maxDecodableFrameSize) {
videoRepresentationsList.add(representation);
} else {
// The device isn't capable of playing this stream.
}
}
int videoAdaptationSetIndex = period.getAdaptationSetIndex(AdaptationSet.TYPE_VIDEO);
List<Representation> videoRepresentations =
period.adaptationSets.get(videoAdaptationSetIndex).representations;
ArrayList<Integer> videoRepresentationIndexList = new ArrayList<Integer>();
for (int i = 0; i < videoRepresentations.size(); i++) {
Format format = videoRepresentations.get(i).format;
if (format.width * format.height > maxDecodableFrameSize) {
// Filtering stream that device cannot play
} else if (!format.mimeType.equals(MimeTypes.VIDEO_MP4)
&& !format.mimeType.equals(MimeTypes.VIDEO_WEBM)) {
// Filtering unsupported mime type
} else {
videoRepresentationIndexList.add(i);
}
}
Representation[] videoRepresentations = new Representation[videoRepresentationsList.size()];
videoRepresentationsList.toArray(videoRepresentations);
// Build the video renderer.
DataSource videoDataSource = new UriDataSource(userAgent, bandwidthMeter);
ChunkSource videoChunkSource = new DashChunkSource(videoDataSource,
new AdaptiveEvaluator(bandwidthMeter), videoRepresentations);
ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl,
VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true);
MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(videoSampleSource,
MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 0, mainHandler, playerActivity, 50);
final MediaCodecVideoTrackRenderer videoRenderer;
if (videoRepresentationIndexList.isEmpty()) {
videoRenderer = null;
} else {
int[] videoRepresentationIndices = Util.toArray(videoRepresentationIndexList);
DataSource videoDataSource = new UriDataSource(userAgent, bandwidthMeter);
ChunkSource videoChunkSource = new DashChunkSource(manifestFetcher, videoAdaptationSetIndex,
videoRepresentationIndices, videoDataSource, new AdaptiveEvaluator(bandwidthMeter),
LIVE_EDGE_LATENCY_MS);
ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl,
VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true);
videoRenderer = new MediaCodecVideoTrackRenderer(videoSampleSource,
MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 0, mainHandler, playerActivity, 50);
}
// Build the audio renderer.
int audioAdaptationSetIndex = period.getAdaptationSetIndex(AdaptationSet.TYPE_AUDIO);
DataSource audioDataSource = new UriDataSource(userAgent, bandwidthMeter);
ChunkSource audioChunkSource = new DashChunkSource(audioDataSource,
new FormatEvaluator.FixedEvaluator(), audioRepresentation);
ChunkSource audioChunkSource = new DashChunkSource(manifestFetcher, audioAdaptationSetIndex,
new int[] {0}, audioDataSource, new FormatEvaluator.FixedEvaluator(), LIVE_EDGE_LATENCY_MS);
SampleSource audioSampleSource = new ChunkSampleSource(audioChunkSource, loadControl,
AUDIO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true);
MediaCodecAudioTrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer(

View file

@ -164,8 +164,8 @@ public class SimplePlayerActivity extends Activity implements SurfaceHolder.Call
case DemoUtil.TYPE_SS:
return new SmoothStreamingRendererBuilder(this, userAgent, contentUri.toString(),
contentId);
case DemoUtil.TYPE_DASH_VOD:
return new DashVodRendererBuilder(this, userAgent, contentUri.toString(), contentId);
case DemoUtil.TYPE_DASH:
return new DashRendererBuilder(this, userAgent, contentUri.toString(), contentId);
case DemoUtil.TYPE_HLS:
return new HlsRendererBuilder(this, userAgent, contentUri.toString(), contentId);
default:

View file

@ -38,6 +38,7 @@ import com.google.android.exoplayer.upstream.DefaultBandwidthMeter;
import com.google.android.exoplayer.upstream.UriDataSource;
import com.google.android.exoplayer.util.ManifestFetcher;
import com.google.android.exoplayer.util.ManifestFetcher.ManifestCallback;
import com.google.android.exoplayer.util.Util;
import android.media.MediaCodec;
import android.os.Handler;
@ -115,10 +116,7 @@ import java.util.ArrayList;
}
}
}
int[] videoTrackIndices = new int[videoTrackIndexList.size()];
for (int i = 0; i < videoTrackIndexList.size(); i++) {
videoTrackIndices[i] = videoTrackIndexList.get(i);
}
int[] videoTrackIndices = Util.toArray(videoTrackIndexList);
// Build the video renderer.
DataSource videoDataSource = new UriDataSource(userAgent, bandwidthMeter);

View file

@ -20,6 +20,16 @@ package com.google.android.exoplayer;
*/
public final class C {
/**
* Represents an unknown microsecond time or duration.
*/
public static final long UNKNOWN_TIME_US = -1L;
/**
* The number of microseconds in one second.
*/
public static final long MICROS_PER_SECOND = 1000000L;
/**
* Represents an unbounded length of data.
*/

View file

@ -71,10 +71,10 @@ public final class FrameworkSampleSource implements SampleSource {
trackInfos = new TrackInfo[trackStates.length];
for (int i = 0; i < trackStates.length; i++) {
android.media.MediaFormat format = extractor.getTrackFormat(i);
long duration = format.containsKey(android.media.MediaFormat.KEY_DURATION) ?
format.getLong(android.media.MediaFormat.KEY_DURATION) : TrackRenderer.UNKNOWN_TIME_US;
long durationUs = format.containsKey(android.media.MediaFormat.KEY_DURATION) ?
format.getLong(android.media.MediaFormat.KEY_DURATION) : C.UNKNOWN_TIME_US;
String mime = format.getString(android.media.MediaFormat.KEY_MIME);
trackInfos[i] = new TrackInfo(mime, duration);
trackInfos[i] = new TrackInfo(mime, durationUs);
}
prepared = true;
}

View file

@ -20,9 +20,21 @@ package com.google.android.exoplayer;
*/
public final class TrackInfo {
/**
* The mime type.
*/
public final String mimeType;
/**
* The duration in microseconds, or {@link C#UNKNOWN_TIME_US} if the duration is unknown.
*/
public final long durationUs;
/**
* @param mimeType The mime type.
* @param durationUs The duration in microseconds, or {@link C#UNKNOWN_TIME_US} if the duration
* is unknown.
*/
public TrackInfo(String mimeType, long durationUs) {
this.mimeType = mimeType;
this.durationUs = durationUs;

View file

@ -67,9 +67,9 @@ public abstract class TrackRenderer implements ExoPlayerComponent {
protected static final int STATE_STARTED = 3;
/**
* Represents an unknown time or duration.
* Represents an unknown time or duration. Equal to {@link C#UNKNOWN_TIME_US}.
*/
public static final long UNKNOWN_TIME_US = -1;
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.

View file

@ -40,7 +40,7 @@ public final class AudioCapabilitiesReceiver {
}
/** Default to stereo PCM on SDK <= 21 and when HDMI is unplugged. */
/** Default to stereo PCM on SDK < 21 and when HDMI is unplugged. */
private static final AudioCapabilities DEFAULT_AUDIO_CAPABILITIES =
new AudioCapabilities(new int[] {AudioFormat.ENCODING_PCM_16BIT}, 2);

View file

@ -15,9 +15,11 @@
*/
package com.google.android.exoplayer.audio;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.Util;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.media.AudioFormat;
import android.media.AudioManager;
@ -80,22 +82,23 @@ public final class AudioTrack {
private static final String TAG = "AudioTrack";
private static final long MICROS_PER_SECOND = 1000000L;
/**
* AudioTrack timestamps are deemed spurious if they are offset from the system clock by more
* than this amount.
*
* <p>This is a fail safe that should not be required on correctly functioning devices.
*/
private static final long MAX_AUDIO_TIMESTAMP_OFFSET_US = 10 * MICROS_PER_SECOND;
private static final long MAX_AUDIO_TIMESTAMP_OFFSET_US = 10 * C.MICROS_PER_SECOND;
/**
* AudioTrack latencies are deemed impossibly large if they are greater than this amount.
*
* <p>This is a fail safe that should not be required on correctly functioning devices.
*/
private static final long MAX_LATENCY_US = 10 * MICROS_PER_SECOND;
private static final long MAX_LATENCY_US = 10 * C.MICROS_PER_SECOND;
/** Value for ac3Bitrate before the bitrate has been calculated. */
private static final int UNKNOWN_AC3_BITRATE = 0;
private static final int START_NOT_SET = 0;
private static final int START_IN_SYNC = 1;
@ -139,6 +142,11 @@ public final class AudioTrack {
private int temporaryBufferOffset;
private int temporaryBufferSize;
private boolean isAc3;
/** Bitrate measured in kilobits per second, if {@link #isAc3} is true. */
private int ac3Bitrate;
/** Constructs an audio track using the default minimum buffer size multiplier. */
public AudioTrack() {
this(DEFAULT_MIN_BUFFER_MULTIPLICATION_FACTOR);
@ -277,6 +285,7 @@ public final class AudioTrack {
* @param bufferSize The total size of the playback buffer in bytes. Specify 0 to use a buffer
* size based on the minimum for format.
*/
@SuppressLint("InlinedApi")
public void reconfigure(MediaFormat format, int encoding, int bufferSize) {
int channelCount = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
int channelConfig;
@ -300,8 +309,9 @@ public final class AudioTrack {
int sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE);
// TODO: Does channelConfig determine channelCount?
boolean isAc3 = encoding == AudioFormat.ENCODING_AC3 || encoding == AudioFormat.ENCODING_E_AC3;
if (audioTrack != null && this.sampleRate == sampleRate
&& this.channelConfig == channelConfig) {
&& this.channelConfig == channelConfig && !this.isAc3 && !isAc3) {
// We already have an existing audio track with the correct sample rate and channel config.
return;
}
@ -315,7 +325,8 @@ public final class AudioTrack {
bufferSize == 0 ? (int) (minBufferMultiplicationFactor * minBufferSize) : bufferSize;
this.sampleRate = sampleRate;
this.channelConfig = channelConfig;
this.isAc3 = isAc3;
ac3Bitrate = UNKNOWN_AC3_BITRATE; // Calculated on receiving the first buffer if isAc3 is true.
frameSize = 2 * channelCount; // 2 bytes per 16 bit sample * number of channels.
}
@ -353,6 +364,14 @@ public final class AudioTrack {
int result = 0;
if (temporaryBufferSize == 0 && size != 0) {
if (isAc3 && ac3Bitrate == UNKNOWN_AC3_BITRATE) {
// Each AC-3 buffer contains 1536 frames of audio, so the AudioTrack playback position
// advances by 1536 per buffer (32 ms at 48 kHz). Calculate the bitrate in kbit/s.
int unscaledAc3Bitrate = size * 8 * sampleRate;
int divisor = 1000 * 1536;
ac3Bitrate = (unscaledAc3Bitrate + divisor / 2) / divisor;
}
// This is the first time we've seen this {@code buffer}.
// Note: presentationTimeUs corresponds to the end of the sample, not the start.
long bufferStartTime = presentationTimeUs - framesToDurationUs(bytesToFrames(size));
@ -616,19 +635,24 @@ public final class AudioTrack {
}
private long framesToBytes(long frameCount) {
// This method is unused on SDK >= 21.
return frameCount * frameSize;
}
private long bytesToFrames(long byteCount) {
return byteCount / frameSize;
if (isAc3) {
return byteCount * 8 * sampleRate / (1000 * ac3Bitrate);
} else {
return byteCount / frameSize;
}
}
private long framesToDurationUs(long frameCount) {
return (frameCount * MICROS_PER_SECOND) / sampleRate;
return (frameCount * C.MICROS_PER_SECOND) / sampleRate;
}
private long durationUsToFrames(long durationUs) {
return (durationUs * sampleRate) / MICROS_PER_SECOND;
return (durationUs * sampleRate) / C.MICROS_PER_SECOND;
}
private void resetSyncParams() {

View file

@ -46,6 +46,10 @@ public class MultiTrackChunkSource implements ChunkSource, ExoPlayerComponent {
this.selectedSource = sources[0];
}
public MultiTrackChunkSource(List<ChunkSource> sources) {
this(toChunkSourceArray(sources));
}
/**
* Gets the number of tracks that this source can switch between. May be called safely from any
* thread.
@ -107,4 +111,10 @@ public class MultiTrackChunkSource implements ChunkSource, ExoPlayerComponent {
selectedSource.onChunkLoadError(chunk, e);
}
private static ChunkSource[] toChunkSourceArray(List<ChunkSource> sources) {
ChunkSource[] chunkSourceArray = new ChunkSource[sources.size()];
sources.toArray(chunkSourceArray);
return chunkSourceArray;
}
}

View file

@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer.chunk;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.TrackInfo;
import com.google.android.exoplayer.upstream.DataSource;
@ -42,7 +43,8 @@ public class SingleSampleChunkSource implements ChunkSource {
* @param dataSource A {@link DataSource} suitable for loading the sample data.
* @param dataSpec Defines the location of the sample.
* @param format The format of the sample.
* @param durationUs The duration of the sample in microseconds.
* @param durationUs The duration of the sample in microseconds, or {@link C#UNKNOWN_TIME_US} if
* the duration is unknown.
* @param mediaFormat The sample media format. May be null.
*/
public SingleSampleChunkSource(DataSource dataSource, DataSpec dataSpec, Format format,

View file

@ -15,9 +15,11 @@
*/
package com.google.android.exoplayer.dash;
import com.google.android.exoplayer.BehindLiveWindowException;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.TrackInfo;
import com.google.android.exoplayer.TrackRenderer;
import com.google.android.exoplayer.chunk.Chunk;
import com.google.android.exoplayer.chunk.ChunkOperationHolder;
import com.google.android.exoplayer.chunk.ChunkSource;
@ -27,74 +29,175 @@ import com.google.android.exoplayer.chunk.FormatEvaluator;
import com.google.android.exoplayer.chunk.FormatEvaluator.Evaluation;
import com.google.android.exoplayer.chunk.MediaChunk;
import com.google.android.exoplayer.chunk.Mp4MediaChunk;
import com.google.android.exoplayer.chunk.SingleSampleMediaChunk;
import com.google.android.exoplayer.dash.mpd.AdaptationSet;
import com.google.android.exoplayer.dash.mpd.MediaPresentationDescription;
import com.google.android.exoplayer.dash.mpd.Period;
import com.google.android.exoplayer.dash.mpd.RangedUri;
import com.google.android.exoplayer.dash.mpd.Representation;
import com.google.android.exoplayer.parser.Extractor;
import com.google.android.exoplayer.parser.mp4.FragmentedMp4Extractor;
import com.google.android.exoplayer.parser.webm.WebmExtractor;
import com.google.android.exoplayer.text.webvtt.WebvttParser;
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DataSpec;
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
import com.google.android.exoplayer.util.ManifestFetcher;
import com.google.android.exoplayer.util.MimeTypes;
import android.net.Uri;
import android.os.SystemClock;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
/**
* An {@link ChunkSource} for DASH streams.
* <p>
* This implementation currently supports fMP4 and webm.
* This implementation currently supports fMP4, webm, and webvtt.
*/
public class DashChunkSource implements ChunkSource {
/**
* Thrown when an AdaptationSet is missing from the MPD.
*/
public static class NoAdaptationSetException extends IOException {
public NoAdaptationSetException(String message) {
super(message);
}
}
/**
* Specifies that we should process all tracks.
*/
public static final int USE_ALL_TRACKS = -1;
private final TrackInfo trackInfo;
private final DataSource dataSource;
private final FormatEvaluator evaluator;
private final Evaluation evaluation;
private final StringBuilder headerBuilder;
private final long liveEdgeLatencyUs;
private final int maxWidth;
private final int maxHeight;
private final Format[] formats;
private final HashMap<String, Representation> representations;
private final HashMap<String, Extractor> extractors;
private final HashMap<String, DashSegmentIndex> segmentIndexes;
private final HashMap<String, RepresentationHolder> representationHolders;
private final ManifestFetcher<MediaPresentationDescription> manifestFetcher;
private final int adaptationSetIndex;
private final int[] representationIndices;
private MediaPresentationDescription currentManifest;
private boolean finishedCurrentManifest;
private boolean lastChunkWasInitialization;
private IOException fatalError;
/**
* Lightweight constructor to use for fixed duration content.
*
* @param dataSource A {@link DataSource} suitable for loading the media data.
* @param evaluator Selects from the available formats.
* @param formatEvaluator Selects from the available formats.
* @param representations The representations to be considered by the source.
*/
public DashChunkSource(DataSource dataSource, FormatEvaluator evaluator,
public DashChunkSource(DataSource dataSource, FormatEvaluator formatEvaluator,
Representation... representations) {
this(buildManifest(Arrays.asList(representations)), 0, null, dataSource, formatEvaluator);
}
/**
* Lightweight constructor to use for fixed duration content.
*
* @param dataSource A {@link DataSource} suitable for loading the media data.
* @param formatEvaluator Selects from the available formats.
* @param representations The representations to be considered by the source.
*/
public DashChunkSource(DataSource dataSource, FormatEvaluator formatEvaluator,
List<Representation> representations) {
this(buildManifest(representations), 0, null, dataSource, formatEvaluator);
}
/**
* Constructor to use for fixed duration content.
*
* @param manifest The manifest.
* @param adaptationSetIndex The index of the adaptation set that should be used.
* @param representationIndices The indices of the representations within the adaptations set
* that should be used. May be null if all representations within the adaptation set should
* be considered.
* @param dataSource A {@link DataSource} suitable for loading the media data.
* @param formatEvaluator Selects from the available formats.
*/
public DashChunkSource(MediaPresentationDescription manifest, int adaptationSetIndex,
int[] representationIndices, DataSource dataSource, FormatEvaluator formatEvaluator) {
this(null, manifest, adaptationSetIndex, representationIndices, dataSource, formatEvaluator, 0);
}
/**
* Constructor to use for live streaming.
* <p>
* May also be used for fixed duration content, in which case the call is equivalent to calling
* the other constructor, passing {@code manifestFetcher.getManifest()} is the first argument.
*
* @param manifestFetcher A fetcher for the manifest, which must have already successfully
* completed an initial load.
* @param adaptationSetIndex The index of the adaptation set that should be used.
* @param representationIndices The indices of the representations within the adaptations set
* that should be used. May be null if all representations within the adaptation set should
* be considered.
* @param dataSource A {@link DataSource} suitable for loading the media data.
* @param formatEvaluator Selects from the available formats.
* @param liveEdgeLatencyMs For live streams, the number of milliseconds that the playback should
* lag behind the "live edge" (i.e. the end of the most recently defined media in the
* manifest). Choosing a small value will minimize latency introduced by the player, however
* note that the value sets an upper bound on the length of media that the player can buffer.
* Hence a small value may increase the probability of rebuffering and playback failures.
*/
public DashChunkSource(ManifestFetcher<MediaPresentationDescription> manifestFetcher,
int adaptationSetIndex, int[] representationIndices, DataSource dataSource,
FormatEvaluator formatEvaluator, long liveEdgeLatencyMs) {
this(manifestFetcher, manifestFetcher.getManifest(), adaptationSetIndex, representationIndices,
dataSource, formatEvaluator, liveEdgeLatencyMs * 1000);
}
private DashChunkSource(ManifestFetcher<MediaPresentationDescription> manifestFetcher,
MediaPresentationDescription initialManifest, int adaptationSetIndex,
int[] representationIndices, DataSource dataSource, FormatEvaluator formatEvaluator,
long liveEdgeLatencyUs) {
this.manifestFetcher = manifestFetcher;
this.currentManifest = initialManifest;
this.adaptationSetIndex = adaptationSetIndex;
this.representationIndices = representationIndices;
this.dataSource = dataSource;
this.evaluator = evaluator;
this.formats = new Format[representations.length];
this.extractors = new HashMap<String, Extractor>();
this.segmentIndexes = new HashMap<String, DashSegmentIndex>();
this.representations = new HashMap<String, Representation>();
this.trackInfo = new TrackInfo(representations[0].format.mimeType,
representations[0].periodDurationMs * 1000);
this.evaluator = formatEvaluator;
this.liveEdgeLatencyUs = liveEdgeLatencyUs;
this.evaluation = new Evaluation();
this.headerBuilder = new StringBuilder();
Representation[] representations = getFilteredRepresentations(currentManifest,
adaptationSetIndex, representationIndices);
long periodDurationUs = (representations[0].periodDurationMs == TrackRenderer.UNKNOWN_TIME_US)
? TrackRenderer.UNKNOWN_TIME_US : representations[0].periodDurationMs * 1000;
this.trackInfo = new TrackInfo(representations[0].format.mimeType, periodDurationUs);
this.formats = new Format[representations.length];
this.representationHolders = new HashMap<String, RepresentationHolder>();
int maxWidth = 0;
int maxHeight = 0;
for (int i = 0; i < representations.length; i++) {
formats[i] = representations[i].format;
maxWidth = Math.max(formats[i].width, maxWidth);
maxHeight = Math.max(formats[i].height, maxHeight);
Extractor extractor = mimeTypeIsWebm(formats[i].mimeType)
? new WebmExtractor() : new FragmentedMp4Extractor();
extractors.put(formats[i].id, extractor);
this.representations.put(formats[i].id, representations[i]);
DashSegmentIndex segmentIndex = representations[i].getIndex();
if (segmentIndex != null) {
segmentIndexes.put(formats[i].id, segmentIndex);
}
Extractor extractor = mimeTypeIsWebm(formats[i].mimeType) ? new WebmExtractor()
: new FragmentedMp4Extractor();
representationHolders.put(formats[i].id,
new RepresentationHolder(representations[i], extractor));
}
this.maxWidth = maxWidth;
this.maxHeight = maxHeight;
@ -116,21 +219,67 @@ public class DashChunkSource implements ChunkSource {
@Override
public void enable() {
evaluator.enable();
if (manifestFetcher != null) {
manifestFetcher.enable();
}
}
@Override
public void disable(List<? extends MediaChunk> queue) {
evaluator.disable();
if (manifestFetcher != null) {
manifestFetcher.disable();
}
}
@Override
public void continueBuffering(long playbackPositionUs) {
// Do nothing
if (manifestFetcher == null || !currentManifest.dynamic || fatalError != null) {
return;
}
MediaPresentationDescription newManifest = manifestFetcher.getManifest();
if (currentManifest != newManifest && newManifest != null) {
Representation[] newRepresentations = DashChunkSource.getFilteredRepresentations(newManifest,
adaptationSetIndex, representationIndices);
for (Representation representation : newRepresentations) {
RepresentationHolder representationHolder =
representationHolders.get(representation.format.id);
DashSegmentIndex oldIndex = representationHolder.segmentIndex;
DashSegmentIndex newIndex = representation.getIndex();
int newFirstSegmentNum = newIndex.getFirstSegmentNum();
int segmentNumShift = oldIndex.getSegmentNum(newIndex.getTimeUs(newFirstSegmentNum))
- newFirstSegmentNum;
representationHolder.segmentNumShift += segmentNumShift;
representationHolder.segmentIndex = newIndex;
}
currentManifest = newManifest;
finishedCurrentManifest = false;
}
// TODO: This is a temporary hack to avoid constantly refreshing the MPD in cases where
// minUpdatePeriod is set to 0. In such cases we shouldn't refresh unless there is explicit
// signaling in the stream, according to:
// http://azure.microsoft.com/blog/2014/09/13/dash-live-streaming-with-azure-media-service/
long minUpdatePeriod = currentManifest.minUpdatePeriod;
if (minUpdatePeriod == 0) {
minUpdatePeriod = 5000;
}
if (finishedCurrentManifest && (SystemClock.elapsedRealtime()
> manifestFetcher.getManifestLoadTimestamp() + minUpdatePeriod)) {
manifestFetcher.requestRefresh();
}
}
@Override
public final void getChunkOperation(List<? extends MediaChunk> queue, long seekPositionUs,
long playbackPositionUs, ChunkOperationHolder out) {
if (fatalError != null) {
out.chunk = null;
return;
}
evaluation.queueSize = queue.size();
if (evaluation.format == null || !lastChunkWasInitialization) {
evaluator.evaluate(queue, playbackPositionUs, formats, evaluation);
@ -148,17 +297,21 @@ public class DashChunkSource implements ChunkSource {
return;
}
Representation selectedRepresentation = representations.get(selectedFormat.id);
Extractor extractor = extractors.get(selectedRepresentation.format.id);
RepresentationHolder representationHolder = representationHolders.get(selectedFormat.id);
Representation selectedRepresentation = representationHolder.representation;
DashSegmentIndex segmentIndex = representationHolder.segmentIndex;
Extractor extractor = representationHolder.extractor;
RangedUri pendingInitializationUri = null;
RangedUri pendingIndexUri = null;
if (extractor.getFormat() == null) {
pendingInitializationUri = selectedRepresentation.getInitializationUri();
}
if (!segmentIndexes.containsKey(selectedRepresentation.format.id)) {
if (segmentIndex == null) {
pendingIndexUri = selectedRepresentation.getIndexUri();
}
if (pendingInitializationUri != null || pendingIndexUri != null) {
// We have initialization and/or index requests to make.
Chunk initializationChunk = newInitializationChunk(pendingInitializationUri, pendingIndexUri,
@ -168,28 +321,48 @@ public class DashChunkSource implements ChunkSource {
return;
}
int nextSegmentNum;
DashSegmentIndex segmentIndex = segmentIndexes.get(selectedRepresentation.format.id);
int segmentNum;
if (queue.isEmpty()) {
nextSegmentNum = segmentIndex.getSegmentNum(seekPositionUs);
if (currentManifest.dynamic) {
seekPositionUs = getLiveSeekPosition();
}
segmentNum = segmentIndex.getSegmentNum(seekPositionUs);
} else {
nextSegmentNum = queue.get(out.queueSize - 1).nextChunkIndex;
segmentNum = queue.get(out.queueSize - 1).nextChunkIndex
- representationHolder.segmentNumShift;
}
if (nextSegmentNum == -1) {
if (currentManifest.dynamic) {
if (segmentNum < segmentIndex.getFirstSegmentNum()) {
// This is before the first chunk in the current manifest.
fatalError = new BehindLiveWindowException();
return;
} else if (segmentNum > segmentIndex.getLastSegmentNum()) {
// This is beyond the last chunk in the current manifest.
finishedCurrentManifest = true;
return;
} else if (segmentNum == segmentIndex.getLastSegmentNum()) {
// This is the last chunk in the current manifest. Mark the manifest as being finished,
// but continue to return the final chunk.
finishedCurrentManifest = true;
}
}
if (segmentNum == -1) {
out.chunk = null;
return;
}
Chunk nextMediaChunk = newMediaChunk(selectedRepresentation, segmentIndex, extractor,
dataSource, nextSegmentNum, evaluation.trigger);
Chunk nextMediaChunk = newMediaChunk(representationHolder, dataSource, segmentNum,
evaluation.trigger);
lastChunkWasInitialization = false;
out.chunk = nextMediaChunk;
}
@Override
public IOException getError() {
return null;
return fatalError != null ? fatalError
: (manifestFetcher != null ? manifestFetcher.getError() : null);
}
@Override
@ -229,22 +402,90 @@ public class DashChunkSource implements ChunkSource {
}
DataSpec dataSpec = new DataSpec(requestUri.getUri(), requestUri.start, requestUri.length,
representation.getCacheKey());
return new InitializationLoadable(dataSource, dataSpec, trigger, representation.format,
extractor, expectedExtractorResult, indexAnchor);
}
private Chunk newMediaChunk(Representation representation, DashSegmentIndex segmentIndex,
Extractor extractor, DataSource dataSource, int segmentNum, int trigger) {
int lastSegmentNum = segmentIndex.getLastSegmentNum();
int nextSegmentNum = segmentNum == lastSegmentNum ? -1 : segmentNum + 1;
private Chunk newMediaChunk(RepresentationHolder representationHolder, DataSource dataSource,
int segmentNum, int trigger) {
Representation representation = representationHolder.representation;
DashSegmentIndex segmentIndex = representationHolder.segmentIndex;
long startTimeUs = segmentIndex.getTimeUs(segmentNum);
long endTimeUs = segmentNum < lastSegmentNum ? segmentIndex.getTimeUs(segmentNum + 1)
: startTimeUs + segmentIndex.getDurationUs(segmentNum);
long endTimeUs = startTimeUs + segmentIndex.getDurationUs(segmentNum);
boolean isLastSegment = !currentManifest.dynamic
&& segmentNum == segmentIndex.getLastSegmentNum();
int nextAbsoluteSegmentNum = isLastSegment ? -1
: (representationHolder.segmentNumShift + segmentNum + 1);
RangedUri segmentUri = segmentIndex.getSegmentUrl(segmentNum);
DataSpec dataSpec = new DataSpec(segmentUri.getUri(), segmentUri.start, segmentUri.length,
representation.getCacheKey());
return new Mp4MediaChunk(dataSource, dataSpec, representation.format, trigger, startTimeUs,
endTimeUs, nextSegmentNum, extractor, false, 0);
long presentationTimeOffsetUs = representation.presentationTimeOffsetMs * 1000;
if (representation.format.mimeType.equals(MimeTypes.TEXT_VTT)) {
if (representationHolder.vttHeaderOffsetUs != presentationTimeOffsetUs) {
// Update the VTT header.
headerBuilder.setLength(0);
headerBuilder.append(WebvttParser.EXO_HEADER).append("=")
.append(WebvttParser.OFFSET).append(presentationTimeOffsetUs).append("\n");
representationHolder.vttHeader = headerBuilder.toString().getBytes();
representationHolder.vttHeaderOffsetUs = presentationTimeOffsetUs;
}
return new SingleSampleMediaChunk(dataSource, dataSpec, representation.format, 0,
startTimeUs, endTimeUs, nextAbsoluteSegmentNum, null, representationHolder.vttHeader);
} else {
return new Mp4MediaChunk(dataSource, dataSpec, representation.format, trigger, startTimeUs,
endTimeUs, nextAbsoluteSegmentNum, representationHolder.extractor, false,
presentationTimeOffsetUs);
}
}
/**
* For live playbacks, determines the seek position that snaps playback to be
* {@link #liveEdgeLatencyUs} behind the live edge of the current manifest
*
* @return The seek position in microseconds.
*/
private long getLiveSeekPosition() {
long liveEdgeTimestampUs = Long.MIN_VALUE;
for (RepresentationHolder representationHolder : representationHolders.values()) {
DashSegmentIndex segmentIndex = representationHolder.segmentIndex;
int lastSegmentNum = segmentIndex.getLastSegmentNum();
long indexLiveEdgeTimestampUs = segmentIndex.getTimeUs(lastSegmentNum)
+ segmentIndex.getDurationUs(lastSegmentNum);
liveEdgeTimestampUs = Math.max(liveEdgeTimestampUs, indexLiveEdgeTimestampUs);
}
return liveEdgeTimestampUs - liveEdgeLatencyUs;
}
private static Representation[] getFilteredRepresentations(MediaPresentationDescription manifest,
int adaptationSetIndex, int[] representationIndices) {
List<Representation> representations =
manifest.periods.get(0).adaptationSets.get(adaptationSetIndex).representations;
if (representationIndices == null) {
Representation[] filteredRepresentations = new Representation[representations.size()];
representations.toArray(filteredRepresentations);
return filteredRepresentations;
} else {
Representation[] filteredRepresentations = new Representation[representationIndices.length];
for (int i = 0; i < representationIndices.length; i++) {
filteredRepresentations[i] = representations.get(representationIndices[i]);
}
return filteredRepresentations;
}
}
private static MediaPresentationDescription buildManifest(List<Representation> representations) {
Representation firstRepresentation = representations.get(0);
AdaptationSet adaptationSet = new AdaptationSet(0, AdaptationSet.TYPE_UNKNOWN, representations);
Period period = new Period(null, firstRepresentation.periodStartMs,
firstRepresentation.periodDurationMs, Collections.singletonList(adaptationSet));
long duration = firstRepresentation.periodDurationMs - firstRepresentation.periodStartMs;
return new MediaPresentationDescription(-1, duration, -1, false, -1, -1, null,
Collections.singletonList(period));
}
private class InitializationLoadable extends Chunk {
@ -272,11 +513,30 @@ public class DashChunkSource implements ChunkSource {
+ expectedExtractorResult + ", got " + result);
}
if ((result & Extractor.RESULT_READ_INDEX) != 0) {
segmentIndexes.put(format.id,
new DashWrappingSegmentIndex(extractor.getIndex(), uri, indexAnchor));
representationHolders.get(format.id).segmentIndex =
new DashWrappingSegmentIndex(extractor.getIndex(), uri, indexAnchor);
}
}
}
private static class RepresentationHolder {
public final Representation representation;
public final Extractor extractor;
public DashSegmentIndex segmentIndex;
public int segmentNumShift;
public long vttHeaderOffsetUs;
public byte[] vttHeader;
public RepresentationHolder(Representation representation, Extractor extractor) {
this.representation = representation;
this.extractor = extractor;
this.segmentIndex = representation.getIndex();
}
}
}

View file

@ -56,4 +56,21 @@ public class Period {
this.adaptationSets = Collections.unmodifiableList(adaptationSets);
}
/**
* Returns the index of the first adaptation set of a given type, or -1 if no adaptation set of
* the specified type exists.
*
* @param type An adaptation set type.
* @return The index of the first adaptation set of the specified type, or -1.
*/
public int getAdaptationSetIndex(int type) {
int adaptationCount = adaptationSets.size();
for (int i = 0; i < adaptationCount; i++) {
if (adaptationSets.get(i).type == type) {
return i;
}
}
return -1;
}
}

View file

@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer.dash.mpd;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.util.Util;
import android.net.Uri;
@ -141,11 +142,12 @@ public abstract class SegmentBase {
public final long getSegmentDurationUs(int sequenceNumber) {
if (segmentTimeline != null) {
return (segmentTimeline.get(sequenceNumber - startNumber).duration * 1000000) / timescale;
long duration = segmentTimeline.get(sequenceNumber - startNumber).duration;
return (duration * C.MICROS_PER_SECOND) / timescale;
} else {
return sequenceNumber == getLastSegmentNum()
? (periodDurationMs * 1000) - getSegmentTimeUs(sequenceNumber)
: ((duration * 1000000L) / timescale);
? ((periodDurationMs * 1000) - getSegmentTimeUs(sequenceNumber))
: ((duration * C.MICROS_PER_SECOND) / timescale);
}
}
@ -157,7 +159,7 @@ public abstract class SegmentBase {
} else {
unscaledSegmentTime = (sequenceNumber - startNumber) * duration;
}
return Util.scaleLargeTimestamp(unscaledSegmentTime, 1000000, timescale);
return Util.scaleLargeTimestamp(unscaledSegmentTime, C.MICROS_PER_SECOND, timescale);
}
public abstract RangedUri getSegmentUrl(Representation representation, int index);

View file

@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer.parser.mp4;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.SampleHolder;
@ -26,6 +27,7 @@ import com.google.android.exoplayer.upstream.NonBlockingInputStream;
import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.CodecSpecificDataUtil;
import com.google.android.exoplayer.util.MimeTypes;
import com.google.android.exoplayer.util.Util;
import android.annotation.SuppressLint;
import android.media.MediaCodec;
@ -1053,6 +1055,7 @@ public final class FragmentedMp4Extractor implements Extractor {
long offset = firstOffset;
long time = earliestPresentationTime;
long timeUs = Util.scaleLargeTimestamp(time, C.MICROS_PER_SECOND, timescale);
for (int i = 0; i < referenceCount; i++) {
int firstInt = atom.readInt();
@ -1067,10 +1070,10 @@ public final class FragmentedMp4Extractor implements Extractor {
// Calculate time and duration values such that any rounding errors are consistent. i.e. That
// timesUs[i] + durationsUs[i] == timesUs[i + 1].
timesUs[i] = (time * 1000000L) / timescale;
long nextTimeUs = ((time + referenceDuration) * 1000000L) / timescale;
durationsUs[i] = nextTimeUs - timesUs[i];
timesUs[i] = timeUs;
time += referenceDuration;
timeUs = Util.scaleLargeTimestamp(time, C.MICROS_PER_SECOND, timescale);
durationsUs[i] = timeUs - timesUs[i];
atom.skip(4);
offset += sizes[i];

View file

@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer.smoothstreaming;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.Util;
@ -31,30 +32,77 @@ import java.util.UUID;
*/
public class SmoothStreamingManifest {
private static final long MICROS_PER_SECOND = 1000000L;
/**
* The client manifest major version.
*/
public final int majorVersion;
/**
* The client manifest minor version.
*/
public final int minorVersion;
public final long timescale;
/**
* The number of fragments in a lookahead, or -1 if the lookahead is unspecified.
*/
public final int lookAheadCount;
/**
* True if the manifest describes a live presentation still in progress. False otherwise.
*/
public final boolean isLive;
/**
* Content protection information, or null if the content is not protected.
*/
public final ProtectionElement protectionElement;
/**
* The contained stream elements.
*/
public final StreamElement[] streamElements;
/**
* The overall presentation duration of the media in microseconds, or {@link C#UNKNOWN_TIME_US}
* if the duration is unknown.
*/
public final long durationUs;
/**
* The length of the trailing window for a live broadcast in microseconds, or
* {@link C#UNKNOWN_TIME_US} if the stream is not live or if the window length is unspecified.
*/
public final long dvrWindowLengthUs;
/**
* @param majorVersion The client manifest major version.
* @param minorVersion The client manifest minor version.
* @param timescale The timescale of the media as the number of units that pass in one second.
* @param duration The overall presentation duration in units of the timescale attribute, or 0
* if the duration is unknown.
* @param dvrWindowLength The length of the trailing window in units of the timescale attribute,
* or 0 if this attribute is unspecified or not applicable.
* @param lookAheadCount The number of fragments in a lookahead, or -1 if this attribute is
* unspecified or not applicable.
* @param isLive True if the manifest describes a live presentation still in progress. False
* otherwise.
* @param protectionElement Content protection information, or null if the content is not
* protected.
* @param streamElements The contained stream elements.
*/
public SmoothStreamingManifest(int majorVersion, int minorVersion, long timescale, long duration,
long dvrWindowLength, int lookAheadCount, boolean isLive, ProtectionElement protectionElement,
StreamElement[] streamElements) {
this.majorVersion = majorVersion;
this.minorVersion = minorVersion;
this.timescale = timescale;
this.lookAheadCount = lookAheadCount;
this.isLive = isLive;
this.protectionElement = protectionElement;
this.streamElements = streamElements;
dvrWindowLengthUs = Util.scaleLargeTimestamp(dvrWindowLength, MICROS_PER_SECOND, timescale);
durationUs = Util.scaleLargeTimestamp(duration, MICROS_PER_SECOND, timescale);
dvrWindowLengthUs = dvrWindowLength == 0 ? C.UNKNOWN_TIME_US
: Util.scaleLargeTimestamp(dvrWindowLength, C.MICROS_PER_SECOND, timescale);
durationUs = duration == 0 ? C.UNKNOWN_TIME_US
: Util.scaleLargeTimestamp(duration, C.MICROS_PER_SECOND, timescale);
}
/**
@ -176,9 +224,9 @@ public class SmoothStreamingManifest {
this.chunkCount = chunkStartTimes.size();
this.chunkStartTimes = chunkStartTimes;
lastChunkDurationUs =
Util.scaleLargeTimestamp(lastChunkDuration, MICROS_PER_SECOND, timescale);
Util.scaleLargeTimestamp(lastChunkDuration, C.MICROS_PER_SECOND, timescale);
chunkStartTimesUs =
Util.scaleLargeTimestamps(chunkStartTimes, MICROS_PER_SECOND, timescale);
Util.scaleLargeTimestamps(chunkStartTimes, C.MICROS_PER_SECOND, timescale);
}
/**

View file

@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer.text.ttml;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.text.Subtitle;
import com.google.android.exoplayer.text.SubtitleParser;
@ -254,7 +255,7 @@ public class TtmlParser implements SubtitleParser {
String subframes = matcher.group(6);
durationSeconds += (subframes != null) ?
((double) Long.parseLong(subframes)) / subframeRate / frameRate : 0;
return (long) (durationSeconds * 1000000);
return (long) (durationSeconds * C.MICROS_PER_SECOND);
}
matcher = OFFSET_TIME.matcher(time);
if (matcher.matches()) {
@ -274,7 +275,7 @@ public class TtmlParser implements SubtitleParser {
} else if (unit.equals("t")) {
offsetSeconds /= tickRate;
}
return (long) (offsetSeconds * 1000000);
return (long) (offsetSeconds * C.MICROS_PER_SECOND);
}
throw new ParserException("Malformed time expression: " + time);
}

View file

@ -18,6 +18,7 @@ package com.google.android.exoplayer.util;
import com.google.android.exoplayer.upstream.Loader;
import com.google.android.exoplayer.upstream.Loader.Loadable;
import android.os.Handler;
import android.os.Looper;
import android.os.SystemClock;
import android.util.Pair;
@ -29,12 +30,25 @@ import java.net.URLConnection;
import java.util.concurrent.CancellationException;
/**
* Performs both single and repeated loads of media manfifests.
* Performs both single and repeated loads of media manifests.
*
* @param <T> The type of manifest.
*/
public class ManifestFetcher<T> implements Loader.Callback {
/**
* Interface definition for a callback to be notified of {@link ManifestFetcher} events.
*/
public interface EventListener {
public void onManifestRefreshStarted();
public void onManifestRefreshed();
public void onManifestError(IOException e);
}
/**
* Callback for the result of a single load.
*
@ -61,9 +75,12 @@ public class ManifestFetcher<T> implements Loader.Callback {
}
/* package */ final ManifestParser<T> parser;
/* package */ final String manifestUrl;
/* package */ final String contentId;
/* package */ final String userAgent;
private final Handler eventHandler;
private final EventListener eventListener;
/* package */ volatile String manifestUrl;
private int enabledCount;
private Loader loader;
@ -76,6 +93,11 @@ public class ManifestFetcher<T> implements Loader.Callback {
private volatile T manifest;
private volatile long manifestLoadTimestamp;
public ManifestFetcher(ManifestParser<T> parser, String contentId, String manifestUrl,
String userAgent) {
this(parser, contentId, manifestUrl, userAgent, null, null);
}
/**
* @param parser A parser to parse the loaded manifest data.
* @param contentId The content id of the content being loaded. May be null.
@ -83,11 +105,22 @@ public class ManifestFetcher<T> implements Loader.Callback {
* @param userAgent The User-Agent string that should be used.
*/
public ManifestFetcher(ManifestParser<T> parser, String contentId, String manifestUrl,
String userAgent) {
String userAgent, Handler eventHandler, EventListener eventListener) {
this.parser = parser;
this.contentId = contentId;
this.manifestUrl = manifestUrl;
this.userAgent = userAgent;
this.eventHandler = eventHandler;
this.eventListener = eventListener;
}
/**
* Updates the manifest location.
*
* @param manifestUrl The manifest location.
*/
public void updateManifestUrl(String manifestUrl) {
this.manifestUrl = manifestUrl;
}
/**
@ -173,6 +206,7 @@ public class ManifestFetcher<T> implements Loader.Callback {
if (!loader.isLoading()) {
currentLoadable = new ManifestLoadable();
loader.startLoading(currentLoadable, this);
notifyManifestRefreshStarted();
}
}
@ -187,6 +221,8 @@ public class ManifestFetcher<T> implements Loader.Callback {
manifestLoadTimestamp = SystemClock.elapsedRealtime();
loadExceptionCount = 0;
loadException = null;
notifyManifestRefreshed();
}
@Override
@ -204,12 +240,47 @@ public class ManifestFetcher<T> implements Loader.Callback {
loadExceptionCount++;
loadExceptionTimestamp = SystemClock.elapsedRealtime();
loadException = new IOException(exception);
notifyManifestError(loadException);
}
private long getRetryDelayMillis(long errorCount) {
return Math.min((errorCount - 1) * 1000, 5000);
}
private void notifyManifestRefreshStarted() {
if (eventHandler != null && eventListener != null) {
eventHandler.post(new Runnable() {
@Override
public void run() {
eventListener.onManifestRefreshStarted();
}
});
}
}
private void notifyManifestRefreshed() {
if (eventHandler != null && eventListener != null) {
eventHandler.post(new Runnable() {
@Override
public void run() {
eventListener.onManifestRefreshed();
}
});
}
}
private void notifyManifestError(final IOException e) {
if (eventHandler != null && eventListener != null) {
eventHandler.post(new Runnable() {
@Override
public void run() {
eventListener.onManifestError(e);
}
});
}
}
private class SingleFetchHelper implements Loader.Callback {
private final Looper callbackLooper;

View file

@ -70,12 +70,14 @@ public class PlayerControl implements MediaPlayerControl {
@Override
public int getCurrentPosition() {
return (int) exoPlayer.getCurrentPosition();
return exoPlayer.getDuration() == ExoPlayer.UNKNOWN_TIME ? 0
: (int) exoPlayer.getCurrentPosition();
}
@Override
public int getDuration() {
return (int) exoPlayer.getDuration();
return exoPlayer.getDuration() == ExoPlayer.UNKNOWN_TIME ? 0
: (int) exoPlayer.getDuration();
}
@Override
@ -95,8 +97,9 @@ public class PlayerControl implements MediaPlayerControl {
@Override
public void seekTo(int timeMillis) {
// MediaController arrow keys generate unbounded values.
exoPlayer.seekTo(Math.min(Math.max(0, timeMillis), getDuration()));
long seekPosition = exoPlayer.getDuration() == ExoPlayer.UNKNOWN_TIME ? 0
: Math.min(Math.max(0, timeMillis), getDuration());
exoPlayer.seekTo(seekPosition);
}
}

View file

@ -399,4 +399,22 @@ public final class Util {
return scaledTimestamps;
}
/**
* Converts a list of integers to a primitive array.
*
* @param list A list of integers.
* @return The list in array form, or null if the input list was null.
*/
public static int[] toArray(List<Integer> list) {
if (list == null) {
return null;
}
int length = list.size();
int[] intArray = new int[length];
for (int i = 0; i < length; i++) {
intArray[i] = list.get(i);
}
return intArray;
}
}