mirror of
https://github.com/samsonjs/media.git
synced 2026-04-27 15:07:40 +00:00
Add #EXT-X-PROGRAM-DATE-TIME support for HLS media playlists
Issue:#747 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=140525595
This commit is contained in:
parent
91c58627be
commit
501f54a8a6
6 changed files with 85 additions and 116 deletions
|
|
@ -212,7 +212,8 @@ import java.util.Locale;
|
||||||
// If the playlist is too old to contain the chunk, we need to refresh it.
|
// If the playlist is too old to contain the chunk, we need to refresh it.
|
||||||
chunkMediaSequence = mediaPlaylist.mediaSequence + mediaPlaylist.segments.size();
|
chunkMediaSequence = mediaPlaylist.mediaSequence + mediaPlaylist.segments.size();
|
||||||
} else {
|
} else {
|
||||||
chunkMediaSequence = Util.binarySearchFloor(mediaPlaylist.segments, targetPositionUs, true,
|
chunkMediaSequence = Util.binarySearchFloor(mediaPlaylist.segments,
|
||||||
|
targetPositionUs - mediaPlaylist.startTimeUs, true,
|
||||||
!playlistTracker.isLive() || previous == null) + mediaPlaylist.mediaSequence;
|
!playlistTracker.isLive() || previous == null) + mediaPlaylist.mediaSequence;
|
||||||
if (chunkMediaSequence < mediaPlaylist.mediaSequence && previous != null) {
|
if (chunkMediaSequence < mediaPlaylist.mediaSequence && previous != null) {
|
||||||
// We try getting the next chunk without adapting in case that's the reason for falling
|
// We try getting the next chunk without adapting in case that's the reason for falling
|
||||||
|
|
@ -259,16 +260,6 @@ import java.util.Locale;
|
||||||
clearEncryptionData();
|
clearEncryptionData();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compute start time and sequence number of the next chunk.
|
|
||||||
long startTimeUs = segment.startTimeUs;
|
|
||||||
if (previous != null && !switchingVariant) {
|
|
||||||
startTimeUs = previous.getAdjustedEndTimeUs();
|
|
||||||
}
|
|
||||||
Uri chunkUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, segment.url);
|
|
||||||
|
|
||||||
TimestampAdjuster timestampAdjuster = timestampAdjusterProvider.getAdjuster(
|
|
||||||
segment.discontinuitySequenceNumber, startTimeUs);
|
|
||||||
|
|
||||||
DataSpec initDataSpec = null;
|
DataSpec initDataSpec = null;
|
||||||
Segment initSegment = mediaPlaylist.initializationSegment;
|
Segment initSegment = mediaPlaylist.initializationSegment;
|
||||||
if (initSegment != null) {
|
if (initSegment != null) {
|
||||||
|
|
@ -277,13 +268,20 @@ import java.util.Locale;
|
||||||
initSegment.byterangeLength, null);
|
initSegment.byterangeLength, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Compute start time of the next chunk.
|
||||||
|
long startTimeUs = mediaPlaylist.startTimeUs + segment.relativeStartTimeUs;
|
||||||
|
TimestampAdjuster timestampAdjuster = timestampAdjusterProvider.getAdjuster(
|
||||||
|
segment.discontinuitySequenceNumber, startTimeUs);
|
||||||
|
|
||||||
// Configure the data source and spec for the chunk.
|
// Configure the data source and spec for the chunk.
|
||||||
|
Uri chunkUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, segment.url);
|
||||||
DataSpec dataSpec = new DataSpec(chunkUri, segment.byterangeOffset, segment.byterangeLength,
|
DataSpec dataSpec = new DataSpec(chunkUri, segment.byterangeOffset, segment.byterangeLength,
|
||||||
null);
|
null);
|
||||||
out.chunk = new HlsMediaChunk(dataSource, dataSpec, initDataSpec, variants[newVariantIndex],
|
out.chunk = new HlsMediaChunk(dataSource, dataSpec, initDataSpec, variants[newVariantIndex],
|
||||||
trackSelection.getSelectionReason(), trackSelection.getSelectionData(), segment,
|
trackSelection.getSelectionReason(), trackSelection.getSelectionData(),
|
||||||
chunkMediaSequence, isTimestampMaster, timestampAdjuster, previous, encryptionKey,
|
startTimeUs, startTimeUs + segment.durationUs, chunkMediaSequence,
|
||||||
encryptionIv);
|
segment.discontinuitySequenceNumber, isTimestampMaster, timestampAdjuster, previous,
|
||||||
|
encryptionKey, encryptionIv);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,6 @@ import com.google.android.exoplayer2.extractor.ts.DefaultTsPayloadReaderFactory;
|
||||||
import com.google.android.exoplayer2.extractor.ts.TsExtractor;
|
import com.google.android.exoplayer2.extractor.ts.TsExtractor;
|
||||||
import com.google.android.exoplayer2.source.chunk.MediaChunk;
|
import com.google.android.exoplayer2.source.chunk.MediaChunk;
|
||||||
import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.HlsUrl;
|
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.DataSource;
|
||||||
import com.google.android.exoplayer2.upstream.DataSpec;
|
import com.google.android.exoplayer2.upstream.DataSpec;
|
||||||
import com.google.android.exoplayer2.util.MimeTypes;
|
import com.google.android.exoplayer2.util.MimeTypes;
|
||||||
|
|
@ -89,8 +88,10 @@ import java.util.concurrent.atomic.AtomicInteger;
|
||||||
* @param hlsUrl The url of the playlist from which this chunk was obtained.
|
* @param hlsUrl The url of the playlist from which this chunk was obtained.
|
||||||
* @param trackSelectionReason See {@link #trackSelectionReason}.
|
* @param trackSelectionReason See {@link #trackSelectionReason}.
|
||||||
* @param trackSelectionData See {@link #trackSelectionData}.
|
* @param trackSelectionData See {@link #trackSelectionData}.
|
||||||
* @param segment The {@link Segment} for which this media chunk is created.
|
* @param startTimeUs The start time of the chunk in microseconds.
|
||||||
|
* @param endTimeUs The end time of the chunk in microseconds.
|
||||||
* @param chunkIndex The media sequence number of the chunk.
|
* @param chunkIndex The media sequence number of the chunk.
|
||||||
|
* @param discontinuitySequenceNumber The discontinuity sequence number of the chunk.
|
||||||
* @param isMasterTimestampSource True if the chunk can initialize the timestamp adjuster.
|
* @param isMasterTimestampSource True if the chunk can initialize the timestamp adjuster.
|
||||||
* @param timestampAdjuster Adjuster corresponding to the provided discontinuity sequence number.
|
* @param timestampAdjuster Adjuster corresponding to the provided discontinuity sequence number.
|
||||||
* @param previousChunk The {@link HlsMediaChunk} that preceded this one. May be null.
|
* @param previousChunk The {@link HlsMediaChunk} that preceded this one. May be null.
|
||||||
|
|
@ -98,21 +99,21 @@ import java.util.concurrent.atomic.AtomicInteger;
|
||||||
* @param encryptionIv For AES encryption chunks, the encryption initialization vector.
|
* @param encryptionIv For AES encryption chunks, the encryption initialization vector.
|
||||||
*/
|
*/
|
||||||
public HlsMediaChunk(DataSource dataSource, DataSpec dataSpec, DataSpec initDataSpec,
|
public HlsMediaChunk(DataSource dataSource, DataSpec dataSpec, DataSpec initDataSpec,
|
||||||
HlsUrl hlsUrl, int trackSelectionReason, Object trackSelectionData, Segment segment,
|
HlsUrl hlsUrl, int trackSelectionReason, Object trackSelectionData, long startTimeUs,
|
||||||
int chunkIndex, boolean isMasterTimestampSource, TimestampAdjuster timestampAdjuster,
|
long endTimeUs, int chunkIndex, int discontinuitySequenceNumber,
|
||||||
|
boolean isMasterTimestampSource, TimestampAdjuster timestampAdjuster,
|
||||||
HlsMediaChunk previousChunk, byte[] encryptionKey, byte[] encryptionIv) {
|
HlsMediaChunk previousChunk, byte[] encryptionKey, byte[] encryptionIv) {
|
||||||
super(buildDataSource(dataSource, encryptionKey, encryptionIv), dataSpec, hlsUrl.format,
|
super(buildDataSource(dataSource, encryptionKey, encryptionIv), dataSpec, hlsUrl.format,
|
||||||
trackSelectionReason, trackSelectionData, segment.startTimeUs,
|
trackSelectionReason, trackSelectionData, startTimeUs, endTimeUs, chunkIndex);
|
||||||
segment.startTimeUs + segment.durationUs, chunkIndex);
|
|
||||||
this.initDataSpec = initDataSpec;
|
this.initDataSpec = initDataSpec;
|
||||||
this.hlsUrl = hlsUrl;
|
this.hlsUrl = hlsUrl;
|
||||||
this.isMasterTimestampSource = isMasterTimestampSource;
|
this.isMasterTimestampSource = isMasterTimestampSource;
|
||||||
this.timestampAdjuster = timestampAdjuster;
|
this.timestampAdjuster = timestampAdjuster;
|
||||||
|
this.discontinuitySequenceNumber = discontinuitySequenceNumber;
|
||||||
this.previousChunk = previousChunk;
|
this.previousChunk = previousChunk;
|
||||||
// Note: this.dataSource and dataSource may be different.
|
// Note: this.dataSource and dataSource may be different.
|
||||||
this.isEncrypted = this.dataSource instanceof Aes128DataSource;
|
this.isEncrypted = this.dataSource instanceof Aes128DataSource;
|
||||||
initDataSource = dataSource;
|
initDataSource = dataSource;
|
||||||
discontinuitySequenceNumber = segment.discontinuitySequenceNumber;
|
|
||||||
adjustedEndTimeUs = endTimeUs;
|
adjustedEndTimeUs = endTimeUs;
|
||||||
uid = UID_SOURCE.getAndIncrement();
|
uid = UID_SOURCE.getAndIncrement();
|
||||||
}
|
}
|
||||||
|
|
@ -136,7 +137,7 @@ import java.util.concurrent.atomic.AtomicInteger;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the presentation time in microseconds of the last sample in the chunk
|
* Returns the presentation time in microseconds of the last sample in the chunk.
|
||||||
*/
|
*/
|
||||||
public long getAdjustedEndTimeUs() {
|
public long getAdjustedEndTimeUs() {
|
||||||
return adjustedEndTimeUs;
|
return adjustedEndTimeUs;
|
||||||
|
|
@ -231,8 +232,8 @@ import java.util.concurrent.atomic.AtomicInteger;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void maybeLoadInitData() throws IOException, InterruptedException {
|
private void maybeLoadInitData() throws IOException, InterruptedException {
|
||||||
if (previousChunk == null || previousChunk.extractor != extractor || initLoadCompleted
|
if ((previousChunk != null && previousChunk.extractor == extractor)
|
||||||
|| initDataSpec == null) {
|
|| initLoadCompleted || initDataSpec == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
DataSpec initSegmentDataSpec = Util.getRemainderDataSpec(initDataSpec, initSegmentBytesLoaded);
|
DataSpec initSegmentDataSpec = Util.getRemainderDataSpec(initDataSpec, initSegmentBytesLoaded);
|
||||||
|
|
|
||||||
|
|
@ -103,10 +103,10 @@ public final class HlsMediaSource implements MediaSource,
|
||||||
SinglePeriodTimeline timeline;
|
SinglePeriodTimeline timeline;
|
||||||
if (playlistTracker.isLive()) {
|
if (playlistTracker.isLive()) {
|
||||||
// TODO: fix windowPositionInPeriodUs when playlist is empty.
|
// TODO: fix windowPositionInPeriodUs when playlist is empty.
|
||||||
long windowPositionInPeriodUs = playlist.getStartTimeUs();
|
long windowPositionInPeriodUs = playlist.startTimeUs;
|
||||||
List<HlsMediaPlaylist.Segment> segments = playlist.segments;
|
List<HlsMediaPlaylist.Segment> segments = playlist.segments;
|
||||||
long windowDefaultStartPositionUs = segments.isEmpty() ? 0
|
long windowDefaultStartPositionUs = segments.isEmpty() ? 0
|
||||||
: segments.get(Math.max(0, segments.size() - 3)).startTimeUs - windowPositionInPeriodUs;
|
: segments.get(Math.max(0, segments.size() - 3)).relativeStartTimeUs;
|
||||||
timeline = new SinglePeriodTimeline(C.TIME_UNSET, playlist.durationUs,
|
timeline = new SinglePeriodTimeline(C.TIME_UNSET, playlist.durationUs,
|
||||||
windowPositionInPeriodUs, windowDefaultStartPositionUs, true, !playlist.hasEndTag);
|
windowPositionInPeriodUs, windowDefaultStartPositionUs, true, !playlist.hasEndTag);
|
||||||
} else /* not live */ {
|
} else /* not live */ {
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@
|
||||||
package com.google.android.exoplayer2.source.hls.playlist;
|
package com.google.android.exoplayer2.source.hls.playlist;
|
||||||
|
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
|
@ -33,7 +32,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
|
||||||
public final String url;
|
public final String url;
|
||||||
public final long durationUs;
|
public final long durationUs;
|
||||||
public final int discontinuitySequenceNumber;
|
public final int discontinuitySequenceNumber;
|
||||||
public final long startTimeUs;
|
public final long relativeStartTimeUs;
|
||||||
public final boolean isEncrypted;
|
public final boolean isEncrypted;
|
||||||
public final String encryptionKeyUri;
|
public final String encryptionKeyUri;
|
||||||
public final String encryptionIV;
|
public final String encryptionIV;
|
||||||
|
|
@ -45,12 +44,12 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
|
||||||
}
|
}
|
||||||
|
|
||||||
public Segment(String uri, long durationUs, int discontinuitySequenceNumber,
|
public Segment(String uri, long durationUs, int discontinuitySequenceNumber,
|
||||||
long startTimeUs, boolean isEncrypted, String encryptionKeyUri, String encryptionIV,
|
long relativeStartTimeUs, boolean isEncrypted, String encryptionKeyUri, String encryptionIV,
|
||||||
long byterangeOffset, long byterangeLength) {
|
long byterangeOffset, long byterangeLength) {
|
||||||
this.url = uri;
|
this.url = uri;
|
||||||
this.durationUs = durationUs;
|
this.durationUs = durationUs;
|
||||||
this.discontinuitySequenceNumber = discontinuitySequenceNumber;
|
this.discontinuitySequenceNumber = discontinuitySequenceNumber;
|
||||||
this.startTimeUs = startTimeUs;
|
this.relativeStartTimeUs = relativeStartTimeUs;
|
||||||
this.isEncrypted = isEncrypted;
|
this.isEncrypted = isEncrypted;
|
||||||
this.encryptionKeyUri = encryptionKeyUri;
|
this.encryptionKeyUri = encryptionKeyUri;
|
||||||
this.encryptionIV = encryptionIV;
|
this.encryptionIV = encryptionIV;
|
||||||
|
|
@ -59,64 +58,55 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int compareTo(Long startTimeUs) {
|
public int compareTo(Long relativeStartTimeUs) {
|
||||||
return this.startTimeUs > startTimeUs ? 1 : (this.startTimeUs < startTimeUs ? -1 : 0);
|
return this.relativeStartTimeUs > relativeStartTimeUs
|
||||||
}
|
? 1 : (this.relativeStartTimeUs < relativeStartTimeUs ? -1 : 0);
|
||||||
|
|
||||||
public Segment copyWithStartTimeUs(long startTimeUs) {
|
|
||||||
return new Segment(url, durationUs, discontinuitySequenceNumber, startTimeUs, isEncrypted,
|
|
||||||
encryptionKeyUri, encryptionIV, byterangeOffset, byterangeLength);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public final long startTimeUs;
|
||||||
public final int mediaSequence;
|
public final int mediaSequence;
|
||||||
public final int version;
|
public final int version;
|
||||||
public final Segment initializationSegment;
|
public final Segment initializationSegment;
|
||||||
public final List<Segment> segments;
|
public final List<Segment> segments;
|
||||||
public final boolean hasEndTag;
|
public final boolean hasEndTag;
|
||||||
|
public final boolean hasProgramDateTime;
|
||||||
public final long durationUs;
|
public final long durationUs;
|
||||||
|
|
||||||
public HlsMediaPlaylist(String baseUri, int mediaSequence, int version,
|
public HlsMediaPlaylist(String baseUri, long startTimeUs, int mediaSequence, int version,
|
||||||
boolean hasEndTag, Segment initializationSegment, List<Segment> segments) {
|
boolean hasEndTag, boolean hasProgramDateTime, Segment initializationSegment,
|
||||||
|
List<Segment> segments) {
|
||||||
super(baseUri, HlsPlaylist.TYPE_MEDIA);
|
super(baseUri, HlsPlaylist.TYPE_MEDIA);
|
||||||
|
this.startTimeUs = startTimeUs;
|
||||||
this.mediaSequence = mediaSequence;
|
this.mediaSequence = mediaSequence;
|
||||||
this.version = version;
|
this.version = version;
|
||||||
this.hasEndTag = hasEndTag;
|
this.hasEndTag = hasEndTag;
|
||||||
|
this.hasProgramDateTime = hasProgramDateTime;
|
||||||
this.initializationSegment = initializationSegment;
|
this.initializationSegment = initializationSegment;
|
||||||
this.segments = Collections.unmodifiableList(segments);
|
this.segments = Collections.unmodifiableList(segments);
|
||||||
|
|
||||||
if (!segments.isEmpty()) {
|
if (!segments.isEmpty()) {
|
||||||
Segment first = segments.get(0);
|
|
||||||
Segment last = segments.get(segments.size() - 1);
|
Segment last = segments.get(segments.size() - 1);
|
||||||
durationUs = last.startTimeUs + last.durationUs - first.startTimeUs;
|
durationUs = last.relativeStartTimeUs + last.durationUs;
|
||||||
} else {
|
} else {
|
||||||
durationUs = 0;
|
durationUs = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public long getStartTimeUs() {
|
public boolean isNewerThan(HlsMediaPlaylist other) {
|
||||||
return segments.isEmpty() ? 0 : segments.get(0).startTimeUs;
|
return other == null || mediaSequence > other.mediaSequence
|
||||||
|
|| (mediaSequence == other.mediaSequence && segments.size() > other.segments.size())
|
||||||
|
|| (hasEndTag && !other.hasEndTag);
|
||||||
}
|
}
|
||||||
|
|
||||||
public long getEndTimeUs() {
|
public long getEndTimeUs() {
|
||||||
return getStartTimeUs() + durationUs;
|
return startTimeUs + durationUs;
|
||||||
}
|
}
|
||||||
|
|
||||||
public HlsMediaPlaylist copyWithStartTimeUs(long newStartTimeUs) {
|
public HlsMediaPlaylist copyWithStartTimeUs(long startTimeUs) {
|
||||||
long startTimeOffsetUs = newStartTimeUs - getStartTimeUs();
|
return new HlsMediaPlaylist(baseUri, startTimeUs, mediaSequence, version, hasEndTag,
|
||||||
int segmentsSize = segments.size();
|
hasProgramDateTime, initializationSegment, segments);
|
||||||
List<Segment> newSegments = new ArrayList<>(segmentsSize);
|
|
||||||
for (int i = 0; i < segmentsSize; i++) {
|
|
||||||
Segment segment = segments.get(i);
|
|
||||||
newSegments.add(segment.copyWithStartTimeUs(segment.startTimeUs + startTimeOffsetUs));
|
|
||||||
}
|
|
||||||
return copyWithSegments(newSegments);
|
|
||||||
}
|
|
||||||
|
|
||||||
public HlsMediaPlaylist copyWithSegments(List<Segment> segments) {
|
|
||||||
return new HlsMediaPlaylist(baseUri, mediaSequence, version, hasEndTag,
|
|
||||||
initializationSegment, segments);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import com.google.android.exoplayer2.ParserException;
|
||||||
import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Segment;
|
import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Segment;
|
||||||
import com.google.android.exoplayer2.upstream.ParsingLoadable;
|
import com.google.android.exoplayer2.upstream.ParsingLoadable;
|
||||||
import com.google.android.exoplayer2.util.MimeTypes;
|
import com.google.android.exoplayer2.util.MimeTypes;
|
||||||
|
import com.google.android.exoplayer2.util.Util;
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
|
@ -43,6 +44,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
|
||||||
private static final String TAG_MEDIA = "#EXT-X-MEDIA";
|
private static final String TAG_MEDIA = "#EXT-X-MEDIA";
|
||||||
private static final String TAG_DISCONTINUITY = "#EXT-X-DISCONTINUITY";
|
private static final String TAG_DISCONTINUITY = "#EXT-X-DISCONTINUITY";
|
||||||
private static final String TAG_DISCONTINUITY_SEQUENCE = "#EXT-X-DISCONTINUITY-SEQUENCE";
|
private static final String TAG_DISCONTINUITY_SEQUENCE = "#EXT-X-DISCONTINUITY-SEQUENCE";
|
||||||
|
private static final String TAG_PROGRAM_DATE_TIME = "#EXT-X-PROGRAM-DATE-TIME";
|
||||||
private static final String TAG_INIT_SEGMENT = "#EXT-X-MAP";
|
private static final String TAG_INIT_SEGMENT = "#EXT-X-MAP";
|
||||||
private static final String TAG_MEDIA_DURATION = "#EXTINF";
|
private static final String TAG_MEDIA_DURATION = "#EXTINF";
|
||||||
private static final String TAG_MEDIA_SEQUENCE = "#EXT-X-MEDIA-SEQUENCE";
|
private static final String TAG_MEDIA_SEQUENCE = "#EXT-X-MEDIA-SEQUENCE";
|
||||||
|
|
@ -62,17 +64,10 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
|
||||||
private static final String BOOLEAN_TRUE = "YES";
|
private static final String BOOLEAN_TRUE = "YES";
|
||||||
private static final String BOOLEAN_FALSE = "NO";
|
private static final String BOOLEAN_FALSE = "NO";
|
||||||
|
|
||||||
private static final Pattern REGEX_GROUP_ID = Pattern.compile("GROUP-ID=\"(.+?)\"");
|
|
||||||
private static final Pattern REGEX_VIDEO = Pattern.compile("VIDEO=\"(.+?)\"");
|
|
||||||
private static final Pattern REGEX_AUDIO = Pattern.compile("AUDIO=\"(.+?)\"");
|
|
||||||
private static final Pattern REGEX_CLOSED_CAPTIONS = Pattern.compile("CLOSED-CAPTIONS=\"(.+?)\"");
|
|
||||||
private static final Pattern REGEX_SUBTITLES = Pattern.compile("SUBTITLES=\"(.+?)\"");
|
|
||||||
private static final Pattern REGEX_BANDWIDTH = Pattern.compile("BANDWIDTH=(\\d+)\\b");
|
private static final Pattern REGEX_BANDWIDTH = Pattern.compile("BANDWIDTH=(\\d+)\\b");
|
||||||
private static final Pattern REGEX_CODECS = Pattern.compile("CODECS=\"(.+?)\"");
|
private static final Pattern REGEX_CODECS = Pattern.compile("CODECS=\"(.+?)\"");
|
||||||
private static final Pattern REGEX_RESOLUTION = Pattern.compile("RESOLUTION=(\\d+x\\d+)");
|
private static final Pattern REGEX_RESOLUTION = Pattern.compile("RESOLUTION=(\\d+x\\d+)");
|
||||||
private static final Pattern REGEX_VERSION = Pattern.compile(TAG_VERSION + ":(\\d+)\\b");
|
private static final Pattern REGEX_VERSION = Pattern.compile(TAG_VERSION + ":(\\d+)\\b");
|
||||||
private static final Pattern REGEX_TARGET_DURATION = Pattern.compile(TAG_TARGET_DURATION
|
|
||||||
+ ":(\\d+)\\b");
|
|
||||||
private static final Pattern REGEX_MEDIA_SEQUENCE = Pattern.compile(TAG_MEDIA_SEQUENCE
|
private static final Pattern REGEX_MEDIA_SEQUENCE = Pattern.compile(TAG_MEDIA_SEQUENCE
|
||||||
+ ":(\\d+)\\b");
|
+ ":(\\d+)\\b");
|
||||||
private static final Pattern REGEX_MEDIA_DURATION = Pattern.compile(TAG_MEDIA_DURATION
|
private static final Pattern REGEX_MEDIA_DURATION = Pattern.compile(TAG_MEDIA_DURATION
|
||||||
|
|
@ -211,7 +206,6 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
|
||||||
private static HlsMediaPlaylist parseMediaPlaylist(LineIterator iterator, String baseUri)
|
private static HlsMediaPlaylist parseMediaPlaylist(LineIterator iterator, String baseUri)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
int mediaSequence = 0;
|
int mediaSequence = 0;
|
||||||
int targetDurationSecs = 0;
|
|
||||||
int version = 1; // Default version == 1.
|
int version = 1; // Default version == 1.
|
||||||
boolean hasEndTag = false;
|
boolean hasEndTag = false;
|
||||||
Segment initializationSegment = null;
|
Segment initializationSegment = null;
|
||||||
|
|
@ -219,6 +213,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
|
||||||
|
|
||||||
long segmentDurationUs = 0;
|
long segmentDurationUs = 0;
|
||||||
int discontinuitySequenceNumber = 0;
|
int discontinuitySequenceNumber = 0;
|
||||||
|
long playlistStartTimeUs = 0;
|
||||||
long segmentStartTimeUs = 0;
|
long segmentStartTimeUs = 0;
|
||||||
long segmentByteRangeOffset = 0;
|
long segmentByteRangeOffset = 0;
|
||||||
long segmentByteRangeLength = C.LENGTH_UNSET;
|
long segmentByteRangeLength = C.LENGTH_UNSET;
|
||||||
|
|
@ -244,8 +239,6 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
|
||||||
initializationSegment = new Segment(uri, segmentByteRangeOffset, segmentByteRangeLength);
|
initializationSegment = new Segment(uri, segmentByteRangeOffset, segmentByteRangeLength);
|
||||||
segmentByteRangeOffset = 0;
|
segmentByteRangeOffset = 0;
|
||||||
segmentByteRangeLength = C.LENGTH_UNSET;
|
segmentByteRangeLength = C.LENGTH_UNSET;
|
||||||
} else if (line.startsWith(TAG_TARGET_DURATION)) {
|
|
||||||
targetDurationSecs = parseIntAttr(line, REGEX_TARGET_DURATION);
|
|
||||||
} else if (line.startsWith(TAG_MEDIA_SEQUENCE)) {
|
} else if (line.startsWith(TAG_MEDIA_SEQUENCE)) {
|
||||||
mediaSequence = parseIntAttr(line, REGEX_MEDIA_SEQUENCE);
|
mediaSequence = parseIntAttr(line, REGEX_MEDIA_SEQUENCE);
|
||||||
segmentMediaSequence = mediaSequence;
|
segmentMediaSequence = mediaSequence;
|
||||||
|
|
@ -275,6 +268,12 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
|
||||||
discontinuitySequenceNumber = Integer.parseInt(line.substring(line.indexOf(':') + 1));
|
discontinuitySequenceNumber = Integer.parseInt(line.substring(line.indexOf(':') + 1));
|
||||||
} else if (line.equals(TAG_DISCONTINUITY)) {
|
} else if (line.equals(TAG_DISCONTINUITY)) {
|
||||||
discontinuitySequenceNumber++;
|
discontinuitySequenceNumber++;
|
||||||
|
} else if (line.startsWith(TAG_PROGRAM_DATE_TIME)) {
|
||||||
|
if (playlistStartTimeUs == 0) {
|
||||||
|
long programDatetimeUs =
|
||||||
|
C.msToUs(Util.parseXsDateTime(line.substring(line.indexOf(':') + 1)));
|
||||||
|
playlistStartTimeUs = programDatetimeUs - segmentStartTimeUs;
|
||||||
|
}
|
||||||
} else if (!line.startsWith("#")) {
|
} else if (!line.startsWith("#")) {
|
||||||
String segmentEncryptionIV;
|
String segmentEncryptionIV;
|
||||||
if (!isEncrypted) {
|
if (!isEncrypted) {
|
||||||
|
|
@ -301,8 +300,8 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
|
||||||
hasEndTag = true;
|
hasEndTag = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return new HlsMediaPlaylist(baseUri, mediaSequence, version, hasEndTag,
|
return new HlsMediaPlaylist(baseUri, playlistStartTimeUs, mediaSequence, version, hasEndTag,
|
||||||
initializationSegment, segments);
|
playlistStartTimeUs != 0, initializationSegment, segments);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String parseStringAttr(String line, Pattern pattern) throws ParserException {
|
private static String parseStringAttr(String line, Pattern pattern) throws ParserException {
|
||||||
|
|
|
||||||
|
|
@ -292,46 +292,39 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable
|
||||||
*/
|
*/
|
||||||
private HlsMediaPlaylist adjustPlaylistTimestamps(HlsMediaPlaylist oldPlaylist,
|
private HlsMediaPlaylist adjustPlaylistTimestamps(HlsMediaPlaylist oldPlaylist,
|
||||||
HlsMediaPlaylist newPlaylist) {
|
HlsMediaPlaylist newPlaylist) {
|
||||||
|
if (newPlaylist.hasProgramDateTime) {
|
||||||
|
if (newPlaylist.isNewerThan(oldPlaylist)) {
|
||||||
|
return newPlaylist;
|
||||||
|
} else {
|
||||||
|
return oldPlaylist;
|
||||||
|
}
|
||||||
|
}
|
||||||
HlsMediaPlaylist primaryPlaylistSnapshot =
|
HlsMediaPlaylist primaryPlaylistSnapshot =
|
||||||
playlistBundles.get(primaryHlsUrl).latestPlaylistSnapshot;
|
playlistBundles.get(primaryHlsUrl).latestPlaylistSnapshot;
|
||||||
if (oldPlaylist == null) {
|
if (oldPlaylist == null) {
|
||||||
if (primaryPlaylistSnapshot == null) {
|
if (primaryPlaylistSnapshot == null
|
||||||
// Playback has just started so no adjustment is needed.
|
|| primaryPlaylistSnapshot.startTimeUs == newPlaylist.startTimeUs) {
|
||||||
|
// Playback has just started or is VOD so no adjustment is needed.
|
||||||
return newPlaylist;
|
return newPlaylist;
|
||||||
} else {
|
} else {
|
||||||
return newPlaylist.copyWithStartTimeUs(primaryPlaylistSnapshot.getStartTimeUs());
|
return newPlaylist.copyWithStartTimeUs(primaryPlaylistSnapshot.startTimeUs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
List<HlsMediaPlaylist.Segment> oldSegments = oldPlaylist.segments;
|
List<Segment> oldSegments = oldPlaylist.segments;
|
||||||
int oldPlaylistSize = oldSegments.size();
|
int oldPlaylistSize = oldSegments.size();
|
||||||
int newPlaylistSize = newPlaylist.segments.size();
|
if (!newPlaylist.isNewerThan(oldPlaylist)) {
|
||||||
int mediaSequenceOffset = newPlaylist.mediaSequence - oldPlaylist.mediaSequence;
|
|
||||||
if (newPlaylistSize == oldPlaylistSize && mediaSequenceOffset == 0
|
|
||||||
&& oldPlaylist.hasEndTag == newPlaylist.hasEndTag) {
|
|
||||||
// Playlist has not changed.
|
// Playlist has not changed.
|
||||||
return oldPlaylist;
|
return oldPlaylist;
|
||||||
}
|
}
|
||||||
if (mediaSequenceOffset < 0) {
|
int mediaSequenceOffset = newPlaylist.mediaSequence - oldPlaylist.mediaSequence;
|
||||||
// Playlist has changed but media sequence has regressed.
|
|
||||||
return oldPlaylist;
|
|
||||||
}
|
|
||||||
if (mediaSequenceOffset <= oldPlaylistSize) {
|
if (mediaSequenceOffset <= oldPlaylistSize) {
|
||||||
// We can extrapolate the start time of new segments from the segments of the old snapshot.
|
long adjustedNewPlaylistStartTimeUs = mediaSequenceOffset == oldPlaylistSize
|
||||||
ArrayList<HlsMediaPlaylist.Segment> newSegments = new ArrayList<>(newPlaylistSize);
|
? oldPlaylist.getEndTimeUs()
|
||||||
for (int i = mediaSequenceOffset; i < oldPlaylistSize; i++) {
|
: oldPlaylist.startTimeUs + oldSegments.get(mediaSequenceOffset).relativeStartTimeUs;
|
||||||
newSegments.add(oldSegments.get(i));
|
return newPlaylist.copyWithStartTimeUs(adjustedNewPlaylistStartTimeUs);
|
||||||
}
|
|
||||||
HlsMediaPlaylist.Segment lastSegment = oldSegments.get(oldPlaylistSize - 1);
|
|
||||||
for (int i = newSegments.size(); i < newPlaylistSize; i++) {
|
|
||||||
lastSegment = newPlaylist.segments.get(i).copyWithStartTimeUs(
|
|
||||||
lastSegment.startTimeUs + lastSegment.durationUs);
|
|
||||||
newSegments.add(lastSegment);
|
|
||||||
}
|
|
||||||
return newPlaylist.copyWithSegments(newSegments);
|
|
||||||
} else {
|
|
||||||
// No segments overlap, we assume the new playlist start coincides with the primary playlist.
|
|
||||||
return newPlaylist.copyWithStartTimeUs(primaryPlaylistSnapshot.getStartTimeUs());
|
|
||||||
}
|
}
|
||||||
|
// No segments overlap, we assume the new playlist start coincides with the primary playlist.
|
||||||
|
return newPlaylist.copyWithStartTimeUs(primaryPlaylistSnapshot.startTimeUs);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -375,31 +368,19 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable
|
||||||
}
|
}
|
||||||
|
|
||||||
public void adjustTimestampsOfPlaylist(int chunkMediaSequence, long adjustedStartTimeUs) {
|
public void adjustTimestampsOfPlaylist(int chunkMediaSequence, long adjustedStartTimeUs) {
|
||||||
ArrayList<Segment> segments = new ArrayList<>(latestPlaylistSnapshot.segments);
|
|
||||||
int indexOfChunk = chunkMediaSequence - latestPlaylistSnapshot.mediaSequence;
|
int indexOfChunk = chunkMediaSequence - latestPlaylistSnapshot.mediaSequence;
|
||||||
if (indexOfChunk < 0) {
|
if (latestPlaylistSnapshot.hasProgramDateTime || indexOfChunk < 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Segment actualSegment = segments.get(indexOfChunk);
|
Segment actualSegment = latestPlaylistSnapshot.segments.get(indexOfChunk);
|
||||||
long timestampDriftUs = Math.abs(actualSegment.startTimeUs - adjustedStartTimeUs);
|
long segmentAbsoluteStartTimeUs =
|
||||||
|
actualSegment.relativeStartTimeUs + latestPlaylistSnapshot.startTimeUs;
|
||||||
|
long timestampDriftUs = Math.abs(segmentAbsoluteStartTimeUs - adjustedStartTimeUs);
|
||||||
if (timestampDriftUs < TIMESTAMP_ADJUSTMENT_THRESHOLD_US) {
|
if (timestampDriftUs < TIMESTAMP_ADJUSTMENT_THRESHOLD_US) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
segments.set(indexOfChunk, actualSegment.copyWithStartTimeUs(adjustedStartTimeUs));
|
latestPlaylistSnapshot = latestPlaylistSnapshot.copyWithStartTimeUs(
|
||||||
// Propagate the adjustment backwards.
|
adjustedStartTimeUs - actualSegment.relativeStartTimeUs);
|
||||||
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.
|
// Loader.Callback implementation.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue