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:
aquilescanta 2016-10-08 14:16:37 +01:00 committed by Oliver Woodman
parent 91c58627be
commit 501f54a8a6
6 changed files with 85 additions and 116 deletions

View file

@ -212,7 +212,8 @@ import java.util.Locale;
// If the playlist is too old to contain the chunk, we need to refresh it.
chunkMediaSequence = mediaPlaylist.mediaSequence + mediaPlaylist.segments.size();
} else {
chunkMediaSequence = Util.binarySearchFloor(mediaPlaylist.segments, targetPositionUs, true,
chunkMediaSequence = Util.binarySearchFloor(mediaPlaylist.segments,
targetPositionUs - mediaPlaylist.startTimeUs, true,
!playlistTracker.isLive() || previous == null) + mediaPlaylist.mediaSequence;
if (chunkMediaSequence < mediaPlaylist.mediaSequence && previous != null) {
// 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();
}
// 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;
Segment initSegment = mediaPlaylist.initializationSegment;
if (initSegment != null) {
@ -277,13 +268,20 @@ import java.util.Locale;
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.
Uri chunkUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, segment.url);
DataSpec dataSpec = new DataSpec(chunkUri, segment.byterangeOffset, segment.byterangeLength,
null);
out.chunk = new HlsMediaChunk(dataSource, dataSpec, initDataSpec, variants[newVariantIndex],
trackSelection.getSelectionReason(), trackSelection.getSelectionData(), segment,
chunkMediaSequence, isTimestampMaster, timestampAdjuster, previous, encryptionKey,
encryptionIv);
trackSelection.getSelectionReason(), trackSelection.getSelectionData(),
startTimeUs, startTimeUs + segment.durationUs, chunkMediaSequence,
segment.discontinuitySequenceNumber, isTimestampMaster, timestampAdjuster, previous,
encryptionKey, encryptionIv);
}
/**

View file

@ -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.source.chunk.MediaChunk;
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.DataSpec;
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 trackSelectionReason See {@link #trackSelectionReason}.
* @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 discontinuitySequenceNumber The discontinuity sequence number of the chunk.
* @param isMasterTimestampSource True if the chunk can initialize the timestamp adjuster.
* @param timestampAdjuster Adjuster corresponding to the provided discontinuity sequence number.
* @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.
*/
public HlsMediaChunk(DataSource dataSource, DataSpec dataSpec, DataSpec initDataSpec,
HlsUrl hlsUrl, int trackSelectionReason, Object trackSelectionData, Segment segment,
int chunkIndex, boolean isMasterTimestampSource, TimestampAdjuster timestampAdjuster,
HlsUrl hlsUrl, int trackSelectionReason, Object trackSelectionData, long startTimeUs,
long endTimeUs, int chunkIndex, int discontinuitySequenceNumber,
boolean isMasterTimestampSource, TimestampAdjuster timestampAdjuster,
HlsMediaChunk previousChunk, byte[] encryptionKey, byte[] encryptionIv) {
super(buildDataSource(dataSource, encryptionKey, encryptionIv), dataSpec, hlsUrl.format,
trackSelectionReason, trackSelectionData, segment.startTimeUs,
segment.startTimeUs + segment.durationUs, chunkIndex);
trackSelectionReason, trackSelectionData, startTimeUs, endTimeUs, chunkIndex);
this.initDataSpec = initDataSpec;
this.hlsUrl = hlsUrl;
this.isMasterTimestampSource = isMasterTimestampSource;
this.timestampAdjuster = timestampAdjuster;
this.discontinuitySequenceNumber = discontinuitySequenceNumber;
this.previousChunk = previousChunk;
// Note: this.dataSource and dataSource may be different.
this.isEncrypted = this.dataSource instanceof Aes128DataSource;
initDataSource = dataSource;
discontinuitySequenceNumber = segment.discontinuitySequenceNumber;
adjustedEndTimeUs = endTimeUs;
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() {
return adjustedEndTimeUs;
@ -231,8 +232,8 @@ import java.util.concurrent.atomic.AtomicInteger;
}
private void maybeLoadInitData() throws IOException, InterruptedException {
if (previousChunk == null || previousChunk.extractor != extractor || initLoadCompleted
|| initDataSpec == null) {
if ((previousChunk != null && previousChunk.extractor == extractor)
|| initLoadCompleted || initDataSpec == null) {
return;
}
DataSpec initSegmentDataSpec = Util.getRemainderDataSpec(initDataSpec, initSegmentBytesLoaded);

View file

@ -103,10 +103,10 @@ public final class HlsMediaSource implements MediaSource,
SinglePeriodTimeline timeline;
if (playlistTracker.isLive()) {
// TODO: fix windowPositionInPeriodUs when playlist is empty.
long windowPositionInPeriodUs = playlist.getStartTimeUs();
long windowPositionInPeriodUs = playlist.startTimeUs;
List<HlsMediaPlaylist.Segment> segments = playlist.segments;
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,
windowPositionInPeriodUs, windowDefaultStartPositionUs, true, !playlist.hasEndTag);
} else /* not live */ {

View file

@ -16,7 +16,6 @@
package com.google.android.exoplayer2.source.hls.playlist;
import com.google.android.exoplayer2.C;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@ -33,7 +32,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
public final String url;
public final long durationUs;
public final int discontinuitySequenceNumber;
public final long startTimeUs;
public final long relativeStartTimeUs;
public final boolean isEncrypted;
public final String encryptionKeyUri;
public final String encryptionIV;
@ -45,12 +44,12 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
}
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) {
this.url = uri;
this.durationUs = durationUs;
this.discontinuitySequenceNumber = discontinuitySequenceNumber;
this.startTimeUs = startTimeUs;
this.relativeStartTimeUs = relativeStartTimeUs;
this.isEncrypted = isEncrypted;
this.encryptionKeyUri = encryptionKeyUri;
this.encryptionIV = encryptionIV;
@ -59,64 +58,55 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
}
@Override
public int compareTo(Long startTimeUs) {
return this.startTimeUs > startTimeUs ? 1 : (this.startTimeUs < startTimeUs ? -1 : 0);
}
public Segment copyWithStartTimeUs(long startTimeUs) {
return new Segment(url, durationUs, discontinuitySequenceNumber, startTimeUs, isEncrypted,
encryptionKeyUri, encryptionIV, byterangeOffset, byterangeLength);
public int compareTo(Long relativeStartTimeUs) {
return this.relativeStartTimeUs > relativeStartTimeUs
? 1 : (this.relativeStartTimeUs < relativeStartTimeUs ? -1 : 0);
}
}
public final long startTimeUs;
public final int mediaSequence;
public final int version;
public final Segment initializationSegment;
public final List<Segment> segments;
public final boolean hasEndTag;
public final boolean hasProgramDateTime;
public final long durationUs;
public HlsMediaPlaylist(String baseUri, int mediaSequence, int version,
boolean hasEndTag, Segment initializationSegment, List<Segment> segments) {
public HlsMediaPlaylist(String baseUri, long startTimeUs, int mediaSequence, int version,
boolean hasEndTag, boolean hasProgramDateTime, Segment initializationSegment,
List<Segment> segments) {
super(baseUri, HlsPlaylist.TYPE_MEDIA);
this.startTimeUs = startTimeUs;
this.mediaSequence = mediaSequence;
this.version = version;
this.hasEndTag = hasEndTag;
this.hasProgramDateTime = hasProgramDateTime;
this.initializationSegment = initializationSegment;
this.segments = Collections.unmodifiableList(segments);
if (!segments.isEmpty()) {
Segment first = segments.get(0);
Segment last = segments.get(segments.size() - 1);
durationUs = last.startTimeUs + last.durationUs - first.startTimeUs;
durationUs = last.relativeStartTimeUs + last.durationUs;
} else {
durationUs = 0;
}
}
public long getStartTimeUs() {
return segments.isEmpty() ? 0 : segments.get(0).startTimeUs;
public boolean isNewerThan(HlsMediaPlaylist other) {
return other == null || mediaSequence > other.mediaSequence
|| (mediaSequence == other.mediaSequence && segments.size() > other.segments.size())
|| (hasEndTag && !other.hasEndTag);
}
public long getEndTimeUs() {
return getStartTimeUs() + durationUs;
return startTimeUs + durationUs;
}
public HlsMediaPlaylist copyWithStartTimeUs(long newStartTimeUs) {
long startTimeOffsetUs = newStartTimeUs - getStartTimeUs();
int segmentsSize = segments.size();
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);
public HlsMediaPlaylist copyWithStartTimeUs(long startTimeUs) {
return new HlsMediaPlaylist(baseUri, startTimeUs, mediaSequence, version, hasEndTag,
hasProgramDateTime, initializationSegment, segments);
}
}

View file

@ -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.upstream.ParsingLoadable;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util;
import java.io.BufferedReader;
import java.io.IOException;
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_DISCONTINUITY = "#EXT-X-DISCONTINUITY";
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_MEDIA_DURATION = "#EXTINF";
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_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_CODECS = Pattern.compile("CODECS=\"(.+?)\"");
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_TARGET_DURATION = Pattern.compile(TAG_TARGET_DURATION
+ ":(\\d+)\\b");
private static final Pattern REGEX_MEDIA_SEQUENCE = Pattern.compile(TAG_MEDIA_SEQUENCE
+ ":(\\d+)\\b");
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)
throws IOException {
int mediaSequence = 0;
int targetDurationSecs = 0;
int version = 1; // Default version == 1.
boolean hasEndTag = false;
Segment initializationSegment = null;
@ -219,6 +213,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
long segmentDurationUs = 0;
int discontinuitySequenceNumber = 0;
long playlistStartTimeUs = 0;
long segmentStartTimeUs = 0;
long segmentByteRangeOffset = 0;
long segmentByteRangeLength = C.LENGTH_UNSET;
@ -244,8 +239,6 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
initializationSegment = new Segment(uri, segmentByteRangeOffset, segmentByteRangeLength);
segmentByteRangeOffset = 0;
segmentByteRangeLength = C.LENGTH_UNSET;
} else if (line.startsWith(TAG_TARGET_DURATION)) {
targetDurationSecs = parseIntAttr(line, REGEX_TARGET_DURATION);
} else if (line.startsWith(TAG_MEDIA_SEQUENCE)) {
mediaSequence = parseIntAttr(line, REGEX_MEDIA_SEQUENCE);
segmentMediaSequence = mediaSequence;
@ -275,6 +268,12 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
discontinuitySequenceNumber = Integer.parseInt(line.substring(line.indexOf(':') + 1));
} else if (line.equals(TAG_DISCONTINUITY)) {
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("#")) {
String segmentEncryptionIV;
if (!isEncrypted) {
@ -301,8 +300,8 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
hasEndTag = true;
}
}
return new HlsMediaPlaylist(baseUri, mediaSequence, version, hasEndTag,
initializationSegment, segments);
return new HlsMediaPlaylist(baseUri, playlistStartTimeUs, mediaSequence, version, hasEndTag,
playlistStartTimeUs != 0, initializationSegment, segments);
}
private static String parseStringAttr(String line, Pattern pattern) throws ParserException {

View file

@ -292,46 +292,39 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable
*/
private HlsMediaPlaylist adjustPlaylistTimestamps(HlsMediaPlaylist oldPlaylist,
HlsMediaPlaylist newPlaylist) {
if (newPlaylist.hasProgramDateTime) {
if (newPlaylist.isNewerThan(oldPlaylist)) {
return newPlaylist;
} else {
return oldPlaylist;
}
}
HlsMediaPlaylist primaryPlaylistSnapshot =
playlistBundles.get(primaryHlsUrl).latestPlaylistSnapshot;
if (oldPlaylist == null) {
if (primaryPlaylistSnapshot == null) {
// Playback has just started so no adjustment is needed.
if (primaryPlaylistSnapshot == null
|| primaryPlaylistSnapshot.startTimeUs == newPlaylist.startTimeUs) {
// Playback has just started or is VOD so no adjustment is needed.
return newPlaylist;
} 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 newPlaylistSize = newPlaylist.segments.size();
int mediaSequenceOffset = newPlaylist.mediaSequence - oldPlaylist.mediaSequence;
if (newPlaylistSize == oldPlaylistSize && mediaSequenceOffset == 0
&& oldPlaylist.hasEndTag == newPlaylist.hasEndTag) {
if (!newPlaylist.isNewerThan(oldPlaylist)) {
// Playlist has not changed.
return oldPlaylist;
}
if (mediaSequenceOffset < 0) {
// Playlist has changed but media sequence has regressed.
return oldPlaylist;
}
int mediaSequenceOffset = newPlaylist.mediaSequence - oldPlaylist.mediaSequence;
if (mediaSequenceOffset <= oldPlaylistSize) {
// We can extrapolate the start time of new segments from the segments of the old snapshot.
ArrayList<HlsMediaPlaylist.Segment> newSegments = new ArrayList<>(newPlaylistSize);
for (int i = mediaSequenceOffset; i < oldPlaylistSize; i++) {
newSegments.add(oldSegments.get(i));
}
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());
long adjustedNewPlaylistStartTimeUs = mediaSequenceOffset == oldPlaylistSize
? oldPlaylist.getEndTimeUs()
: oldPlaylist.startTimeUs + oldSegments.get(mediaSequenceOffset).relativeStartTimeUs;
return newPlaylist.copyWithStartTimeUs(adjustedNewPlaylistStartTimeUs);
}
// 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) {
ArrayList<Segment> segments = new ArrayList<>(latestPlaylistSnapshot.segments);
int indexOfChunk = chunkMediaSequence - latestPlaylistSnapshot.mediaSequence;
if (indexOfChunk < 0) {
if (latestPlaylistSnapshot.hasProgramDateTime || indexOfChunk < 0) {
return;
}
Segment actualSegment = segments.get(indexOfChunk);
long timestampDriftUs = Math.abs(actualSegment.startTimeUs - adjustedStartTimeUs);
Segment actualSegment = latestPlaylistSnapshot.segments.get(indexOfChunk);
long segmentAbsoluteStartTimeUs =
actualSegment.relativeStartTimeUs + latestPlaylistSnapshot.startTimeUs;
long timestampDriftUs = Math.abs(segmentAbsoluteStartTimeUs - 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);
latestPlaylistSnapshot = latestPlaylistSnapshot.copyWithStartTimeUs(
adjustedStartTimeUs - actualSegment.relativeStartTimeUs);
}
// Loader.Callback implementation.