Commit 6749623c by hoangtc Committed by Oliver Woodman

Handle DASH `emsg' events targeting player.

For live streaming, there are several types of DASH `emsg' events that directly
target the player. These events can signal whether the manifest is expired, or
the live streaming has ended, and should be handle directly within the player.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=182034591
parent 6bed2ffc
......@@ -32,7 +32,13 @@
seeking to the closest sync points before, either side or after specified seek
positions.
* Note: `SeekParameters` are not currently supported when playing HLS streams.
* DASH: Support DASH manifest EventStream elements.
* DRM: Optimistically attempt playback of DRM protected content that does not
declare scheme specific init data
([#3630](https://github.com/google/ExoPlayer/issues/3630)).
* DASH:
* Support in-band Emsg events targeting player with scheme id
"urn:mpeg:dash:event:2012" and scheme value of either "1", "2" or "3".
* Support DASH manifest EventStream elements.
* HLS: Add opt-in support for chunkless preparation in HLS. This allows an
HLS source to finish preparation without downloading any chunks, which can
significantly reduce initial buffering time
......
......@@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.source.chunk;
import android.support.annotation.Nullable;
import android.util.Log;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
......@@ -41,6 +42,17 @@ import java.util.List;
public class ChunkSampleStream<T extends ChunkSource> implements SampleStream, SequenceableLoader,
Loader.Callback<Chunk>, Loader.ReleaseCallback {
/** A callback to be notified when a sample stream has finished being released. */
public interface ReleaseCallback<T extends ChunkSource> {
/**
* Called when the {@link ChunkSampleStream} has finished being released.
*
* @param chunkSampleStream The released sample stream.
*/
void onSampleStreamReleased(ChunkSampleStream<T> chunkSampleStream);
}
private static final String TAG = "ChunkSampleStream";
public final int primaryTrackType;
......@@ -61,6 +73,7 @@ public class ChunkSampleStream<T extends ChunkSource> implements SampleStream, S
private final BaseMediaChunkOutput mediaChunkOutput;
private Format primaryDownstreamTrackFormat;
private ReleaseCallback<T> releaseCallback;
private long pendingResetPositionUs;
/* package */ long lastSeekPositionUs;
/* package */ boolean loadingFinished;
......@@ -247,10 +260,26 @@ public class ChunkSampleStream<T extends ChunkSource> implements SampleStream, S
/**
* Releases the stream.
* <p>
* This method should be called when the stream is no longer required.
*
* <p>This method should be called when the stream is no longer required. Either this method or
* {@link #release(ReleaseCallback)} can be used to release this stream.
*/
public void release() {
release(null);
}
/**
* Releases the stream.
*
* <p>This method should be called when the stream is no longer required. Either this method or
* {@link #release()} can be used to release this stream.
*
* @param callback A callback to be called when the release ends. Will be called synchronously
* from this method if no load is in progress, or asynchronously once the load has been
* canceled otherwise.
*/
public void release(@Nullable ReleaseCallback<T> callback) {
this.releaseCallback = callback;
boolean releasedSynchronously = loader.release(this);
if (!releasedSynchronously) {
// Discard as much as we can synchronously.
......@@ -267,6 +296,9 @@ public class ChunkSampleStream<T extends ChunkSource> implements SampleStream, S
for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) {
embeddedSampleQueue.reset();
}
if (releaseCallback != null) {
releaseCallback.onSampleStreamReleased(this);
}
}
// SampleStream implementation.
......
......@@ -16,7 +16,9 @@
package com.google.android.exoplayer2.source.dash;
import android.os.SystemClock;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.source.chunk.ChunkSource;
import com.google.android.exoplayer2.source.dash.PlayerEmsgHandler.PlayerTrackEmsgHandler;
import com.google.android.exoplayer2.source.dash.manifest.DashManifest;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.upstream.LoaderErrorThrower;
......@@ -53,7 +55,8 @@ public interface DashChunkSource extends ChunkSource {
int type,
long elapsedRealtimeOffsetMs,
boolean enableEventMessageTrack,
boolean enableCea608Track);
boolean enableCea608Track,
@Nullable PlayerTrackEmsgHandler playerEmsgHandler);
}
/**
......
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.source.dash;
import java.io.IOException;
/** Thrown when a live playback's manifest is expired and a new manifest could not be loaded. */
public final class DashManifestExpiredException extends IOException {}
......@@ -31,6 +31,8 @@ import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.source.chunk.ChunkSampleStream;
import com.google.android.exoplayer2.source.chunk.ChunkSampleStream.EmbeddedSampleStream;
import com.google.android.exoplayer2.source.dash.PlayerEmsgHandler.PlayerEmsgCallback;
import com.google.android.exoplayer2.source.dash.PlayerEmsgHandler.PlayerTrackEmsgHandler;
import com.google.android.exoplayer2.source.dash.manifest.AdaptationSet;
import com.google.android.exoplayer2.source.dash.manifest.DashManifest;
import com.google.android.exoplayer2.source.dash.manifest.Descriptor;
......@@ -47,14 +49,15 @@ import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
/**
* A DASH {@link MediaPeriod}.
*/
/* package */ final class DashMediaPeriod implements MediaPeriod,
SequenceableLoader.Callback<ChunkSampleStream<DashChunkSource>> {
/** A DASH {@link MediaPeriod}. */
/* package */ final class DashMediaPeriod
implements MediaPeriod,
SequenceableLoader.Callback<ChunkSampleStream<DashChunkSource>>,
ChunkSampleStream.ReleaseCallback<DashChunkSource> {
/* package */ final int id;
private final DashChunkSource.Factory chunkSourceFactory;
......@@ -66,6 +69,9 @@ import java.util.Map;
private final TrackGroupArray trackGroups;
private final TrackGroupInfo[] trackGroupInfos;
private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
private final PlayerEmsgHandler playerEmsgHandler;
private final IdentityHashMap<ChunkSampleStream<DashChunkSource>, PlayerTrackEmsgHandler>
trackEmsgHandlerBySampleStream;
private Callback callback;
private ChunkSampleStream<DashChunkSource>[] sampleStreams;
......@@ -75,11 +81,18 @@ import java.util.Map;
private int periodIndex;
private List<EventStream> eventStreams;
public DashMediaPeriod(int id, DashManifest manifest, int periodIndex,
DashChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount,
EventDispatcher eventDispatcher, long elapsedRealtimeOffset,
LoaderErrorThrower manifestLoaderErrorThrower, Allocator allocator,
CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory) {
public DashMediaPeriod(
int id,
DashManifest manifest,
int periodIndex,
DashChunkSource.Factory chunkSourceFactory,
int minLoadableRetryCount,
EventDispatcher eventDispatcher,
long elapsedRealtimeOffset,
LoaderErrorThrower manifestLoaderErrorThrower,
Allocator allocator,
CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory,
PlayerEmsgCallback playerEmsgCallback) {
this.id = id;
this.manifest = manifest;
this.periodIndex = periodIndex;
......@@ -90,8 +103,10 @@ import java.util.Map;
this.manifestLoaderErrorThrower = manifestLoaderErrorThrower;
this.allocator = allocator;
this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory;
playerEmsgHandler = new PlayerEmsgHandler(manifest, playerEmsgCallback, allocator);
sampleStreams = newSampleStreamArray(0);
eventSampleStreams = new EventSampleStream[0];
trackEmsgHandlerBySampleStream = new IdentityHashMap<>();
compositeSequenceableLoader =
compositeSequenceableLoaderFactory.createCompositeSequenceableLoader(sampleStreams);
Period period = manifest.getPeriod(periodIndex);
......@@ -111,14 +126,14 @@ import java.util.Map;
public void updateManifest(DashManifest manifest, int periodIndex) {
this.manifest = manifest;
this.periodIndex = periodIndex;
Period period = manifest.getPeriod(periodIndex);
playerEmsgHandler.updateManifest(manifest);
if (sampleStreams != null) {
for (ChunkSampleStream<DashChunkSource> sampleStream : sampleStreams) {
sampleStream.getChunkSource().updateManifest(manifest, periodIndex);
}
callback.onContinueLoadingRequested(this);
}
eventStreams = period.eventStreams;
eventStreams = manifest.getPeriod(periodIndex).eventStreams;
for (EventSampleStream eventSampleStream : eventSampleStreams) {
for (EventStream eventStream : eventStreams) {
if (eventStream.id().equals(eventSampleStream.eventStreamId())) {
......@@ -130,11 +145,24 @@ import java.util.Map;
}
public void release() {
playerEmsgHandler.release();
for (ChunkSampleStream<DashChunkSource> sampleStream : sampleStreams) {
sampleStream.release();
sampleStream.release(this);
}
}
// ChunkSampleStream.ReleaseCallback implementation.
@Override
public void onSampleStreamReleased(ChunkSampleStream<DashChunkSource> stream) {
PlayerTrackEmsgHandler trackEmsgHandler = trackEmsgHandlerBySampleStream.remove(stream);
if (trackEmsgHandler != null) {
trackEmsgHandler.release();
}
}
// MediaPeriod implementation.
@Override
public void prepare(Callback callback, long positionUs) {
this.callback = callback;
......@@ -181,7 +209,7 @@ import java.util.Map;
@SuppressWarnings("unchecked")
ChunkSampleStream<DashChunkSource> stream = (ChunkSampleStream<DashChunkSource>) streams[i];
if (selections[i] == null || !mayRetainStreamFlags[i]) {
stream.release();
stream.release(this);
streams[i] = null;
} else {
int trackGroupIndex = trackGroups.indexOf(selections[i].getTrackGroup());
......@@ -501,10 +529,22 @@ import java.util.Map;
embeddedTrackFormats = Arrays.copyOf(embeddedTrackFormats, embeddedTrackCount);
embeddedTrackTypes = Arrays.copyOf(embeddedTrackTypes, embeddedTrackCount);
}
DashChunkSource chunkSource = chunkSourceFactory.createDashChunkSource(
manifestLoaderErrorThrower, manifest, periodIndex, trackGroupInfo.adaptationSetIndices,
selection, trackGroupInfo.trackType, elapsedRealtimeOffset, enableEventMessageTrack,
enableCea608Track);
PlayerTrackEmsgHandler trackPlayerEmsgHandler =
manifest.dynamic && enableEventMessageTrack
? playerEmsgHandler.newPlayerTrackEmsgHandler()
: null;
DashChunkSource chunkSource =
chunkSourceFactory.createDashChunkSource(
manifestLoaderErrorThrower,
manifest,
periodIndex,
trackGroupInfo.adaptationSetIndices,
selection,
trackGroupInfo.trackType,
elapsedRealtimeOffset,
enableEventMessageTrack,
enableCea608Track,
trackPlayerEmsgHandler);
ChunkSampleStream<DashChunkSource> stream =
new ChunkSampleStream<>(
trackGroupInfo.trackType,
......@@ -516,6 +556,7 @@ import java.util.Map;
positionUs,
minLoadableRetryCount,
eventDispatcher);
trackEmsgHandlerBySampleStream.put(stream, trackPlayerEmsgHandler);
return stream;
}
......@@ -581,9 +622,8 @@ import java.util.Map;
private static final int CATEGORY_PRIMARY = 0;
/**
* A track group whose samples are embedded within one of the primary streams.
* For example: an EMSG track has its sample embedded in `emsg' atoms in one of the primary
* streams.
* A track group whose samples are embedded within one of the primary streams. For example: an
* EMSG track has its sample embedded in emsg atoms in one of the primary streams.
*/
private static final int CATEGORY_EMBEDDED = 1;
......
......@@ -35,6 +35,7 @@ import com.google.android.exoplayer2.source.MediaSourceEventListener;
import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;
import com.google.android.exoplayer2.source.SequenceableLoader;
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
import com.google.android.exoplayer2.source.dash.PlayerEmsgHandler.PlayerEmsgCallback;
import com.google.android.exoplayer2.source.dash.manifest.DashManifest;
import com.google.android.exoplayer2.source.dash.manifest.DashManifestParser;
import com.google.android.exoplayer2.source.dash.manifest.UtcTimingElement;
......@@ -56,9 +57,7 @@ import java.util.TimeZone;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* A DASH {@link MediaSource}.
*/
/** A DASH {@link MediaSource}. */
public final class DashMediaSource implements MediaSource {
static {
......@@ -280,6 +279,7 @@ public final class DashMediaSource implements MediaSource {
private final SparseArray<DashMediaPeriod> periodsById;
private final Runnable refreshManifestRunnable;
private final Runnable simulateManifestRefreshRunnable;
private final PlayerEmsgCallback playerEmsgCallback;
private Listener sourceListener;
private DataSource dataSource;
......@@ -291,7 +291,11 @@ public final class DashMediaSource implements MediaSource {
private long manifestLoadEndTimestamp;
private DashManifest manifest;
private Handler handler;
private boolean pendingManifestLoading;
private long elapsedRealtimeOffsetMs;
private long expiredManifestPublishTimeUs;
private boolean dynamicMediaPresentationEnded;
private int staleManifestReloadAttempt;
private int firstPeriodId;
......@@ -446,6 +450,8 @@ public final class DashMediaSource implements MediaSource {
eventDispatcher = new EventDispatcher(eventHandler, eventListener);
manifestUriLock = new Object();
periodsById = new SparseArray<>();
playerEmsgCallback = new DefaultPlayerEmsgCallback();
expiredManifestPublishTimeUs = C.TIME_UNSET;
if (sideloadedManifest) {
Assertions.checkState(!manifest.dynamic);
manifestCallback = null;
......@@ -507,9 +513,19 @@ public final class DashMediaSource implements MediaSource {
int periodIndex = periodId.periodIndex;
EventDispatcher periodEventDispatcher = eventDispatcher.copyWithMediaTimeOffsetMs(
manifest.getPeriod(periodIndex).startMs);
DashMediaPeriod mediaPeriod = new DashMediaPeriod(firstPeriodId + periodIndex, manifest,
periodIndex, chunkSourceFactory, minLoadableRetryCount, periodEventDispatcher,
elapsedRealtimeOffsetMs, loaderErrorThrower, allocator, compositeSequenceableLoaderFactory);
DashMediaPeriod mediaPeriod =
new DashMediaPeriod(
firstPeriodId + periodIndex,
manifest,
periodIndex,
chunkSourceFactory,
minLoadableRetryCount,
periodEventDispatcher,
elapsedRealtimeOffsetMs,
loaderErrorThrower,
allocator,
compositeSequenceableLoaderFactory,
playerEmsgCallback);
periodsById.put(mediaPeriod.id, mediaPeriod);
return mediaPeriod;
}
......@@ -523,6 +539,7 @@ public final class DashMediaSource implements MediaSource {
@Override
public void releaseSource() {
pendingManifestLoading = false;
dataSource = null;
loaderErrorThrower = null;
if (loader != null) {
......@@ -540,6 +557,24 @@ public final class DashMediaSource implements MediaSource {
periodsById.clear();
}
// PlayerEmsgCallback callbacks.
/* package */ void onDashManifestRefreshRequested() {
handler.removeCallbacks(simulateManifestRefreshRunnable);
startLoadingManifest();
}
/* package */ void onDashLiveMediaPresentationEndSignalEncountered() {
this.dynamicMediaPresentationEnded = true;
}
/* package */ void onDashManifestPublishTimeExpired(long expiredManifestPublishTimeUs) {
if (this.expiredManifestPublishTimeUs == C.TIME_UNSET
|| this.expiredManifestPublishTimeUs < expiredManifestPublishTimeUs) {
this.expiredManifestPublishTimeUs = expiredManifestPublishTimeUs;
}
}
// Loadable callbacks.
/* package */ void onManifestLoadCompleted(ParsingLoadable<DashManifest> loadable,
......@@ -566,9 +601,16 @@ public final class DashMediaSource implements MediaSource {
return;
}
if (maybeReloadStaleDynamicManifest(newManifest)) {
return;
}
manifest = newManifest;
manifestLoadStartTimestamp = elapsedRealtimeMs - loadDurationMs;
manifestLoadEndTimestamp = elapsedRealtimeMs;
staleManifestReloadAttempt = 0;
if (!manifest.dynamic) {
pendingManifestLoading = false;
}
if (manifest.location != null) {
synchronized (manifestUriLock) {
// This condition checks that replaceManifestUri wasn't called between the start and end of
......@@ -622,11 +664,41 @@ public final class DashMediaSource implements MediaSource {
// Internal methods.
/**
* Reloads a stale dynamic manifest to get a more recent version if possible.
*
* @return True if the reload is scheduled. False if we have already retried too many times.
*/
private boolean maybeReloadStaleDynamicManifest(DashManifest manifest) {
if (!isManifestStale(manifest)) {
return false;
}
String warning =
"Loaded a stale dynamic manifest "
+ manifest.publishTimeMs
+ " "
+ dynamicMediaPresentationEnded
+ " "
+ expiredManifestPublishTimeUs;
Log.w(TAG, warning);
if (staleManifestReloadAttempt++ < minLoadableRetryCount) {
startLoadingManifest();
return true;
}
return false;
}
private void startLoadingManifest() {
handler.removeCallbacks(refreshManifestRunnable);
if (loader.isLoading()) {
pendingManifestLoading = true;
return;
}
Uri manifestUri;
synchronized (manifestUriLock) {
manifestUri = this.manifestUri;
}
pendingManifestLoading = false;
startLoading(new ParsingLoadable<>(dataSource, manifestUri, C.DATA_TYPE_MANIFEST,
manifestParser), manifestCallback, minLoadableRetryCount);
}
......@@ -753,13 +825,21 @@ public final class DashMediaSource implements MediaSource {
if (windowChangingImplicitly) {
handler.postDelayed(simulateManifestRefreshRunnable, NOTIFY_MANIFEST_INTERVAL_MS);
}
// Schedule an explicit refresh if needed.
if (scheduleRefresh) {
if (pendingManifestLoading) {
startLoadingManifest();
} else if (scheduleRefresh) {
// Schedule an explicit refresh if needed.
scheduleManifestRefresh();
}
}
}
private boolean isManifestStale(DashManifest manifest) {
return manifest.dynamic
&& (dynamicMediaPresentationEnded
|| manifest.publishTimeMs <= expiredManifestPublishTimeUs);
}
private void scheduleManifestRefresh() {
if (!manifest.dynamic) {
return;
......@@ -948,6 +1028,24 @@ public final class DashMediaSource implements MediaSource {
}
private final class DefaultPlayerEmsgCallback implements PlayerEmsgCallback {
@Override
public void onDashManifestRefreshRequested() {
DashMediaSource.this.onDashManifestRefreshRequested();
}
@Override
public void onDashManifestPublishTimeExpired(long expiredManifestPublishTimeUs) {
DashMediaSource.this.onDashManifestPublishTimeExpired(expiredManifestPublishTimeUs);
}
@Override
public void onDashLiveMediaPresentationEndSignalEncountered() {
DashMediaSource.this.onDashLiveMediaPresentationEndSignalEncountered();
}
}
private final class ManifestCallback implements Loader.Callback<ParsingLoadable<DashManifest>> {
@Override
......@@ -1039,5 +1137,4 @@ public final class DashMediaSource implements MediaSource {
}
}
}
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