Skip to content

Commit

Permalink
Add #EXT-X-PROGRAM-DATE-TIME support for HLS media playlists
Browse files Browse the repository at this point in the history
Issue:#747

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=140525595
  • Loading branch information
AquilesCanta authored and ojw28 committed Nov 30, 2016
1 parent 91c5862 commit 501f54a
Show file tree
Hide file tree
Showing 6 changed files with 85 additions and 116 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,8 @@ public void getNextChunk(HlsMediaChunk previous, long playbackPositionUs, HlsChu
// 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
Expand Down Expand Up @@ -259,16 +260,6 @@ public void getNextChunk(HlsMediaChunk previous, long playbackPositionUs, HlsChu
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) {
Expand All @@ -277,13 +268,20 @@ public void getNextChunk(HlsMediaChunk previous, long playbackPositionUs, HlsChu
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);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@
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;
Expand Down Expand Up @@ -89,30 +88,32 @@
* @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.
* @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, 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();
}
Expand All @@ -136,7 +137,7 @@ public long getAdjustedStartTimeUs() {
}

/**
* 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;
Expand Down Expand Up @@ -231,8 +232,8 @@ private Extractor buildExtractor() {
}

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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,10 +103,10 @@ public void onPrimaryPlaylistRefreshed(HlsMediaPlaylist playlist) {
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 */ {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -33,7 +32,7 @@ public static final class Segment implements Comparable<Long> {
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;
Expand All @@ -45,12 +44,12 @@ public Segment(String uri, long byterangeOffset, long byterangeLength) {
}

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;
Expand All @@ -59,64 +58,55 @@ public Segment(String uri, long durationUs, int discontinuitySequenceNumber,
}

@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;
}

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);
return startTimeUs + durationUs;
}

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);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
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;
Expand All @@ -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";
Expand All @@ -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
Expand Down Expand Up @@ -211,14 +206,14 @@ private static int parseSelectionFlags(String line) {
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;
List<Segment> segments = new ArrayList<>();

long segmentDurationUs = 0;
int discontinuitySequenceNumber = 0;
long playlistStartTimeUs = 0;
long segmentStartTimeUs = 0;
long segmentByteRangeOffset = 0;
long segmentByteRangeLength = C.LENGTH_UNSET;
Expand All @@ -244,8 +239,6 @@ private static HlsMediaPlaylist parseMediaPlaylist(LineIterator iterator, String
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;
Expand Down Expand Up @@ -275,6 +268,12 @@ private static HlsMediaPlaylist parseMediaPlaylist(LineIterator iterator, String
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) {
Expand All @@ -301,8 +300,8 @@ private static HlsMediaPlaylist parseMediaPlaylist(LineIterator iterator, String
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 {
Expand Down
Loading

0 comments on commit 501f54a

Please sign in to comment.