mirror of
https://github.com/samsonjs/media.git
synced 2026-03-30 10:15:48 +00:00
Feed timestamps from loaded chunks back to the playlist tracker
This is the first step towards allowing discontinuities in the playlist tracking. Also changed durationSecs for durationUs in MediaPlaylist.Segment. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=138207732
This commit is contained in:
parent
a5a2bc89f4
commit
992cfdecc2
6 changed files with 84 additions and 34 deletions
|
|
@ -72,7 +72,6 @@ public class HlsMediaPlaylistParserTest extends TestCase {
|
|||
HlsMediaPlaylist mediaPlaylist = (HlsMediaPlaylist) playlist;
|
||||
|
||||
assertEquals(2679, mediaPlaylist.mediaSequence);
|
||||
assertEquals(8, mediaPlaylist.targetDurationSecs);
|
||||
assertEquals(3, mediaPlaylist.version);
|
||||
assertEquals(true, mediaPlaylist.hasEndTag);
|
||||
List<HlsMediaPlaylist.Segment> segments = mediaPlaylist.segments;
|
||||
|
|
@ -80,7 +79,7 @@ public class HlsMediaPlaylistParserTest extends TestCase {
|
|||
assertEquals(5, segments.size());
|
||||
|
||||
assertEquals(4, segments.get(0).discontinuitySequenceNumber);
|
||||
assertEquals(7.975, segments.get(0).durationSecs);
|
||||
assertEquals(7975000, segments.get(0).durationUs);
|
||||
assertEquals(false, segments.get(0).isEncrypted);
|
||||
assertEquals(null, segments.get(0).encryptionKeyUri);
|
||||
assertEquals(null, segments.get(0).encryptionIV);
|
||||
|
|
@ -89,7 +88,7 @@ public class HlsMediaPlaylistParserTest extends TestCase {
|
|||
assertEquals("https://priv.example.com/fileSequence2679.ts", segments.get(0).url);
|
||||
|
||||
assertEquals(4, segments.get(1).discontinuitySequenceNumber);
|
||||
assertEquals(7.975, segments.get(1).durationSecs);
|
||||
assertEquals(7975000, segments.get(1).durationUs);
|
||||
assertEquals(true, segments.get(1).isEncrypted);
|
||||
assertEquals("https://priv.example.com/key.php?r=2680", segments.get(1).encryptionKeyUri);
|
||||
assertEquals("0x1566B", segments.get(1).encryptionIV);
|
||||
|
|
@ -98,7 +97,7 @@ public class HlsMediaPlaylistParserTest extends TestCase {
|
|||
assertEquals("https://priv.example.com/fileSequence2680.ts", segments.get(1).url);
|
||||
|
||||
assertEquals(4, segments.get(2).discontinuitySequenceNumber);
|
||||
assertEquals(7.941, segments.get(2).durationSecs);
|
||||
assertEquals(7941000, segments.get(2).durationUs);
|
||||
assertEquals(false, segments.get(2).isEncrypted);
|
||||
assertEquals(null, segments.get(2).encryptionKeyUri);
|
||||
assertEquals(null, segments.get(2).encryptionIV);
|
||||
|
|
@ -107,7 +106,7 @@ public class HlsMediaPlaylistParserTest extends TestCase {
|
|||
assertEquals("https://priv.example.com/fileSequence2681.ts", segments.get(2).url);
|
||||
|
||||
assertEquals(5, segments.get(3).discontinuitySequenceNumber);
|
||||
assertEquals(7.975, segments.get(3).durationSecs);
|
||||
assertEquals(7975000, segments.get(3).durationUs);
|
||||
assertEquals(true, segments.get(3).isEncrypted);
|
||||
assertEquals("https://priv.example.com/key.php?r=2682", segments.get(3).encryptionKeyUri);
|
||||
// 0xA7A == 2682.
|
||||
|
|
@ -118,7 +117,7 @@ public class HlsMediaPlaylistParserTest extends TestCase {
|
|||
assertEquals("https://priv.example.com/fileSequence2682.ts", segments.get(3).url);
|
||||
|
||||
assertEquals(5, segments.get(4).discontinuitySequenceNumber);
|
||||
assertEquals(7.975, segments.get(4).durationSecs);
|
||||
assertEquals(7975000, segments.get(4).durationUs);
|
||||
assertEquals(true, segments.get(4).isEncrypted);
|
||||
assertEquals("https://priv.example.com/key.php?r=2682", segments.get(4).encryptionKeyUri);
|
||||
// 0xA7B == 2683.
|
||||
|
|
|
|||
|
|
@ -267,7 +267,7 @@ import java.util.Locale;
|
|||
if (previous != null && !switchingVariant) {
|
||||
startTimeUs = previous.getAdjustedEndTimeUs();
|
||||
}
|
||||
long endTimeUs = startTimeUs + (long) (segment.durationSecs * C.MICROS_PER_SECOND);
|
||||
long endTimeUs = startTimeUs + segment.durationUs;
|
||||
Format format = variants[newVariantIndex].format;
|
||||
|
||||
Uri chunkUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, segment.url);
|
||||
|
|
@ -322,7 +322,7 @@ import java.util.Locale;
|
|||
// This flag ensures the change of pid between streams does not affect the sample queues.
|
||||
@DefaultTsPayloadReaderFactory.Flags
|
||||
int esReaderFactoryFlags = 0;
|
||||
String codecs = variants[newVariantIndex].format.codecs;
|
||||
String codecs = format.codecs;
|
||||
if (!TextUtils.isEmpty(codecs)) {
|
||||
// Sometimes AAC and H264 streams are declared in TS chunks even though they don't really
|
||||
// exist. If we know from the codec attribute that they don't exist, then we can
|
||||
|
|
@ -353,7 +353,7 @@ import java.util.Locale;
|
|||
// Configure the data source and spec for the chunk.
|
||||
DataSpec dataSpec = new DataSpec(chunkUri, segment.byterangeOffset, segment.byterangeLength,
|
||||
null);
|
||||
out.chunk = new HlsMediaChunk(dataSource, dataSpec, format,
|
||||
out.chunk = new HlsMediaChunk(dataSource, dataSpec, variants[newVariantIndex],
|
||||
trackSelection.getSelectionReason(), trackSelection.getSelectionData(),
|
||||
startTimeUs, endTimeUs, chunkMediaSequence, segment.discontinuitySequenceNumber,
|
||||
isTimestampMaster, timestampAdjuster, extractor, extractorNeedsInit, switchingVariant,
|
||||
|
|
@ -367,6 +367,11 @@ import java.util.Locale;
|
|||
* @param chunk The chunk whose load has been completed.
|
||||
*/
|
||||
public void onChunkLoadCompleted(Chunk chunk) {
|
||||
if (chunk instanceof HlsMediaChunk) {
|
||||
HlsMediaChunk mediaChunk = (HlsMediaChunk) chunk;
|
||||
playlistTracker.onChunkLoaded(mediaChunk.hlsUrl, mediaChunk.chunkIndex,
|
||||
mediaChunk.getAdjustedStartTimeUs());
|
||||
}
|
||||
if (chunk instanceof HlsInitializationChunk) {
|
||||
lastLoadedInitializationChunk = (HlsInitializationChunk) chunk;
|
||||
} else if (chunk instanceof EncryptionKeyChunk) {
|
||||
|
|
|
|||
|
|
@ -15,12 +15,12 @@
|
|||
*/
|
||||
package com.google.android.exoplayer2.source.hls;
|
||||
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.extractor.DefaultExtractorInput;
|
||||
import com.google.android.exoplayer2.extractor.Extractor;
|
||||
import com.google.android.exoplayer2.extractor.ExtractorInput;
|
||||
import com.google.android.exoplayer2.extractor.TimestampAdjuster;
|
||||
import com.google.android.exoplayer2.source.chunk.MediaChunk;
|
||||
import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.HlsUrl;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.upstream.DataSpec;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
|
|
@ -49,6 +49,11 @@ import java.util.concurrent.atomic.AtomicInteger;
|
|||
*/
|
||||
public final Extractor extractor;
|
||||
|
||||
/**
|
||||
* The url of the playlist from which this chunk was obtained.
|
||||
*/
|
||||
public final HlsUrl hlsUrl;
|
||||
|
||||
private final boolean isEncrypted;
|
||||
private final boolean extractorNeedsInit;
|
||||
private final boolean shouldSpliceIn;
|
||||
|
|
@ -64,7 +69,7 @@ import java.util.concurrent.atomic.AtomicInteger;
|
|||
/**
|
||||
* @param dataSource The source from which the data should be loaded.
|
||||
* @param dataSpec Defines the data to be loaded.
|
||||
* @param trackFormat See {@link #trackFormat}.
|
||||
* @param hlsUrl The url of the playlist from which this chunk was obtained.
|
||||
* @param trackSelectionReason See {@link #trackSelectionReason}.
|
||||
* @param trackSelectionData See {@link #trackSelectionData}.
|
||||
* @param startTimeUs The start time of the media contained by the chunk, in microseconds.
|
||||
|
|
@ -81,13 +86,14 @@ import java.util.concurrent.atomic.AtomicInteger;
|
|||
* @param encryptionKey For AES encryption chunks, the encryption key.
|
||||
* @param encryptionIv For AES encryption chunks, the encryption initialization vector.
|
||||
*/
|
||||
public HlsMediaChunk(DataSource dataSource, DataSpec dataSpec, Format trackFormat,
|
||||
public HlsMediaChunk(DataSource dataSource, DataSpec dataSpec, HlsUrl hlsUrl,
|
||||
int trackSelectionReason, Object trackSelectionData, long startTimeUs, long endTimeUs,
|
||||
int chunkIndex, int discontinuitySequenceNumber, boolean isMasterTimestampSource,
|
||||
TimestampAdjuster timestampAdjuster, Extractor extractor, boolean extractorNeedsInit,
|
||||
boolean shouldSpliceIn, byte[] encryptionKey, byte[] encryptionIv) {
|
||||
super(buildDataSource(dataSource, encryptionKey, encryptionIv), dataSpec, trackFormat,
|
||||
super(buildDataSource(dataSource, encryptionKey, encryptionIv), dataSpec, hlsUrl.format,
|
||||
trackSelectionReason, trackSelectionData, startTimeUs, endTimeUs, chunkIndex);
|
||||
this.hlsUrl = hlsUrl;
|
||||
this.discontinuitySequenceNumber = discontinuitySequenceNumber;
|
||||
this.isMasterTimestampSource = isMasterTimestampSource;
|
||||
this.timestampAdjuster = timestampAdjuster;
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
|
|||
public static final class Segment implements Comparable<Long> {
|
||||
|
||||
public final String url;
|
||||
public final double durationSecs;
|
||||
public final long durationUs;
|
||||
public final int discontinuitySequenceNumber;
|
||||
public final long startTimeUs;
|
||||
public final boolean isEncrypted;
|
||||
|
|
@ -44,11 +44,11 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
|
|||
this(uri, 0, -1, C.TIME_UNSET, false, null, null, byterangeOffset, byterangeLength);
|
||||
}
|
||||
|
||||
public Segment(String uri, double durationSecs, int discontinuitySequenceNumber,
|
||||
public Segment(String uri, long durationUs, int discontinuitySequenceNumber,
|
||||
long startTimeUs, boolean isEncrypted, String encryptionKeyUri, String encryptionIV,
|
||||
long byterangeOffset, long byterangeLength) {
|
||||
this.url = uri;
|
||||
this.durationSecs = durationSecs;
|
||||
this.durationUs = durationUs;
|
||||
this.discontinuitySequenceNumber = discontinuitySequenceNumber;
|
||||
this.startTimeUs = startTimeUs;
|
||||
this.isEncrypted = isEncrypted;
|
||||
|
|
@ -64,28 +64,23 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
|
|||
}
|
||||
|
||||
public Segment copyWithStartTimeUs(long startTimeUs) {
|
||||
return new Segment(url, durationSecs, discontinuitySequenceNumber, startTimeUs, isEncrypted,
|
||||
return new Segment(url, durationUs, discontinuitySequenceNumber, startTimeUs, isEncrypted,
|
||||
encryptionKeyUri, encryptionIV, byterangeOffset, byterangeLength);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static final String ENCRYPTION_METHOD_NONE = "NONE";
|
||||
public static final String ENCRYPTION_METHOD_AES_128 = "AES-128";
|
||||
|
||||
public final int mediaSequence;
|
||||
public final int targetDurationSecs;
|
||||
public final int version;
|
||||
public final Segment initializationSegment;
|
||||
public final List<Segment> segments;
|
||||
public final boolean hasEndTag;
|
||||
public final long durationUs;
|
||||
|
||||
public HlsMediaPlaylist(String baseUri, int mediaSequence, int targetDurationSecs, int version,
|
||||
public HlsMediaPlaylist(String baseUri, int mediaSequence, int version,
|
||||
boolean hasEndTag, Segment initializationSegment, List<Segment> segments) {
|
||||
super(baseUri, HlsPlaylist.TYPE_MEDIA);
|
||||
this.mediaSequence = mediaSequence;
|
||||
this.targetDurationSecs = targetDurationSecs;
|
||||
this.version = version;
|
||||
this.hasEndTag = hasEndTag;
|
||||
this.initializationSegment = initializationSegment;
|
||||
|
|
@ -94,8 +89,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
|
|||
if (!segments.isEmpty()) {
|
||||
Segment first = segments.get(0);
|
||||
Segment last = segments.get(segments.size() - 1);
|
||||
durationUs = last.startTimeUs + (long) (last.durationSecs * C.MICROS_PER_SECOND)
|
||||
- first.startTimeUs;
|
||||
durationUs = last.startTimeUs + last.durationUs - first.startTimeUs;
|
||||
} else {
|
||||
durationUs = 0;
|
||||
}
|
||||
|
|
@ -121,7 +115,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
|
|||
}
|
||||
|
||||
public HlsMediaPlaylist copyWithSegments(List<Segment> segments) {
|
||||
return new HlsMediaPlaylist(baseUri, mediaSequence, targetDurationSecs, version, hasEndTag,
|
||||
return new HlsMediaPlaylist(baseUri, mediaSequence, version, hasEndTag,
|
||||
initializationSegment, segments);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -217,7 +217,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
|
|||
Segment initializationSegment = null;
|
||||
List<Segment> segments = new ArrayList<>();
|
||||
|
||||
double segmentDurationSecs = 0.0;
|
||||
long segmentDurationUs = 0;
|
||||
int discontinuitySequenceNumber = 0;
|
||||
long segmentStartTimeUs = 0;
|
||||
long segmentByteRangeOffset = 0;
|
||||
|
|
@ -252,7 +252,8 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
|
|||
} else if (line.startsWith(TAG_VERSION)) {
|
||||
version = parseIntAttr(line, REGEX_VERSION);
|
||||
} else if (line.startsWith(TAG_MEDIA_DURATION)) {
|
||||
segmentDurationSecs = parseDoubleAttr(line, REGEX_MEDIA_DURATION);
|
||||
segmentDurationUs =
|
||||
(long) (parseDoubleAttr(line, REGEX_MEDIA_DURATION) * C.MICROS_PER_SECOND);
|
||||
} else if (line.startsWith(TAG_KEY)) {
|
||||
String method = parseStringAttr(line, REGEX_METHOD);
|
||||
isEncrypted = METHOD_AES128.equals(method);
|
||||
|
|
@ -287,11 +288,11 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
|
|||
if (segmentByteRangeLength == C.LENGTH_UNSET) {
|
||||
segmentByteRangeOffset = 0;
|
||||
}
|
||||
segments.add(new Segment(line, segmentDurationSecs, discontinuitySequenceNumber,
|
||||
segments.add(new Segment(line, segmentDurationUs, discontinuitySequenceNumber,
|
||||
segmentStartTimeUs, isEncrypted, encryptionKeyUri, segmentEncryptionIV,
|
||||
segmentByteRangeOffset, segmentByteRangeLength));
|
||||
segmentStartTimeUs += (long) (segmentDurationSecs * C.MICROS_PER_SECOND);
|
||||
segmentDurationSecs = 0.0;
|
||||
segmentStartTimeUs += segmentDurationUs;
|
||||
segmentDurationUs = 0;
|
||||
if (segmentByteRangeLength != C.LENGTH_UNSET) {
|
||||
segmentByteRangeOffset += segmentByteRangeLength;
|
||||
}
|
||||
|
|
@ -300,7 +301,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
|
|||
hasEndTag = true;
|
||||
}
|
||||
}
|
||||
return new HlsMediaPlaylist(baseUri, mediaSequence, targetDurationSecs, version, hasEndTag,
|
||||
return new HlsMediaPlaylist(baseUri, mediaSequence, version, hasEndTag,
|
||||
initializationSegment, segments);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import com.google.android.exoplayer2.C;
|
|||
import com.google.android.exoplayer2.ParserException;
|
||||
import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher;
|
||||
import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.HlsUrl;
|
||||
import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Segment;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.upstream.Loader;
|
||||
import com.google.android.exoplayer2.upstream.ParsingLoadable;
|
||||
|
|
@ -62,6 +63,12 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable
|
|||
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the minimum amount of time by which a media playlist segment's start time has to
|
||||
* drift from the actual start time of the chunk it refers to for it to be adjusted.
|
||||
*/
|
||||
private static final long TIMESTAMP_ADJUSTMENT_THRESHOLD_US = 500000;
|
||||
|
||||
/**
|
||||
* Period for refreshing playlists.
|
||||
*/
|
||||
|
|
@ -182,6 +189,17 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable
|
|||
return isLive;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a chunk from a media playlist is loaded.
|
||||
*
|
||||
* @param hlsUrl The url of the playlist from which the chunk was obtained.
|
||||
* @param chunkMediaSequence The media sequence number of the loaded chunk.
|
||||
* @param adjustedStartTimeUs The adjusted start time of the loaded chunk.
|
||||
*/
|
||||
public void onChunkLoaded(HlsUrl hlsUrl, int chunkMediaSequence, long adjustedStartTimeUs) {
|
||||
playlistBundles.get(hlsUrl).adjustTimestampsOfPlaylist(chunkMediaSequence, adjustedStartTimeUs);
|
||||
}
|
||||
|
||||
// Loader.Callback implementation.
|
||||
|
||||
@Override
|
||||
|
|
@ -262,7 +280,6 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable
|
|||
}
|
||||
|
||||
/**
|
||||
* TODO: Allow chunks to feed adjusted timestamps back to the playlist tracker.
|
||||
* TODO: Track discontinuities for media playlists that don't include the discontinuity number.
|
||||
*/
|
||||
private HlsMediaPlaylist adjustPlaylistTimestamps(HlsMediaPlaylist oldPlaylist,
|
||||
|
|
@ -293,7 +310,7 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable
|
|||
HlsMediaPlaylist.Segment lastSegment = oldSegments.get(oldPlaylistSize - 1);
|
||||
for (int i = newPlaylistSize - newSegmentsCount; i < newPlaylistSize; i++) {
|
||||
lastSegment = newPlaylist.segments.get(i).copyWithStartTimeUs(
|
||||
lastSegment.startTimeUs + (long) lastSegment.durationSecs * C.MICROS_PER_SECOND);
|
||||
lastSegment.startTimeUs + lastSegment.durationUs);
|
||||
newSegments.add(lastSegment);
|
||||
}
|
||||
return newPlaylist.copyWithSegments(newSegments);
|
||||
|
|
@ -343,6 +360,34 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable
|
|||
this.callback = callback;
|
||||
}
|
||||
|
||||
public void adjustTimestampsOfPlaylist(int chunkMediaSequence, long adjustedStartTimeUs) {
|
||||
ArrayList<Segment> segments = new ArrayList<>(latestPlaylistSnapshot.segments);
|
||||
int indexOfChunk = chunkMediaSequence - latestPlaylistSnapshot.mediaSequence;
|
||||
if (indexOfChunk < 0) {
|
||||
return;
|
||||
}
|
||||
Segment actualSegment = segments.get(indexOfChunk);
|
||||
long timestampDriftUs = Math.abs(actualSegment.startTimeUs - adjustedStartTimeUs);
|
||||
if (timestampDriftUs < TIMESTAMP_ADJUSTMENT_THRESHOLD_US) {
|
||||
return;
|
||||
}
|
||||
segments.set(indexOfChunk, actualSegment.copyWithStartTimeUs(adjustedStartTimeUs));
|
||||
// Propagate the adjustment backwards.
|
||||
for (int i = indexOfChunk - 1; i >= 0; i--) {
|
||||
Segment segment = segments.get(i);
|
||||
segments.set(i,
|
||||
segment.copyWithStartTimeUs(segments.get(i + 1).startTimeUs - segment.durationUs));
|
||||
}
|
||||
// Propagate the adjustment forward.
|
||||
int segmentsSize = segments.size();
|
||||
for (int i = indexOfChunk + 1; i < segmentsSize; i++) {
|
||||
Segment segment = segments.get(i);
|
||||
segments.set(i,
|
||||
segment.copyWithStartTimeUs(segments.get(i - 1).startTimeUs + segment.durationUs));
|
||||
}
|
||||
latestPlaylistSnapshot = latestPlaylistSnapshot.copyWithSegments(segments);
|
||||
}
|
||||
|
||||
// Loader.Callback implementation.
|
||||
|
||||
@Override
|
||||
|
|
|
|||
Loading…
Reference in a new issue