mirror of
https://github.com/samsonjs/media.git
synced 2026-04-27 15:07:40 +00:00
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
This commit is contained in:
parent
6bed2ffc04
commit
6749623cd1
8 changed files with 775 additions and 56 deletions
|
|
@ -32,7 +32,13 @@
|
||||||
seeking to the closest sync points before, either side or after specified seek
|
seeking to the closest sync points before, either side or after specified seek
|
||||||
positions.
|
positions.
|
||||||
* Note: `SeekParameters` are not currently supported when playing HLS streams.
|
* 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: Add opt-in support for chunkless preparation in HLS. This allows an
|
||||||
HLS source to finish preparation without downloading any chunks, which can
|
HLS source to finish preparation without downloading any chunks, which can
|
||||||
significantly reduce initial buffering time
|
significantly reduce initial buffering time
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.source.chunk;
|
package com.google.android.exoplayer2.source.chunk;
|
||||||
|
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.Format;
|
import com.google.android.exoplayer2.Format;
|
||||||
|
|
@ -41,6 +42,17 @@ import java.util.List;
|
||||||
public class ChunkSampleStream<T extends ChunkSource> implements SampleStream, SequenceableLoader,
|
public class ChunkSampleStream<T extends ChunkSource> implements SampleStream, SequenceableLoader,
|
||||||
Loader.Callback<Chunk>, Loader.ReleaseCallback {
|
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";
|
private static final String TAG = "ChunkSampleStream";
|
||||||
|
|
||||||
public final int primaryTrackType;
|
public final int primaryTrackType;
|
||||||
|
|
@ -61,6 +73,7 @@ public class ChunkSampleStream<T extends ChunkSource> implements SampleStream, S
|
||||||
private final BaseMediaChunkOutput mediaChunkOutput;
|
private final BaseMediaChunkOutput mediaChunkOutput;
|
||||||
|
|
||||||
private Format primaryDownstreamTrackFormat;
|
private Format primaryDownstreamTrackFormat;
|
||||||
|
private ReleaseCallback<T> releaseCallback;
|
||||||
private long pendingResetPositionUs;
|
private long pendingResetPositionUs;
|
||||||
/* package */ long lastSeekPositionUs;
|
/* package */ long lastSeekPositionUs;
|
||||||
/* package */ boolean loadingFinished;
|
/* package */ boolean loadingFinished;
|
||||||
|
|
@ -247,10 +260,26 @@ public class ChunkSampleStream<T extends ChunkSource> implements SampleStream, S
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Releases the stream.
|
* 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() {
|
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);
|
boolean releasedSynchronously = loader.release(this);
|
||||||
if (!releasedSynchronously) {
|
if (!releasedSynchronously) {
|
||||||
// Discard as much as we can synchronously.
|
// Discard as much as we can synchronously.
|
||||||
|
|
@ -267,6 +296,9 @@ public class ChunkSampleStream<T extends ChunkSource> implements SampleStream, S
|
||||||
for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) {
|
for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) {
|
||||||
embeddedSampleQueue.reset();
|
embeddedSampleQueue.reset();
|
||||||
}
|
}
|
||||||
|
if (releaseCallback != null) {
|
||||||
|
releaseCallback.onSampleStreamReleased(this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SampleStream implementation.
|
// SampleStream implementation.
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,9 @@
|
||||||
package com.google.android.exoplayer2.source.dash;
|
package com.google.android.exoplayer2.source.dash;
|
||||||
|
|
||||||
import android.os.SystemClock;
|
import android.os.SystemClock;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
import com.google.android.exoplayer2.source.chunk.ChunkSource;
|
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.source.dash.manifest.DashManifest;
|
||||||
import com.google.android.exoplayer2.trackselection.TrackSelection;
|
import com.google.android.exoplayer2.trackselection.TrackSelection;
|
||||||
import com.google.android.exoplayer2.upstream.LoaderErrorThrower;
|
import com.google.android.exoplayer2.upstream.LoaderErrorThrower;
|
||||||
|
|
@ -53,7 +55,8 @@ public interface DashChunkSource extends ChunkSource {
|
||||||
int type,
|
int type,
|
||||||
long elapsedRealtimeOffsetMs,
|
long elapsedRealtimeOffsetMs,
|
||||||
boolean enableEventMessageTrack,
|
boolean enableEventMessageTrack,
|
||||||
boolean enableCea608Track);
|
boolean enableCea608Track,
|
||||||
|
@Nullable PlayerTrackEmsgHandler playerEmsgHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
/*
|
||||||
|
* 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.TrackGroupArray;
|
||||||
import com.google.android.exoplayer2.source.chunk.ChunkSampleStream;
|
import com.google.android.exoplayer2.source.chunk.ChunkSampleStream;
|
||||||
import com.google.android.exoplayer2.source.chunk.ChunkSampleStream.EmbeddedSampleStream;
|
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.AdaptationSet;
|
||||||
import com.google.android.exoplayer2.source.dash.manifest.DashManifest;
|
import com.google.android.exoplayer2.source.dash.manifest.DashManifest;
|
||||||
import com.google.android.exoplayer2.source.dash.manifest.Descriptor;
|
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.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.IdentityHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/** A DASH {@link MediaPeriod}. */
|
||||||
* A DASH {@link MediaPeriod}.
|
/* package */ final class DashMediaPeriod
|
||||||
*/
|
implements MediaPeriod,
|
||||||
/* package */ final class DashMediaPeriod implements MediaPeriod,
|
SequenceableLoader.Callback<ChunkSampleStream<DashChunkSource>>,
|
||||||
SequenceableLoader.Callback<ChunkSampleStream<DashChunkSource>> {
|
ChunkSampleStream.ReleaseCallback<DashChunkSource> {
|
||||||
|
|
||||||
/* package */ final int id;
|
/* package */ final int id;
|
||||||
private final DashChunkSource.Factory chunkSourceFactory;
|
private final DashChunkSource.Factory chunkSourceFactory;
|
||||||
|
|
@ -66,6 +69,9 @@ import java.util.Map;
|
||||||
private final TrackGroupArray trackGroups;
|
private final TrackGroupArray trackGroups;
|
||||||
private final TrackGroupInfo[] trackGroupInfos;
|
private final TrackGroupInfo[] trackGroupInfos;
|
||||||
private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
|
private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
|
||||||
|
private final PlayerEmsgHandler playerEmsgHandler;
|
||||||
|
private final IdentityHashMap<ChunkSampleStream<DashChunkSource>, PlayerTrackEmsgHandler>
|
||||||
|
trackEmsgHandlerBySampleStream;
|
||||||
|
|
||||||
private Callback callback;
|
private Callback callback;
|
||||||
private ChunkSampleStream<DashChunkSource>[] sampleStreams;
|
private ChunkSampleStream<DashChunkSource>[] sampleStreams;
|
||||||
|
|
@ -75,11 +81,18 @@ import java.util.Map;
|
||||||
private int periodIndex;
|
private int periodIndex;
|
||||||
private List<EventStream> eventStreams;
|
private List<EventStream> eventStreams;
|
||||||
|
|
||||||
public DashMediaPeriod(int id, DashManifest manifest, int periodIndex,
|
public DashMediaPeriod(
|
||||||
DashChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount,
|
int id,
|
||||||
EventDispatcher eventDispatcher, long elapsedRealtimeOffset,
|
DashManifest manifest,
|
||||||
LoaderErrorThrower manifestLoaderErrorThrower, Allocator allocator,
|
int periodIndex,
|
||||||
CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory) {
|
DashChunkSource.Factory chunkSourceFactory,
|
||||||
|
int minLoadableRetryCount,
|
||||||
|
EventDispatcher eventDispatcher,
|
||||||
|
long elapsedRealtimeOffset,
|
||||||
|
LoaderErrorThrower manifestLoaderErrorThrower,
|
||||||
|
Allocator allocator,
|
||||||
|
CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory,
|
||||||
|
PlayerEmsgCallback playerEmsgCallback) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.manifest = manifest;
|
this.manifest = manifest;
|
||||||
this.periodIndex = periodIndex;
|
this.periodIndex = periodIndex;
|
||||||
|
|
@ -90,8 +103,10 @@ import java.util.Map;
|
||||||
this.manifestLoaderErrorThrower = manifestLoaderErrorThrower;
|
this.manifestLoaderErrorThrower = manifestLoaderErrorThrower;
|
||||||
this.allocator = allocator;
|
this.allocator = allocator;
|
||||||
this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory;
|
this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory;
|
||||||
|
playerEmsgHandler = new PlayerEmsgHandler(manifest, playerEmsgCallback, allocator);
|
||||||
sampleStreams = newSampleStreamArray(0);
|
sampleStreams = newSampleStreamArray(0);
|
||||||
eventSampleStreams = new EventSampleStream[0];
|
eventSampleStreams = new EventSampleStream[0];
|
||||||
|
trackEmsgHandlerBySampleStream = new IdentityHashMap<>();
|
||||||
compositeSequenceableLoader =
|
compositeSequenceableLoader =
|
||||||
compositeSequenceableLoaderFactory.createCompositeSequenceableLoader(sampleStreams);
|
compositeSequenceableLoaderFactory.createCompositeSequenceableLoader(sampleStreams);
|
||||||
Period period = manifest.getPeriod(periodIndex);
|
Period period = manifest.getPeriod(periodIndex);
|
||||||
|
|
@ -111,14 +126,14 @@ import java.util.Map;
|
||||||
public void updateManifest(DashManifest manifest, int periodIndex) {
|
public void updateManifest(DashManifest manifest, int periodIndex) {
|
||||||
this.manifest = manifest;
|
this.manifest = manifest;
|
||||||
this.periodIndex = periodIndex;
|
this.periodIndex = periodIndex;
|
||||||
Period period = manifest.getPeriod(periodIndex);
|
playerEmsgHandler.updateManifest(manifest);
|
||||||
if (sampleStreams != null) {
|
if (sampleStreams != null) {
|
||||||
for (ChunkSampleStream<DashChunkSource> sampleStream : sampleStreams) {
|
for (ChunkSampleStream<DashChunkSource> sampleStream : sampleStreams) {
|
||||||
sampleStream.getChunkSource().updateManifest(manifest, periodIndex);
|
sampleStream.getChunkSource().updateManifest(manifest, periodIndex);
|
||||||
}
|
}
|
||||||
callback.onContinueLoadingRequested(this);
|
callback.onContinueLoadingRequested(this);
|
||||||
}
|
}
|
||||||
eventStreams = period.eventStreams;
|
eventStreams = manifest.getPeriod(periodIndex).eventStreams;
|
||||||
for (EventSampleStream eventSampleStream : eventSampleStreams) {
|
for (EventSampleStream eventSampleStream : eventSampleStreams) {
|
||||||
for (EventStream eventStream : eventStreams) {
|
for (EventStream eventStream : eventStreams) {
|
||||||
if (eventStream.id().equals(eventSampleStream.eventStreamId())) {
|
if (eventStream.id().equals(eventSampleStream.eventStreamId())) {
|
||||||
|
|
@ -130,11 +145,24 @@ import java.util.Map;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void release() {
|
public void release() {
|
||||||
|
playerEmsgHandler.release();
|
||||||
for (ChunkSampleStream<DashChunkSource> sampleStream : sampleStreams) {
|
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
|
@Override
|
||||||
public void prepare(Callback callback, long positionUs) {
|
public void prepare(Callback callback, long positionUs) {
|
||||||
this.callback = callback;
|
this.callback = callback;
|
||||||
|
|
@ -181,7 +209,7 @@ import java.util.Map;
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
ChunkSampleStream<DashChunkSource> stream = (ChunkSampleStream<DashChunkSource>) streams[i];
|
ChunkSampleStream<DashChunkSource> stream = (ChunkSampleStream<DashChunkSource>) streams[i];
|
||||||
if (selections[i] == null || !mayRetainStreamFlags[i]) {
|
if (selections[i] == null || !mayRetainStreamFlags[i]) {
|
||||||
stream.release();
|
stream.release(this);
|
||||||
streams[i] = null;
|
streams[i] = null;
|
||||||
} else {
|
} else {
|
||||||
int trackGroupIndex = trackGroups.indexOf(selections[i].getTrackGroup());
|
int trackGroupIndex = trackGroups.indexOf(selections[i].getTrackGroup());
|
||||||
|
|
@ -501,10 +529,22 @@ import java.util.Map;
|
||||||
embeddedTrackFormats = Arrays.copyOf(embeddedTrackFormats, embeddedTrackCount);
|
embeddedTrackFormats = Arrays.copyOf(embeddedTrackFormats, embeddedTrackCount);
|
||||||
embeddedTrackTypes = Arrays.copyOf(embeddedTrackTypes, embeddedTrackCount);
|
embeddedTrackTypes = Arrays.copyOf(embeddedTrackTypes, embeddedTrackCount);
|
||||||
}
|
}
|
||||||
DashChunkSource chunkSource = chunkSourceFactory.createDashChunkSource(
|
PlayerTrackEmsgHandler trackPlayerEmsgHandler =
|
||||||
manifestLoaderErrorThrower, manifest, periodIndex, trackGroupInfo.adaptationSetIndices,
|
manifest.dynamic && enableEventMessageTrack
|
||||||
selection, trackGroupInfo.trackType, elapsedRealtimeOffset, enableEventMessageTrack,
|
? playerEmsgHandler.newPlayerTrackEmsgHandler()
|
||||||
enableCea608Track);
|
: null;
|
||||||
|
DashChunkSource chunkSource =
|
||||||
|
chunkSourceFactory.createDashChunkSource(
|
||||||
|
manifestLoaderErrorThrower,
|
||||||
|
manifest,
|
||||||
|
periodIndex,
|
||||||
|
trackGroupInfo.adaptationSetIndices,
|
||||||
|
selection,
|
||||||
|
trackGroupInfo.trackType,
|
||||||
|
elapsedRealtimeOffset,
|
||||||
|
enableEventMessageTrack,
|
||||||
|
enableCea608Track,
|
||||||
|
trackPlayerEmsgHandler);
|
||||||
ChunkSampleStream<DashChunkSource> stream =
|
ChunkSampleStream<DashChunkSource> stream =
|
||||||
new ChunkSampleStream<>(
|
new ChunkSampleStream<>(
|
||||||
trackGroupInfo.trackType,
|
trackGroupInfo.trackType,
|
||||||
|
|
@ -516,6 +556,7 @@ import java.util.Map;
|
||||||
positionUs,
|
positionUs,
|
||||||
minLoadableRetryCount,
|
minLoadableRetryCount,
|
||||||
eventDispatcher);
|
eventDispatcher);
|
||||||
|
trackEmsgHandlerBySampleStream.put(stream, trackPlayerEmsgHandler);
|
||||||
return stream;
|
return stream;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -581,9 +622,8 @@ import java.util.Map;
|
||||||
private static final int CATEGORY_PRIMARY = 0;
|
private static final int CATEGORY_PRIMARY = 0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A track group whose samples are embedded within one of the primary streams.
|
* A track group whose samples are embedded within one of the primary streams. For example: an
|
||||||
* For example: an EMSG track has its sample embedded in `emsg' atoms in one of the primary
|
* EMSG track has its sample embedded in emsg atoms in one of the primary streams.
|
||||||
* streams.
|
|
||||||
*/
|
*/
|
||||||
private static final int CATEGORY_EMBEDDED = 1;
|
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.MediaSourceEventListener.EventDispatcher;
|
||||||
import com.google.android.exoplayer2.source.SequenceableLoader;
|
import com.google.android.exoplayer2.source.SequenceableLoader;
|
||||||
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
|
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.DashManifest;
|
||||||
import com.google.android.exoplayer2.source.dash.manifest.DashManifestParser;
|
import com.google.android.exoplayer2.source.dash.manifest.DashManifestParser;
|
||||||
import com.google.android.exoplayer2.source.dash.manifest.UtcTimingElement;
|
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.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
/**
|
/** A DASH {@link MediaSource}. */
|
||||||
* A DASH {@link MediaSource}.
|
|
||||||
*/
|
|
||||||
public final class DashMediaSource implements MediaSource {
|
public final class DashMediaSource implements MediaSource {
|
||||||
|
|
||||||
static {
|
static {
|
||||||
|
|
@ -280,6 +279,7 @@ public final class DashMediaSource implements MediaSource {
|
||||||
private final SparseArray<DashMediaPeriod> periodsById;
|
private final SparseArray<DashMediaPeriod> periodsById;
|
||||||
private final Runnable refreshManifestRunnable;
|
private final Runnable refreshManifestRunnable;
|
||||||
private final Runnable simulateManifestRefreshRunnable;
|
private final Runnable simulateManifestRefreshRunnable;
|
||||||
|
private final PlayerEmsgCallback playerEmsgCallback;
|
||||||
|
|
||||||
private Listener sourceListener;
|
private Listener sourceListener;
|
||||||
private DataSource dataSource;
|
private DataSource dataSource;
|
||||||
|
|
@ -291,7 +291,11 @@ public final class DashMediaSource implements MediaSource {
|
||||||
private long manifestLoadEndTimestamp;
|
private long manifestLoadEndTimestamp;
|
||||||
private DashManifest manifest;
|
private DashManifest manifest;
|
||||||
private Handler handler;
|
private Handler handler;
|
||||||
|
private boolean pendingManifestLoading;
|
||||||
private long elapsedRealtimeOffsetMs;
|
private long elapsedRealtimeOffsetMs;
|
||||||
|
private long expiredManifestPublishTimeUs;
|
||||||
|
private boolean dynamicMediaPresentationEnded;
|
||||||
|
private int staleManifestReloadAttempt;
|
||||||
|
|
||||||
private int firstPeriodId;
|
private int firstPeriodId;
|
||||||
|
|
||||||
|
|
@ -446,6 +450,8 @@ public final class DashMediaSource implements MediaSource {
|
||||||
eventDispatcher = new EventDispatcher(eventHandler, eventListener);
|
eventDispatcher = new EventDispatcher(eventHandler, eventListener);
|
||||||
manifestUriLock = new Object();
|
manifestUriLock = new Object();
|
||||||
periodsById = new SparseArray<>();
|
periodsById = new SparseArray<>();
|
||||||
|
playerEmsgCallback = new DefaultPlayerEmsgCallback();
|
||||||
|
expiredManifestPublishTimeUs = C.TIME_UNSET;
|
||||||
if (sideloadedManifest) {
|
if (sideloadedManifest) {
|
||||||
Assertions.checkState(!manifest.dynamic);
|
Assertions.checkState(!manifest.dynamic);
|
||||||
manifestCallback = null;
|
manifestCallback = null;
|
||||||
|
|
@ -507,9 +513,19 @@ public final class DashMediaSource implements MediaSource {
|
||||||
int periodIndex = periodId.periodIndex;
|
int periodIndex = periodId.periodIndex;
|
||||||
EventDispatcher periodEventDispatcher = eventDispatcher.copyWithMediaTimeOffsetMs(
|
EventDispatcher periodEventDispatcher = eventDispatcher.copyWithMediaTimeOffsetMs(
|
||||||
manifest.getPeriod(periodIndex).startMs);
|
manifest.getPeriod(periodIndex).startMs);
|
||||||
DashMediaPeriod mediaPeriod = new DashMediaPeriod(firstPeriodId + periodIndex, manifest,
|
DashMediaPeriod mediaPeriod =
|
||||||
periodIndex, chunkSourceFactory, minLoadableRetryCount, periodEventDispatcher,
|
new DashMediaPeriod(
|
||||||
elapsedRealtimeOffsetMs, loaderErrorThrower, allocator, compositeSequenceableLoaderFactory);
|
firstPeriodId + periodIndex,
|
||||||
|
manifest,
|
||||||
|
periodIndex,
|
||||||
|
chunkSourceFactory,
|
||||||
|
minLoadableRetryCount,
|
||||||
|
periodEventDispatcher,
|
||||||
|
elapsedRealtimeOffsetMs,
|
||||||
|
loaderErrorThrower,
|
||||||
|
allocator,
|
||||||
|
compositeSequenceableLoaderFactory,
|
||||||
|
playerEmsgCallback);
|
||||||
periodsById.put(mediaPeriod.id, mediaPeriod);
|
periodsById.put(mediaPeriod.id, mediaPeriod);
|
||||||
return mediaPeriod;
|
return mediaPeriod;
|
||||||
}
|
}
|
||||||
|
|
@ -523,6 +539,7 @@ public final class DashMediaSource implements MediaSource {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void releaseSource() {
|
public void releaseSource() {
|
||||||
|
pendingManifestLoading = false;
|
||||||
dataSource = null;
|
dataSource = null;
|
||||||
loaderErrorThrower = null;
|
loaderErrorThrower = null;
|
||||||
if (loader != null) {
|
if (loader != null) {
|
||||||
|
|
@ -540,6 +557,24 @@ public final class DashMediaSource implements MediaSource {
|
||||||
periodsById.clear();
|
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.
|
// Loadable callbacks.
|
||||||
|
|
||||||
/* package */ void onManifestLoadCompleted(ParsingLoadable<DashManifest> loadable,
|
/* package */ void onManifestLoadCompleted(ParsingLoadable<DashManifest> loadable,
|
||||||
|
|
@ -566,9 +601,16 @@ public final class DashMediaSource implements MediaSource {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (maybeReloadStaleDynamicManifest(newManifest)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
manifest = newManifest;
|
manifest = newManifest;
|
||||||
manifestLoadStartTimestamp = elapsedRealtimeMs - loadDurationMs;
|
manifestLoadStartTimestamp = elapsedRealtimeMs - loadDurationMs;
|
||||||
manifestLoadEndTimestamp = elapsedRealtimeMs;
|
manifestLoadEndTimestamp = elapsedRealtimeMs;
|
||||||
|
staleManifestReloadAttempt = 0;
|
||||||
|
if (!manifest.dynamic) {
|
||||||
|
pendingManifestLoading = false;
|
||||||
|
}
|
||||||
if (manifest.location != null) {
|
if (manifest.location != null) {
|
||||||
synchronized (manifestUriLock) {
|
synchronized (manifestUriLock) {
|
||||||
// This condition checks that replaceManifestUri wasn't called between the start and end of
|
// 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.
|
// 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() {
|
private void startLoadingManifest() {
|
||||||
|
handler.removeCallbacks(refreshManifestRunnable);
|
||||||
|
if (loader.isLoading()) {
|
||||||
|
pendingManifestLoading = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
Uri manifestUri;
|
Uri manifestUri;
|
||||||
synchronized (manifestUriLock) {
|
synchronized (manifestUriLock) {
|
||||||
manifestUri = this.manifestUri;
|
manifestUri = this.manifestUri;
|
||||||
}
|
}
|
||||||
|
pendingManifestLoading = false;
|
||||||
startLoading(new ParsingLoadable<>(dataSource, manifestUri, C.DATA_TYPE_MANIFEST,
|
startLoading(new ParsingLoadable<>(dataSource, manifestUri, C.DATA_TYPE_MANIFEST,
|
||||||
manifestParser), manifestCallback, minLoadableRetryCount);
|
manifestParser), manifestCallback, minLoadableRetryCount);
|
||||||
}
|
}
|
||||||
|
|
@ -753,13 +825,21 @@ public final class DashMediaSource implements MediaSource {
|
||||||
if (windowChangingImplicitly) {
|
if (windowChangingImplicitly) {
|
||||||
handler.postDelayed(simulateManifestRefreshRunnable, NOTIFY_MANIFEST_INTERVAL_MS);
|
handler.postDelayed(simulateManifestRefreshRunnable, NOTIFY_MANIFEST_INTERVAL_MS);
|
||||||
}
|
}
|
||||||
// Schedule an explicit refresh if needed.
|
if (pendingManifestLoading) {
|
||||||
if (scheduleRefresh) {
|
startLoadingManifest();
|
||||||
|
} else if (scheduleRefresh) {
|
||||||
|
// Schedule an explicit refresh if needed.
|
||||||
scheduleManifestRefresh();
|
scheduleManifestRefresh();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isManifestStale(DashManifest manifest) {
|
||||||
|
return manifest.dynamic
|
||||||
|
&& (dynamicMediaPresentationEnded
|
||||||
|
|| manifest.publishTimeMs <= expiredManifestPublishTimeUs);
|
||||||
|
}
|
||||||
|
|
||||||
private void scheduleManifestRefresh() {
|
private void scheduleManifestRefresh() {
|
||||||
if (!manifest.dynamic) {
|
if (!manifest.dynamic) {
|
||||||
return;
|
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>> {
|
private final class ManifestCallback implements Loader.Callback<ParsingLoadable<DashManifest>> {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -1039,5 +1137,4 @@ public final class DashMediaSource implements MediaSource {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,12 +17,14 @@ package com.google.android.exoplayer2.source.dash;
|
||||||
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.SystemClock;
|
import android.os.SystemClock;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.Format;
|
import com.google.android.exoplayer2.Format;
|
||||||
import com.google.android.exoplayer2.SeekParameters;
|
import com.google.android.exoplayer2.SeekParameters;
|
||||||
import com.google.android.exoplayer2.extractor.ChunkIndex;
|
import com.google.android.exoplayer2.extractor.ChunkIndex;
|
||||||
import com.google.android.exoplayer2.extractor.Extractor;
|
import com.google.android.exoplayer2.extractor.Extractor;
|
||||||
import com.google.android.exoplayer2.extractor.SeekMap;
|
import com.google.android.exoplayer2.extractor.SeekMap;
|
||||||
|
import com.google.android.exoplayer2.extractor.TrackOutput;
|
||||||
import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;
|
import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;
|
||||||
import com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor;
|
import com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor;
|
||||||
import com.google.android.exoplayer2.extractor.rawcc.RawCcExtractor;
|
import com.google.android.exoplayer2.extractor.rawcc.RawCcExtractor;
|
||||||
|
|
@ -35,6 +37,7 @@ import com.google.android.exoplayer2.source.chunk.ContainerMediaChunk;
|
||||||
import com.google.android.exoplayer2.source.chunk.InitializationChunk;
|
import com.google.android.exoplayer2.source.chunk.InitializationChunk;
|
||||||
import com.google.android.exoplayer2.source.chunk.MediaChunk;
|
import com.google.android.exoplayer2.source.chunk.MediaChunk;
|
||||||
import com.google.android.exoplayer2.source.chunk.SingleSampleMediaChunk;
|
import com.google.android.exoplayer2.source.chunk.SingleSampleMediaChunk;
|
||||||
|
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.AdaptationSet;
|
||||||
import com.google.android.exoplayer2.source.dash.manifest.DashManifest;
|
import com.google.android.exoplayer2.source.dash.manifest.DashManifest;
|
||||||
import com.google.android.exoplayer2.source.dash.manifest.RangedUri;
|
import com.google.android.exoplayer2.source.dash.manifest.RangedUri;
|
||||||
|
|
@ -71,14 +74,31 @@ public class DefaultDashChunkSource implements DashChunkSource {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public DashChunkSource createDashChunkSource(LoaderErrorThrower manifestLoaderErrorThrower,
|
public DashChunkSource createDashChunkSource(
|
||||||
DashManifest manifest, int periodIndex, int[] adaptationSetIndices,
|
LoaderErrorThrower manifestLoaderErrorThrower,
|
||||||
TrackSelection trackSelection, int trackType, long elapsedRealtimeOffsetMs,
|
DashManifest manifest,
|
||||||
boolean enableEventMessageTrack, boolean enableCea608Track) {
|
int periodIndex,
|
||||||
|
int[] adaptationSetIndices,
|
||||||
|
TrackSelection trackSelection,
|
||||||
|
int trackType,
|
||||||
|
long elapsedRealtimeOffsetMs,
|
||||||
|
boolean enableEventMessageTrack,
|
||||||
|
boolean enableCea608Track,
|
||||||
|
@Nullable PlayerTrackEmsgHandler playerEmsgHandler) {
|
||||||
DataSource dataSource = dataSourceFactory.createDataSource();
|
DataSource dataSource = dataSourceFactory.createDataSource();
|
||||||
return new DefaultDashChunkSource(manifestLoaderErrorThrower, manifest, periodIndex,
|
return new DefaultDashChunkSource(
|
||||||
adaptationSetIndices, trackSelection, trackType, dataSource, elapsedRealtimeOffsetMs,
|
manifestLoaderErrorThrower,
|
||||||
maxSegmentsPerLoad, enableEventMessageTrack, enableCea608Track);
|
manifest,
|
||||||
|
periodIndex,
|
||||||
|
adaptationSetIndices,
|
||||||
|
trackSelection,
|
||||||
|
trackType,
|
||||||
|
dataSource,
|
||||||
|
elapsedRealtimeOffsetMs,
|
||||||
|
maxSegmentsPerLoad,
|
||||||
|
enableEventMessageTrack,
|
||||||
|
enableCea608Track,
|
||||||
|
playerEmsgHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -90,6 +110,7 @@ public class DefaultDashChunkSource implements DashChunkSource {
|
||||||
private final DataSource dataSource;
|
private final DataSource dataSource;
|
||||||
private final long elapsedRealtimeOffsetMs;
|
private final long elapsedRealtimeOffsetMs;
|
||||||
private final int maxSegmentsPerLoad;
|
private final int maxSegmentsPerLoad;
|
||||||
|
@Nullable private final PlayerTrackEmsgHandler playerTrackEmsgHandler;
|
||||||
|
|
||||||
protected final RepresentationHolder[] representationHolders;
|
protected final RepresentationHolder[] representationHolders;
|
||||||
|
|
||||||
|
|
@ -110,18 +131,28 @@ public class DefaultDashChunkSource implements DashChunkSource {
|
||||||
* @param elapsedRealtimeOffsetMs If known, an estimate of the instantaneous difference between
|
* @param elapsedRealtimeOffsetMs If known, an estimate of the instantaneous difference between
|
||||||
* server-side unix time and {@link SystemClock#elapsedRealtime()} in milliseconds, specified
|
* server-side unix time and {@link SystemClock#elapsedRealtime()} in milliseconds, specified
|
||||||
* as the server's unix time minus the local elapsed time. If unknown, set to 0.
|
* as the server's unix time minus the local elapsed time. If unknown, set to 0.
|
||||||
* @param maxSegmentsPerLoad The maximum number of segments to combine into a single request.
|
* @param maxSegmentsPerLoad The maximum number of segments to combine into a single request. Note
|
||||||
* Note that segments will only be combined if their {@link Uri}s are the same and if their
|
* that segments will only be combined if their {@link Uri}s are the same and if their data
|
||||||
* data ranges are adjacent.
|
* ranges are adjacent.
|
||||||
* @param enableEventMessageTrack Whether the chunks generated by the source may output an event
|
* @param enableEventMessageTrack Whether the chunks generated by the source may output an event
|
||||||
* message track.
|
* message track.
|
||||||
* @param enableCea608Track Whether the chunks generated by the source may output a CEA-608 track.
|
* @param enableCea608Track Whether the chunks generated by the source may output a CEA-608 track.
|
||||||
|
* @param playerTrackEmsgHandler The {@link PlayerTrackEmsgHandler} instance to handle emsg
|
||||||
|
* messages targeting the player. Maybe null if this is not necessary.
|
||||||
*/
|
*/
|
||||||
public DefaultDashChunkSource(LoaderErrorThrower manifestLoaderErrorThrower,
|
public DefaultDashChunkSource(
|
||||||
DashManifest manifest, int periodIndex, int[] adaptationSetIndices,
|
LoaderErrorThrower manifestLoaderErrorThrower,
|
||||||
TrackSelection trackSelection, int trackType, DataSource dataSource,
|
DashManifest manifest,
|
||||||
long elapsedRealtimeOffsetMs, int maxSegmentsPerLoad, boolean enableEventMessageTrack,
|
int periodIndex,
|
||||||
boolean enableCea608Track) {
|
int[] adaptationSetIndices,
|
||||||
|
TrackSelection trackSelection,
|
||||||
|
int trackType,
|
||||||
|
DataSource dataSource,
|
||||||
|
long elapsedRealtimeOffsetMs,
|
||||||
|
int maxSegmentsPerLoad,
|
||||||
|
boolean enableEventMessageTrack,
|
||||||
|
boolean enableCea608Track,
|
||||||
|
@Nullable PlayerTrackEmsgHandler playerTrackEmsgHandler) {
|
||||||
this.manifestLoaderErrorThrower = manifestLoaderErrorThrower;
|
this.manifestLoaderErrorThrower = manifestLoaderErrorThrower;
|
||||||
this.manifest = manifest;
|
this.manifest = manifest;
|
||||||
this.adaptationSetIndices = adaptationSetIndices;
|
this.adaptationSetIndices = adaptationSetIndices;
|
||||||
|
|
@ -131,15 +162,23 @@ public class DefaultDashChunkSource implements DashChunkSource {
|
||||||
this.periodIndex = periodIndex;
|
this.periodIndex = periodIndex;
|
||||||
this.elapsedRealtimeOffsetMs = elapsedRealtimeOffsetMs;
|
this.elapsedRealtimeOffsetMs = elapsedRealtimeOffsetMs;
|
||||||
this.maxSegmentsPerLoad = maxSegmentsPerLoad;
|
this.maxSegmentsPerLoad = maxSegmentsPerLoad;
|
||||||
|
this.playerTrackEmsgHandler = playerTrackEmsgHandler;
|
||||||
|
|
||||||
long periodDurationUs = manifest.getPeriodDurationUs(periodIndex);
|
long periodDurationUs = manifest.getPeriodDurationUs(periodIndex);
|
||||||
liveEdgeTimeUs = C.TIME_UNSET;
|
liveEdgeTimeUs = C.TIME_UNSET;
|
||||||
|
|
||||||
List<Representation> representations = getRepresentations();
|
List<Representation> representations = getRepresentations();
|
||||||
representationHolders = new RepresentationHolder[trackSelection.length()];
|
representationHolders = new RepresentationHolder[trackSelection.length()];
|
||||||
for (int i = 0; i < representationHolders.length; i++) {
|
for (int i = 0; i < representationHolders.length; i++) {
|
||||||
Representation representation = representations.get(trackSelection.getIndexInTrackGroup(i));
|
Representation representation = representations.get(trackSelection.getIndexInTrackGroup(i));
|
||||||
representationHolders[i] = new RepresentationHolder(periodDurationUs, trackType,
|
representationHolders[i] =
|
||||||
representation, enableEventMessageTrack, enableCea608Track);
|
new RepresentationHolder(
|
||||||
|
periodDurationUs,
|
||||||
|
trackType,
|
||||||
|
representation,
|
||||||
|
enableEventMessageTrack,
|
||||||
|
enableCea608Track,
|
||||||
|
playerTrackEmsgHandler);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -203,6 +242,20 @@ public class DefaultDashChunkSource implements DashChunkSource {
|
||||||
|
|
||||||
long bufferedDurationUs = loadPositionUs - playbackPositionUs;
|
long bufferedDurationUs = loadPositionUs - playbackPositionUs;
|
||||||
long timeToLiveEdgeUs = resolveTimeToLiveEdgeUs(playbackPositionUs);
|
long timeToLiveEdgeUs = resolveTimeToLiveEdgeUs(playbackPositionUs);
|
||||||
|
long presentationPositionUs =
|
||||||
|
C.msToUs(manifest.availabilityStartTimeMs)
|
||||||
|
+ C.msToUs(manifest.getPeriod(periodIndex).startMs)
|
||||||
|
+ loadPositionUs;
|
||||||
|
try {
|
||||||
|
if (playerTrackEmsgHandler != null
|
||||||
|
&& playerTrackEmsgHandler.maybeRefreshManifestBeforeLoadingNextChunk(
|
||||||
|
presentationPositionUs)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (DashManifestExpiredException e) {
|
||||||
|
fatalError = e;
|
||||||
|
return;
|
||||||
|
}
|
||||||
trackSelection.updateSelectedTrack(playbackPositionUs, bufferedDurationUs, timeToLiveEdgeUs);
|
trackSelection.updateSelectedTrack(playbackPositionUs, bufferedDurationUs, timeToLiveEdgeUs);
|
||||||
|
|
||||||
RepresentationHolder representationHolder =
|
RepresentationHolder representationHolder =
|
||||||
|
|
@ -298,6 +351,9 @@ public class DefaultDashChunkSource implements DashChunkSource {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (playerTrackEmsgHandler != null) {
|
||||||
|
playerTrackEmsgHandler.onChunkLoadCompleted(chunk);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -305,6 +361,10 @@ public class DefaultDashChunkSource implements DashChunkSource {
|
||||||
if (!cancelable) {
|
if (!cancelable) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (playerTrackEmsgHandler != null
|
||||||
|
&& playerTrackEmsgHandler.maybeRefreshManifestOnLoadingError(chunk)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
// Workaround for missing segment at the end of the period
|
// Workaround for missing segment at the end of the period
|
||||||
if (!manifest.dynamic && chunk instanceof MediaChunk
|
if (!manifest.dynamic && chunk instanceof MediaChunk
|
||||||
&& e instanceof InvalidResponseCodeException
|
&& e instanceof InvalidResponseCodeException
|
||||||
|
|
@ -426,8 +486,13 @@ public class DefaultDashChunkSource implements DashChunkSource {
|
||||||
private long periodDurationUs;
|
private long periodDurationUs;
|
||||||
private int segmentNumShift;
|
private int segmentNumShift;
|
||||||
|
|
||||||
/* package */ RepresentationHolder(long periodDurationUs, int trackType,
|
/* package */ RepresentationHolder(
|
||||||
Representation representation, boolean enableEventMessageTrack, boolean enableCea608Track) {
|
long periodDurationUs,
|
||||||
|
int trackType,
|
||||||
|
Representation representation,
|
||||||
|
boolean enableEventMessageTrack,
|
||||||
|
boolean enableCea608Track,
|
||||||
|
TrackOutput playerEmsgTrackOutput) {
|
||||||
this.periodDurationUs = periodDurationUs;
|
this.periodDurationUs = periodDurationUs;
|
||||||
this.representation = representation;
|
this.representation = representation;
|
||||||
String containerMimeType = representation.format.containerMimeType;
|
String containerMimeType = representation.format.containerMimeType;
|
||||||
|
|
@ -449,7 +514,10 @@ public class DefaultDashChunkSource implements DashChunkSource {
|
||||||
? Collections.singletonList(
|
? Collections.singletonList(
|
||||||
Format.createTextSampleFormat(null, MimeTypes.APPLICATION_CEA608, 0, null))
|
Format.createTextSampleFormat(null, MimeTypes.APPLICATION_CEA608, 0, null))
|
||||||
: Collections.<Format>emptyList();
|
: Collections.<Format>emptyList();
|
||||||
extractor = new FragmentedMp4Extractor(flags, null, null, null, closedCaptionFormats);
|
|
||||||
|
extractor =
|
||||||
|
new FragmentedMp4Extractor(
|
||||||
|
flags, null, null, null, closedCaptionFormats, playerEmsgTrackOutput);
|
||||||
}
|
}
|
||||||
// Prefer drmInitData obtained from the manifest over drmInitData obtained from the stream,
|
// Prefer drmInitData obtained from the manifest over drmInitData obtained from the stream,
|
||||||
// as per DASH IF Interoperability Recommendations V3.0, 7.5.3.
|
// as per DASH IF Interoperability Recommendations V3.0, 7.5.3.
|
||||||
|
|
@ -534,7 +602,5 @@ public class DefaultDashChunkSource implements DashChunkSource {
|
||||||
private static boolean mimeTypeIsRawText(String mimeType) {
|
private static boolean mimeTypeIsRawText(String mimeType) {
|
||||||
return MimeTypes.isText(mimeType) || MimeTypes.APPLICATION_TTML.equals(mimeType);
|
return MimeTypes.isText(mimeType) || MimeTypes.APPLICATION_TTML.equals(mimeType);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,454 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2017 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 static com.google.android.exoplayer2.util.Util.parseXsDateTime;
|
||||||
|
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Message;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
import com.google.android.exoplayer2.C;
|
||||||
|
import com.google.android.exoplayer2.Format;
|
||||||
|
import com.google.android.exoplayer2.FormatHolder;
|
||||||
|
import com.google.android.exoplayer2.ParserException;
|
||||||
|
import com.google.android.exoplayer2.extractor.ExtractorInput;
|
||||||
|
import com.google.android.exoplayer2.extractor.TrackOutput;
|
||||||
|
import com.google.android.exoplayer2.metadata.Metadata;
|
||||||
|
import com.google.android.exoplayer2.metadata.MetadataInputBuffer;
|
||||||
|
import com.google.android.exoplayer2.metadata.emsg.EventMessage;
|
||||||
|
import com.google.android.exoplayer2.metadata.emsg.EventMessageDecoder;
|
||||||
|
import com.google.android.exoplayer2.source.SampleQueue;
|
||||||
|
import com.google.android.exoplayer2.source.chunk.Chunk;
|
||||||
|
import com.google.android.exoplayer2.source.dash.manifest.DashManifest;
|
||||||
|
import com.google.android.exoplayer2.upstream.Allocator;
|
||||||
|
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles all emsg messages from all media tracks for the player.
|
||||||
|
*
|
||||||
|
* <p>This class will only respond to emsg messages which have schemeIdUri
|
||||||
|
* "urn:mpeg:dash:event:2012", and value "1"/"2"/"3". When it encounters one of these messages, it
|
||||||
|
* will handle the message according to Section 4.5.2.1 DASH -IF IOP Version 4.1:
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li>If both presentation time delta and event duration are zero, it means the media
|
||||||
|
* presentation has ended.
|
||||||
|
* <li>Else, it will parse the message data from the emsg message to find the publishTime of the
|
||||||
|
* expired manifest, and mark manifest with publishTime smaller than that values to be
|
||||||
|
* expired.
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* In both cases, the DASH media source will be notified, and a manifest reload should be triggered.
|
||||||
|
*/
|
||||||
|
public final class PlayerEmsgHandler implements Handler.Callback {
|
||||||
|
|
||||||
|
private static final int EMSG_MEDIA_PRESENTATION_ENDED = 1;
|
||||||
|
private static final int EMSG_MANIFEST_EXPIRED = 2;
|
||||||
|
|
||||||
|
/** Callbacks for player emsg events encountered during DASH live stream. */
|
||||||
|
public interface PlayerEmsgCallback {
|
||||||
|
|
||||||
|
/** Called when the current manifest should be refreshed. */
|
||||||
|
void onDashManifestRefreshRequested();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the manifest with the publish time has been expired.
|
||||||
|
*
|
||||||
|
* @param expiredManifestPublishTimeUs The manifest publish time that has been expired.
|
||||||
|
*/
|
||||||
|
void onDashManifestPublishTimeExpired(long expiredManifestPublishTimeUs);
|
||||||
|
|
||||||
|
/** Called when a media presentation end signal is encountered during live stream. * */
|
||||||
|
void onDashLiveMediaPresentationEndSignalEncountered();
|
||||||
|
}
|
||||||
|
|
||||||
|
private final Allocator allocator;
|
||||||
|
private final PlayerEmsgCallback playerEmsgCallback;
|
||||||
|
private final EventMessageDecoder decoder;
|
||||||
|
private final Handler handler;
|
||||||
|
private final TreeMap<Long, Long> manifestPublishTimeToExpiryTimeUs;
|
||||||
|
|
||||||
|
private DashManifest manifest;
|
||||||
|
|
||||||
|
private boolean dynamicMediaPresentationEnded;
|
||||||
|
private long expiredManifestPublishTimeUs;
|
||||||
|
private long lastLoadedChunkEndTimeUs;
|
||||||
|
private long lastLoadedChunkEndTimeBeforeRefreshUs;
|
||||||
|
private boolean isWaitingForManifestRefresh;
|
||||||
|
private boolean released;
|
||||||
|
private DashManifestExpiredException fatalError;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param manifest The initial manifest.
|
||||||
|
* @param playerEmsgCallback The callback that this event handler can invoke when handling emsg
|
||||||
|
* messages that generate DASH media source events.
|
||||||
|
* @param allocator An {@link Allocator} from which allocations can be obtained.
|
||||||
|
*/
|
||||||
|
public PlayerEmsgHandler(
|
||||||
|
DashManifest manifest, PlayerEmsgCallback playerEmsgCallback, Allocator allocator) {
|
||||||
|
this.manifest = manifest;
|
||||||
|
this.playerEmsgCallback = playerEmsgCallback;
|
||||||
|
this.allocator = allocator;
|
||||||
|
|
||||||
|
manifestPublishTimeToExpiryTimeUs = new TreeMap<>();
|
||||||
|
handler = new Handler(this);
|
||||||
|
decoder = new EventMessageDecoder();
|
||||||
|
lastLoadedChunkEndTimeUs = C.TIME_UNSET;
|
||||||
|
lastLoadedChunkEndTimeBeforeRefreshUs = C.TIME_UNSET;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the {@link DashManifest} that this handler works on.
|
||||||
|
*
|
||||||
|
* @param newManifest The updated manifest.
|
||||||
|
*/
|
||||||
|
public void updateManifest(DashManifest newManifest) {
|
||||||
|
if (isManifestStale(newManifest)) {
|
||||||
|
fatalError = new DashManifestExpiredException();
|
||||||
|
}
|
||||||
|
|
||||||
|
isWaitingForManifestRefresh = false;
|
||||||
|
expiredManifestPublishTimeUs = C.TIME_UNSET;
|
||||||
|
this.manifest = newManifest;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isManifestStale(DashManifest manifest) {
|
||||||
|
return manifest.dynamic
|
||||||
|
&& (dynamicMediaPresentationEnded
|
||||||
|
|| manifest.publishTimeMs <= expiredManifestPublishTimeUs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* package*/ boolean maybeRefreshManifestBeforeLoadingNextChunk(long presentationPositionUs)
|
||||||
|
throws DashManifestExpiredException {
|
||||||
|
if (fatalError != null) {
|
||||||
|
throw fatalError;
|
||||||
|
}
|
||||||
|
if (!manifest.dynamic) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (isWaitingForManifestRefresh) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
boolean manifestRefreshNeeded = false;
|
||||||
|
if (dynamicMediaPresentationEnded) {
|
||||||
|
// The manifest we have is dynamic, but we know a non-dynamic one representing the final state
|
||||||
|
// should be available.
|
||||||
|
manifestRefreshNeeded = true;
|
||||||
|
} else {
|
||||||
|
// Find the smallest publishTime (greater than or equal to the current manifest's publish
|
||||||
|
// time) that has a corresponding expiry time.
|
||||||
|
Map.Entry<Long, Long> expiredEntry = ceilingExpiryEntryForPublishTime(manifest.publishTimeMs);
|
||||||
|
if (expiredEntry != null) {
|
||||||
|
long expiredPointUs = expiredEntry.getValue();
|
||||||
|
if (expiredPointUs < presentationPositionUs) {
|
||||||
|
expiredManifestPublishTimeUs = expiredEntry.getKey();
|
||||||
|
notifyManifestPublishTimeExpired();
|
||||||
|
manifestRefreshNeeded = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (manifestRefreshNeeded) {
|
||||||
|
maybeNotifyDashManifestRefreshNeeded();
|
||||||
|
}
|
||||||
|
return manifestRefreshNeeded;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For live streaming with emsg event stream, forward seeking can seek pass the emsg messages that
|
||||||
|
* signals end-of-stream or Manifest expiry, which results in load error. In this case, we should
|
||||||
|
* notify the Dash media source to refresh its manifest.
|
||||||
|
*
|
||||||
|
* @param chunk The chunk whose load encountered the error.
|
||||||
|
* @return True if manifest refresh has been requested, false otherwise.
|
||||||
|
*/
|
||||||
|
/* package */ boolean maybeRefreshManifestOnLoadingError(Chunk chunk) {
|
||||||
|
if (!manifest.dynamic) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (isWaitingForManifestRefresh) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
boolean isAfterForwardSeek =
|
||||||
|
lastLoadedChunkEndTimeUs != C.TIME_UNSET && lastLoadedChunkEndTimeUs < chunk.startTimeUs;
|
||||||
|
if (isAfterForwardSeek) {
|
||||||
|
// if we are after a forward seek, and the playback is dynamic with embedded emsg stream,
|
||||||
|
// there's a chance that we have seek over the emsg messages, in which case we should ask
|
||||||
|
// media source for a refresh.
|
||||||
|
maybeNotifyDashManifestRefreshNeeded();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the a new chunk in the current media stream has been loaded.
|
||||||
|
*
|
||||||
|
* @param chunk The chunk whose load has been completed.
|
||||||
|
*/
|
||||||
|
/* package */ void onChunkLoadCompleted(Chunk chunk) {
|
||||||
|
if (lastLoadedChunkEndTimeUs != C.TIME_UNSET || chunk.endTimeUs > lastLoadedChunkEndTimeUs) {
|
||||||
|
lastLoadedChunkEndTimeUs = chunk.endTimeUs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether an event with given schemeIdUri and value is a DASH emsg event targeting the
|
||||||
|
* player.
|
||||||
|
*/
|
||||||
|
public static boolean isPlayerEmsgEvent(String schemeIdUri, String value) {
|
||||||
|
return "urn:mpeg:dash:event:2012".equals(schemeIdUri)
|
||||||
|
&& ("1".equals(value) || "2".equals(value) || "3".equals(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns a {@link TrackOutput} that emsg messages could be written to. */
|
||||||
|
public PlayerTrackEmsgHandler newPlayerTrackEmsgHandler() {
|
||||||
|
return new PlayerTrackEmsgHandler(new SampleQueue(allocator));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Release this emsg handler. It should not be reused after this call. */
|
||||||
|
public void release() {
|
||||||
|
released = true;
|
||||||
|
handler.removeCallbacksAndMessages(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean handleMessage(Message message) {
|
||||||
|
if (released) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
switch (message.what) {
|
||||||
|
case (EMSG_MEDIA_PRESENTATION_ENDED):
|
||||||
|
handleMediaPresentationEndedMessageEncountered();
|
||||||
|
return true;
|
||||||
|
case (EMSG_MANIFEST_EXPIRED):
|
||||||
|
ManifestExpiryEventInfo messageObj = (ManifestExpiryEventInfo) message.obj;
|
||||||
|
handleManifestExpiredMessage(
|
||||||
|
messageObj.eventTimeUs, messageObj.manifestPublishTimeMsInEmsg);
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
// Do nothing.
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Internal methods.
|
||||||
|
|
||||||
|
private void handleManifestExpiredMessage(long eventTimeUs, long manifestPublishTimeMsInEmsg) {
|
||||||
|
if (!manifestPublishTimeToExpiryTimeUs.containsKey(manifestPublishTimeMsInEmsg)) {
|
||||||
|
manifestPublishTimeToExpiryTimeUs.put(manifestPublishTimeMsInEmsg, eventTimeUs);
|
||||||
|
} else {
|
||||||
|
long previousExpiryTimeUs =
|
||||||
|
manifestPublishTimeToExpiryTimeUs.get(manifestPublishTimeMsInEmsg);
|
||||||
|
if (previousExpiryTimeUs > eventTimeUs) {
|
||||||
|
manifestPublishTimeToExpiryTimeUs.put(manifestPublishTimeMsInEmsg, eventTimeUs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleMediaPresentationEndedMessageEncountered() {
|
||||||
|
dynamicMediaPresentationEnded = true;
|
||||||
|
notifySourceMediaPresentationEnded();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map.Entry<Long, Long> ceilingExpiryEntryForPublishTime(long publishTimeMs) {
|
||||||
|
if (manifestPublishTimeToExpiryTimeUs.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return manifestPublishTimeToExpiryTimeUs.ceilingEntry(publishTimeMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void notifyManifestPublishTimeExpired() {
|
||||||
|
playerEmsgCallback.onDashManifestPublishTimeExpired(expiredManifestPublishTimeUs);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void notifySourceMediaPresentationEnded() {
|
||||||
|
playerEmsgCallback.onDashLiveMediaPresentationEndSignalEncountered();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Requests DASH media manifest to be refreshed if necessary. */
|
||||||
|
private void maybeNotifyDashManifestRefreshNeeded() {
|
||||||
|
if (lastLoadedChunkEndTimeBeforeRefreshUs != C.TIME_UNSET
|
||||||
|
&& lastLoadedChunkEndTimeBeforeRefreshUs == lastLoadedChunkEndTimeUs) {
|
||||||
|
// Already requested manifest refresh.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
isWaitingForManifestRefresh = true;
|
||||||
|
lastLoadedChunkEndTimeBeforeRefreshUs = lastLoadedChunkEndTimeUs;
|
||||||
|
playerEmsgCallback.onDashManifestRefreshRequested();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long getManifestPublishTimeMsInEmsg(EventMessage eventMessage) {
|
||||||
|
try {
|
||||||
|
return parseXsDateTime(new String(eventMessage.messageData));
|
||||||
|
} catch (ParserException ignored) {
|
||||||
|
// if we can't parse this event, ignore
|
||||||
|
return C.TIME_UNSET;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isMessageSignalingMediaPresentationEnded(EventMessage eventMessage) {
|
||||||
|
// According to section 4.5.2.1 DASH-IF IOP, if both presentation time delta and event duration
|
||||||
|
// are zero, the media presentation is ended.
|
||||||
|
return eventMessage.presentationTimeUs == 0 && eventMessage.durationMs == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Handles emsg messages for a specific track for the player. */
|
||||||
|
public final class PlayerTrackEmsgHandler implements TrackOutput {
|
||||||
|
|
||||||
|
private final SampleQueue sampleQueue;
|
||||||
|
private final FormatHolder formatHolder;
|
||||||
|
private final MetadataInputBuffer buffer;
|
||||||
|
|
||||||
|
/* package */ PlayerTrackEmsgHandler(SampleQueue sampleQueue) {
|
||||||
|
this.sampleQueue = sampleQueue;
|
||||||
|
|
||||||
|
formatHolder = new FormatHolder();
|
||||||
|
buffer = new MetadataInputBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void format(Format format) {
|
||||||
|
sampleQueue.format(format);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int sampleData(ExtractorInput input, int length, boolean allowEndOfInput)
|
||||||
|
throws IOException, InterruptedException {
|
||||||
|
return sampleQueue.sampleData(input, length, allowEndOfInput);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void sampleData(ParsableByteArray data, int length) {
|
||||||
|
sampleQueue.sampleData(data, length);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void sampleMetadata(
|
||||||
|
long timeUs, int flags, int size, int offset, CryptoData encryptionData) {
|
||||||
|
sampleQueue.sampleMetadata(timeUs, flags, size, offset, encryptionData);
|
||||||
|
parseAndDiscardSamples();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For live streaming, check if the DASH manifest is expired before the next segment start time.
|
||||||
|
* If it is, the DASH media source will be notified to refresh the manifest.
|
||||||
|
*
|
||||||
|
* @param presentationPositionUs The next load position in presentation time.
|
||||||
|
* @return True if manifest refresh has been requested, false otherwise.
|
||||||
|
* @throws DashManifestExpiredException If the current DASH manifest is expired, but a new
|
||||||
|
* manifest could not be loaded.
|
||||||
|
*/
|
||||||
|
public boolean maybeRefreshManifestBeforeLoadingNextChunk(long presentationPositionUs)
|
||||||
|
throws DashManifestExpiredException {
|
||||||
|
return PlayerEmsgHandler.this.maybeRefreshManifestBeforeLoadingNextChunk(
|
||||||
|
presentationPositionUs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the a new chunk in the current media stream has been loaded.
|
||||||
|
*
|
||||||
|
* @param chunk The chunk whose load has been completed.
|
||||||
|
*/
|
||||||
|
public void onChunkLoadCompleted(Chunk chunk) {
|
||||||
|
PlayerEmsgHandler.this.onChunkLoadCompleted(chunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For live streaming with emsg event stream, forward seeking can seek pass the emsg messages
|
||||||
|
* that signals end-of-stream or Manifest expiry, which results in load error. In this case, we
|
||||||
|
* should notify the Dash media source to refresh its manifest.
|
||||||
|
*
|
||||||
|
* @param chunk The chunk whose load encountered the error.
|
||||||
|
* @return True if manifest refresh has been requested, false otherwise.
|
||||||
|
*/
|
||||||
|
public boolean maybeRefreshManifestOnLoadingError(Chunk chunk) {
|
||||||
|
return PlayerEmsgHandler.this.maybeRefreshManifestOnLoadingError(chunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Release this track emsg handler. It should not be reused after this call. */
|
||||||
|
public void release() {
|
||||||
|
sampleQueue.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Internal methods.
|
||||||
|
|
||||||
|
private void parseAndDiscardSamples() {
|
||||||
|
while (sampleQueue.hasNextSample()) {
|
||||||
|
MetadataInputBuffer inputBuffer = dequeueSample();
|
||||||
|
if (inputBuffer == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
long eventTimeUs = inputBuffer.timeUs;
|
||||||
|
Metadata metadata = decoder.decode(inputBuffer);
|
||||||
|
EventMessage eventMessage = (EventMessage) metadata.get(0);
|
||||||
|
if (isPlayerEmsgEvent(eventMessage.schemeIdUri, eventMessage.value)) {
|
||||||
|
parsePlayerEmsgEvent(eventTimeUs, eventMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sampleQueue.discardToRead();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private MetadataInputBuffer dequeueSample() {
|
||||||
|
buffer.clear();
|
||||||
|
int result = sampleQueue.read(formatHolder, buffer, false, false, 0);
|
||||||
|
if (result == C.RESULT_BUFFER_READ) {
|
||||||
|
buffer.flip();
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void parsePlayerEmsgEvent(long eventTimeUs, EventMessage eventMessage) {
|
||||||
|
long manifestPublishTimeMsInEmsg = getManifestPublishTimeMsInEmsg(eventMessage);
|
||||||
|
if (manifestPublishTimeMsInEmsg == C.TIME_UNSET) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isMessageSignalingMediaPresentationEnded(eventMessage)) {
|
||||||
|
onMediaPresentationEndedMessageEncountered();
|
||||||
|
} else {
|
||||||
|
onManifestExpiredMessageEncountered(eventTimeUs, manifestPublishTimeMsInEmsg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onMediaPresentationEndedMessageEncountered() {
|
||||||
|
handler.sendMessage(handler.obtainMessage(EMSG_MEDIA_PRESENTATION_ENDED));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onManifestExpiredMessageEncountered(
|
||||||
|
long eventTimeUs, long manifestPublishTimeMsInEmsg) {
|
||||||
|
ManifestExpiryEventInfo manifestExpiryEventInfo =
|
||||||
|
new ManifestExpiryEventInfo(eventTimeUs, manifestPublishTimeMsInEmsg);
|
||||||
|
handler.sendMessage(handler.obtainMessage(EMSG_MANIFEST_EXPIRED, manifestExpiryEventInfo));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Holds information related to a manifest expiry event. */
|
||||||
|
private static final class ManifestExpiryEventInfo {
|
||||||
|
|
||||||
|
public final long eventTimeUs;
|
||||||
|
public final long manifestPublishTimeMsInEmsg;
|
||||||
|
|
||||||
|
public ManifestExpiryEventInfo(long eventTimeUs, long manifestPublishTimeMsInEmsg) {
|
||||||
|
this.eventTimeUs = eventTimeUs;
|
||||||
|
this.manifestPublishTimeMsInEmsg = manifestPublishTimeMsInEmsg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue