SmoothStreaming Live support.

Issue: #12
This commit is contained in:
ojw28 2014-09-25 20:15:59 +01:00
parent 4adf8f77f4
commit dd30632aa1
7 changed files with 220 additions and 41 deletions

View File

@ -45,7 +45,7 @@ public class DemoUtil {
public static final String CONTENT_ID_EXTRA = "content_id";
public static final int TYPE_DASH_VOD = 0;
public static final int TYPE_SS_VOD = 1;
public static final int TYPE_SS = 1;
public static final int TYPE_OTHER = 2;
public static final boolean EXPOSE_EXPERIMENTAL_FEATURES = false;

View File

@ -56,7 +56,7 @@ package com.google.android.exoplayer.demo;
false),
new Sample("Super speed (SmoothStreaming)", "uid:ss:superspeed",
"http://playready.directtaps.net/smoothstreaming/SSWSS720H264/SuperSpeedway_720.ism",
DemoUtil.TYPE_SS_VOD, false, false),
DemoUtil.TYPE_SS, false, false),
new Sample("Dizzy (Misc)", "uid:misc:dizzy",
"http://html5demos.com/assets/dizzy.mp4", DemoUtil.TYPE_OTHER, false, false),
};
@ -92,10 +92,10 @@ package com.google.android.exoplayer.demo;
public static final Sample[] SMOOTHSTREAMING = new Sample[] {
new Sample("Super speed", "uid:ss:superspeed",
"http://playready.directtaps.net/smoothstreaming/SSWSS720H264/SuperSpeedway_720.ism",
DemoUtil.TYPE_SS_VOD, false, true),
DemoUtil.TYPE_SS, false, true),
new Sample("Super speed (PlayReady)", "uid:ss:pr:superspeed",
"http://playready.directtaps.net/smoothstreaming/SSWSS720H264PR/SuperSpeedway_720.ism",
DemoUtil.TYPE_SS_VOD, true, true),
DemoUtil.TYPE_SS, true, true),
};
public static final Sample[] WIDEVINE_GTS = new Sample[] {

View File

@ -167,7 +167,7 @@ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callba
private RendererBuilder getRendererBuilder() {
String userAgent = DemoUtil.getUserAgent(this);
switch (contentType) {
case DemoUtil.TYPE_SS_VOD:
case DemoUtil.TYPE_SS:
return new SmoothStreamingRendererBuilder(userAgent, contentUri.toString(), contentId,
new SmoothStreamingTestMediaDrmCallback(), debugTextView);
case DemoUtil.TYPE_DASH_VOD:

View File

@ -65,6 +65,7 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder,
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 LIVE_EDGE_LATENCY_MS = 30000;
private final String userAgent;
private final String url;
@ -74,6 +75,7 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder,
private DemoPlayer player;
private RendererBuilderCallback callback;
private ManifestFetcher<SmoothStreamingManifest> manifestFetcher;
public SmoothStreamingRendererBuilder(String userAgent, String url, String contentId,
MediaDrmCallback drmCallback, TextView debugTextView) {
@ -89,8 +91,8 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder,
this.player = player;
this.callback = callback;
SmoothStreamingManifestParser parser = new SmoothStreamingManifestParser();
ManifestFetcher<SmoothStreamingManifest> manifestFetcher =
new ManifestFetcher<SmoothStreamingManifest>(parser, contentId, url + "/Manifest");
manifestFetcher = new ManifestFetcher<SmoothStreamingManifest>(parser, contentId,
url + "/Manifest");
manifestFetcher.singleLoad(player.getMainHandler().getLooper(), this);
}
@ -154,9 +156,9 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder,
// Build the video renderer.
DataSource videoDataSource = new HttpDataSource(userAgent, null, bandwidthMeter);
ChunkSource videoChunkSource = new SmoothStreamingChunkSource(manifest,
ChunkSource videoChunkSource = new SmoothStreamingChunkSource(manifestFetcher,
videoStreamElementIndex, videoTrackIndices, videoDataSource,
new AdaptiveEvaluator(bandwidthMeter));
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);
@ -181,8 +183,9 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder,
for (int i = 0; i < manifest.streamElements.length; i++) {
if (manifest.streamElements[i].type == StreamElement.TYPE_AUDIO) {
audioTrackNames[audioStreamElementCount] = manifest.streamElements[i].name;
audioChunkSources[audioStreamElementCount] = new SmoothStreamingChunkSource(manifest,
i, new int[] {0}, audioDataSource, audioFormatEvaluator);
audioChunkSources[audioStreamElementCount] = new SmoothStreamingChunkSource(
manifestFetcher, i, new int[] {0}, audioDataSource, audioFormatEvaluator,
LIVE_EDGE_LATENCY_MS);
audioStreamElementCount++;
}
}
@ -211,8 +214,8 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder,
for (int i = 0; i < manifest.streamElements.length; i++) {
if (manifest.streamElements[i].type == StreamElement.TYPE_TEXT) {
textTrackNames[textStreamElementCount] = manifest.streamElements[i].language;
textChunkSources[textStreamElementCount] = new SmoothStreamingChunkSource(manifest,
i, new int[] {0}, ttmlDataSource, ttmlFormatEvaluator);
textChunkSources[textStreamElementCount] = new SmoothStreamingChunkSource(manifestFetcher,
i, new int[] {0}, ttmlDataSource, ttmlFormatEvaluator, LIVE_EDGE_LATENCY_MS);
textStreamElementCount++;
}
}

View File

@ -54,6 +54,7 @@ import java.util.ArrayList;
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;
@ -61,6 +62,7 @@ import java.util.ArrayList;
private final String contentId;
private RendererBuilderCallback callback;
private ManifestFetcher<SmoothStreamingManifest> manifestFetcher;
public SmoothStreamingRendererBuilder(SimplePlayerActivity playerActivity, String userAgent,
String url, String contentId) {
@ -74,8 +76,8 @@ import java.util.ArrayList;
public void buildRenderers(RendererBuilderCallback callback) {
this.callback = callback;
SmoothStreamingManifestParser parser = new SmoothStreamingManifestParser();
ManifestFetcher<SmoothStreamingManifest> manifestFetcher =
new ManifestFetcher<SmoothStreamingManifest>(parser, contentId, url + "/Manifest");
manifestFetcher = new ManifestFetcher<SmoothStreamingManifest>(parser, contentId,
url + "/Manifest");
manifestFetcher.singleLoad(playerActivity.getMainLooper(), this);
}
@ -120,8 +122,9 @@ import java.util.ArrayList;
// Build the video renderer.
DataSource videoDataSource = new HttpDataSource(userAgent, null, bandwidthMeter);
ChunkSource videoChunkSource = new SmoothStreamingChunkSource(manifest, videoStreamElementIndex,
videoTrackIndices, videoDataSource, new AdaptiveEvaluator(bandwidthMeter));
ChunkSource videoChunkSource = new SmoothStreamingChunkSource(manifestFetcher,
videoStreamElementIndex, videoTrackIndices, videoDataSource,
new AdaptiveEvaluator(bandwidthMeter), LIVE_EDGE_LATENCY_MS);
ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl,
VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true);
MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(videoSampleSource,
@ -129,8 +132,9 @@ import java.util.ArrayList;
// Build the audio renderer.
DataSource audioDataSource = new HttpDataSource(userAgent, null, bandwidthMeter);
ChunkSource audioChunkSource = new SmoothStreamingChunkSource(manifest, audioStreamElementIndex,
new int[] {0}, audioDataSource, new FormatEvaluator.FixedEvaluator());
ChunkSource audioChunkSource = new SmoothStreamingChunkSource(manifestFetcher,
audioStreamElementIndex, 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

@ -0,0 +1,33 @@
/*
* 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;
/**
* Thrown when a live playback falls behind the available media window.
*/
public class BehindLiveWindowException extends IOException {
public BehindLiveWindowException() {
super();
}
public BehindLiveWindowException(String message) {
super(message);
}
}

View File

@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer.smoothstreaming;
import com.google.android.exoplayer.BehindLiveWindowException;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.TrackInfo;
import com.google.android.exoplayer.chunk.Chunk;
@ -36,8 +37,10 @@ import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.Trac
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DataSpec;
import com.google.android.exoplayer.util.CodecSpecificDataUtil;
import com.google.android.exoplayer.util.ManifestFetcher;
import android.net.Uri;
import android.os.SystemClock;
import android.util.Base64;
import android.util.SparseArray;
@ -51,13 +54,16 @@ import java.util.List;
*/
public class SmoothStreamingChunkSource implements ChunkSource {
private static final int MINIMUM_MANIFEST_REFRESH_PERIOD_MS = 5000;
private static final int INITIALIZATION_VECTOR_SIZE = 8;
private final StreamElement streamElement;
private final ManifestFetcher<SmoothStreamingManifest> manifestFetcher;
private final int streamElementIndex;
private final TrackInfo trackInfo;
private final DataSource dataSource;
private final FormatEvaluator formatEvaluator;
private final Evaluation evaluation;
private final long liveEdgeLatencyUs;
private final int maxWidth;
private final int maxHeight;
@ -65,7 +71,42 @@ public class SmoothStreamingChunkSource implements ChunkSource {
private final SparseArray<FragmentedMp4Extractor> extractors;
private final SmoothStreamingFormat[] formats;
private SmoothStreamingManifest currentManifest;
private int currentManifestChunkOffset;
private boolean finishedCurrentManifest;
private IOException fatalError;
/**
* 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 streamElementIndex The index of the stream element in the manifest to be provided by
* the source.
* @param trackIndices The indices of the tracks within the stream element to be considered by
* the source. May be null if all tracks within the element 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 SmoothStreamingChunkSource(ManifestFetcher<SmoothStreamingManifest> manifestFetcher,
int streamElementIndex, int[] trackIndices, DataSource dataSource,
FormatEvaluator formatEvaluator, long liveEdgeLatencyMs) {
this(manifestFetcher, manifestFetcher.getManifest(), streamElementIndex, trackIndices,
dataSource, formatEvaluator, liveEdgeLatencyMs);
}
/**
* Constructor to use for fixed duration content.
*
* @param manifest The manifest parsed from {@code baseUrl + "/Manifest"}.
* @param streamElementIndex The index of the stream element in the manifest to be provided by
* the source.
@ -76,14 +117,25 @@ public class SmoothStreamingChunkSource implements ChunkSource {
*/
public SmoothStreamingChunkSource(SmoothStreamingManifest manifest, int streamElementIndex,
int[] trackIndices, DataSource dataSource, FormatEvaluator formatEvaluator) {
this.streamElement = manifest.streamElements[streamElementIndex];
this.trackInfo = new TrackInfo(streamElement.tracks[0].mimeType, manifest.durationUs);
this(null, manifest, streamElementIndex, trackIndices, dataSource, formatEvaluator, 0);
}
private SmoothStreamingChunkSource(ManifestFetcher<SmoothStreamingManifest> manifestFetcher,
SmoothStreamingManifest initialManifest, int streamElementIndex, int[] trackIndices,
DataSource dataSource, FormatEvaluator formatEvaluator, long liveEdgeLatencyMs) {
this.manifestFetcher = manifestFetcher;
this.streamElementIndex = streamElementIndex;
this.currentManifest = initialManifest;
this.dataSource = dataSource;
this.formatEvaluator = formatEvaluator;
this.evaluation = new Evaluation();
this.liveEdgeLatencyUs = liveEdgeLatencyMs * 1000;
StreamElement streamElement = getElement(initialManifest);
trackInfo = new TrackInfo(streamElement.tracks[0].mimeType, initialManifest.durationUs);
evaluation = new Evaluation();
TrackEncryptionBox[] trackEncryptionBoxes = null;
ProtectionElement protectionElement = manifest.protectionElement;
ProtectionElement protectionElement = initialManifest.protectionElement;
if (protectionElement != null) {
byte[] keyId = getKeyId(protectionElement.data);
trackEncryptionBoxes = new TrackEncryptionBox[1];
@ -135,22 +187,52 @@ public class SmoothStreamingChunkSource implements ChunkSource {
@Override
public void enable() {
// Do nothing.
fatalError = null;
if (manifestFetcher != null) {
manifestFetcher.enable();
}
}
@Override
public void disable(List<? extends MediaChunk> queue) {
// Do nothing.
if (manifestFetcher != null) {
manifestFetcher.disable();
}
}
@Override
public void continueBuffering(long playbackPositionUs) {
// Do nothing
if (manifestFetcher == null || !currentManifest.isLive || fatalError != null) {
return;
}
SmoothStreamingManifest newManifest = manifestFetcher.getManifest();
if (currentManifest != newManifest && newManifest != null) {
StreamElement currentElement = getElement(currentManifest);
StreamElement newElement = getElement(newManifest);
if (newElement.chunkCount == 0) {
currentManifestChunkOffset += currentElement.chunkCount;
} else if (currentElement.chunkCount > 0) {
currentManifestChunkOffset += currentElement.getChunkIndex(newElement.getStartTimeUs(0));
}
currentManifest = newManifest;
finishedCurrentManifest = false;
}
if (finishedCurrentManifest && (SystemClock.elapsedRealtime()
> manifestFetcher.getManifestLoadTimestamp() + MINIMUM_MANIFEST_REFRESH_PERIOD_MS)) {
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();
formatEvaluator.evaluate(queue, playbackPositionUs, formats, evaluation);
SmoothStreamingFormat selectedFormat = (SmoothStreamingFormat) evaluation.format;
@ -166,30 +248,63 @@ public class SmoothStreamingChunkSource implements ChunkSource {
return;
}
int nextChunkIndex;
if (queue.isEmpty()) {
nextChunkIndex = streamElement.getChunkIndex(seekPositionUs);
} else {
nextChunkIndex = queue.get(out.queueSize - 1).nextChunkIndex;
}
// In all cases where we return before instantiating a new chunk at the bottom of this method,
// we want out.chunk to be null.
out.chunk = null;
if (nextChunkIndex == -1) {
out.chunk = null;
StreamElement streamElement = getElement(currentManifest);
if (streamElement.chunkCount == 0) {
// The manifest is currently empty for this stream.
finishedCurrentManifest = true;
return;
}
boolean isLastChunk = nextChunkIndex == streamElement.chunkCount - 1;
Uri uri = streamElement.buildRequestUri(selectedFormat.trackIndex, nextChunkIndex);
int chunkIndex;
if (queue.isEmpty()) {
if (currentManifest.isLive) {
seekPositionUs = getLiveSeekPosition();
}
chunkIndex = streamElement.getChunkIndex(seekPositionUs);
} else {
chunkIndex = queue.get(out.queueSize - 1).nextChunkIndex - currentManifestChunkOffset;
}
if (currentManifest.isLive) {
if (chunkIndex < 0) {
// This is before the first chunk in the current manifest.
fatalError = new BehindLiveWindowException();
return;
} else if (chunkIndex >= streamElement.chunkCount) {
// This is beyond the last chunk in the current manifest.
finishedCurrentManifest = true;
return;
} else if (chunkIndex == streamElement.chunkCount - 1) {
// This is the last chunk in the current manifest. Mark the manifest as being finished,
// but continue to return the final chunk.
finishedCurrentManifest = true;
}
} else if (chunkIndex == -1) {
// We've reached the end of the stream.
return;
}
boolean isLastChunk = !currentManifest.isLive && chunkIndex == streamElement.chunkCount - 1;
long chunkStartTimeUs = streamElement.getStartTimeUs(chunkIndex);
long nextChunkStartTimeUs = isLastChunk ? -1
: chunkStartTimeUs + streamElement.getChunkDurationUs(chunkIndex);
int currentAbsoluteChunkIndex = chunkIndex + currentManifestChunkOffset;
Uri uri = streamElement.buildRequestUri(selectedFormat.trackIndex, chunkIndex);
Chunk mediaChunk = newMediaChunk(selectedFormat, uri, null,
extractors.get(Integer.parseInt(selectedFormat.id)), dataSource, nextChunkIndex,
isLastChunk, streamElement.getStartTimeUs(nextChunkIndex),
isLastChunk ? -1 : streamElement.getStartTimeUs(nextChunkIndex + 1), 0);
extractors.get(Integer.parseInt(selectedFormat.id)), dataSource, currentAbsoluteChunkIndex,
isLastChunk, chunkStartTimeUs, nextChunkStartTimeUs, 0);
out.chunk = mediaChunk;
}
@Override
public IOException getError() {
return null;
return fatalError != null ? fatalError
: (manifestFetcher != null ? manifestFetcher.getError() : null);
}
@Override
@ -197,6 +312,30 @@ public class SmoothStreamingChunkSource implements ChunkSource {
// Do nothing.
}
/**
* 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 (int i = 0; i < currentManifest.streamElements.length; i++) {
StreamElement streamElement = currentManifest.streamElements[i];
if (streamElement.chunkCount > 0) {
long elementLiveEdgeTimestampUs =
streamElement.getStartTimeUs(streamElement.chunkCount - 1)
+ streamElement.getChunkDurationUs(streamElement.chunkCount - 1);
liveEdgeTimestampUs = Math.max(liveEdgeTimestampUs, elementLiveEdgeTimestampUs);
}
}
return liveEdgeTimestampUs - liveEdgeLatencyUs;
}
private StreamElement getElement(SmoothStreamingManifest manifest) {
return manifest.streamElements[streamElementIndex];
}
private static MediaFormat getMediaFormat(StreamElement streamElement, int trackIndex) {
TrackElement trackElement = streamElement.tracks[trackIndex];
String mimeType = trackElement.mimeType;