diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/player/DemoPlayer.java b/demo/src/main/java/com/google/android/exoplayer/demo/player/DemoPlayer.java index 6d3925f7519..8f477612bf8 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/player/DemoPlayer.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/player/DemoPlayer.java @@ -33,6 +33,7 @@ import com.google.android.exoplayer.dash.DashChunkSource; import com.google.android.exoplayer.drm.StreamingDrmSessionManager; import com.google.android.exoplayer.extractor.ExtractorSampleSource; +import com.google.android.exoplayer.hls.HlsChunkSource; import com.google.android.exoplayer.hls.HlsSampleSource; import com.google.android.exoplayer.metadata.MetadataTrackRenderer.MetadataRenderer; import com.google.android.exoplayer.metadata.id3.Id3Frame; @@ -63,7 +64,8 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi SingleSampleSource.EventListener, DefaultBandwidthMeter.EventListener, MediaCodecVideoTrackRenderer.EventListener, MediaCodecAudioTrackRenderer.EventListener, StreamingDrmSessionManager.EventListener, DashChunkSource.EventListener, TextRenderer, - MetadataRenderer>, DebugTextViewHelper.Provider { + MetadataRenderer>, DebugTextViewHelper.Provider, + HlsChunkSource.EventListener { /** * Builds renderers for the player. @@ -533,6 +535,9 @@ public void onAvailableRangeChanged(int sourceId, TimeRange availableRange) { if (infoListener != null) { infoListener.onAvailableRangeChanged(sourceId, availableRange); } + if (playerControl != null) { + playerControl.setAvailableRange(availableRange); + } } @Override diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/player/HlsRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/player/HlsRendererBuilder.java index b9c34fe5956..a2e513256aa 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/player/HlsRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/player/HlsRendererBuilder.java @@ -147,7 +147,8 @@ public void onSingleManifest(HlsPlaylist manifest) { DataSource dataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent); HlsChunkSource chunkSource = new HlsChunkSource(true /* isMaster */, dataSource, url, manifest, DefaultHlsTrackSelector.newDefaultInstance(context), bandwidthMeter, - timestampAdjusterProvider, HlsChunkSource.ADAPTIVE_MODE_SPLICE); + timestampAdjusterProvider, HlsChunkSource.ADAPTIVE_MODE_SPLICE, + player.getMainHandler(), player, DemoPlayer.TYPE_VIDEO); HlsSampleSource sampleSource = new HlsSampleSource(chunkSource, loadControl, MAIN_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player, DemoPlayer.TYPE_VIDEO); MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(context, @@ -162,7 +163,8 @@ public void onSingleManifest(HlsPlaylist manifest) { DataSource audioDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent); HlsChunkSource audioChunkSource = new HlsChunkSource(false /* isMaster */, audioDataSource, url, manifest, DefaultHlsTrackSelector.newAudioInstance(), bandwidthMeter, - timestampAdjusterProvider, HlsChunkSource.ADAPTIVE_MODE_SPLICE); + timestampAdjusterProvider, HlsChunkSource.ADAPTIVE_MODE_SPLICE, + player.getMainHandler(), player,DemoPlayer.TYPE_AUDIO); HlsSampleSource audioSampleSource = new HlsSampleSource(audioChunkSource, loadControl, AUDIO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player, DemoPlayer.TYPE_AUDIO); @@ -182,7 +184,8 @@ public void onSingleManifest(HlsPlaylist manifest) { DataSource textDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent); HlsChunkSource textChunkSource = new HlsChunkSource(false /* isMaster */, textDataSource, url, manifest, DefaultHlsTrackSelector.newSubtitleInstance(), bandwidthMeter, - timestampAdjusterProvider, HlsChunkSource.ADAPTIVE_MODE_SPLICE); + timestampAdjusterProvider, HlsChunkSource.ADAPTIVE_MODE_SPLICE, null, null, + DemoPlayer.TYPE_VIDEO); HlsSampleSource textSampleSource = new HlsSampleSource(textChunkSource, loadControl, TEXT_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player, DemoPlayer.TYPE_TEXT); textRenderer = new TextTrackRenderer(textSampleSource, player, mainHandler.getLooper()); diff --git a/library/src/main/java/com/google/android/exoplayer/hls/HlsChunkSource.java b/library/src/main/java/com/google/android/exoplayer/hls/HlsChunkSource.java index 853f6b3c67f..ac2991d2bd0 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/HlsChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/HlsChunkSource.java @@ -18,6 +18,7 @@ import com.google.android.exoplayer.BehindLiveWindowException; import com.google.android.exoplayer.C; import com.google.android.exoplayer.MediaFormat; +import com.google.android.exoplayer.TimeRange; import com.google.android.exoplayer.chunk.BaseChunkSampleSourceEventListener; import com.google.android.exoplayer.chunk.Chunk; import com.google.android.exoplayer.chunk.ChunkOperationHolder; @@ -38,6 +39,7 @@ import com.google.android.exoplayer.util.Util; import android.net.Uri; +import android.os.Handler; import android.os.SystemClock; import android.text.TextUtils; import android.util.Log; @@ -60,7 +62,14 @@ public class HlsChunkSource implements HlsTrackSelector.Output { /** * Interface definition for a callback to be notified of {@link HlsChunkSource} events. */ - public interface EventListener extends BaseChunkSampleSourceEventListener {} + public interface EventListener extends BaseChunkSampleSourceEventListener { + /** + * Invoked when the available seek range of the stream has changed. + * + * @param availableRange The range which specifies available content that can be seeked to. + */ + public void onAvailableRangeChanged(int sourceId, TimeRange availableRange); + } /** * Adaptive switching is disabled. @@ -159,6 +168,17 @@ public interface EventListener extends BaseChunkSampleSourceEventListener {} private String encryptionIvString; private byte[] encryptionIv; + private TimeRange availableRange[]; + private int currentAvailableRangeIndex = -1; + private final long[] availableRangeBoundsUs; + private long mediaTimeOffsetUs; + private boolean isMediaTimeBaseSet; + private long mediaSequenceOffset; + private final Handler eventHandler; + private final EventListener eventListener; + private final int eventSourceId; + private static final int LIVE_EDGE_CHUNK_INDEX_OFFSET = 3; + /** * @param isMaster True if this is the master source for the playback. False otherwise. Each * playback must have exactly one master source, which should be the source providing video @@ -174,15 +194,19 @@ public interface EventListener extends BaseChunkSampleSourceEventListener {} * @param adaptiveMode The mode for switching from one variant to another. One of * {@link #ADAPTIVE_MODE_NONE}, {@link #ADAPTIVE_MODE_ABRUPT} and * {@link #ADAPTIVE_MODE_SPLICE}. + * @param eventHandler A handler to use when delivering events to {@code EventListener}. May be + * null if delivery of events is not required. + * @param eventListener A listener of events. May be null if delivery of events is not required. + * @param eventSourceId An identifier that gets passed to {@code eventListener} methods. */ public HlsChunkSource(boolean isMaster, DataSource dataSource, String playlistUrl, HlsPlaylist playlist, HlsTrackSelector trackSelector, BandwidthMeter bandwidthMeter, - PtsTimestampAdjusterProvider timestampAdjusterProvider, int adaptiveMode) { + PtsTimestampAdjusterProvider timestampAdjusterProvider, int adaptiveMode, + Handler eventHandler, EventListener eventListener, int eventSourceId) { this(isMaster, dataSource, playlistUrl, playlist, trackSelector, bandwidthMeter, - timestampAdjusterProvider, adaptiveMode, DEFAULT_MIN_BUFFER_TO_SWITCH_UP_MS, - DEFAULT_MAX_BUFFER_TO_SWITCH_DOWN_MS); + timestampAdjusterProvider, adaptiveMode, eventHandler, eventListener, eventSourceId, + DEFAULT_MIN_BUFFER_TO_SWITCH_UP_MS, DEFAULT_MAX_BUFFER_TO_SWITCH_DOWN_MS); } - /** * @param isMaster True if this is the master source for the playback. False otherwise. Each * playback must have exactly one master source, which should be the source providing video @@ -198,6 +222,10 @@ public HlsChunkSource(boolean isMaster, DataSource dataSource, String playlistUr * @param adaptiveMode The mode for switching from one variant to another. One of * {@link #ADAPTIVE_MODE_NONE}, {@link #ADAPTIVE_MODE_ABRUPT} and * {@link #ADAPTIVE_MODE_SPLICE}. + * @param eventHandler A handler to use when delivering events to {@code EventListener}. May be + * null if delivery of events is not required. + * @param eventListener A listener of events. May be null if delivery of events is not required. + * @param eventSourceId An identifier that gets passed to {@code eventListener} methods. * @param minBufferDurationToSwitchUpMs The minimum duration of media that needs to be buffered * for a switch to a higher quality variant to be considered. * @param maxBufferDurationToSwitchDownMs The maximum duration of media that needs to be buffered @@ -206,6 +234,7 @@ public HlsChunkSource(boolean isMaster, DataSource dataSource, String playlistUr public HlsChunkSource(boolean isMaster, DataSource dataSource, String playlistUrl, HlsPlaylist playlist, HlsTrackSelector trackSelector, BandwidthMeter bandwidthMeter, PtsTimestampAdjusterProvider timestampAdjusterProvider, int adaptiveMode, + Handler eventHandler, EventListener eventListener, int eventSourceId, long minBufferDurationToSwitchUpMs, long maxBufferDurationToSwitchDownMs) { this.isMaster = isMaster; this.dataSource = dataSource; @@ -215,6 +244,11 @@ public HlsChunkSource(boolean isMaster, DataSource dataSource, String playlistUr this.adaptiveMode = adaptiveMode; minBufferDurationToSwitchUpUs = minBufferDurationToSwitchUpMs * 1000; maxBufferDurationToSwitchDownUs = maxBufferDurationToSwitchDownMs * 1000; + this.eventHandler = eventHandler; + this.eventListener = eventListener; + this.eventSourceId = eventSourceId; + isMediaTimeBaseSet = false; + availableRangeBoundsUs = new long[2]; baseUri = playlist.baseUri; playlistParser = new HlsPlaylistParser(); tracks = new ArrayList<>(); @@ -350,6 +384,7 @@ public void selectTrack(int index) { selectedVariantIndex = selectedTrack.defaultVariantIndex; variants = selectedTrack.variants; variantPlaylists = new HlsMediaPlaylist[variants.length]; + availableRange = new TimeRange[variants.length]; variantLastPlaylistLoadTimesMs = new long[variants.length]; variantBlacklistTimes = new long[variants.length]; } @@ -382,11 +417,14 @@ public void reset() { * @param playbackPositionUs The current playback position. If previousTsChunk is null then this * parameter is the position from which playback is expected to start (or restart) and hence * should be interpreted as a seek position. + * @param shouldLoadTschunk A boolean to indicate whether the Ts chunk should be loaded or not. + * If false, this function just checks if we need to refresh the live media playlist or not, + * and if needed triggers the re-fetch. * @param out The holder to populate with the result. {@link ChunkOperationHolder#queueSize} is * unused. */ public void getChunkOperation(TsChunk previousTsChunk, long playbackPositionUs, - ChunkOperationHolder out) { + ChunkOperationHolder out, boolean shouldLoadTschunk) { int nextVariantIndex; boolean switchingVariantSpliced; if (adaptiveMode == ADAPTIVE_MODE_NONE) { @@ -406,18 +444,28 @@ public void getChunkOperation(TsChunk previousTsChunk, long playbackPositionUs, return; } + // if the media playlist is live + // we need to check if it needs a reload + if (mediaPlaylist.live && + shouldRerequestLiveMediaPlaylist(nextVariantIndex)) { + out.chunk = newMediaPlaylistChunk(nextVariantIndex); + return; + } + if (!shouldLoadTschunk) { + out.chunk = null; + return; + } + selectedVariantIndex = nextVariantIndex; int chunkMediaSequence = 0; if (live) { if (previousTsChunk == null) { - chunkMediaSequence = getLiveStartChunkMediaSequence(nextVariantIndex); + chunkMediaSequence = getLiveChunkMediaSequence(nextVariantIndex, + playbackPositionUs, true); } else { - chunkMediaSequence = switchingVariantSpliced - ? previousTsChunk.chunkIndex : previousTsChunk.chunkIndex + 1; - if (chunkMediaSequence < mediaPlaylist.mediaSequence) { - fatalError = new BehindLiveWindowException(); - return; - } + chunkMediaSequence = getLiveChunkMediaSequence(nextVariantIndex, + switchingVariantSpliced ? previousTsChunk.startTimeUs : + previousTsChunk.endTimeUs, false); } } else { // Not live. @@ -465,13 +513,14 @@ public void getChunkOperation(TsChunk previousTsChunk, long playbackPositionUs, // Compute start and end times, and the sequence number of the next chunk. long startTimeUs; if (live) { - if (previousTsChunk == null) { - startTimeUs = 0; - } else if (switchingVariantSpliced) { - startTimeUs = previousTsChunk.startTimeUs; - } else { - startTimeUs = previousTsChunk.endTimeUs; - } + // Always calculate the chunk start time because the previous chunk's timestamp + // cannot always be used to calculate this chunk timestamp. + // For example: when the playback is near the lower available range and the user + // paused the playback for some time so that the current variant's DVR window + // would have moved ahead than the previous variant. In this scenario, we cannot blindly + // use previous TS chunk's timestamp to calculate the current chunk's Timestamp. + availableRange[selectedVariantIndex].getCurrentBoundsUs(availableRangeBoundsUs); + startTimeUs = segment.startTimeUs + availableRangeBoundsUs[0]; } else /* Not live */ { startTimeUs = segment.startTimeUs; } @@ -718,13 +767,52 @@ private boolean shouldRerequestLiveMediaPlaylist(int nextVariantIndex) { return timeSinceLastMediaPlaylistLoadMs >= (mediaPlaylist.targetDurationSecs * 1000) / 2; } - private int getLiveStartChunkMediaSequence(int variantIndex) { + + private int getLiveChunkMediaSequence(int variantIndex, long playbackPositionUs, boolean isReset) { // For live start playback from the third chunk from the end. HlsMediaPlaylist mediaPlaylist = variantPlaylists[variantIndex]; - int chunkIndex = mediaPlaylist.segments.size() > 3 ? mediaPlaylist.segments.size() - 3 : 0; + int chunkIndex; + // playbackPositionUs 0 is a special case of seeking/reseting to start playback from + // live edge + if (playbackPositionUs == 0) { + chunkIndex = mediaPlaylist.segments.size() > LIVE_EDGE_CHUNK_INDEX_OFFSET ? + mediaPlaylist.segments.size() - LIVE_EDGE_CHUNK_INDEX_OFFSET : 0; + } else { + availableRange[selectedVariantIndex].getCurrentBoundsUs(availableRangeBoundsUs); + long relativePlaybackPositionUs = Math.abs(playbackPositionUs) - availableRangeBoundsUs[0]; + if (isReset) { + // for seek case we try to find the right chunk index. + // if seek position is less than lower bound we cap it at lower bound. + // if seek position is greater than upper bound, we cap it at the live edge + if (playbackPositionUs > availableRangeBoundsUs[1]) { + chunkIndex = mediaPlaylist.segments.size() - LIVE_EDGE_CHUNK_INDEX_OFFSET; + } else if (playbackPositionUs < availableRangeBoundsUs[0]) { + chunkIndex = 0; + } else { + chunkIndex = Util.binarySearchFloor(mediaPlaylist.segments, relativePlaybackPositionUs, + true, true); + } + } else { + // for normal playback case, we always try to find the chunk index by searching + // in the segments. We cannot use Util.binarySearch,because if the timestamp is more than + // the upper bound, it returns the last chunk index. Hence we cannot use it here because + // we want to indicate to the callee that we have reached beyond the last chunk index. + // negative playbackPositionUs will never happen here because the seek case would have + // handled it already. + chunkIndex = Collections.binarySearch(mediaPlaylist.segments, relativePlaybackPositionUs); + if (chunkIndex == -1) { + chunkIndex = 0;// fallen behind live window - maybe due to long pause + } else if (chunkIndex < (-1 * mediaPlaylist.segments.size())) { + chunkIndex = mediaPlaylist.segments.size(); // reached the end + } else if (chunkIndex < 0) { + chunkIndex = -(chunkIndex + 2); // select the chunk index containing the position + } + } + } return chunkIndex + mediaPlaylist.mediaSequence; } + private MediaPlaylistChunk newMediaPlaylistChunk(int variantIndex) { Uri mediaPlaylistUri = UriUtil.resolveToUri(baseUri, variants[variantIndex].url); DataSpec dataSpec = new DataSpec(mediaPlaylistUri, 0, C.LENGTH_UNBOUNDED, null, @@ -770,6 +858,16 @@ private void setMediaPlaylist(int variantIndex, HlsMediaPlaylist mediaPlaylist) variantPlaylists[variantIndex] = mediaPlaylist; live |= mediaPlaylist.live; durationUs = live ? C.UNKNOWN_TIME_US : mediaPlaylist.durationUs; + + // get this variant's TimeRange + TimeRange variantRange = getAvailableRange(mediaPlaylist); + + // now may be notify app about new time range + maybeUpdateAvailableRange(variantIndex,variantRange); + + // now update this variant's time range + availableRange[variantIndex] = variantRange; + } private boolean allVariantsBlacklisted() { @@ -801,6 +899,77 @@ private int getVariantIndex(Format format) { throw new IllegalStateException("Invalid format: " + format); } + private void maybeUpdateAvailableRange(int variantIndex, TimeRange variantRange) { + // It is very much possible that this variant's playlist's window is behind the currently + // playing variant's window because the server might be still generating the content for it. + // For simplicity, we only update the availableRange to the app if the window has moved + // forward - to avoid moving the window back and forth temporarily. + // Hence we need to maintain a currentAvailableRangeIndex to indicate which variant's + // range was last notified to the app. + if (currentAvailableRangeIndex == -1) { + currentAvailableRangeIndex = variantIndex; + notifyAvailableRangeChanged(variantRange); + } else { + availableRange[currentAvailableRangeIndex].getCurrentBoundsUs(availableRangeBoundsUs); + long currentMinBoundUs = availableRangeBoundsUs[0]; + + variantRange.getCurrentBoundsUs(availableRangeBoundsUs); + long variantMinBoundUs = availableRangeBoundsUs[0]; + // notify app only if the new variant time range is ahead than current time range + if (variantMinBoundUs > currentMinBoundUs) { + currentAvailableRangeIndex = variantIndex; + notifyAvailableRangeChanged(variantRange); + } + } + } + + private TimeRange getAvailableRange(HlsMediaPlaylist mediaPlaylist) { + int lastIndex = mediaPlaylist.segments.size() - + (mediaPlaylist.live ? LIVE_EDGE_CHUNK_INDEX_OFFSET : 1); + HlsMediaPlaylist.Segment lastSegment = mediaPlaylist.segments.get(lastIndex); + if (!isMediaTimeBaseSet) { + if (mediaPlaylist.programDateTime != null) { + mediaTimeOffsetUs = mediaPlaylist.programDateTime.getTime() * 1000; + } else { + // program date time not present!! trying to use media sequence + mediaSequenceOffset = mediaPlaylist.mediaSequence; + } + isMediaTimeBaseSet = true; + } + long minStartPositionUs = 0; + if (mediaPlaylist.programDateTime != null) { + // Use media Time Offset to calculate the start position + minStartPositionUs = (mediaPlaylist.programDateTime.getTime() * 1000) - mediaTimeOffsetUs; + } else { + // Use media sequence offset to calculate the start position + // TODO : We right now assume all segments are of equal duration. + // Need TO handle case where each media segment can be of different duration + minStartPositionUs = (long) ((mediaPlaylist.mediaSequence - mediaSequenceOffset) * + mediaPlaylist.segments.get(0).durationSecs * + C.MICROS_PER_SECOND); + + } + long maxEndPositionUs = lastSegment.startTimeUs + + (long) (lastSegment.durationSecs * C.MICROS_PER_SECOND) + + minStartPositionUs; + // We don't use DynamicTimeRange because we want to allow app to + // seek within the chunk duration as long as the chunk is available in the playlist, + // because unlike MPEG-DASH, there is no timeShiftBufferDepth that is + // enforced by the HLS spec + return new TimeRange.StaticTimeRange(minStartPositionUs, maxEndPositionUs); + } + + private void notifyAvailableRangeChanged(final TimeRange seekRange) { + if (eventHandler != null && eventListener != null) { + eventHandler.post(new Runnable() { + @Override + public void run() { + eventListener.onAvailableRangeChanged(eventSourceId, seekRange); + } + }); + } + } + // Private classes. private static final class ExposedTrack { diff --git a/library/src/main/java/com/google/android/exoplayer/hls/HlsMediaPlaylist.java b/library/src/main/java/com/google/android/exoplayer/hls/HlsMediaPlaylist.java index 44c760bccf8..755c45d9e17 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/HlsMediaPlaylist.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/HlsMediaPlaylist.java @@ -17,6 +17,7 @@ import com.google.android.exoplayer.C; +import java.util.Date; import java.util.List; /** @@ -68,15 +69,16 @@ public int compareTo(Long startTimeUs) { public final List segments; public final boolean live; public final long durationUs; - + public final Date programDateTime; public HlsMediaPlaylist(String baseUri, int mediaSequence, int targetDurationSecs, int version, - boolean live, List segments) { + boolean live, List segments, Date programDateTime) { super(baseUri, HlsPlaylist.TYPE_MEDIA); this.mediaSequence = mediaSequence; this.targetDurationSecs = targetDurationSecs; this.version = version; this.live = live; this.segments = segments; + this.programDateTime = programDateTime; if (!segments.isEmpty()) { Segment last = segments.get(segments.size() - 1); diff --git a/library/src/main/java/com/google/android/exoplayer/hls/HlsPlaylistParser.java b/library/src/main/java/com/google/android/exoplayer/hls/HlsPlaylistParser.java index eb9812bc97e..c8b1d8d59a1 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/HlsPlaylistParser.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/HlsPlaylistParser.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer.hls; +import android.util.Log; + import com.google.android.exoplayer.C; import com.google.android.exoplayer.ParserException; import com.google.android.exoplayer.chunk.Format; @@ -26,6 +28,10 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; +import java.text.SimpleDateFormat; + +import java.util.Date; +import java.util.TimeZone; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedList; @@ -49,6 +55,7 @@ public final class HlsPlaylistParser implements UriLoadable.Parser private static final String ENDLIST_TAG = "#EXT-X-ENDLIST"; private static final String KEY_TAG = "#EXT-X-KEY"; private static final String BYTERANGE_TAG = "#EXT-X-BYTERANGE"; + private static final String PROGRAM_DATE_TIME_TAG = "#EXT-X-PROGRAM-DATE-TIME"; private static final String BANDWIDTH_ATTR = "BANDWIDTH"; private static final String CODECS_ATTR = "CODECS"; @@ -85,6 +92,8 @@ public final class HlsPlaylistParser implements UriLoadable.Parser Pattern.compile(VERSION_TAG + ":(\\d+)\\b"); private static final Pattern BYTERANGE_REGEX = Pattern.compile(BYTERANGE_TAG + ":(\\d+(?:@\\d+)?)\\b"); + private static final Pattern PROGRAM_DATE_TIME_REGEX = + Pattern.compile(PROGRAM_DATE_TIME_TAG + ":(\\S*)"); private static final Pattern METHOD_ATTR_REGEX = Pattern.compile(METHOD_ATTR + "=(" + METHOD_NONE + "|" + METHOD_AES128 + ")"); @@ -127,7 +136,8 @@ public HlsPlaylist parse(String connectionUrl, InputStream inputStream) || line.startsWith(BYTERANGE_TAG) || line.equals(DISCONTINUITY_TAG) || line.equals(DISCONTINUITY_SEQUENCE_TAG) - || line.equals(ENDLIST_TAG)) { + || line.equals(ENDLIST_TAG) + || line.equals(PROGRAM_DATE_TIME_TAG)) { extraLines.add(line); return parseMediaPlaylist(new LineIterator(extraLines, reader), connectionUrl); } else { @@ -242,7 +252,7 @@ private static HlsMediaPlaylist parseMediaPlaylist(LineIterator iterator, String int segmentByterangeOffset = 0; int segmentByterangeLength = C.LENGTH_UNBOUNDED; int segmentMediaSequence = 0; - + Date programDateTime = null; boolean isEncrypted = false; String encryptionKeyUri = null; String encryptionIV = null; @@ -278,6 +288,18 @@ private static HlsMediaPlaylist parseMediaPlaylist(LineIterator iterator, String if (splitByteRange.length > 1) { segmentByterangeOffset = Integer.parseInt(splitByteRange[1]); } + } else if (line.startsWith(PROGRAM_DATE_TIME_TAG) && programDateTime == null) { + String programDateTimeStr = HlsParserUtil.parseStringAttr(line, PROGRAM_DATE_TIME_REGEX, PROGRAM_DATE_TIME_TAG); + // TODO: Need to handle the Timezone & fractional part too + // Currently, either the playlist is assumed to always have timezone, or + // never have timezone. If this assumption fails, live DVR functionality will be broken. + SimpleDateFormat ISO8601Datefmt = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); + ISO8601Datefmt.setTimeZone(TimeZone.getTimeZone("UTC")); + try { + programDateTime = ISO8601Datefmt.parse(programDateTimeStr); + } catch (Exception e) { + Log.e("HLS", "Error in parsing program date " + programDateTimeStr, e); + } } else if (line.startsWith(DISCONTINUITY_SEQUENCE_TAG)) { discontinuitySequenceNumber = Integer.parseInt(line.substring(line.indexOf(':') + 1)); } else if (line.equals(DISCONTINUITY_TAG)) { @@ -310,7 +332,7 @@ private static HlsMediaPlaylist parseMediaPlaylist(LineIterator iterator, String } } return new HlsMediaPlaylist(baseUri, mediaSequence, targetDurationSecs, version, live, - Collections.unmodifiableList(segments)); + Collections.unmodifiableList(segments), programDateTime); } private static class LineIterator { diff --git a/library/src/main/java/com/google/android/exoplayer/hls/HlsSampleSource.java b/library/src/main/java/com/google/android/exoplayer/hls/HlsSampleSource.java index 1ed68da072e..8203e87ecb4 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/HlsSampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/HlsSampleSource.java @@ -206,8 +206,7 @@ public void enable(int track, long positionUs) { loadControl.register(this, bufferSizeContribution); loadControlRegistered = true; } - // Treat enabling of a live stream as occurring at t=0 in both of the blocks below. - positionUs = chunkSource.isLive() ? 0 : positionUs; + int chunkSourceTrack = chunkSourceTrackIndices[track]; if (chunkSourceTrack != -1 && chunkSourceTrack != chunkSource.getSelectedTrackIndex()) { // This is a primary track whose corresponding chunk source track is different to the one @@ -361,8 +360,6 @@ public void maybeThrowError() throws IOException { public void seekToUs(long positionUs) { Assertions.checkState(prepared); Assertions.checkState(enabledTrackCount > 0); - // Treat all seeks into live streams as being to t=0. - positionUs = chunkSource.isLive() ? 0 : positionUs; // Ignore seeks to the current position. long currentPositionUs = isPendingReset() ? pendingResetPositionUs : downstreamPositionUs; @@ -697,14 +694,17 @@ private void maybeStartLoading() { } return; } - - if (loader.isLoading() || !nextLoader || (prepared && enabledTrackCount == 0)) { + // We now call chunkSource.getChunkOperation even if nextLoader is false, so that it is given + // an opportunity to frequently re-fetch the playlist for live DVR use case. However, we + // pass the boolean nextLoader to the getChunkOperation function so that it knows not to load + // the TS Chunk and instead only re-fetch the playlist if needed + if (loader.isLoading() || (prepared && enabledTrackCount == 0)) { return; } chunkSource.getChunkOperation(previousTsLoadable, pendingResetPositionUs != NO_RESET_PENDING ? pendingResetPositionUs : downstreamPositionUs, - chunkOperationHolder); + chunkOperationHolder, nextLoader); boolean endOfStream = chunkOperationHolder.endOfStream; Chunk nextLoadable = chunkOperationHolder.chunk; chunkOperationHolder.clear(); diff --git a/library/src/main/java/com/google/android/exoplayer/util/PlayerControl.java b/library/src/main/java/com/google/android/exoplayer/util/PlayerControl.java index b9c4899de2d..5109c36c4e0 100644 --- a/library/src/main/java/com/google/android/exoplayer/util/PlayerControl.java +++ b/library/src/main/java/com/google/android/exoplayer/util/PlayerControl.java @@ -15,7 +15,9 @@ */ package com.google.android.exoplayer.util; + import com.google.android.exoplayer.ExoPlayer; +import com.google.android.exoplayer.TimeRange; import android.widget.MediaController.MediaPlayerControl; @@ -29,6 +31,9 @@ public class PlayerControl implements MediaPlayerControl { private final ExoPlayer exoPlayer; + private TimeRange availableRange; + private long availableRangeValuesMs[] = new long[2]; + public PlayerControl(ExoPlayer exoPlayer) { this.exoPlayer = exoPlayer; } @@ -70,8 +75,9 @@ public int getBufferPercentage() { @Override public int getCurrentPosition() { - return exoPlayer.getDuration() == ExoPlayer.UNKNOWN_TIME ? 0 - : (int) exoPlayer.getCurrentPosition(); + // we allow query of current position for live content + // to allow seeking within DVR window. See the PlayerActivity.java + return (int) exoPlayer.getCurrentPosition(); } @Override @@ -97,9 +103,24 @@ public void pause() { @Override public void seekTo(int timeMillis) { - long seekPosition = exoPlayer.getDuration() == ExoPlayer.UNKNOWN_TIME ? 0 - : Math.min(Math.max(0, timeMillis), getDuration()); + long seekPosition = (long) timeMillis; + if (exoPlayer.getDuration() == ExoPlayer.UNKNOWN_TIME) { + if (availableRange != null) { + availableRange.getCurrentBoundsMs(availableRangeValuesMs); + seekPosition = Math.min(Math.max(availableRangeValuesMs[0], timeMillis), availableRangeValuesMs[1]); + } + } else { + seekPosition = Math.min(Math.max(0, timeMillis), getDuration()); + } exoPlayer.seekTo(seekPosition); } + public void setAvailableRange(TimeRange availableRange) { + this.availableRange = availableRange; + } + + public TimeRange getAvailableRange() { + return availableRange; + } + }