From d64036c5ed0d34d3dd3c17cb2ef3101882e2aeab Mon Sep 17 00:00:00 2001 From: Andrey Udovenko Date: Wed, 1 Oct 2014 16:54:50 -0400 Subject: [PATCH 01/55] Add basic HLS support (VOD and Live) with EXT-X-DISCONTINUITY. --- .../android/exoplayer/demo/DemoUtil.java | 2 + .../exoplayer/demo/SampleChooserActivity.java | 2 + .../android/exoplayer/demo/Samples.java | 9 + .../demo/full/FullPlayerActivity.java | 7 + .../demo/full/player/HlsRendererBuilder.java | 113 +++ .../android/exoplayer/hls/HlsChunk.java | 176 +++++ .../hls/HlsChunkOperationHolder.java | 37 + .../android/exoplayer/hls/HlsChunkSource.java | 206 +++++ .../exoplayer/hls/HlsMasterPlaylist.java | 48 ++ .../hls/HlsMasterPlaylistParser.java | 71 ++ .../exoplayer/hls/HlsMediaPlaylist.java | 74 ++ .../exoplayer/hls/HlsMediaPlaylistParser.java | 107 +++ .../android/exoplayer/hls/HlsParserUtil.java | 49 ++ .../exoplayer/hls/HlsSampleSource.java | 560 ++++++++++++++ .../google/android/exoplayer/hls/TsChunk.java | 125 +++ .../exoplayer/parser/ts/BitsArray.java | 280 +++++++ .../exoplayer/parser/ts/TsExtractor.java | 722 ++++++++++++++++++ 17 files changed, 2588 insertions(+) create mode 100644 demo/src/main/java/com/google/android/exoplayer/demo/full/player/HlsRendererBuilder.java create mode 100644 library/src/main/java/com/google/android/exoplayer/hls/HlsChunk.java create mode 100644 library/src/main/java/com/google/android/exoplayer/hls/HlsChunkOperationHolder.java create mode 100644 library/src/main/java/com/google/android/exoplayer/hls/HlsChunkSource.java create mode 100644 library/src/main/java/com/google/android/exoplayer/hls/HlsMasterPlaylist.java create mode 100644 library/src/main/java/com/google/android/exoplayer/hls/HlsMasterPlaylistParser.java create mode 100644 library/src/main/java/com/google/android/exoplayer/hls/HlsMediaPlaylist.java create mode 100644 library/src/main/java/com/google/android/exoplayer/hls/HlsMediaPlaylistParser.java create mode 100644 library/src/main/java/com/google/android/exoplayer/hls/HlsParserUtil.java create mode 100644 library/src/main/java/com/google/android/exoplayer/hls/HlsSampleSource.java create mode 100644 library/src/main/java/com/google/android/exoplayer/hls/TsChunk.java create mode 100644 library/src/main/java/com/google/android/exoplayer/parser/ts/BitsArray.java create mode 100644 library/src/main/java/com/google/android/exoplayer/parser/ts/TsExtractor.java diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/DemoUtil.java b/demo/src/main/java/com/google/android/exoplayer/demo/DemoUtil.java index 6479b28a7e..7b4a6b41f1 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/DemoUtil.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/DemoUtil.java @@ -47,6 +47,8 @@ public class DemoUtil { public static final int TYPE_DASH_VOD = 0; public static final int TYPE_SS = 1; public static final int TYPE_OTHER = 2; + public static final int TYPE_HLS_MASTER = 3; + public static final int TYPE_HLS_MEDIA = 4; public static final boolean EXPOSE_EXPERIMENTAL_FEATURES = false; diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/SampleChooserActivity.java b/demo/src/main/java/com/google/android/exoplayer/demo/SampleChooserActivity.java index adb28ef0dc..14827f05c2 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/SampleChooserActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/SampleChooserActivity.java @@ -58,6 +58,8 @@ public class SampleChooserActivity extends Activity { sampleAdapter.addAll((Object[]) Samples.SMOOTHSTREAMING); sampleAdapter.add(new Header("Misc")); sampleAdapter.addAll((Object[]) Samples.MISC); + sampleAdapter.add(new Header("HLS")); + sampleAdapter.addAll((Object[]) Samples.HLS); if (DemoUtil.EXPOSE_EXPERIMENTAL_FEATURES) { sampleAdapter.add(new Header("YouTube WebM DASH (Experimental)")); sampleAdapter.addAll((Object[]) Samples.YOUTUBE_DASH_WEBM); diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/Samples.java b/demo/src/main/java/com/google/android/exoplayer/demo/Samples.java index 93d08af4cc..e01eb12d39 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/Samples.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/Samples.java @@ -131,6 +131,15 @@ package com.google.android.exoplayer.demo; + "22727BB612D24AA4FACE4EF62726F9461A9BF57A&key=ik0", DemoUtil.TYPE_DASH_VOD, true, true), }; + public static final Sample[] HLS = new Sample[] { + new Sample("Apple master playlist", "uid:hls:applemaster", + "https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/" + + "bipbop_4x3_variant.m3u8", DemoUtil.TYPE_HLS_MASTER, false, true), + new Sample("Apple single media playlist", "uid:hls:applesinglemedia", + "https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/gear1/" + + "prog_index.m3u8", DemoUtil.TYPE_HLS_MEDIA, false, true), + }; + public static final Sample[] MISC = new Sample[] { new Sample("Dizzy", "uid:misc:dizzy", "http://html5demos.com/assets/dizzy.mp4", DemoUtil.TYPE_OTHER, false, true), diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/FullPlayerActivity.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/FullPlayerActivity.java index 9966124ced..29a812b11d 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/FullPlayerActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/FullPlayerActivity.java @@ -23,6 +23,7 @@ import com.google.android.exoplayer.demo.full.player.DashVodRendererBuilder; import com.google.android.exoplayer.demo.full.player.DefaultRendererBuilder; import com.google.android.exoplayer.demo.full.player.DemoPlayer; import com.google.android.exoplayer.demo.full.player.DemoPlayer.RendererBuilder; +import com.google.android.exoplayer.demo.full.player.HlsRendererBuilder; import com.google.android.exoplayer.demo.full.player.SmoothStreamingRendererBuilder; import com.google.android.exoplayer.text.CaptionStyleCompat; import com.google.android.exoplayer.text.SubtitleView; @@ -173,6 +174,12 @@ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callba case DemoUtil.TYPE_DASH_VOD: return new DashVodRendererBuilder(userAgent, contentUri.toString(), contentId, new WidevineTestMediaDrmCallback(contentId), debugTextView); + case DemoUtil.TYPE_HLS_MASTER: + return new HlsRendererBuilder(userAgent, contentUri.toString(), contentId, + HlsRendererBuilder.TYPE_MASTER); + case DemoUtil.TYPE_HLS_MEDIA: + return new HlsRendererBuilder(userAgent, contentUri.toString(), contentId, + HlsRendererBuilder.TYPE_MEDIA); default: return new DefaultRendererBuilder(this, contentUri, debugTextView); } diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/HlsRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/HlsRendererBuilder.java new file mode 100644 index 0000000000..b7c08e97dc --- /dev/null +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/HlsRendererBuilder.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer.demo.full.player; + +import com.google.android.exoplayer.DefaultLoadControl; +import com.google.android.exoplayer.LoadControl; +import com.google.android.exoplayer.MediaCodecAudioTrackRenderer; +import com.google.android.exoplayer.MediaCodecVideoTrackRenderer; +import com.google.android.exoplayer.TrackRenderer; +import com.google.android.exoplayer.demo.full.player.DemoPlayer.RendererBuilder; +import com.google.android.exoplayer.demo.full.player.DemoPlayer.RendererBuilderCallback; +import com.google.android.exoplayer.hls.HlsChunkSource; +import com.google.android.exoplayer.hls.HlsMasterPlaylist; +import com.google.android.exoplayer.hls.HlsMasterPlaylist.Variant; +import com.google.android.exoplayer.hls.HlsMasterPlaylistParser; +import com.google.android.exoplayer.hls.HlsSampleSource; +import com.google.android.exoplayer.upstream.BufferPool; +import com.google.android.exoplayer.upstream.DataSource; +import com.google.android.exoplayer.upstream.HttpDataSource; +import com.google.android.exoplayer.util.ManifestFetcher; +import com.google.android.exoplayer.util.ManifestFetcher.ManifestCallback; + +import android.media.MediaCodec; +import android.net.Uri; + +import java.io.IOException; +import java.util.Collections; + +/** + * A {@link RendererBuilder} for HLS. + */ +public class HlsRendererBuilder implements RendererBuilder, ManifestCallback { + + public static final int TYPE_MASTER = 0; + public static final int TYPE_MEDIA = 1; + + private static final int BUFFER_SEGMENT_SIZE = 64 * 1024; + private static final int VIDEO_BUFFER_SEGMENTS = 200; + + private final String userAgent; + private final String url; + private final String contentId; + private final int playlistType; + + private DemoPlayer player; + private RendererBuilderCallback callback; + + public HlsRendererBuilder(String userAgent, String url, String contentId, int playlistType) { + this.userAgent = userAgent; + this.url = url; + this.contentId = contentId; + this.playlistType = playlistType; + } + + @Override + public void buildRenderers(DemoPlayer player, RendererBuilderCallback callback) { + this.player = player; + this.callback = callback; + switch (playlistType) { + case TYPE_MASTER: + HlsMasterPlaylistParser parser = new HlsMasterPlaylistParser(); + ManifestFetcher mediaPlaylistFetcher = + new ManifestFetcher(parser, contentId, url); + mediaPlaylistFetcher.singleLoad(player.getMainHandler().getLooper(), this); + break; + case TYPE_MEDIA: + onManifest(contentId, newSimpleMasterPlaylist(url)); + break; + } + } + + @Override + public void onManifestError(String contentId, IOException e) { + callback.onRenderersError(e); + } + + @Override + public void onManifest(String contentId, HlsMasterPlaylist manifest) { + LoadControl loadControl = new DefaultLoadControl(new BufferPool(BUFFER_SEGMENT_SIZE)); + + DataSource dataSource = new HttpDataSource(userAgent, null, null); + HlsChunkSource chunkSource = new HlsChunkSource(dataSource, manifest); + HlsSampleSource sampleSource = new HlsSampleSource(chunkSource, loadControl, + VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true, 2); + MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(sampleSource, + MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 0, player.getMainHandler(), player, 50); + MediaCodecAudioTrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer(sampleSource); + + TrackRenderer[] renderers = new TrackRenderer[DemoPlayer.RENDERER_COUNT]; + renderers[DemoPlayer.TYPE_VIDEO] = videoRenderer; + renderers[DemoPlayer.TYPE_AUDIO] = audioRenderer; + callback.onRenderers(null, null, renderers); + } + + private HlsMasterPlaylist newSimpleMasterPlaylist(String mediaPlaylistUrl) { + return new HlsMasterPlaylist(Uri.parse(""), + Collections.singletonList(new Variant(mediaPlaylistUrl, 0))); + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/hls/HlsChunk.java b/library/src/main/java/com/google/android/exoplayer/hls/HlsChunk.java new file mode 100644 index 0000000000..256921f9ea --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/hls/HlsChunk.java @@ -0,0 +1,176 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer.hls; + +import com.google.android.exoplayer.C; +import com.google.android.exoplayer.upstream.Allocation; +import com.google.android.exoplayer.upstream.Allocator; +import com.google.android.exoplayer.upstream.DataSource; +import com.google.android.exoplayer.upstream.DataSourceStream; +import com.google.android.exoplayer.upstream.DataSpec; +import com.google.android.exoplayer.upstream.Loader.Loadable; +import com.google.android.exoplayer.upstream.NonBlockingInputStream; +import com.google.android.exoplayer.util.Assertions; + +import java.io.IOException; + +/** + * An abstract base class for {@link Loadable} implementations that load chunks of data required + * for the playback of streams. + *

+ * TODO: Figure out whether this should merge with the chunk package, or whether the hls + * implementation is going to naturally diverge. + */ +public abstract class HlsChunk implements Loadable { + + /** + * The reason for a {@link HlsChunkSource} having generated this chunk. For reporting only. + * Possible values for this variable are defined by the specific {@link HlsChunkSource} + * implementations. + */ + public final int trigger; + + private final DataSource dataSource; + private final DataSpec dataSpec; + + private DataSourceStream dataSourceStream; + + /** + * @param dataSource The source from which the data should be loaded. + * @param dataSpec Defines the data to be loaded. {@code dataSpec.length} must not exceed + * {@link Integer#MAX_VALUE}. If {@code dataSpec.length == C.LENGTH_UNBOUNDED} then + * the length resolved by {@code dataSource.open(dataSpec)} must not exceed + * {@link Integer#MAX_VALUE}. + * @param trigger See {@link #trigger}. + */ + public HlsChunk(DataSource dataSource, DataSpec dataSpec, int trigger) { + Assertions.checkState(dataSpec.length <= Integer.MAX_VALUE); + this.dataSource = Assertions.checkNotNull(dataSource); + this.dataSpec = Assertions.checkNotNull(dataSpec); + this.trigger = trigger; + } + + /** + * Initializes the {@link HlsChunk}. + * + * @param allocator An {@link Allocator} from which the {@link Allocation} needed to contain the + * data can be obtained. + */ + public final void init(Allocator allocator) { + Assertions.checkState(dataSourceStream == null); + dataSourceStream = new DataSourceStream(dataSource, dataSpec, allocator); + } + + /** + * Releases the {@link HlsChunk}, releasing any backing {@link Allocation}s. + */ + public final void release() { + if (dataSourceStream != null) { + dataSourceStream.close(); + dataSourceStream = null; + } + } + + /** + * Gets the length of the chunk in bytes. + * + * @return The length of the chunk in bytes, or {@link C#LENGTH_UNBOUNDED} if the length has yet + * to be determined. + */ + public final long getLength() { + return dataSourceStream.getLength(); + } + + /** + * Whether the whole of the data has been consumed. + * + * @return True if the whole of the data has been consumed. False otherwise. + */ + public final boolean isReadFinished() { + return dataSourceStream.isEndOfStream(); + } + + /** + * Whether the whole of the chunk has been loaded. + * + * @return True if the whole of the chunk has been loaded. False otherwise. + */ + public final boolean isLoadFinished() { + return dataSourceStream.isLoadFinished(); + } + + /** + * Gets the number of bytes that have been loaded. + * + * @return The number of bytes that have been loaded. + */ + public final long bytesLoaded() { + return dataSourceStream.getLoadPosition(); + } + + /** + * Causes loaded data to be consumed. + * + * @throws IOException If an error occurs consuming the loaded data. + */ + public final void consume() throws IOException { + Assertions.checkState(dataSourceStream != null); + consumeStream(dataSourceStream); + } + + /** + * Invoked by {@link #consume()}. Implementations may override this method if they wish to + * consume the loaded data at this point. + *

+ * The default implementation is a no-op. + * + * @param stream The stream of loaded data. + * @throws IOException If an error occurs consuming the loaded data. + */ + protected void consumeStream(NonBlockingInputStream stream) throws IOException { + // Do nothing. + } + + protected final NonBlockingInputStream getNonBlockingInputStream() { + return dataSourceStream; + } + + protected final void resetReadPosition() { + if (dataSourceStream != null) { + dataSourceStream.resetReadPosition(); + } else { + // We haven't been initialized yet, so the read position must already be 0. + } + } + + // Loadable implementation + + @Override + public final void cancelLoad() { + dataSourceStream.cancelLoad(); + } + + @Override + public final boolean isLoadCanceled() { + return dataSourceStream.isLoadCanceled(); + } + + @Override + public final void load() throws IOException, InterruptedException { + dataSourceStream.load(); + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/hls/HlsChunkOperationHolder.java b/library/src/main/java/com/google/android/exoplayer/hls/HlsChunkOperationHolder.java new file mode 100644 index 0000000000..27b11c2ebd --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/hls/HlsChunkOperationHolder.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer.hls; + +/** + * Holds a hls chunk operation, which consists of a {@link HlsChunk} to load together with the + * number of {@link TsChunk}s that should be retained on the queue. + *

+ * TODO: Figure out whether this should merge with the chunk package, or whether the hls + * implementation is going to naturally diverge. + */ +public final class HlsChunkOperationHolder { + + /** + * The number of {@link TsChunk}s to retain in a queue. + */ + public int queueSize; + + /** + * The chunk. + */ + public HlsChunk chunk; + +} 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 new file mode 100644 index 0000000000..88f2791080 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/hls/HlsChunkSource.java @@ -0,0 +1,206 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer.hls; + +import com.google.android.exoplayer.C; +import com.google.android.exoplayer.MediaFormat; +import com.google.android.exoplayer.TrackRenderer; +import com.google.android.exoplayer.parser.ts.TsExtractor; +import com.google.android.exoplayer.upstream.DataSource; +import com.google.android.exoplayer.upstream.DataSpec; +import com.google.android.exoplayer.upstream.NonBlockingInputStream; +import com.google.android.exoplayer.util.Util; + +import android.net.Uri; +import android.os.SystemClock; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.List; + +/** + * A temporary test source of HLS chunks. + *

+ * TODO: Figure out whether this should merge with the chunk package, or whether the hls + * implementation is going to naturally diverge. + */ +public class HlsChunkSource { + + private final DataSource dataSource; + private final TsExtractor extractor; + private final HlsMasterPlaylist masterPlaylist; + private final HlsMediaPlaylistParser mediaPlaylistParser; + + /* package */ HlsMediaPlaylist mediaPlaylist; + /* package */ boolean mediaPlaylistWasLive; + /* package */ long lastMediaPlaylistLoadTimeMs; + + // TODO: Once proper m3u8 parsing is in place, actually use the url! + public HlsChunkSource(DataSource dataSource, HlsMasterPlaylist masterPlaylist) { + this.dataSource = dataSource; + this.masterPlaylist = masterPlaylist; + extractor = new TsExtractor(); + mediaPlaylistParser = new HlsMediaPlaylistParser(); + } + + public long getDurationUs() { + return mediaPlaylistWasLive ? TrackRenderer.UNKNOWN_TIME_US : mediaPlaylist.durationUs; + } + + /** + * Adaptive implementations must set the maximum video dimensions on the supplied + * {@link MediaFormat}. Other implementations do nothing. + *

+ * Only called when the source is enabled. + * + * @param out The {@link MediaFormat} on which the maximum video dimensions should be set. + */ + public void getMaxVideoDimensions(MediaFormat out) { + // TODO: Implement this. + } + + /** + * Updates the provided {@link HlsChunkOperationHolder} to contain the next operation that should + * be performed by the calling {@link HlsSampleSource}. + *

+ * The next operation comprises of a possibly shortened queue length (shortened if the + * implementation wishes for the caller to discard {@link TsChunk}s from the queue), together + * with the next {@link HlsChunk} to load. The next chunk may be a {@link TsChunk} to be added to + * the queue, or another {@link HlsChunk} type (e.g. to load initialization data), or null if the + * source is not able to provide a chunk in its current state. + * + * @param queue A representation of the currently buffered {@link TsChunk}s. + * @param seekPositionUs If the queue is empty, this parameter must specify the seek position. If + * the queue is non-empty then this parameter is ignored. + * @param playbackPositionUs The current playback position. + * @param out A holder for the next operation, whose {@link HlsChunkOperationHolder#queueSize} is + * initially equal to the length of the queue, and whose {@linkHls ChunkOperationHolder#chunk} + * is initially equal to null or a {@link TsChunk} previously supplied by the + * {@link HlsChunkSource} that the caller has not yet finished loading. In the latter case the + * chunk can either be replaced or left unchanged. Note that leaving the chunk unchanged is + * both preferred and more efficient than replacing it with a new but identical chunk. + */ + public void getChunkOperation(List queue, long seekPositionUs, long playbackPositionUs, + HlsChunkOperationHolder out) { + if (out.chunk != null) { + // We already have a chunk. Keep it. + return; + } + + if (mediaPlaylist == null) { + out.chunk = newMediaPlaylistChunk(); + return; + } + + int chunkMediaSequence = 0; + if (mediaPlaylistWasLive) { + if (queue.isEmpty()) { + chunkMediaSequence = getLiveStartChunkMediaSequence(); + } else { + // For live nextChunkIndex contains chunk media sequence number. + chunkMediaSequence = queue.get(queue.size() - 1).nextChunkIndex; + // If the updated playlist is far ahead and doesn't even have the last chunk from the + // queue, then try to catch up, skip a few chunks and start as if it was a new playlist. + if (chunkMediaSequence < mediaPlaylist.mediaSequence) { + // TODO: Trigger discontinuity in this case. + chunkMediaSequence = getLiveStartChunkMediaSequence(); + } + } + } else { + if (queue.isEmpty()) { + chunkMediaSequence = Util.binarySearchFloor(mediaPlaylist.segments, seekPositionUs, true, + true) + mediaPlaylist.mediaSequence; + } else { + chunkMediaSequence = queue.get(queue.size() - 1).nextChunkIndex; + } + } + + if (chunkMediaSequence == -1) { + out.chunk = null; + return; + } + + int chunkIndex = chunkMediaSequence - mediaPlaylist.mediaSequence; + // If the end of the playlist is reached. + if (chunkIndex >= mediaPlaylist.segments.size()) { + if (mediaPlaylist.live && shouldRerequestMediaPlaylist()) { + out.chunk = newMediaPlaylistChunk(); + } else { + out.chunk = null; + } + return; + } + + HlsMediaPlaylist.Segment segment = mediaPlaylist.segments.get(chunkIndex); + + Uri chunkUri = Util.getMergedUri(mediaPlaylist.baseUri, segment.url); + DataSpec dataSpec = new DataSpec(chunkUri, 0, C.LENGTH_UNBOUNDED, null); + + long startTimeUs = segment.startTimeUs; + long endTimeUs = startTimeUs + (long) (segment.durationSecs * 1000000); + + int nextChunkMediaSequence = chunkMediaSequence + 1; + if (!mediaPlaylist.live && chunkIndex == mediaPlaylist.segments.size() - 1) { + nextChunkMediaSequence = -1; + } + + out.chunk = new TsChunk(dataSource, dataSpec, 0, extractor, startTimeUs, endTimeUs, + nextChunkMediaSequence, segment.discontinuity); + } + + private boolean shouldRerequestMediaPlaylist() { + // Don't re-request media playlist more often than one-half of the target duration. + long timeSinceLastMediaPlaylistLoadMs = + SystemClock.elapsedRealtime() - lastMediaPlaylistLoadTimeMs; + return timeSinceLastMediaPlaylistLoadMs >= (mediaPlaylist.targetDurationSecs * 1000) / 2; + } + + private int getLiveStartChunkMediaSequence() { + // For live start playback from the third chunk from the end. + int chunkIndex = mediaPlaylist.segments.size() > 3 ? mediaPlaylist.segments.size() - 3 : 0; + return chunkIndex + mediaPlaylist.mediaSequence; + } + + private MediaPlaylistChunk newMediaPlaylistChunk() { + Uri mediaPlaylistUri = Util.getMergedUri(masterPlaylist.baseUri, + masterPlaylist.variants.get(0).url); + DataSpec dataSpec = new DataSpec(mediaPlaylistUri, 0, C.LENGTH_UNBOUNDED, null); + Uri mediaPlaylistBaseUri = Util.parseBaseUri(mediaPlaylistUri.toString()); + return new MediaPlaylistChunk(dataSource, dataSpec, 0, mediaPlaylistBaseUri); + } + + private class MediaPlaylistChunk extends HlsChunk { + + private final Uri baseUri; + + public MediaPlaylistChunk(DataSource dataSource, DataSpec dataSpec, int trigger, Uri baseUri) { + super(dataSource, dataSpec, trigger); + this.baseUri = baseUri; + } + + @Override + protected void consumeStream(NonBlockingInputStream stream) throws IOException { + byte[] data = new byte[(int) stream.getAvailableByteCount()]; + stream.read(data, 0, data.length); + lastMediaPlaylistLoadTimeMs = SystemClock.elapsedRealtime(); + mediaPlaylist = mediaPlaylistParser.parse( + new ByteArrayInputStream(data), null, null, baseUri); + mediaPlaylistWasLive |= mediaPlaylist.live; + } + + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/hls/HlsMasterPlaylist.java b/library/src/main/java/com/google/android/exoplayer/hls/HlsMasterPlaylist.java new file mode 100644 index 0000000000..f118734def --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/hls/HlsMasterPlaylist.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer.hls; + +import android.net.Uri; + +import java.util.List; + +/** + * Represents an HLS master playlist. + */ +public final class HlsMasterPlaylist { + + /** + * Variant stream reference. + */ + public static final class Variant { + public final int bandwidth; + public final String url; + + public Variant(String url, int bandwidth) { + this.bandwidth = bandwidth; + this.url = url; + } + } + + public final Uri baseUri; + public final List variants; + + public HlsMasterPlaylist(Uri baseUri, List variants) { + this.baseUri = baseUri; + this.variants = variants; + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/hls/HlsMasterPlaylistParser.java b/library/src/main/java/com/google/android/exoplayer/hls/HlsMasterPlaylistParser.java new file mode 100644 index 0000000000..d00eecc28b --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/hls/HlsMasterPlaylistParser.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer.hls; + +import com.google.android.exoplayer.hls.HlsMasterPlaylist.Variant; +import com.google.android.exoplayer.util.ManifestParser; + +import android.net.Uri; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.regex.Pattern; + +/** + * HLS Master playlists parsing logic. + */ +public final class HlsMasterPlaylistParser implements ManifestParser { + + private static final String STREAM_INF_TAG = "#EXT-X-STREAM-INF"; + private static final String BANDWIDTH_ATTR = "BANDWIDTH"; + + private static final Pattern BANDWIDTH_ATTR_REGEX = + Pattern.compile(BANDWIDTH_ATTR + "=(\\d+)\\b"); + + @Override + public HlsMasterPlaylist parse(InputStream inputStream, String inputEncoding, + String contentId, Uri baseUri) throws IOException { + return parseMasterPlaylist(inputStream, inputEncoding, baseUri); + } + + private static HlsMasterPlaylist parseMasterPlaylist(InputStream inputStream, + String inputEncoding, Uri baseUri) throws IOException { + BufferedReader reader = new BufferedReader((inputEncoding == null) + ? new InputStreamReader(inputStream) : new InputStreamReader(inputStream, inputEncoding)); + List variants = new ArrayList(); + int bandwidth = 0; + String line; + while ((line = reader.readLine()) != null) { + line = line.trim(); + if (line.isEmpty()) { + continue; + } + if (line.startsWith(STREAM_INF_TAG)) { + bandwidth = HlsParserUtil.parseIntAttr(line, BANDWIDTH_ATTR_REGEX, BANDWIDTH_ATTR); + } else if (!line.startsWith("#")) { + variants.add(new Variant(line, bandwidth)); + bandwidth = 0; + } + } + return new HlsMasterPlaylist(baseUri, Collections.unmodifiableList(variants)); + } + +} 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 new file mode 100644 index 0000000000..fda3e50b03 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/hls/HlsMediaPlaylist.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer.hls; + +import android.net.Uri; + +import java.util.List; + +/** + * Represents an HLS media playlist. + */ +public final class HlsMediaPlaylist { + + /** + * Media segment reference. + */ + public static final class Segment implements Comparable { + public final boolean discontinuity; + public final double durationSecs; + public final String url; + public final long startTimeUs; + + public Segment(String uri, double durationSecs, boolean discontinuity, long startTimeUs) { + this.url = uri; + this.durationSecs = durationSecs; + this.discontinuity = discontinuity; + this.startTimeUs = startTimeUs; + } + + @Override + public int compareTo(Long startTimeUs) { + return (int) (this.startTimeUs - startTimeUs); + } + } + + public final Uri baseUri; + public final int mediaSequence; + public final int targetDurationSecs; + public final int version; + public final List segments; + public final boolean live; + public final long durationUs; + + public HlsMediaPlaylist(Uri baseUri, int mediaSequence, int targetDurationSecs, int version, + boolean live, List segments) { + this.baseUri = baseUri; + this.mediaSequence = mediaSequence; + this.targetDurationSecs = targetDurationSecs; + this.version = version; + this.live = live; + this.segments = segments; + + if (this.segments.size() > 0) { + Segment lastSegment = segments.get(this.segments.size() - 1); + this.durationUs = lastSegment.startTimeUs + (long) (lastSegment.durationSecs * 1000000); + } else { + this.durationUs = 0; + } + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/hls/HlsMediaPlaylistParser.java b/library/src/main/java/com/google/android/exoplayer/hls/HlsMediaPlaylistParser.java new file mode 100644 index 0000000000..d2d514c01e --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/hls/HlsMediaPlaylistParser.java @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer.hls; + +import com.google.android.exoplayer.hls.HlsMediaPlaylist.Segment; +import com.google.android.exoplayer.util.ManifestParser; + +import android.net.Uri; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.regex.Pattern; + +/** + * HLS Media playlists parsing logic. + */ +public final class HlsMediaPlaylistParser implements ManifestParser { + + private static final String DISCONTINUITY_TAG = "#EXT-X-DISCONTINUITY"; + private static final String MEDIA_DURATION_TAG = "#EXTINF"; + private static final String MEDIA_SEQUENCE_TAG = "#EXT-X-MEDIA-SEQUENCE"; + private static final String TARGET_DURATION_TAG = "#EXT-X-TARGETDURATION"; + private static final String VERSION_TAG = "#EXT-X-VERSION"; + private static final String ENDLIST_TAG = "#EXT-X-ENDLIST"; + + private static final Pattern MEDIA_DURATION_REGEX = + Pattern.compile(MEDIA_DURATION_TAG + ":([\\d.]+),"); + private static final Pattern MEDIA_SEQUENCE_REGEX = + Pattern.compile(MEDIA_SEQUENCE_TAG + ":(\\d+)\\b"); + private static final Pattern TARGET_DURATION_REGEX = + Pattern.compile(TARGET_DURATION_TAG + ":(\\d+)\\b"); + private static final Pattern VERSION_REGEX = + Pattern.compile(VERSION_TAG + ":(\\d+)\\b"); + + @Override + public HlsMediaPlaylist parse(InputStream inputStream, String inputEncoding, + String contentId, Uri baseUri) throws IOException { + return parseMediaPlaylist(inputStream, inputEncoding, baseUri); + } + + private static HlsMediaPlaylist parseMediaPlaylist(InputStream inputStream, String inputEncoding, + Uri baseUri) throws IOException { + BufferedReader reader = new BufferedReader((inputEncoding == null) + ? new InputStreamReader(inputStream) : new InputStreamReader(inputStream, inputEncoding)); + + int mediaSequence = 0; + int targetDurationSecs = 0; + int version = 1; // Default version == 1. + boolean live = true; + List segments = new ArrayList(); + + double segmentDurationSecs = 0.0; + boolean segmentDiscontinuity = false; + long segmentStartTimeUs = 0; + + String line; + while ((line = reader.readLine()) != null) { + line = line.trim(); + if (line.isEmpty()) { + continue; + } + if (line.startsWith(TARGET_DURATION_TAG)) { + targetDurationSecs = HlsParserUtil.parseIntAttr(line, TARGET_DURATION_REGEX, + TARGET_DURATION_TAG); + } else if (line.startsWith(MEDIA_SEQUENCE_TAG)) { + mediaSequence = HlsParserUtil.parseIntAttr(line, MEDIA_SEQUENCE_REGEX, MEDIA_SEQUENCE_TAG); + } else if (line.startsWith(VERSION_TAG)) { + version = HlsParserUtil.parseIntAttr(line, VERSION_REGEX, VERSION_TAG); + } else if (line.startsWith(MEDIA_DURATION_TAG)) { + segmentDurationSecs = HlsParserUtil.parseDoubleAttr(line, MEDIA_DURATION_REGEX, + MEDIA_DURATION_TAG); + } else if (line.equals(DISCONTINUITY_TAG)) { + segmentDiscontinuity = true; + } else if (!line.startsWith("#")) { + segments.add(new Segment(line, segmentDurationSecs, segmentDiscontinuity, + segmentStartTimeUs)); + segmentStartTimeUs += (long) (segmentDurationSecs * 1000000); + segmentDiscontinuity = false; + segmentDurationSecs = 0.0; + } else if (line.equals(ENDLIST_TAG)) { + live = false; + break; + } + } + return new HlsMediaPlaylist(baseUri, mediaSequence, targetDurationSecs, version, live, + Collections.unmodifiableList(segments)); + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/hls/HlsParserUtil.java b/library/src/main/java/com/google/android/exoplayer/hls/HlsParserUtil.java new file mode 100644 index 0000000000..e61483f42c --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/hls/HlsParserUtil.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer.hls; + +import com.google.android.exoplayer.ParserException; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Utility methods for HLS manifest parsing. + */ +/* package */ class HlsParserUtil { + + private HlsParserUtil() {} + + public static String parseStringAttr(String line, Pattern pattern, String tag) + throws ParserException { + Matcher matcher = pattern.matcher(line); + if (matcher.find() && matcher.groupCount() == 1) { + return matcher.group(1); + } + throw new ParserException(String.format("Couldn't match %s tag in %s", tag, line)); + } + + public static int parseIntAttr(String line, Pattern pattern, String tag) + throws ParserException { + return Integer.parseInt(parseStringAttr(line, pattern, tag)); + } + + public static double parseDoubleAttr(String line, Pattern pattern, String tag) + throws ParserException { + return Double.parseDouble(parseStringAttr(line, pattern, tag)); + } + +} 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 new file mode 100644 index 0000000000..ad25c12dea --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/hls/HlsSampleSource.java @@ -0,0 +1,560 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer.hls; + +import com.google.android.exoplayer.C; +import com.google.android.exoplayer.LoadControl; +import com.google.android.exoplayer.MediaFormat; +import com.google.android.exoplayer.MediaFormatHolder; +import com.google.android.exoplayer.SampleHolder; +import com.google.android.exoplayer.SampleSource; +import com.google.android.exoplayer.TrackInfo; +import com.google.android.exoplayer.TrackRenderer; +import com.google.android.exoplayer.upstream.Loader; +import com.google.android.exoplayer.upstream.Loader.Loadable; +import com.google.android.exoplayer.util.Assertions; + +import android.os.SystemClock; + +import java.io.IOException; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; + +/** + * A {@link SampleSource} for HLS streams. + *

+ * TODO: Figure out whether this should merge with the chunk package, or whether the hls + * implementation is going to naturally diverge. + */ +public class HlsSampleSource implements SampleSource, Loader.Callback { + + private static final int NO_RESET_PENDING = -1; + + private final LoadControl loadControl; + private final HlsChunkSource chunkSource; + private final HlsChunkOperationHolder currentLoadableHolder; + private final LinkedList mediaChunks; + private final List readOnlyHlsChunks; + private final int bufferSizeContribution; + private final boolean frameAccurateSeeking; + + private int remainingReleaseCount; + private boolean prepared; + private int trackCount; + private int enabledTrackCount; + private boolean[] trackEnabledStates; + private boolean[] pendingDiscontinuities; + private TrackInfo[] trackInfos; + private MediaFormat[] downstreamMediaFormats; + + private long downstreamPositionUs; + private long lastSeekPositionUs; + private long pendingResetTime; + private long lastPerformedBufferOperation; + + private Loader loader; + private IOException currentLoadableException; + private boolean currentLoadableExceptionFatal; + private int currentLoadableExceptionCount; + private long currentLoadableExceptionTimestamp; + + private boolean pendingTimestampOffsetUpdate; + private long timestampOffsetUs; + + public HlsSampleSource(HlsChunkSource chunkSource, LoadControl loadControl, + int bufferSizeContribution, boolean frameAccurateSeeking, int downstreamRendererCount) { + this.chunkSource = chunkSource; + this.loadControl = loadControl; + this.bufferSizeContribution = bufferSizeContribution; + this.frameAccurateSeeking = frameAccurateSeeking; + this.remainingReleaseCount = downstreamRendererCount; + currentLoadableHolder = new HlsChunkOperationHolder(); + mediaChunks = new LinkedList(); + readOnlyHlsChunks = Collections.unmodifiableList(mediaChunks); + } + + @Override + public boolean prepare() { + if (prepared) { + return true; + } + + if (loader == null) { + loader = new Loader("Loader:HLS"); + loadControl.register(this, bufferSizeContribution); + } + updateLoadControl(); + if (mediaChunks.isEmpty()) { + return false; + } + TsChunk mediaChunk = mediaChunks.getFirst(); + if (mediaChunk.prepare()) { + trackCount = mediaChunk.getTrackCount(); + trackEnabledStates = new boolean[trackCount]; + pendingDiscontinuities = new boolean[trackCount]; + downstreamMediaFormats = new MediaFormat[trackCount]; + trackInfos = new TrackInfo[trackCount]; + for (int i = 0; i < trackCount; i++) { + MediaFormat format = mediaChunk.getMediaFormat(i); + trackInfos[i] = new TrackInfo(format.mimeType, chunkSource.getDurationUs()); + } + prepared = true; + } + + return prepared; + } + + @Override + public int getTrackCount() { + Assertions.checkState(prepared); + return trackCount; + } + + @Override + public TrackInfo getTrackInfo(int track) { + Assertions.checkState(prepared); + return trackInfos[track]; + } + + @Override + public void enable(int track, long timeUs) { + Assertions.checkState(prepared); + Assertions.checkState(!trackEnabledStates[track]); + enabledTrackCount++; + trackEnabledStates[track] = true; + downstreamMediaFormats[track] = null; + if (enabledTrackCount == 1) { + downstreamPositionUs = timeUs; + lastSeekPositionUs = timeUs; + restartFrom(timeUs); + } + } + + @Override + public void disable(int track) { + Assertions.checkState(prepared); + Assertions.checkState(trackEnabledStates[track]); + enabledTrackCount--; + trackEnabledStates[track] = false; + pendingDiscontinuities[track] = false; + if (enabledTrackCount == 0) { + if (loader.isLoading()) { + loader.cancelLoading(); + } else { + clearHlsChunks(); + clearCurrentLoadable(); + } + } + } + + @Override + public boolean continueBuffering(long playbackPositionUs) throws IOException { + Assertions.checkState(prepared); + Assertions.checkState(enabledTrackCount > 0); + downstreamPositionUs = playbackPositionUs; + updateLoadControl(); + if (isPendingReset() || mediaChunks.isEmpty()) { + return false; + } else if (mediaChunks.getFirst().sampleAvailable()) { + // There's a sample available to be read from the current chunk. + return true; + } else { + // It may be the case that the current chunk has been fully read but not yet discarded and + // that the next chunk has an available sample. Return true if so, otherwise false. + return mediaChunks.size() > 1 && mediaChunks.get(1).sampleAvailable(); + } + } + + @Override + public int readData(int track, long playbackPositionUs, MediaFormatHolder formatHolder, + SampleHolder sampleHolder, boolean onlyReadDiscontinuity) throws IOException { + Assertions.checkState(prepared); + + if (pendingDiscontinuities[track]) { + pendingDiscontinuities[track] = false; + return DISCONTINUITY_READ; + } + + if (onlyReadDiscontinuity) { + return NOTHING_READ; + } + + downstreamPositionUs = playbackPositionUs; + if (isPendingReset()) { + if (currentLoadableException != null) { + throw currentLoadableException; + } + return NOTHING_READ; + } + + TsChunk mediaChunk = mediaChunks.getFirst(); + + if (mediaChunk.readDiscontinuity()) { + pendingTimestampOffsetUpdate = true; + for (int i = 0; i < pendingDiscontinuities.length; i++) { + pendingDiscontinuities[i] = true; + } + pendingDiscontinuities[track] = false; + return DISCONTINUITY_READ; + } + + if (mediaChunk.isReadFinished()) { + // We've read all of the samples from the current media chunk. + if (mediaChunks.size() > 1) { + discardDownstreamHlsChunk(); + mediaChunk = mediaChunks.getFirst(); + return readData(track, playbackPositionUs, formatHolder, sampleHolder, false); + } else if (mediaChunk.isLastChunk()) { + return END_OF_STREAM; + } + return NOTHING_READ; + } + + if (!mediaChunk.prepare()) { + if (currentLoadableException != null) { + throw currentLoadableException; + } + return NOTHING_READ; + } + + MediaFormat mediaFormat = mediaChunk.getMediaFormat(track); + if (mediaFormat != null && !mediaFormat.equals(downstreamMediaFormats[track], true)) { + chunkSource.getMaxVideoDimensions(mediaFormat); + formatHolder.format = mediaFormat; + downstreamMediaFormats[track] = mediaFormat; + return FORMAT_READ; + } + + if (mediaChunk.read(track, sampleHolder)) { + if (pendingTimestampOffsetUpdate) { + pendingTimestampOffsetUpdate = false; + timestampOffsetUs = sampleHolder.timeUs - mediaChunk.startTimeUs; + } + sampleHolder.timeUs -= timestampOffsetUs; + sampleHolder.decodeOnly = frameAccurateSeeking && sampleHolder.timeUs < lastSeekPositionUs; + return SAMPLE_READ; + } else { + if (currentLoadableException != null) { + throw currentLoadableException; + } + return NOTHING_READ; + } + } + + @Override + public void seekToUs(long timeUs) { + Assertions.checkState(prepared); + Assertions.checkState(enabledTrackCount > 0); + downstreamPositionUs = timeUs; + lastSeekPositionUs = timeUs; + if (pendingResetTime == timeUs) { + return; + } + + for (int i = 0; i < pendingDiscontinuities.length; i++) { + pendingDiscontinuities[i] = true; + } + TsChunk mediaChunk = getHlsChunk(timeUs); + if (mediaChunk == null) { + restartFrom(timeUs); + } else { + pendingTimestampOffsetUpdate = true; + mediaChunk.reset(); + discardDownstreamHlsChunks(mediaChunk); + updateLoadControl(); + } + } + + private TsChunk getHlsChunk(long timeUs) { + Iterator mediaChunkIterator = mediaChunks.iterator(); + while (mediaChunkIterator.hasNext()) { + TsChunk mediaChunk = mediaChunkIterator.next(); + if (timeUs < mediaChunk.startTimeUs) { + return null; + } else if (mediaChunk.isLastChunk() || timeUs < mediaChunk.endTimeUs) { + return mediaChunk; + } + } + return null; + } + + @Override + public long getBufferedPositionUs() { + Assertions.checkState(prepared); + Assertions.checkState(enabledTrackCount > 0); + if (isPendingReset()) { + return pendingResetTime; + } + TsChunk mediaChunk = mediaChunks.getLast(); + HlsChunk currentLoadable = currentLoadableHolder.chunk; + if (currentLoadable != null && mediaChunk == currentLoadable) { + // Linearly interpolate partially-fetched chunk times. + long chunkLength = mediaChunk.getLength(); + if (chunkLength != C.LENGTH_UNBOUNDED) { + return mediaChunk.startTimeUs + ((mediaChunk.endTimeUs - mediaChunk.startTimeUs) + * mediaChunk.bytesLoaded()) / chunkLength; + } else { + return mediaChunk.startTimeUs; + } + } else if (mediaChunk.isLastChunk()) { + return TrackRenderer.END_OF_TRACK_US; + } else { + return mediaChunk.endTimeUs; + } + } + + @Override + public void release() { + Assertions.checkState(remainingReleaseCount > 0); + if (--remainingReleaseCount == 0 && loader != null) { + loadControl.unregister(this); + loader.release(); + loader = null; + } + } + + @Override + public void onLoadCompleted(Loadable loadable) { + HlsChunk currentLoadable = currentLoadableHolder.chunk; + try { + currentLoadable.consume(); + } catch (IOException e) { + currentLoadableException = e; + currentLoadableExceptionCount++; + currentLoadableExceptionTimestamp = SystemClock.elapsedRealtime(); + currentLoadableExceptionFatal = true; + } finally { + if (!isTsChunk(currentLoadable)) { + currentLoadable.release(); + } + if (!currentLoadableExceptionFatal) { + clearCurrentLoadable(); + } + updateLoadControl(); + } + } + + @Override + public void onLoadCanceled(Loadable loadable) { + HlsChunk currentLoadable = currentLoadableHolder.chunk; + if (!isTsChunk(currentLoadable)) { + currentLoadable.release(); + } + clearCurrentLoadable(); + if (enabledTrackCount > 0) { + restartFrom(pendingResetTime); + } else { + clearHlsChunks(); + loadControl.trimAllocator(); + } + } + + @Override + public void onLoadError(Loadable loadable, IOException e) { + currentLoadableException = e; + currentLoadableExceptionCount++; + currentLoadableExceptionTimestamp = SystemClock.elapsedRealtime(); + updateLoadControl(); + } + + private void restartFrom(long timeUs) { + pendingResetTime = timeUs; + if (loader.isLoading()) { + loader.cancelLoading(); + } else { + clearHlsChunks(); + clearCurrentLoadable(); + updateLoadControl(); + } + } + + private void clearHlsChunks() { + discardDownstreamHlsChunks(null); + } + + private void clearCurrentLoadable() { + currentLoadableHolder.chunk = null; + currentLoadableException = null; + currentLoadableExceptionCount = 0; + currentLoadableExceptionFatal = false; + } + + private void updateLoadControl() { + long loadPositionUs; + if (isPendingReset()) { + loadPositionUs = pendingResetTime; + } else { + TsChunk lastHlsChunk = mediaChunks.getLast(); + loadPositionUs = lastHlsChunk.nextChunkIndex == -1 ? -1 : lastHlsChunk.endTimeUs; + } + + boolean isBackedOff = currentLoadableException != null && !currentLoadableExceptionFatal; + boolean nextLoader = loadControl.update(this, downstreamPositionUs, loadPositionUs, + isBackedOff || loader.isLoading(), currentLoadableExceptionFatal); + + if (currentLoadableExceptionFatal) { + return; + } + + long now = SystemClock.elapsedRealtime(); + + if (isBackedOff) { + long elapsedMillis = now - currentLoadableExceptionTimestamp; + if (elapsedMillis >= getRetryDelayMillis(currentLoadableExceptionCount)) { + resumeFromBackOff(); + } + return; + } + + if (!loader.isLoading()) { + if (currentLoadableHolder.chunk == null || now - lastPerformedBufferOperation > 1000) { + lastPerformedBufferOperation = now; + currentLoadableHolder.queueSize = readOnlyHlsChunks.size(); + chunkSource.getChunkOperation(readOnlyHlsChunks, pendingResetTime, downstreamPositionUs, + currentLoadableHolder); + discardUpstreamHlsChunks(currentLoadableHolder.queueSize); + } + if (nextLoader) { + maybeStartLoading(); + } + } + } + + /** + * Resumes loading. + *

+ * If the {@link HlsChunkSource} returns a chunk equivalent to the backed off chunk B, then the + * loading of B will be resumed. In all other cases B will be discarded and the new chunk will + * be loaded. + */ + private void resumeFromBackOff() { + currentLoadableException = null; + + HlsChunk backedOffChunk = currentLoadableHolder.chunk; + if (!isTsChunk(backedOffChunk)) { + currentLoadableHolder.queueSize = readOnlyHlsChunks.size(); + chunkSource.getChunkOperation(readOnlyHlsChunks, pendingResetTime, downstreamPositionUs, + currentLoadableHolder); + discardUpstreamHlsChunks(currentLoadableHolder.queueSize); + if (currentLoadableHolder.chunk == backedOffChunk) { + // HlsChunk was unchanged. Resume loading. + loader.startLoading(backedOffChunk, this); + } else { + backedOffChunk.release(); + maybeStartLoading(); + } + return; + } + + if (backedOffChunk == mediaChunks.getFirst()) { + // We're not able to clear the first media chunk, so we have no choice but to continue + // loading it. + loader.startLoading(backedOffChunk, this); + return; + } + + // The current loadable is the last media chunk. Remove it before we invoke the chunk source, + // and add it back again afterwards. + TsChunk removedChunk = mediaChunks.removeLast(); + Assertions.checkState(backedOffChunk == removedChunk); + currentLoadableHolder.queueSize = readOnlyHlsChunks.size(); + chunkSource.getChunkOperation(readOnlyHlsChunks, pendingResetTime, downstreamPositionUs, + currentLoadableHolder); + mediaChunks.add(removedChunk); + + if (currentLoadableHolder.chunk == backedOffChunk) { + // HlsChunk was unchanged. Resume loading. + loader.startLoading(backedOffChunk, this); + } else { + // This call will remove and release at least one chunk from the end of mediaChunks. Since + // the current loadable is the last media chunk, it is guaranteed to be removed. + discardUpstreamHlsChunks(currentLoadableHolder.queueSize); + clearCurrentLoadable(); + maybeStartLoading(); + } + } + + private void maybeStartLoading() { + HlsChunk currentLoadable = currentLoadableHolder.chunk; + if (currentLoadable == null) { + // Nothing to load. + return; + } + currentLoadable.init(loadControl.getAllocator()); + if (isTsChunk(currentLoadable)) { + TsChunk mediaChunk = (TsChunk) currentLoadable; + if (isPendingReset()) { + pendingTimestampOffsetUpdate = true; + mediaChunk.reset(); + pendingResetTime = NO_RESET_PENDING; + } + mediaChunks.add(mediaChunk); + } + loader.startLoading(currentLoadable, this); + } + + /** + * Discards downstream media chunks until {@code untilChunk} if found. {@code untilChunk} is not + * itself discarded. Null can be passed to discard all media chunks. + * + * @param untilChunk The first media chunk to keep, or null to discard all media chunks. + */ + private void discardDownstreamHlsChunks(TsChunk untilChunk) { + if (mediaChunks.isEmpty() || untilChunk == mediaChunks.getFirst()) { + return; + } + while (!mediaChunks.isEmpty() && untilChunk != mediaChunks.getFirst()) { + mediaChunks.removeFirst().release(); + } + } + + /** + * Discards the first downstream media chunk. + */ + private void discardDownstreamHlsChunk() { + mediaChunks.removeFirst().release(); + } + + /** + * Discard upstream media chunks until the queue length is equal to the length specified. + * + * @param queueLength The desired length of the queue. + */ + private void discardUpstreamHlsChunks(int queueLength) { + while (mediaChunks.size() > queueLength) { + mediaChunks.removeLast().release(); + } + } + + private boolean isTsChunk(HlsChunk chunk) { + return chunk instanceof TsChunk; + } + + private boolean isPendingReset() { + return pendingResetTime != NO_RESET_PENDING; + } + + private long getRetryDelayMillis(long errorCount) { + return Math.min((errorCount - 1) * 1000, 5000); + } + + protected final int usToMs(long timeUs) { + return (int) (timeUs / 1000); + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/hls/TsChunk.java b/library/src/main/java/com/google/android/exoplayer/hls/TsChunk.java new file mode 100644 index 0000000000..9da8eb7459 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/hls/TsChunk.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer.hls; + +import com.google.android.exoplayer.MediaFormat; +import com.google.android.exoplayer.SampleHolder; +import com.google.android.exoplayer.parser.ts.TsExtractor; +import com.google.android.exoplayer.upstream.DataSource; +import com.google.android.exoplayer.upstream.DataSpec; +import com.google.android.exoplayer.upstream.NonBlockingInputStream; + +/** + * A MPEG2TS chunk. + */ +public final class TsChunk extends HlsChunk { + + /** + * The start time of the media contained by the chunk. + */ + public final long startTimeUs; + /** + * The end time of the media contained by the chunk. + */ + public final long endTimeUs; + /** + * The index of the next media chunk, or -1 if this is the last media chunk in the stream. + */ + public final int nextChunkIndex; + /** + * The encoding discontinuity indicator. + */ + private final boolean discontinuity; + + private final TsExtractor extractor; + + private boolean pendingDiscontinuity; + + /** + * @param dataSource A {@link DataSource} for loading the data. + * @param dataSpec Defines the data to be loaded. + * @param extractor The extractor that will be used to extract the samples. + * @param trigger The reason for this chunk being selected. + * @param startTimeUs The start time of the media contained by the chunk, in microseconds. + * @param endTimeUs The end time of the media contained by the chunk, in microseconds. + * @param nextChunkIndex The index of the next chunk, or -1 if this is the last chunk. + * @param discontinuity The encoding discontinuity indicator. + */ + public TsChunk(DataSource dataSource, DataSpec dataSpec, int trigger, TsExtractor extractor, + long startTimeUs, long endTimeUs, int nextChunkIndex, boolean discontinuity) { + super(dataSource, dataSpec, trigger); + this.startTimeUs = startTimeUs; + this.endTimeUs = endTimeUs; + this.nextChunkIndex = nextChunkIndex; + this.extractor = extractor; + this.discontinuity = discontinuity; + this.pendingDiscontinuity = discontinuity; + } + + public boolean readDiscontinuity() { + if (pendingDiscontinuity) { + extractor.reset(); + pendingDiscontinuity = false; + return true; + } + return false; + } + + public boolean prepare() { + return extractor.prepare(getNonBlockingInputStream()); + } + + public int getTrackCount() { + return extractor.getTrackCount(); + } + + public boolean sampleAvailable() { + // TODO: Maybe optimize this to not require looping over the tracks. + if (!prepare()) { + return false; + } + // TODO: Optimize this to not require looping over the tracks. + NonBlockingInputStream inputStream = getNonBlockingInputStream(); + int trackCount = extractor.getTrackCount(); + for (int i = 0; i < trackCount; i++) { + int result = extractor.read(inputStream, i, null); + if ((result & TsExtractor.RESULT_NEED_SAMPLE_HOLDER) != 0) { + return true; + } + } + return false; + } + + public boolean read(int track, SampleHolder holder) { + int result = extractor.read(getNonBlockingInputStream(), track, holder); + return (result & TsExtractor.RESULT_READ_SAMPLE) != 0; + } + + public void reset() { + extractor.reset(); + pendingDiscontinuity = discontinuity; + resetReadPosition(); + } + + public MediaFormat getMediaFormat(int track) { + return extractor.getFormat(track); + } + + public boolean isLastChunk() { + return nextChunkIndex == -1; + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/parser/ts/BitsArray.java b/library/src/main/java/com/google/android/exoplayer/parser/ts/BitsArray.java new file mode 100644 index 0000000000..6e51eceac3 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/parser/ts/BitsArray.java @@ -0,0 +1,280 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer.parser.ts; + +import com.google.android.exoplayer.upstream.NonBlockingInputStream; +import com.google.android.exoplayer.util.Assertions; + +/** + * Wraps a byte array, providing methods that allow it to be read as a bitstream. + */ +public final class BitsArray { + + private byte[] data; + + // The length of the valid data. + private int limit; + + // The offset within the data, stored as the current byte offset, and the bit offset within that + // byte (from 0 to 7). + private int byteOffset; + private int bitOffset; + + /** + * Resets the state. + */ + public void reset() { + byteOffset = 0; + bitOffset = 0; + limit = 0; + } + + /** + * Gets the current byte offset. + * + * @return The current byte offset. + */ + public int getByteOffset() { + return byteOffset; + } + + /** + * Sets the current byte offset. + * + * @param byteOffset The byte offset to set. + */ + public void setByteOffset(int byteOffset) { + this.byteOffset = byteOffset; + } + + /** + * Appends data from a {@link NonBlockingInputStream}. + * + * @param inputStream The {@link NonBlockingInputStream} whose data should be appended. + * @param length The maximum number of bytes to read and append. + * @return The number of bytes that were read and appended. May be 0 if no data was available + * from the stream. -1 is returned if the end of the stream has been reached. + */ + public int append(NonBlockingInputStream inputStream, int length) { + expand(length); + int bytesRead = inputStream.read(data, limit, length); + if (bytesRead == -1) { + return -1; + } + limit += bytesRead; + return bytesRead; + } + + /** + * Appends data from another {@link BitsArray}. + * + * @param bitsArray The {@link BitsArray} whose data should be appended. + * @param length The number of bytes to read and append. + */ + public void append(BitsArray bitsArray, int length) { + expand(length); + bitsArray.readBytes(data, limit, length); + limit += length; + } + + private void expand(int length) { + if (data == null) { + data = new byte[length]; + return; + } + if (data.length - limit < length) { + byte[] newBuffer = new byte[limit + length]; + System.arraycopy(data, 0, newBuffer, 0, limit); + data = newBuffer; + } + } + + /** + * Clears data that has already been read, moving the remaining data to the start of the buffer. + */ + public void clearReadData() { + System.arraycopy(data, byteOffset, data, 0, limit - byteOffset); + limit -= byteOffset; + byteOffset = 0; + } + + /** + * Reads a single unsigned byte. + * + * @return The value of the parsed byte. + */ + public int readUnsignedByte() { + byte b; + if (bitOffset != 0) { + b = (byte) ((data[byteOffset] << bitOffset) + | (data[byteOffset + 1] >> (8 - bitOffset))); + } else { + b = data[byteOffset]; + } + byteOffset++; + // Converting a signed byte into unsigned. + return b & 0xFF; + } + + + /** + * Reads up to 32 bits. + * + * @param n The number of bits to read. + * @return An integer whose bottom n bits hold the read data. + */ + public int readBits(int n) { + return (int) readBitsLong(n); + } + + /** + * Reads up to 64 bits. + * + * @param n The number of bits to read. + * @return A long whose bottom n bits hold the read data. + */ + public long readBitsLong(int n) { + if (n == 0) { + return 0; + } + + long retval = 0; + + // While n >= 8, read whole bytes. + while (n >= 8) { + n -= 8; + retval |= (readUnsignedByte() << n); + } + + if (n > 0) { + int nextBit = bitOffset + n; + byte writeMask = (byte) (0xFF >> (8 - n)); + + if (nextBit > 8) { + // Combine bits from current byte and next byte. + retval |= (((getUnsignedByte(byteOffset) << (nextBit - 8) + | (getUnsignedByte(byteOffset + 1) >> (16 - nextBit))) & writeMask)); + byteOffset++; + } else { + // Bits to be read only within current byte. + retval |= ((getUnsignedByte(byteOffset) >> (8 - nextBit)) & writeMask); + if (nextBit == 8) { + byteOffset++; + } + } + + bitOffset = nextBit % 8; + } + + return retval; + } + + private int getUnsignedByte(int offset) { + return data[offset] & 0xFF; + } + + /** + * Skips bits and moves current reading position forward. + * + * @param n The number of bits to skip. + */ + public void skipBits(int n) { + byteOffset += (n / 8); + bitOffset += (n % 8); + if (bitOffset > 7) { + byteOffset++; + bitOffset -= 8; + } + } + + /** + * Skips bytes and moves current reading position forward. + * + * @param n The number of bytes to skip. + */ + public void skipBytes(int n) { + byteOffset += n; + } + + /** + * Reads multiple bytes and copies them into provided byte array. + *

+ * The read position must be at a whole byte boundary for this method to be called. + * + * @param out The byte array to copy read data. + * @param offset The offset in the out byte array. + * @param length The length of the data to read + * @throws IllegalStateException If the method is called with the read position not at a whole + * byte boundary. + */ + public void readBytes(byte[] out, int offset, int length) { + Assertions.checkState(bitOffset == 0); + System.arraycopy(data, byteOffset, out, offset, length); + byteOffset += length; + } + + /** + * @return The number of whole bytes that are available to read. + */ + public int bytesLeft() { + return limit - byteOffset; + } + + /** + * @return Whether or not there is any data available. + */ + public boolean isEmpty() { + return limit == 0; + } + + // TODO: Find a better place for this method. + /** + * Finds the next Adts sync word. + * + * @return The offset from the current position to the start of the next Adts sync word. If an + * Adts sync word is not found, then the offset to the end of the data is returned. + */ + public int findNextAdtsSyncWord() { + for (int i = byteOffset; i < limit - 1; i++) { + int syncBits = (getUnsignedByte(i) << 8) | getUnsignedByte(i + 1); + if ((syncBits & 0xFFF0) == 0xFFF0 && syncBits != 0xFFFF) { + return i - byteOffset; + } + } + return limit - byteOffset; + } + + //TODO: Find a better place for this method. + /** + * Finds the next NAL unit. + * + * @param nalUnitType The type of the NAL unit to search for. + * @param offset The additional offset in the data to start the search from. + * @return The offset from the current position to the start of the NAL unit. If a NAL unit is + * not found, then the offset to the end of the data is returned. + */ + public int findNextNalUnit(int nalUnitType, int offset) { + for (int i = byteOffset + offset; i < limit - 3; i++) { + // Check for NAL unit start code prefix == 0x000001. + if ((data[i] == 0 && data[i + 1] == 0 && data[i + 2] == 1) + && (nalUnitType == (data[i + 3] & 0x1F))) { + return i - byteOffset; + } + } + return limit - byteOffset; + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/parser/ts/TsExtractor.java b/library/src/main/java/com/google/android/exoplayer/parser/ts/TsExtractor.java new file mode 100644 index 0000000000..cfdc261925 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/parser/ts/TsExtractor.java @@ -0,0 +1,722 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer.parser.ts; + +import com.google.android.exoplayer.MediaFormat; +import com.google.android.exoplayer.SampleHolder; +import com.google.android.exoplayer.upstream.NonBlockingInputStream; +import com.google.android.exoplayer.util.Assertions; +import com.google.android.exoplayer.util.CodecSpecificDataUtil; +import com.google.android.exoplayer.util.MimeTypes; + +import android.annotation.SuppressLint; +import android.media.MediaExtractor; +import android.util.Log; +import android.util.Pair; +import android.util.SparseArray; + +import java.nio.ByteBuffer; +import java.util.Collections; +import java.util.LinkedList; +import java.util.Queue; + +/** + * Facilitates the extraction of data from the MPEG-2 TS container format. + */ +public final class TsExtractor { + + /** + * An attempt to read from the input stream returned insufficient data. + */ + public static final int RESULT_NEED_MORE_DATA = 1; + /** + * A media sample was read. + */ + public static final int RESULT_READ_SAMPLE = 2; + /** + * The next thing to be read is a sample, but a {@link SampleHolder} was not supplied. + */ + public static final int RESULT_NEED_SAMPLE_HOLDER = 4; + + private static final String TAG = "TsExtractor"; + + private static final int TS_PACKET_SIZE = 188; + private static final int TS_SYNC_BYTE = 0x47; // First byte of each TS packet. + private static final int TS_PAT_PID = 0; + + private static final int TS_STREAM_TYPE_AAC = 0x0F; + private static final int TS_STREAM_TYPE_H264 = 0x1B; + + private static final int DEFAULT_BUFFER_SEGMENT_SIZE = 64 * 1024; + + private final BitsArray tsPacketBuffer; + private final SparseArray pesPayloadReaders; // Indexed by streamType + private final SparseArray tsPayloadReaders; // Indexed by pid + private final Queue samplesPool; + + private boolean prepared; + + public TsExtractor() { + tsPacketBuffer = new BitsArray(); + pesPayloadReaders = new SparseArray(); + tsPayloadReaders = new SparseArray(); + tsPayloadReaders.put(TS_PAT_PID, new PatReader()); + samplesPool = new LinkedList(); + } + + /** + * Gets the number of available tracks. + *

+ * This method should only be called after the extractor has been prepared. + * + * @return The number of available tracks. + */ + public int getTrackCount() { + Assertions.checkState(prepared); + return pesPayloadReaders.size(); + } + + /** + * Gets the format of the specified track. + *

+ * This method must only be called after the extractor has been prepared. + * + * @param track The track index. + * @return The corresponding format. + */ + public MediaFormat getFormat(int track) { + Assertions.checkState(prepared); + return pesPayloadReaders.valueAt(track).getMediaFormat(); + } + + /** + * Resets the extractor's internal state. + */ + public void reset() { + prepared = false; + tsPacketBuffer.reset(); + tsPayloadReaders.clear(); + tsPayloadReaders.put(TS_PAT_PID, new PatReader()); + // Clear each reader before discarding it, so as to recycle any queued Sample objects. + for (int i = 0; i < pesPayloadReaders.size(); i++) { + pesPayloadReaders.valueAt(i).clear(); + } + pesPayloadReaders.clear(); + } + + /** + * Attempts to prepare the extractor. The extractor is prepared once it has read sufficient data + * to have established the available tracks and their corresponding media formats. + *

+ * Calling this method is a no-op if the extractor is already prepared. + * + * @param inputStream The input stream from which data can be read. + * @return True if the extractor was prepared. False if more data is required. + */ + public boolean prepare(NonBlockingInputStream inputStream) { + while (!prepared) { + if (readTSPacket(inputStream) == -1) { + return false; + } + prepared = checkPrepared(); + } + return true; + } + + private boolean checkPrepared() { + int pesPayloadReaderCount = pesPayloadReaders.size(); + if (pesPayloadReaderCount == 0) { + return false; + } + for (int i = 0; i < pesPayloadReaderCount; i++) { + if (!pesPayloadReaders.valueAt(i).hasMediaFormat()) { + return false; + } + } + return true; + } + + /** + * Consumes data from a {@link NonBlockingInputStream}. + *

+ * The read terminates if the end of the input stream is reached, if insufficient data is + * available to read a sample, or if a sample is read. The returned flags indicate + * both the reason for termination and data that was parsed during the read. + * + * @param inputStream The input stream from which data should be read. + * @param track The track from which to read. + * @param out A {@link SampleHolder} into which the next sample should be read. If null then + * {@link #RESULT_NEED_SAMPLE_HOLDER} will be returned once a sample has been reached. + * @return One or more of the {@code RESULT_*} flags defined in this class. + */ + public int read(NonBlockingInputStream inputStream, int track, SampleHolder out) { + Assertions.checkState(prepared); + Queue queue = pesPayloadReaders.valueAt(track).samplesQueue; + + // Keep reading if the buffer is empty. + while (queue.isEmpty()) { + if (readTSPacket(inputStream) == -1) { + return RESULT_NEED_MORE_DATA; + } + } + + if (!queue.isEmpty() && out == null) { + return RESULT_NEED_SAMPLE_HOLDER; + } + + Sample sample = queue.remove(); + convert(sample, out); + recycleSample(sample); + return RESULT_READ_SAMPLE; + } + + /** + * Read a single TS packet. + */ + private int readTSPacket(NonBlockingInputStream inputStream) { + // Read entire single TS packet. + if (inputStream.getAvailableByteCount() < TS_PACKET_SIZE) { + return -1; + } + + tsPacketBuffer.reset(); + + int bytesRead = tsPacketBuffer.append(inputStream, TS_PACKET_SIZE); + if (bytesRead != TS_PACKET_SIZE) { + return -1; + } + + // Parse TS header. + // Check sync byte. + int syncByte = tsPacketBuffer.readUnsignedByte(); + if (syncByte != TS_SYNC_BYTE) { + return 0; + } + // Skip transportErrorIndicator. + tsPacketBuffer.skipBits(1); + int payloadUnitStartIndicator = tsPacketBuffer.readBits(1); + // Skip transportPriority. + tsPacketBuffer.skipBits(1); + int pid = tsPacketBuffer.readBits(13); + // Skip transport_scrambling_control. + tsPacketBuffer.skipBits(2); + int adaptationFieldExist = tsPacketBuffer.readBits(1); + int payloadExist = tsPacketBuffer.readBits(1); + // Skip continuityCounter. + tsPacketBuffer.skipBits(4); + + // Read Adaptation Field. + if (adaptationFieldExist == 1) { + int afLength = tsPacketBuffer.readBits(8); + tsPacketBuffer.skipBytes(afLength); + } + + // Read Payload. + if (payloadExist == 1) { + TsPayloadReader payloadReader = tsPayloadReaders.get(pid); + if (payloadReader == null) { + return 0; + } + payloadReader.read(tsPacketBuffer, payloadUnitStartIndicator); + } + return 0; + } + + private void convert(Sample in, SampleHolder out) { + if (out.data == null || out.data.capacity() < in.size) { + if (out.allowDataBufferReplacement) { + out.data = ByteBuffer.allocate(in.size); + } else { + throw new IndexOutOfBoundsException("Buffer too small, and replacement not enabled"); + } + } + out.data.put(in.data, 0, in.size); + out.size = in.size; + out.flags = in.flags; + out.timeUs = in.timeUs; + } + + private Sample getSample() { + if (samplesPool.isEmpty()) { + return new Sample(DEFAULT_BUFFER_SEGMENT_SIZE); + } + return samplesPool.remove(); + } + + private void recycleSample(Sample sample) { + sample.reset(); + samplesPool.add(sample); + } + + /** + * Parses payload data. + */ + private abstract static class TsPayloadReader { + public abstract void read(BitsArray tsBuffer, int payloadUnitStartIndicator); + } + + /** + * Parses Program Association Table data. + */ + private class PatReader extends TsPayloadReader { + + @Override + public void read(BitsArray tsBuffer, int payloadUnitStartIndicator) { + // Skip pointer. + if (payloadUnitStartIndicator == 1) { + int pointerField = tsBuffer.readBits(8); + tsBuffer.skipBytes(pointerField); + } + + // Skip PAT header. + tsBuffer.skipBits(64); // 8+1+1+2+12+16+2+5+1+8+8 + + // Only read the first program and take it. + + // Skip program_number. + tsBuffer.skipBits(16 + 3); + int pid = tsBuffer.readBits(13); + + // Pick the first program. + if (tsPayloadReaders.get(pid) == null) { + tsPayloadReaders.put(pid, new PmtReader()); + } + + // Skip other programs if exist. + // Skip CRC_32. + } + + } + + /** + * Parses Program Map Table. + */ + private class PmtReader extends TsPayloadReader { + + @Override + public void read(BitsArray tsBuffer, int payloadUnitStartIndicator) { + // Skip pointer. + if (payloadUnitStartIndicator == 1) { + int pointerField = tsBuffer.readBits(8); + tsBuffer.skipBytes(pointerField); + } + + // Skip table_id, section_syntax_indicator, etc. + tsBuffer.skipBits(12); // 8+1+1+2 + int sectionLength = tsBuffer.readBits(12); + // Skip the rest of the PMT header. + tsBuffer.skipBits(60); // 16+2+5+1+8+8+3+13+4 + int programInfoLength = tsBuffer.readBits(12); + + // Read descriptors. + readDescriptors(tsBuffer, programInfoLength); + + int entriesSize = sectionLength - 9 /* size of the rest of the fields before descriptors */ + - programInfoLength - 4 /* CRC size */; + while (entriesSize > 0) { + int streamType = tsBuffer.readBits(8); + tsBuffer.skipBits(3); + int elementaryPid = tsBuffer.readBits(13); + tsBuffer.skipBits(4); + int esInfoLength = tsBuffer.readBits(12); + + readDescriptors(tsBuffer, esInfoLength); + entriesSize -= esInfoLength + 5; + + if (pesPayloadReaders.get(streamType) != null) { + continue; + } + + PesPayloadReader pesPayloadReader = null; + switch (streamType) { + case TS_STREAM_TYPE_AAC: + pesPayloadReader = new AdtsReader(); + break; + case TS_STREAM_TYPE_H264: + pesPayloadReader = new H264Reader(); + break; + } + + if (pesPayloadReader != null) { + pesPayloadReaders.put(streamType, pesPayloadReader); + tsPayloadReaders.put(elementaryPid, new PesReader(pesPayloadReader)); + } + } + + // Skip CRC_32. + } + + private void readDescriptors(BitsArray tsBuffer, int descriptorsSize) { + while (descriptorsSize > 0) { + // Skip tag. + tsBuffer.skipBits(8); + int descriptorsLength = tsBuffer.readBits(8); + if (descriptorsLength > 0) { + // Skip entire descriptor data. + tsBuffer.skipBytes(descriptorsLength); + } + descriptorsSize -= descriptorsSize + 2; + } + } + + } + + /** + * Parses PES packet data and extracts samples. + */ + private class PesReader extends TsPayloadReader { + + // Reusable buffer for incomplete PES data. + private final BitsArray pesBuffer; + // Parses PES payload and extracts individual samples. + private final PesPayloadReader pesPayloadReader; + + public PesReader(PesPayloadReader pesPayloadReader) { + this.pesPayloadReader = pesPayloadReader; + pesBuffer = new BitsArray(); + } + + @Override + public void read(BitsArray tsBuffer, int payloadUnitStartIndicator) { + if (payloadUnitStartIndicator == 1 && !pesBuffer.isEmpty()) { + readPES(); + } + pesBuffer.append(tsBuffer, tsBuffer.bytesLeft()); + } + + /** + * Parses completed PES data. + */ + private void readPES() { + int packetStartCodePrefix = pesBuffer.readBits(24); + if (packetStartCodePrefix != 0x000001) { + // Error. + } + // TODO: Read and use stream_id. + // Skip stream_id. + pesBuffer.skipBits(8); + int pesPacketLength = pesBuffer.readBits(16); + + // Skip some fields/flags. + // TODO: might need to use data_alignment_indicator. + pesBuffer.skipBits(8); // 2+2+1+1+1+1 + int ptsFlag = pesBuffer.readBits(1); + // Skip DTS flag. + pesBuffer.skipBits(1); + // Skip some fields/flags. + pesBuffer.skipBits(6); // 1+1+1+1+1+1 + + int pesHeaderDataLength = pesBuffer.readBits(8); + if (pesHeaderDataLength == 0) { + pesHeaderDataLength = pesBuffer.bytesLeft(); + } + + long timeUs = 0; + + if (ptsFlag == 1) { + // Skip prefix. + pesBuffer.skipBits(4); + long pts = pesBuffer.readBitsLong(3) << 30; + pesBuffer.skipBits(1); + pts |= pesBuffer.readBitsLong(15) << 15; + pesBuffer.skipBits(1); + pts |= pesBuffer.readBitsLong(15); + pesBuffer.skipBits(1); + + timeUs = pts * 1000000 / 90000; + + // Skip the rest of the header. + pesBuffer.skipBytes(pesHeaderDataLength - 5); + } else { + // Skip the rest of the header. + pesBuffer.skipBytes(pesHeaderDataLength); + } + + int payloadSize; + if (pesPacketLength == 0) { + // If pesPacketLength is not specified read all available data. + payloadSize = pesBuffer.bytesLeft(); + } else { + payloadSize = pesPacketLength - pesHeaderDataLength - 3; + } + + pesPayloadReader.read(pesBuffer, payloadSize, timeUs); + + pesBuffer.reset(); + } + + } + + /** + * Extracts individual samples from continuous byte stream. + */ + private abstract class PesPayloadReader { + + public final Queue samplesQueue; + + private MediaFormat mediaFormat; + + protected PesPayloadReader() { + this.samplesQueue = new LinkedList(); + } + + public boolean hasMediaFormat() { + return mediaFormat != null; + } + + public MediaFormat getMediaFormat() { + return mediaFormat; + } + + protected void setMediaFormat(MediaFormat mediaFormat) { + this.mediaFormat = mediaFormat; + } + + public abstract void read(BitsArray pesBuffer, int pesPayloadSize, long pesTimeUs); + + public void clear() { + while (!samplesQueue.isEmpty()) { + recycleSample(samplesQueue.remove()); + } + } + + /** + * Creates a new Sample and adds it to the queue. + * + * @param buffer The buffer to read sample data. + * @param sampleSize The size of the sample data. + * @param sampleTimeUs The sample time stamp. + */ + protected void addSample(BitsArray buffer, int sampleSize, long sampleTimeUs, int flags) { + Sample sample = getSample(); + addToSample(sample, buffer, sampleSize); + sample.flags = flags; + sample.timeUs = sampleTimeUs; + samplesQueue.add(sample); + } + + protected void addToSample(Sample sample, BitsArray buffer, int size) { + if (sample.data.length - sample.size < size) { + sample.expand(size - sample.data.length + sample.size); + } + buffer.readBytes(sample.data, sample.size, size); + sample.size += size; + } + + } + + /** + * Parses a continuous H264 byte stream and extracts individual frames. + */ + private class H264Reader extends PesPayloadReader { + + // IDR picture. + private static final int NAL_UNIT_TYPE_IDR = 5; + // Access unit delimiter. + private static final int NAL_UNIT_TYPE_AUD = 9; + + // Used to store uncompleted sample data. + private Sample currentSample; + + public H264Reader() { + // TODO: Parse the format from the stream. + setMediaFormat(MediaFormat.createVideoFormat(MimeTypes.VIDEO_H264, MediaFormat.NO_VALUE, + 1920, 1080, null)); + } + + @Override + public void read(BitsArray pesBuffer, int pesPayloadSize, long pesTimeUs) { + // Read leftover frame data from previous PES packet. + pesPayloadSize -= readOneH264Frame(pesBuffer, true); + + if (pesBuffer.bytesLeft() <= 0 || pesPayloadSize <= 0) { + return; + } + + // Single PES packet should contain only one new H.264 frame. + if (currentSample != null) { + samplesQueue.add(currentSample); + } + currentSample = getSample(); + pesPayloadSize -= readOneH264Frame(pesBuffer, false); + currentSample.timeUs = pesTimeUs; + + if (pesPayloadSize > 0) { + Log.e(TAG, "PES packet contains more frame data than expected"); + } + } + + @SuppressLint("InlinedApi") + private int readOneH264Frame(BitsArray pesBuffer, boolean remainderOnly) { + int offset = remainderOnly ? 0 : 3; + int audStart = pesBuffer.findNextNalUnit(NAL_UNIT_TYPE_AUD, offset); + int idrStart = pesBuffer.findNextNalUnit(NAL_UNIT_TYPE_IDR, offset); + if (audStart > 0) { + if (currentSample != null) { + addToSample(currentSample, pesBuffer, audStart); + if (idrStart < audStart) { + currentSample.flags = MediaExtractor.SAMPLE_FLAG_SYNC; + } + } else { + pesBuffer.skipBytes(audStart); + } + return audStart; + } + return 0; + } + + @Override + public void clear() { + super.clear(); + if (currentSample != null) { + recycleSample(currentSample); + currentSample = null; + } + } + } + + /** + * Parses a continuous ADTS byte stream and extracts individual frames. + */ + private class AdtsReader extends PesPayloadReader { + + private final BitsArray adtsBuffer; + private long timeUs; + + public AdtsReader() { + adtsBuffer = new BitsArray(); + } + + @Override + public void read(BitsArray pesBuffer, int pesPayloadSize, long pesTimeUs) { + boolean needToProcessLeftOvers = !adtsBuffer.isEmpty(); + adtsBuffer.append(pesBuffer, pesPayloadSize); + // If there are leftovers from previous PES packet, process it with last calculated timeUs. + if (needToProcessLeftOvers && !readOneAacFrame(timeUs)) { + return; + } + int frameIndex = 0; + do { + long frameDuration = 0; + // If frameIndex > 0, audioMediaFormat should be already parsed. + // If frameIndex == 0, timeUs = pesTimeUs anyway. + if (hasMediaFormat()) { + frameDuration = 1000000L * 1024L / getMediaFormat().sampleRate; + } + timeUs = pesTimeUs + frameIndex * frameDuration; + frameIndex++; + } while(readOneAacFrame(timeUs)); + } + + @SuppressLint("InlinedApi") + private boolean readOneAacFrame(long timeUs) { + if (adtsBuffer.isEmpty()) { + return false; + } + + int offsetToSyncWord = adtsBuffer.findNextAdtsSyncWord(); + adtsBuffer.skipBytes(offsetToSyncWord); + + int adtsStartOffset = adtsBuffer.getByteOffset(); + + if (adtsBuffer.bytesLeft() < 7) { + adtsBuffer.setByteOffset(adtsStartOffset); + adtsBuffer.clearReadData(); + return false; + } + + adtsBuffer.skipBits(15); + int hasCRC = adtsBuffer.readBits(1); + + if (!hasMediaFormat()) { + int audioObjectType = adtsBuffer.readBits(2) + 1; + int sampleRateIndex = adtsBuffer.readBits(4); + adtsBuffer.skipBits(1); + int channelConfig = adtsBuffer.readBits(3); + + byte[] audioSpecificConfig = CodecSpecificDataUtil.buildAudioSpecificConfig( + audioObjectType, sampleRateIndex, channelConfig); + Pair audioParams = CodecSpecificDataUtil.parseAudioSpecificConfig( + audioSpecificConfig); + + MediaFormat mediaFormat = MediaFormat.createAudioFormat(MimeTypes.AUDIO_AAC, + MediaFormat.NO_VALUE, audioParams.second, audioParams.first, + Collections.singletonList(audioSpecificConfig)); + setMediaFormat(mediaFormat); + } else { + adtsBuffer.skipBits(10); + } + + adtsBuffer.skipBits(4); + int frameSize = adtsBuffer.readBits(13); + adtsBuffer.skipBits(13); + + // Decrement frame size by ADTS header size and CRC. + if (hasCRC == 0) { + // Skip CRC. + adtsBuffer.skipBytes(2); + frameSize -= 9; + } else { + frameSize -= 7; + } + + if (frameSize > adtsBuffer.bytesLeft()) { + adtsBuffer.setByteOffset(adtsStartOffset); + adtsBuffer.clearReadData(); + return false; + } + + addSample(adtsBuffer, frameSize, timeUs, MediaExtractor.SAMPLE_FLAG_SYNC); + return true; + } + + @Override + public void clear() { + super.clear(); + adtsBuffer.reset(); + } + + } + + /** + * Simplified version of SampleHolder for internal buffering. + */ + private static class Sample { + + public byte[] data; + public int flags; + public int size; + public long timeUs; + + public Sample(int length) { + data = new byte[length]; + } + + public void expand(int length) { + byte[] newBuffer = new byte[data.length + length]; + System.arraycopy(data, 0, newBuffer, 0, size); + data = newBuffer; + } + + public void reset() { + flags = 0; + size = 0; + timeUs = 0; + } + + } + +} From 60d162df18076b4db3034b8ff944da86d372a2ef Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Wed, 15 Oct 2014 20:42:26 +0100 Subject: [PATCH 02/55] Fix overflow when comparing HLS Segments for long videos. --- .../java/com/google/android/exoplayer/hls/HlsMediaPlaylist.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 fda3e50b03..e4631d42f6 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 @@ -42,7 +42,7 @@ public final class HlsMediaPlaylist { @Override public int compareTo(Long startTimeUs) { - return (int) (this.startTimeUs - startTimeUs); + return this.startTimeUs > startTimeUs ? 1 : (this.startTimeUs < startTimeUs ? -1 : 0); } } From 5f0be427a46a6b902091cf46bda2a02deba3453e Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 20 Oct 2014 16:53:43 +0100 Subject: [PATCH 03/55] Update HlsSampleSource + correctly propagate error from prepare. Issue: #81 --- .../exoplayer/hls/HlsSampleSource.java | 69 ++++++++++--------- 1 file changed, 37 insertions(+), 32 deletions(-) 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 ad25c12dea..c829d169cb 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 @@ -64,7 +64,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { private long downstreamPositionUs; private long lastSeekPositionUs; - private long pendingResetTime; + private long pendingResetPositionUs; private long lastPerformedBufferOperation; private Loader loader; @@ -89,7 +89,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { } @Override - public boolean prepare() { + public boolean prepare() throws IOException { if (prepared) { return true; } @@ -116,6 +116,9 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { prepared = true; } + if (!prepared && currentLoadableException != null) { + throw currentLoadableException; + } return prepared; } @@ -132,16 +135,16 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { } @Override - public void enable(int track, long timeUs) { + public void enable(int track, long positionUs) { Assertions.checkState(prepared); Assertions.checkState(!trackEnabledStates[track]); enabledTrackCount++; trackEnabledStates[track] = true; downstreamMediaFormats[track] = null; if (enabledTrackCount == 1) { - downstreamPositionUs = timeUs; - lastSeekPositionUs = timeUs; - restartFrom(timeUs); + downstreamPositionUs = positionUs; + lastSeekPositionUs = positionUs; + restartFrom(positionUs); } } @@ -257,21 +260,21 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { } @Override - public void seekToUs(long timeUs) { + public void seekToUs(long positionUs) { Assertions.checkState(prepared); Assertions.checkState(enabledTrackCount > 0); - downstreamPositionUs = timeUs; - lastSeekPositionUs = timeUs; - if (pendingResetTime == timeUs) { + downstreamPositionUs = positionUs; + lastSeekPositionUs = positionUs; + if (pendingResetPositionUs == positionUs) { return; } for (int i = 0; i < pendingDiscontinuities.length; i++) { pendingDiscontinuities[i] = true; } - TsChunk mediaChunk = getHlsChunk(timeUs); + TsChunk mediaChunk = getHlsChunk(positionUs); if (mediaChunk == null) { - restartFrom(timeUs); + restartFrom(positionUs); } else { pendingTimestampOffsetUpdate = true; mediaChunk.reset(); @@ -280,13 +283,13 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { } } - private TsChunk getHlsChunk(long timeUs) { + private TsChunk getHlsChunk(long positionUs) { Iterator mediaChunkIterator = mediaChunks.iterator(); while (mediaChunkIterator.hasNext()) { TsChunk mediaChunk = mediaChunkIterator.next(); - if (timeUs < mediaChunk.startTimeUs) { + if (positionUs < mediaChunk.startTimeUs) { return null; - } else if (mediaChunk.isLastChunk() || timeUs < mediaChunk.endTimeUs) { + } else if (mediaChunk.isLastChunk() || positionUs < mediaChunk.endTimeUs) { return mediaChunk; } } @@ -298,7 +301,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { Assertions.checkState(prepared); Assertions.checkState(enabledTrackCount > 0); if (isPendingReset()) { - return pendingResetTime; + return pendingResetPositionUs; } TsChunk mediaChunk = mediaChunks.getLast(); HlsChunk currentLoadable = currentLoadableHolder.chunk; @@ -357,7 +360,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { } clearCurrentLoadable(); if (enabledTrackCount > 0) { - restartFrom(pendingResetTime); + restartFrom(pendingResetPositionUs); } else { clearHlsChunks(); loadControl.trimAllocator(); @@ -372,8 +375,8 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { updateLoadControl(); } - private void restartFrom(long timeUs) { - pendingResetTime = timeUs; + private void restartFrom(long positionUs) { + pendingResetPositionUs = positionUs; if (loader.isLoading()) { loader.cancelLoading(); } else { @@ -395,21 +398,23 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { } private void updateLoadControl() { + if (currentLoadableExceptionFatal) { + // We've failed, but we still need to update the control with our current state. + loadControl.update(this, downstreamPositionUs, -1, false, true); + return; + } + long loadPositionUs; if (isPendingReset()) { - loadPositionUs = pendingResetTime; + loadPositionUs = pendingResetPositionUs; } else { TsChunk lastHlsChunk = mediaChunks.getLast(); loadPositionUs = lastHlsChunk.nextChunkIndex == -1 ? -1 : lastHlsChunk.endTimeUs; } - boolean isBackedOff = currentLoadableException != null && !currentLoadableExceptionFatal; + boolean isBackedOff = currentLoadableException != null; boolean nextLoader = loadControl.update(this, downstreamPositionUs, loadPositionUs, - isBackedOff || loader.isLoading(), currentLoadableExceptionFatal); - - if (currentLoadableExceptionFatal) { - return; - } + isBackedOff || loader.isLoading(), false); long now = SystemClock.elapsedRealtime(); @@ -425,8 +430,8 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { if (currentLoadableHolder.chunk == null || now - lastPerformedBufferOperation > 1000) { lastPerformedBufferOperation = now; currentLoadableHolder.queueSize = readOnlyHlsChunks.size(); - chunkSource.getChunkOperation(readOnlyHlsChunks, pendingResetTime, downstreamPositionUs, - currentLoadableHolder); + chunkSource.getChunkOperation(readOnlyHlsChunks, pendingResetPositionUs, + downstreamPositionUs, currentLoadableHolder); discardUpstreamHlsChunks(currentLoadableHolder.queueSize); } if (nextLoader) { @@ -448,7 +453,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { HlsChunk backedOffChunk = currentLoadableHolder.chunk; if (!isTsChunk(backedOffChunk)) { currentLoadableHolder.queueSize = readOnlyHlsChunks.size(); - chunkSource.getChunkOperation(readOnlyHlsChunks, pendingResetTime, downstreamPositionUs, + chunkSource.getChunkOperation(readOnlyHlsChunks, pendingResetPositionUs, downstreamPositionUs, currentLoadableHolder); discardUpstreamHlsChunks(currentLoadableHolder.queueSize); if (currentLoadableHolder.chunk == backedOffChunk) { @@ -473,7 +478,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { TsChunk removedChunk = mediaChunks.removeLast(); Assertions.checkState(backedOffChunk == removedChunk); currentLoadableHolder.queueSize = readOnlyHlsChunks.size(); - chunkSource.getChunkOperation(readOnlyHlsChunks, pendingResetTime, downstreamPositionUs, + chunkSource.getChunkOperation(readOnlyHlsChunks, pendingResetPositionUs, downstreamPositionUs, currentLoadableHolder); mediaChunks.add(removedChunk); @@ -501,7 +506,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { if (isPendingReset()) { pendingTimestampOffsetUpdate = true; mediaChunk.reset(); - pendingResetTime = NO_RESET_PENDING; + pendingResetPositionUs = NO_RESET_PENDING; } mediaChunks.add(mediaChunk); } @@ -546,7 +551,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { } private boolean isPendingReset() { - return pendingResetTime != NO_RESET_PENDING; + return pendingResetPositionUs != NO_RESET_PENDING; } private long getRetryDelayMillis(long errorCount) { From 5ba3f1eea34e22ee71e2ef1f83e17b6a4d756fcf Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Tue, 28 Oct 2014 10:08:42 +0000 Subject: [PATCH 04/55] Fix build. --- .../android/exoplayer/parser/ts/TsExtractor.java | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/parser/ts/TsExtractor.java b/library/src/main/java/com/google/android/exoplayer/parser/ts/TsExtractor.java index cfdc261925..fd5ba369b9 100644 --- a/library/src/main/java/com/google/android/exoplayer/parser/ts/TsExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer/parser/ts/TsExtractor.java @@ -28,7 +28,6 @@ import android.util.Log; import android.util.Pair; import android.util.SparseArray; -import java.nio.ByteBuffer; import java.util.Collections; import java.util.LinkedList; import java.util.Queue; @@ -237,13 +236,11 @@ public final class TsExtractor { private void convert(Sample in, SampleHolder out) { if (out.data == null || out.data.capacity() < in.size) { - if (out.allowDataBufferReplacement) { - out.data = ByteBuffer.allocate(in.size); - } else { - throw new IndexOutOfBoundsException("Buffer too small, and replacement not enabled"); - } + out.replaceBuffer(in.size); + } + if (out.data != null) { + out.data.put(in.data, 0, in.size); } - out.data.put(in.data, 0, in.size); out.size = in.size; out.flags = in.flags; out.timeUs = in.timeUs; From ca310100283388140bd5b77f4d17ba4104af9019 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Tue, 28 Oct 2014 14:25:12 +0000 Subject: [PATCH 05/55] Add HLS support to simple variant of demo app. Plus cleanup. --- .../android/exoplayer/demo/Samples.java | 3 + .../demo/full/FullPlayerActivity.java | 5 +- .../demo/full/player/HlsRendererBuilder.java | 20 ++- .../demo/simple/HlsRendererBuilder.java | 115 ++++++++++++++++++ .../demo/simple/SimplePlayerActivity.java | 14 +-- 5 files changed, 135 insertions(+), 22 deletions(-) create mode 100644 demo/src/main/java/com/google/android/exoplayer/demo/simple/HlsRendererBuilder.java diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/Samples.java b/demo/src/main/java/com/google/android/exoplayer/demo/Samples.java index e01eb12d39..0dd3ddc1bd 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/Samples.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/Samples.java @@ -57,6 +57,9 @@ package com.google.android.exoplayer.demo; new Sample("Super speed (SmoothStreaming)", "uid:ss:superspeed", "http://playready.directtaps.net/smoothstreaming/SSWSS720H264/SuperSpeedway_720.ism", DemoUtil.TYPE_SS, false, false), + new Sample("Apple master playlist (HLS)", "uid:hls:applemaster", + "https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/" + + "bipbop_4x3_variant.m3u8", DemoUtil.TYPE_HLS_MASTER, false, false), new Sample("Dizzy (Misc)", "uid:misc:dizzy", "http://html5demos.com/assets/dizzy.mp4", DemoUtil.TYPE_OTHER, false, false), }; diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/FullPlayerActivity.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/FullPlayerActivity.java index 29a812b11d..a64ceead46 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/FullPlayerActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/FullPlayerActivity.java @@ -175,11 +175,8 @@ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callba return new DashVodRendererBuilder(userAgent, contentUri.toString(), contentId, new WidevineTestMediaDrmCallback(contentId), debugTextView); case DemoUtil.TYPE_HLS_MASTER: - return new HlsRendererBuilder(userAgent, contentUri.toString(), contentId, - HlsRendererBuilder.TYPE_MASTER); case DemoUtil.TYPE_HLS_MEDIA: - return new HlsRendererBuilder(userAgent, contentUri.toString(), contentId, - HlsRendererBuilder.TYPE_MEDIA); + return new HlsRendererBuilder(userAgent, contentUri.toString(), contentId, contentType); default: return new DefaultRendererBuilder(this, contentUri, debugTextView); } diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/HlsRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/HlsRendererBuilder.java index b7c08e97dc..2d7383dbf3 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/HlsRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/HlsRendererBuilder.java @@ -20,6 +20,7 @@ import com.google.android.exoplayer.LoadControl; import com.google.android.exoplayer.MediaCodecAudioTrackRenderer; import com.google.android.exoplayer.MediaCodecVideoTrackRenderer; import com.google.android.exoplayer.TrackRenderer; +import com.google.android.exoplayer.demo.DemoUtil; import com.google.android.exoplayer.demo.full.player.DemoPlayer.RendererBuilder; import com.google.android.exoplayer.demo.full.player.DemoPlayer.RendererBuilderCallback; import com.google.android.exoplayer.hls.HlsChunkSource; @@ -29,7 +30,7 @@ import com.google.android.exoplayer.hls.HlsMasterPlaylistParser; import com.google.android.exoplayer.hls.HlsSampleSource; import com.google.android.exoplayer.upstream.BufferPool; import com.google.android.exoplayer.upstream.DataSource; -import com.google.android.exoplayer.upstream.HttpDataSource; +import com.google.android.exoplayer.upstream.UriDataSource; import com.google.android.exoplayer.util.ManifestFetcher; import com.google.android.exoplayer.util.ManifestFetcher.ManifestCallback; @@ -44,39 +45,36 @@ import java.util.Collections; */ public class HlsRendererBuilder implements RendererBuilder, ManifestCallback { - public static final int TYPE_MASTER = 0; - public static final int TYPE_MEDIA = 1; - private static final int BUFFER_SEGMENT_SIZE = 64 * 1024; private static final int VIDEO_BUFFER_SEGMENTS = 200; private final String userAgent; private final String url; private final String contentId; - private final int playlistType; + private final int contentType; private DemoPlayer player; private RendererBuilderCallback callback; - public HlsRendererBuilder(String userAgent, String url, String contentId, int playlistType) { + public HlsRendererBuilder(String userAgent, String url, String contentId, int contentType) { this.userAgent = userAgent; this.url = url; this.contentId = contentId; - this.playlistType = playlistType; + this.contentType = contentType; } @Override public void buildRenderers(DemoPlayer player, RendererBuilderCallback callback) { this.player = player; this.callback = callback; - switch (playlistType) { - case TYPE_MASTER: + switch (contentType) { + case DemoUtil.TYPE_HLS_MASTER: HlsMasterPlaylistParser parser = new HlsMasterPlaylistParser(); ManifestFetcher mediaPlaylistFetcher = new ManifestFetcher(parser, contentId, url); mediaPlaylistFetcher.singleLoad(player.getMainHandler().getLooper(), this); break; - case TYPE_MEDIA: + case DemoUtil.TYPE_HLS_MEDIA: onManifest(contentId, newSimpleMasterPlaylist(url)); break; } @@ -91,7 +89,7 @@ public class HlsRendererBuilder implements RendererBuilder, ManifestCallback { + + private static final int BUFFER_SEGMENT_SIZE = 64 * 1024; + private static final int VIDEO_BUFFER_SEGMENTS = 200; + + private final SimplePlayerActivity playerActivity; + private final String userAgent; + private final String url; + private final String contentId; + private final int playlistType; + + private RendererBuilderCallback callback; + + public HlsRendererBuilder(SimplePlayerActivity playerActivity, String userAgent, String url, + String contentId, int playlistType) { + this.playerActivity = playerActivity; + this.userAgent = userAgent; + this.url = url; + this.contentId = contentId; + this.playlistType = playlistType; + } + + @Override + public void buildRenderers(RendererBuilderCallback callback) { + this.callback = callback; + switch (playlistType) { + case DemoUtil.TYPE_HLS_MASTER: + HlsMasterPlaylistParser parser = new HlsMasterPlaylistParser(); + ManifestFetcher mediaPlaylistFetcher = + new ManifestFetcher(parser, contentId, url); + mediaPlaylistFetcher.singleLoad(playerActivity.getMainLooper(), this); + break; + case DemoUtil.TYPE_HLS_MEDIA: + onManifest(contentId, newSimpleMasterPlaylist(url)); + break; + } + } + + @Override + public void onManifestError(String contentId, IOException e) { + callback.onRenderersError(e); + } + + @Override + public void onManifest(String contentId, HlsMasterPlaylist manifest) { + LoadControl loadControl = new DefaultLoadControl(new BufferPool(BUFFER_SEGMENT_SIZE)); + + DataSource dataSource = new UriDataSource(userAgent, null); + HlsChunkSource chunkSource = new HlsChunkSource(dataSource, manifest); + HlsSampleSource sampleSource = new HlsSampleSource(chunkSource, loadControl, + VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true, 2); + MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(sampleSource, + MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 0, playerActivity.getMainHandler(), + playerActivity, 50); + MediaCodecAudioTrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer(sampleSource); + + TrackRenderer[] renderers = new TrackRenderer[DemoPlayer.RENDERER_COUNT]; + renderers[DemoPlayer.TYPE_VIDEO] = videoRenderer; + renderers[DemoPlayer.TYPE_AUDIO] = audioRenderer; + callback.onRenderers(videoRenderer, audioRenderer); + } + + private HlsMasterPlaylist newSimpleMasterPlaylist(String mediaPlaylistUrl) { + return new HlsMasterPlaylist(Uri.parse(""), + Collections.singletonList(new Variant(mediaPlaylistUrl, 0))); + } + +} diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/simple/SimplePlayerActivity.java b/demo/src/main/java/com/google/android/exoplayer/demo/simple/SimplePlayerActivity.java index 73d2605c94..03dabcbee7 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/simple/SimplePlayerActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/simple/SimplePlayerActivity.java @@ -61,10 +61,6 @@ public class SimplePlayerActivity extends Activity implements SurfaceHolder.Call private static final String TAG = "PlayerActivity"; - public static final int TYPE_DASH_VOD = 0; - public static final int TYPE_SS_VOD = 1; - public static final int TYPE_OTHER = 2; - private MediaController mediaController; private Handler mainHandler; private View shutterView; @@ -90,7 +86,7 @@ public class SimplePlayerActivity extends Activity implements SurfaceHolder.Call Intent intent = getIntent(); contentUri = intent.getData(); - contentType = intent.getIntExtra(DemoUtil.CONTENT_TYPE_EXTRA, TYPE_OTHER); + contentType = intent.getIntExtra(DemoUtil.CONTENT_TYPE_EXTRA, DemoUtil.TYPE_OTHER); contentId = intent.getStringExtra(DemoUtil.CONTENT_ID_EXTRA); mainHandler = new Handler(getMainLooper()); @@ -163,11 +159,15 @@ public class SimplePlayerActivity extends Activity implements SurfaceHolder.Call private RendererBuilder getRendererBuilder() { String userAgent = DemoUtil.getUserAgent(this); switch (contentType) { - case TYPE_SS_VOD: + case DemoUtil.TYPE_SS: return new SmoothStreamingRendererBuilder(this, userAgent, contentUri.toString(), contentId); - case TYPE_DASH_VOD: + case DemoUtil.TYPE_DASH_VOD: return new DashVodRendererBuilder(this, userAgent, contentUri.toString(), contentId); + case DemoUtil.TYPE_HLS_MASTER: + case DemoUtil.TYPE_HLS_MEDIA: + return new HlsRendererBuilder(this, userAgent, contentUri.toString(), contentId, + contentType); default: return new DefaultRendererBuilder(this, contentUri); } From d3a05c9a44cde42097a55058e2906e2395b636a6 Mon Sep 17 00:00:00 2001 From: Andrey Udovenko Date: Tue, 28 Oct 2014 13:24:12 -0400 Subject: [PATCH 06/55] Add ID3 Timed Metadata support for HLS #67 --- .../demo/full/FullPlayerActivity.java | 19 +- .../demo/full/player/DemoPlayer.java | 29 ++- .../demo/full/player/HlsRendererBuilder.java | 6 + .../google/android/exoplayer/MediaFormat.java | 6 + .../android/exoplayer/metadata/Id3Parser.java | 156 +++++++++++++ .../android/exoplayer/metadata/Metadata.java | 31 +++ .../exoplayer/metadata/MetadataParser.java | 45 ++++ .../metadata/MetadataTrackRenderer.java | 211 ++++++++++++++++++ .../exoplayer/parser/ts/BitsArray.java | 24 ++ .../exoplayer/parser/ts/TsExtractor.java | 23 +- .../android/exoplayer/util/MimeTypes.java | 1 + 11 files changed, 546 insertions(+), 5 deletions(-) create mode 100644 library/src/main/java/com/google/android/exoplayer/metadata/Id3Parser.java create mode 100644 library/src/main/java/com/google/android/exoplayer/metadata/Metadata.java create mode 100644 library/src/main/java/com/google/android/exoplayer/metadata/MetadataParser.java create mode 100644 library/src/main/java/com/google/android/exoplayer/metadata/MetadataTrackRenderer.java diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/FullPlayerActivity.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/FullPlayerActivity.java index a64ceead46..63618952fd 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/FullPlayerActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/FullPlayerActivity.java @@ -25,6 +25,7 @@ import com.google.android.exoplayer.demo.full.player.DemoPlayer; import com.google.android.exoplayer.demo.full.player.DemoPlayer.RendererBuilder; import com.google.android.exoplayer.demo.full.player.HlsRendererBuilder; import com.google.android.exoplayer.demo.full.player.SmoothStreamingRendererBuilder; +import com.google.android.exoplayer.metadata.Metadata; import com.google.android.exoplayer.text.CaptionStyleCompat; import com.google.android.exoplayer.text.SubtitleView; import com.google.android.exoplayer.util.Util; @@ -38,6 +39,7 @@ import android.graphics.Point; import android.net.Uri; import android.os.Bundle; import android.text.TextUtils; +import android.util.Log; import android.view.Display; import android.view.Menu; import android.view.MenuItem; @@ -54,11 +56,15 @@ import android.widget.PopupMenu; import android.widget.PopupMenu.OnMenuItemClickListener; import android.widget.TextView; +import java.util.List; + /** * An activity that plays media using {@link DemoPlayer}. */ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callback, OnClickListener, - DemoPlayer.Listener, DemoPlayer.TextListener { + DemoPlayer.Listener, DemoPlayer.TextListener, DemoPlayer.MetadataListener { + + private static final String TAG = "FullPlayerActivity"; private static final float CAPTION_LINE_HEIGHT_RATIO = 0.0533f; private static final int MENU_GROUP_TRACKS = 1; @@ -187,6 +193,7 @@ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callba player = new DemoPlayer(getRendererBuilder()); player.addListener(this); player.setTextListener(this); + player.setMetadataListener(this); player.seekTo(playerPosition); playerNeedsPrepare = true; mediaController.setMediaPlayer(player.getPlayerControl()); @@ -403,6 +410,16 @@ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callba } } + // DemoPlayer.MetadataListener implementation + + @Override + public void onMetadata(List metadata) { + for (int i = 0; i < metadata.size(); i++) { + Metadata next = metadata.get(i); + Log.i(TAG, "ID3 TimedMetadata: key=" + next.key + ", value=" + next.value); + } + } + // SurfaceHolder.Callback implementation @Override diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DemoPlayer.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DemoPlayer.java index b88ce89157..b6a68bc98f 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DemoPlayer.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DemoPlayer.java @@ -26,6 +26,8 @@ import com.google.android.exoplayer.TrackRenderer; import com.google.android.exoplayer.chunk.ChunkSampleSource; import com.google.android.exoplayer.chunk.MultiTrackChunkSource; import com.google.android.exoplayer.drm.StreamingDrmSessionManager; +import com.google.android.exoplayer.metadata.Metadata; +import com.google.android.exoplayer.metadata.MetadataTrackRenderer; import com.google.android.exoplayer.text.TextTrackRenderer; import com.google.android.exoplayer.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer.util.PlayerControl; @@ -36,6 +38,7 @@ import android.os.Looper; import android.view.Surface; import java.io.IOException; +import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; /** @@ -46,7 +49,7 @@ import java.util.concurrent.CopyOnWriteArrayList; public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventListener, DefaultBandwidthMeter.EventListener, MediaCodecVideoTrackRenderer.EventListener, MediaCodecAudioTrackRenderer.EventListener, TextTrackRenderer.TextRenderer, - StreamingDrmSessionManager.EventListener { + MetadataTrackRenderer.MetadataRenderer, StreamingDrmSessionManager.EventListener { /** * Builds renderers for the player. @@ -134,6 +137,13 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi void onText(String text); } + /** + * A listener for receiving metadata parsed from the media stream. + */ + public interface MetadataListener { + void onMetadata(List metadata); + } + // Constants pulled into this class for convenience. public static final int STATE_IDLE = ExoPlayer.STATE_IDLE; public static final int STATE_PREPARING = ExoPlayer.STATE_PREPARING; @@ -144,11 +154,12 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi public static final int DISABLED_TRACK = -1; public static final int PRIMARY_TRACK = 0; - public static final int RENDERER_COUNT = 4; + public static final int RENDERER_COUNT = 5; public static final int TYPE_VIDEO = 0; public static final int TYPE_AUDIO = 1; public static final int TYPE_TEXT = 2; - public static final int TYPE_DEBUG = 3; + public static final int TYPE_TIMED_METADATA = 3; + public static final int TYPE_DEBUG = 4; private static final int RENDERER_BUILDING_STATE_IDLE = 1; private static final int RENDERER_BUILDING_STATE_BUILDING = 2; @@ -173,6 +184,7 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi private int[] selectedTracks; private TextListener textListener; + private MetadataListener metadataListener; private InternalErrorListener internalErrorListener; private InfoListener infoListener; @@ -214,6 +226,10 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi textListener = listener; } + public void setMetadataListener(MetadataListener listener) { + metadataListener = listener; + } + public void setSurface(Surface surface) { this.surface = surface; pushSurfaceAndVideoTrack(false); @@ -458,6 +474,13 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi } } + @Override + public void onMetadata(List metadata) { + if (metadataListener != null) { + metadataListener.onMetadata(metadata); + } + } + @Override public void onPlayWhenReadyCommitted() { // Do nothing. diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/HlsRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/HlsRendererBuilder.java index 2d7383dbf3..dd85f933c8 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/HlsRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/HlsRendererBuilder.java @@ -28,6 +28,8 @@ import com.google.android.exoplayer.hls.HlsMasterPlaylist; import com.google.android.exoplayer.hls.HlsMasterPlaylist.Variant; import com.google.android.exoplayer.hls.HlsMasterPlaylistParser; import com.google.android.exoplayer.hls.HlsSampleSource; +import com.google.android.exoplayer.metadata.Id3Parser; +import com.google.android.exoplayer.metadata.MetadataTrackRenderer; import com.google.android.exoplayer.upstream.BufferPool; import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.UriDataSource; @@ -97,9 +99,13 @@ public class HlsRendererBuilder implements RendererBuilder, ManifestCallback parse(byte[] data, int size) + throws UnsupportedEncodingException, ParserException { + BitsArray id3Buffer = new BitsArray(data, size); + int id3Size = parseId3Header(id3Buffer); + + List metadata = new ArrayList(); + + while (id3Size > 0) { + int frameId0 = id3Buffer.readUnsignedByte(); + int frameId1 = id3Buffer.readUnsignedByte(); + int frameId2 = id3Buffer.readUnsignedByte(); + int frameId3 = id3Buffer.readUnsignedByte(); + + int frameSize = id3Buffer.readSynchSafeInt(); + if (frameSize <= 1) { + break; + } + + id3Buffer.skipBytes(2); // Skip frame flags. + + // Check Frame ID == TXXX. + if (frameId0 == 'T' && frameId1 == 'X' && frameId2 == 'X' && frameId3 == 'X') { + int encoding = id3Buffer.readUnsignedByte(); + String charset = getCharsetName(encoding); + byte[] frame = new byte[frameSize - 1]; + id3Buffer.readBytes(frame, 0, frameSize - 1); + + int firstZeroIndex = indexOf(frame, 0, (byte) 0); + String key = new String(frame, 0, firstZeroIndex, charset); + int valueStartIndex = indexOfNot(frame, firstZeroIndex, (byte) 0); + int valueEndIndex = indexOf(frame, valueStartIndex, (byte) 0); + String value = new String(frame, valueStartIndex, valueEndIndex - valueStartIndex, + charset); + metadata.add(new Metadata(key, value)); + } else { + id3Buffer.skipBytes(frameSize); + } + + id3Size -= frameSize + 10 /* header size */; + } + + return Collections.unmodifiableList(metadata); + } + + private static int indexOf(byte[] data, int fromIndex, byte key) { + for (int i = fromIndex; i < data.length; i++) { + if (data[i] == key) { + return i; + } + } + return data.length; + } + + private static int indexOfNot(byte[] data, int fromIndex, byte key) { + for (int i = fromIndex; i < data.length; i++) { + if (data[i] != key) { + return i; + } + } + return data.length; + } + + /** + * Parses ID3 header. + * @param id3Buffer A {@link BitsArray} with raw ID3 data. + * @return The size of data that contains ID3 frames without header and footer. + * @throws ParserException If ID3 file identifier != "ID3". + */ + private static int parseId3Header(BitsArray id3Buffer) throws ParserException { + int id1 = id3Buffer.readUnsignedByte(); + int id2 = id3Buffer.readUnsignedByte(); + int id3 = id3Buffer.readUnsignedByte(); + if (id1 != 'I' || id2 != 'D' || id3 != '3') { + throw new ParserException(String.format( + "Unexpected ID3 file identifier, expected \"ID3\", actual \"%c%c%c\".", id1, id2, id3)); + } + id3Buffer.skipBytes(2); // Skip version. + + int flags = id3Buffer.readUnsignedByte(); + int id3Size = id3Buffer.readSynchSafeInt(); + + // Check if extended header presents. + if ((flags & 0x2) != 0) { + int extendedHeaderSize = id3Buffer.readSynchSafeInt(); + if (extendedHeaderSize > 4) { + id3Buffer.skipBytes(extendedHeaderSize - 4); + } + id3Size -= extendedHeaderSize; + } + + // Check if footer presents. + if ((flags & 0x8) != 0) { + id3Size -= 10; + } + + return id3Size; + } + + /** + * Maps encoding byte from ID3v2 frame to a Charset. + * @param encodingByte The value of encoding byte from ID3v2 frame. + * @return Charset name. + */ + private static String getCharsetName(int encodingByte) { + switch (encodingByte) { + case 0: + return "ISO-8859-1"; + case 1: + return "UTF-16"; + case 2: + return "UTF-16BE"; + case 3: + return "UTF-8"; + default: + return "ISO-8859-1"; + } + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/metadata/Metadata.java b/library/src/main/java/com/google/android/exoplayer/metadata/Metadata.java new file mode 100644 index 0000000000..d89b02f9bd --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/metadata/Metadata.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer.metadata; + +/** + * A metadata that contains textual data associated with time indices. + */ +public class Metadata { + + public final String key; + public final String value; + + public Metadata(String key, String value) { + this.key = key; + this.value = value; + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/metadata/MetadataParser.java b/library/src/main/java/com/google/android/exoplayer/metadata/MetadataParser.java new file mode 100644 index 0000000000..46d2b1179a --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/metadata/MetadataParser.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer.metadata; + +import java.io.IOException; +import java.util.List; + +/** + * Parses {@link Metadata}s from binary data. + */ +public interface MetadataParser { + + /** + * Checks whether the parser supports a given mime type. + * + * @param mimeType A subtitle mime type. + * @return Whether the mime type is supported. + */ + public boolean canParse(String mimeType); + + /** + * Parses a list of {@link Metadata} objects from the provided binary data. + * + * @param data The raw binary data from which to parse the metadata. + * @param size The size of the input data. + * @return A parsed {@link List} of {@link Metadata} objects. + * @throws IOException If a problem occurred parsing the data. + */ + public List parse(byte[] data, int size) + throws IOException; + +} diff --git a/library/src/main/java/com/google/android/exoplayer/metadata/MetadataTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/metadata/MetadataTrackRenderer.java new file mode 100644 index 0000000000..5d5ad0c50d --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/metadata/MetadataTrackRenderer.java @@ -0,0 +1,211 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer.metadata; + +import com.google.android.exoplayer.ExoPlaybackException; +import com.google.android.exoplayer.MediaFormatHolder; +import com.google.android.exoplayer.SampleHolder; +import com.google.android.exoplayer.SampleSource; +import com.google.android.exoplayer.TrackRenderer; +import com.google.android.exoplayer.util.Assertions; + +import android.os.Handler; +import android.os.Handler.Callback; +import android.os.Looper; +import android.os.Message; + +import java.io.IOException; +import java.util.List; + +/** + * A {@link TrackRenderer} for metadata embedded in a media stream. + */ +public class MetadataTrackRenderer extends TrackRenderer implements Callback { + + /** + * An interface for components that process metadata. + */ + public interface MetadataRenderer { + + /** + * Invoked each time there is a metadata associated with current playback time. + * + * @param metadata The metadata to process. + */ + void onMetadata(List metadata); + + } + + private static final int MSG_INVOKE_RENDERER = 0; + + private final SampleSource source; + private final MetadataParser metadataParser; + private final MetadataRenderer metadataRenderer; + private final Handler metadataHandler; + private final MediaFormatHolder formatHolder; + private final SampleHolder sampleHolder; + + private int trackIndex; + private long currentPositionUs; + private boolean inputStreamEnded; + + private long pendingMetadataTimestamp; + private List pendingMetadata; + + /** + * @param source A source from which samples containing metadata can be read. + * @param metadataParser A parser for parsing the metadata. + * @param metadataRenderer The metadata renderer to receive the parsed metadata. + * @param metadataRendererLooper The looper associated with the thread on which metadataRenderer + * should be invoked. If the renderer makes use of standard Android UI components, then this + * should normally be the looper associated with the applications' main thread, which can be + * obtained using {@link android.app.Activity#getMainLooper()}. Null may be passed if the + * renderer should be invoked directly on the player's internal rendering thread. + */ + public MetadataTrackRenderer(SampleSource source, MetadataParser metadataParser, + MetadataRenderer metadataRenderer, Looper metadataRendererLooper) { + this.source = Assertions.checkNotNull(source); + this.metadataParser = Assertions.checkNotNull(metadataParser); + this.metadataRenderer = Assertions.checkNotNull(metadataRenderer); + this.metadataHandler = metadataRendererLooper == null ? null + : new Handler(metadataRendererLooper, this); + formatHolder = new MediaFormatHolder(); + sampleHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_NORMAL); + } + + @Override + protected int doPrepare() throws ExoPlaybackException { + try { + boolean sourcePrepared = source.prepare(); + if (!sourcePrepared) { + return TrackRenderer.STATE_UNPREPARED; + } + } catch (IOException e) { + throw new ExoPlaybackException(e); + } + for (int i = 0; i < source.getTrackCount(); i++) { + if (metadataParser.canParse(source.getTrackInfo(i).mimeType)) { + trackIndex = i; + return TrackRenderer.STATE_PREPARED; + } + } + return TrackRenderer.STATE_IGNORE; + } + + @Override + protected void onEnabled(long positionUs, boolean joining) { + source.enable(trackIndex, positionUs); + seekToInternal(positionUs); + } + + @Override + protected void seekTo(long positionUs) throws ExoPlaybackException { + source.seekToUs(positionUs); + seekToInternal(positionUs); + } + + private void seekToInternal(long positionUs) { + currentPositionUs = positionUs; + pendingMetadata = null; + inputStreamEnded = false; + } + + @Override + protected void doSomeWork(long positionUs, long elapsedRealtimeUs) + throws ExoPlaybackException { + currentPositionUs = positionUs; + try { + source.continueBuffering(positionUs); + } catch (IOException e) { + throw new ExoPlaybackException(e); + } + + if (!inputStreamEnded && pendingMetadata == null) { + try { + int result = source.readData(trackIndex, positionUs, formatHolder, sampleHolder, false); + if (result == SampleSource.SAMPLE_READ) { + pendingMetadataTimestamp = sampleHolder.timeUs; + pendingMetadata = metadataParser.parse(sampleHolder.data.array(), sampleHolder.size); + sampleHolder.data.clear(); + } else if (result == SampleSource.END_OF_STREAM) { + inputStreamEnded = true; + } + } catch (IOException e) { + throw new ExoPlaybackException(e); + } + } + + if (pendingMetadata != null && pendingMetadataTimestamp <= currentPositionUs) { + invokeRenderer(pendingMetadata); + pendingMetadata = null; + } + } + + @Override + protected void onDisabled() { + pendingMetadata = null; + source.disable(trackIndex); + } + + @Override + protected long getDurationUs() { + return source.getTrackInfo(trackIndex).durationUs; + } + + @Override + protected long getCurrentPositionUs() { + return currentPositionUs; + } + + @Override + protected long getBufferedPositionUs() { + return TrackRenderer.END_OF_TRACK_US; + } + + @Override + protected boolean isEnded() { + return inputStreamEnded; + } + + @Override + protected boolean isReady() { + return true; + } + + private void invokeRenderer(List metadata) { + if (metadataHandler != null) { + metadataHandler.obtainMessage(MSG_INVOKE_RENDERER, metadata).sendToTarget(); + } else { + invokeRendererInternal(metadata); + } + } + + @SuppressWarnings("unchecked") + @Override + public boolean handleMessage(Message msg) { + switch (msg.what) { + case MSG_INVOKE_RENDERER: + invokeRendererInternal((List) msg.obj); + return true; + } + return false; + } + + private void invokeRendererInternal(List metadata) { + metadataRenderer.onMetadata(metadata); + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/parser/ts/BitsArray.java b/library/src/main/java/com/google/android/exoplayer/parser/ts/BitsArray.java index 6e51eceac3..862305f4b3 100644 --- a/library/src/main/java/com/google/android/exoplayer/parser/ts/BitsArray.java +++ b/library/src/main/java/com/google/android/exoplayer/parser/ts/BitsArray.java @@ -33,6 +33,14 @@ public final class BitsArray { private int byteOffset; private int bitOffset; + public BitsArray() { + } + + public BitsArray(byte[] data, int limit) { + this.data = data; + this.limit = limit; + } + /** * Resets the state. */ @@ -240,6 +248,22 @@ public final class BitsArray { return limit == 0; } + /** + * Reads a Synchsafe integer. + * Synchsafe integers are integers that keep the highest bit of every byte zeroed. + * A 32 bit synchsafe integer can store 28 bits of information. + * + * @return The value of the parsed Synchsafe integer. + */ + public int readSynchSafeInt() { + int b1 = readUnsignedByte(); + int b2 = readUnsignedByte(); + int b3 = readUnsignedByte(); + int b4 = readUnsignedByte(); + + return (b1 << 21) | (b2 << 14) | (b3 << 7) | b4; + } + // TODO: Find a better place for this method. /** * Finds the next Adts sync word. diff --git a/library/src/main/java/com/google/android/exoplayer/parser/ts/TsExtractor.java b/library/src/main/java/com/google/android/exoplayer/parser/ts/TsExtractor.java index fd5ba369b9..8b4099ef77 100644 --- a/library/src/main/java/com/google/android/exoplayer/parser/ts/TsExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer/parser/ts/TsExtractor.java @@ -58,6 +58,7 @@ public final class TsExtractor { private static final int TS_STREAM_TYPE_AAC = 0x0F; private static final int TS_STREAM_TYPE_H264 = 0x1B; + private static final int TS_STREAM_TYPE_ID3 = 0x15; private static final int DEFAULT_BUFFER_SEGMENT_SIZE = 64 * 1024; @@ -345,6 +346,9 @@ public final class TsExtractor { case TS_STREAM_TYPE_H264: pesPayloadReader = new H264Reader(); break; + case TS_STREAM_TYPE_ID3: + pesPayloadReader = new Id3Reader(); + break; } if (pesPayloadReader != null) { @@ -689,8 +693,25 @@ public final class TsExtractor { } /** - * Simplified version of SampleHolder for internal buffering. + * Parses ID3 data and extracts individual text information frames. */ + private class Id3Reader extends PesPayloadReader { + + public Id3Reader() { + setMediaFormat(MediaFormat.createId3Format()); + } + + @SuppressLint("InlinedApi") + @Override + public void read(BitsArray pesBuffer, int pesPayloadSize, long pesTimeUs) { + addSample(pesBuffer, pesPayloadSize, pesTimeUs, MediaExtractor.SAMPLE_FLAG_SYNC); + } + + } + + /** + * Simplified version of SampleHolder for internal buffering. + */ private static class Sample { public byte[] data; diff --git a/library/src/main/java/com/google/android/exoplayer/util/MimeTypes.java b/library/src/main/java/com/google/android/exoplayer/util/MimeTypes.java index e3443467f3..55a59d1867 100644 --- a/library/src/main/java/com/google/android/exoplayer/util/MimeTypes.java +++ b/library/src/main/java/com/google/android/exoplayer/util/MimeTypes.java @@ -37,6 +37,7 @@ public class MimeTypes { public static final String TEXT_VTT = BASE_TYPE_TEXT + "/vtt"; + public static final String APPLICATION_ID3 = BASE_TYPE_APPLICATION + "/id3"; public static final String APPLICATION_TTML = BASE_TYPE_APPLICATION + "/ttml+xml"; private MimeTypes() {} From 2422912be841003e2c8f5fd9f657304f1565341b Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Tue, 28 Oct 2014 19:25:10 +0000 Subject: [PATCH 07/55] Refactor HLS support. - The HlsSampleSource now owns the extractor. TsChunk is more or less dumb. The previous model was weird, because you'd end up "reading" samples from TsChunk objects that were actually parsed from the previous chunk (due to the way the extractor was shared and maintained internal queues). - Split out consuming and reading in the extractor. - Make it so we consume 5s ahead. This is a window we allow for uneven interleaving, whilst preventing huge read-ahead (e.g. in the case of sparse ID3 samples). - Avoid flushing the extractor for a discontinuity until it has been fully drained of previously parsed samples. This avoids skipping media shortly before discontinuities. - Also made start-up faster by avoiding double-loading the first segment. Issue: #3 --- .../android/exoplayer/hls/HlsChunkSource.java | 27 +++- .../exoplayer/hls/HlsSampleSource.java | 152 ++++++++---------- .../google/android/exoplayer/hls/TsChunk.java | 74 ++------- .../exoplayer/parser/ts/TsExtractor.java | 140 +++++++++------- 4 files changed, 183 insertions(+), 210 deletions(-) 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 88f2791080..14e9961122 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,7 +18,6 @@ package com.google.android.exoplayer.hls; import com.google.android.exoplayer.C; import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.TrackRenderer; -import com.google.android.exoplayer.parser.ts.TsExtractor; import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DataSpec; import com.google.android.exoplayer.upstream.NonBlockingInputStream; @@ -40,10 +39,10 @@ import java.util.List; public class HlsChunkSource { private final DataSource dataSource; - private final TsExtractor extractor; private final HlsMasterPlaylist masterPlaylist; private final HlsMediaPlaylistParser mediaPlaylistParser; + private long liveStartTimeUs; /* package */ HlsMediaPlaylist mediaPlaylist; /* package */ boolean mediaPlaylistWasLive; /* package */ long lastMediaPlaylistLoadTimeMs; @@ -52,7 +51,6 @@ public class HlsChunkSource { public HlsChunkSource(DataSource dataSource, HlsMasterPlaylist masterPlaylist) { this.dataSource = dataSource; this.masterPlaylist = masterPlaylist; - extractor = new TsExtractor(); mediaPlaylistParser = new HlsMediaPlaylistParser(); } @@ -120,6 +118,7 @@ public class HlsChunkSource { } } } else { + // Not live. if (queue.isEmpty()) { chunkMediaSequence = Util.binarySearchFloor(mediaPlaylist.segments, seekPositionUs, true, true) + mediaPlaylist.mediaSequence; @@ -151,14 +150,26 @@ public class HlsChunkSource { long startTimeUs = segment.startTimeUs; long endTimeUs = startTimeUs + (long) (segment.durationSecs * 1000000); - int nextChunkMediaSequence = chunkMediaSequence + 1; - if (!mediaPlaylist.live && chunkIndex == mediaPlaylist.segments.size() - 1) { - nextChunkMediaSequence = -1; + + if (mediaPlaylistWasLive) { + if (queue.isEmpty()) { + liveStartTimeUs = startTimeUs; + startTimeUs = 0; + endTimeUs -= liveStartTimeUs; + } else { + startTimeUs -= liveStartTimeUs; + endTimeUs -= liveStartTimeUs; + } + } else { + // Not live. + if (chunkIndex == mediaPlaylist.segments.size() - 1) { + nextChunkMediaSequence = -1; + } } - out.chunk = new TsChunk(dataSource, dataSpec, 0, extractor, startTimeUs, endTimeUs, - nextChunkMediaSequence, segment.discontinuity); + out.chunk = new TsChunk(dataSource, dataSpec, 0, startTimeUs, endTimeUs, nextChunkMediaSequence, + segment.discontinuity); } private boolean shouldRerequestMediaPlaylist() { 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 c829d169cb..9a289f9209 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 @@ -23,8 +23,10 @@ import com.google.android.exoplayer.SampleHolder; import com.google.android.exoplayer.SampleSource; import com.google.android.exoplayer.TrackInfo; import com.google.android.exoplayer.TrackRenderer; +import com.google.android.exoplayer.parser.ts.TsExtractor; import com.google.android.exoplayer.upstream.Loader; import com.google.android.exoplayer.upstream.Loader.Loadable; +import com.google.android.exoplayer.upstream.NonBlockingInputStream; import com.google.android.exoplayer.util.Assertions; import android.os.SystemClock; @@ -43,8 +45,10 @@ import java.util.List; */ public class HlsSampleSource implements SampleSource, Loader.Callback { + private static final long MAX_SAMPLE_INTERLEAVING_OFFSET_US = 5000000; private static final int NO_RESET_PENDING = -1; + private final TsExtractor extractor; private final LoadControl loadControl; private final HlsChunkSource chunkSource; private final HlsChunkOperationHolder currentLoadableHolder; @@ -73,9 +77,6 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { private int currentLoadableExceptionCount; private long currentLoadableExceptionTimestamp; - private boolean pendingTimestampOffsetUpdate; - private long timestampOffsetUs; - public HlsSampleSource(HlsChunkSource chunkSource, LoadControl loadControl, int bufferSizeContribution, boolean frameAccurateSeeking, int downstreamRendererCount) { this.chunkSource = chunkSource; @@ -83,6 +84,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { this.bufferSizeContribution = bufferSizeContribution; this.frameAccurateSeeking = frameAccurateSeeking; this.remainingReleaseCount = downstreamRendererCount; + extractor = new TsExtractor(); currentLoadableHolder = new HlsChunkOperationHolder(); mediaChunks = new LinkedList(); readOnlyHlsChunks = Collections.unmodifiableList(mediaChunks); @@ -93,32 +95,23 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { if (prepared) { return true; } - if (loader == null) { loader = new Loader("Loader:HLS"); loadControl.register(this, bufferSizeContribution); } - updateLoadControl(); - if (mediaChunks.isEmpty()) { - return false; - } - TsChunk mediaChunk = mediaChunks.getFirst(); - if (mediaChunk.prepare()) { - trackCount = mediaChunk.getTrackCount(); + continueBufferingInternal(); + if (extractor.isPrepared()) { + trackCount = extractor.getTrackCount(); trackEnabledStates = new boolean[trackCount]; pendingDiscontinuities = new boolean[trackCount]; downstreamMediaFormats = new MediaFormat[trackCount]; trackInfos = new TrackInfo[trackCount]; for (int i = 0; i < trackCount; i++) { - MediaFormat format = mediaChunk.getMediaFormat(i); + MediaFormat format = extractor.getFormat(i); trackInfos[i] = new TrackInfo(format.mimeType, chunkSource.getDurationUs()); } prepared = true; } - - if (!prepared && currentLoadableException != null) { - throw currentLoadableException; - } return prepared; } @@ -142,9 +135,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { trackEnabledStates[track] = true; downstreamMediaFormats[track] = null; if (enabledTrackCount == 1) { - downstreamPositionUs = positionUs; - lastSeekPositionUs = positionUs; - restartFrom(positionUs); + seekToUs(positionUs); } } @@ -170,72 +161,69 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { Assertions.checkState(prepared); Assertions.checkState(enabledTrackCount > 0); downstreamPositionUs = playbackPositionUs; + return continueBufferingInternal(); + } + + private boolean continueBufferingInternal() throws IOException { updateLoadControl(); - if (isPendingReset() || mediaChunks.isEmpty()) { + if (isPendingReset()) { return false; - } else if (mediaChunks.getFirst().sampleAvailable()) { - // There's a sample available to be read from the current chunk. - return true; - } else { - // It may be the case that the current chunk has been fully read but not yet discarded and - // that the next chunk has an available sample. Return true if so, otherwise false. - return mediaChunks.size() > 1 && mediaChunks.get(1).sampleAvailable(); } + + TsChunk mediaChunk = mediaChunks.getFirst(); + if (mediaChunk.isReadFinished() && mediaChunks.size() > 1) { + discardDownstreamHlsChunk(); + mediaChunk = mediaChunks.getFirst(); + } + + boolean haveSufficientSamples = false; + if (mediaChunk.hasPendingDiscontinuity()) { + if (extractor.hasSamples()) { + // There are samples from before the discontinuity yet to be read from the extractor, so + // we don't want to reset the extractor yet. + haveSufficientSamples = true; + } else { + extractor.reset(mediaChunk.startTimeUs); + for (int i = 0; i < pendingDiscontinuities.length; i++) { + pendingDiscontinuities[i] = true; + } + mediaChunk.clearPendingDiscontinuity(); + } + } + + if (!mediaChunk.hasPendingDiscontinuity()) { + // Allow the extractor to consume from the current chunk. + NonBlockingInputStream inputStream = mediaChunk.getNonBlockingInputStream(); + haveSufficientSamples = extractor.consumeUntil(inputStream, + downstreamPositionUs + MAX_SAMPLE_INTERLEAVING_OFFSET_US); + // If we can't read any more, then we always say we have sufficient samples. + if (!haveSufficientSamples) { + haveSufficientSamples = mediaChunk.isLastChunk() && mediaChunk.isReadFinished(); + } + } + + if (!haveSufficientSamples && currentLoadableException != null) { + throw currentLoadableException; + } + return haveSufficientSamples; } @Override public int readData(int track, long playbackPositionUs, MediaFormatHolder formatHolder, - SampleHolder sampleHolder, boolean onlyReadDiscontinuity) throws IOException { + SampleHolder sampleHolder, boolean onlyReadDiscontinuity) { Assertions.checkState(prepared); + downstreamPositionUs = playbackPositionUs; if (pendingDiscontinuities[track]) { pendingDiscontinuities[track] = false; return DISCONTINUITY_READ; } - if (onlyReadDiscontinuity) { + if (onlyReadDiscontinuity || isPendingReset() || !extractor.isPrepared()) { return NOTHING_READ; } - downstreamPositionUs = playbackPositionUs; - if (isPendingReset()) { - if (currentLoadableException != null) { - throw currentLoadableException; - } - return NOTHING_READ; - } - - TsChunk mediaChunk = mediaChunks.getFirst(); - - if (mediaChunk.readDiscontinuity()) { - pendingTimestampOffsetUpdate = true; - for (int i = 0; i < pendingDiscontinuities.length; i++) { - pendingDiscontinuities[i] = true; - } - pendingDiscontinuities[track] = false; - return DISCONTINUITY_READ; - } - - if (mediaChunk.isReadFinished()) { - // We've read all of the samples from the current media chunk. - if (mediaChunks.size() > 1) { - discardDownstreamHlsChunk(); - mediaChunk = mediaChunks.getFirst(); - return readData(track, playbackPositionUs, formatHolder, sampleHolder, false); - } else if (mediaChunk.isLastChunk()) { - return END_OF_STREAM; - } - return NOTHING_READ; - } - - if (!mediaChunk.prepare()) { - if (currentLoadableException != null) { - throw currentLoadableException; - } - return NOTHING_READ; - } - - MediaFormat mediaFormat = mediaChunk.getMediaFormat(track); + MediaFormat mediaFormat = extractor.getFormat(track); if (mediaFormat != null && !mediaFormat.equals(downstreamMediaFormats[track], true)) { chunkSource.getMaxVideoDimensions(mediaFormat); formatHolder.format = mediaFormat; @@ -243,20 +231,17 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { return FORMAT_READ; } - if (mediaChunk.read(track, sampleHolder)) { - if (pendingTimestampOffsetUpdate) { - pendingTimestampOffsetUpdate = false; - timestampOffsetUs = sampleHolder.timeUs - mediaChunk.startTimeUs; - } - sampleHolder.timeUs -= timestampOffsetUs; + if (extractor.getSample(track, sampleHolder)) { sampleHolder.decodeOnly = frameAccurateSeeking && sampleHolder.timeUs < lastSeekPositionUs; return SAMPLE_READ; - } else { - if (currentLoadableException != null) { - throw currentLoadableException; - } - return NOTHING_READ; } + + TsChunk mediaChunk = mediaChunks.getFirst(); + if (mediaChunk.isLastChunk() && mediaChunk.isReadFinished()) { + return END_OF_STREAM; + } + + return NOTHING_READ; } @Override @@ -276,9 +261,9 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { if (mediaChunk == null) { restartFrom(positionUs); } else { - pendingTimestampOffsetUpdate = true; - mediaChunk.reset(); discardDownstreamHlsChunks(mediaChunk); + mediaChunk.reset(); + extractor.reset(mediaChunk.startTimeUs); updateLoadControl(); } } @@ -503,12 +488,11 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { currentLoadable.init(loadControl.getAllocator()); if (isTsChunk(currentLoadable)) { TsChunk mediaChunk = (TsChunk) currentLoadable; + mediaChunks.add(mediaChunk); if (isPendingReset()) { - pendingTimestampOffsetUpdate = true; - mediaChunk.reset(); + extractor.reset(mediaChunk.startTimeUs); pendingResetPositionUs = NO_RESET_PENDING; } - mediaChunks.add(mediaChunk); } loader.startLoading(currentLoadable, this); } diff --git a/library/src/main/java/com/google/android/exoplayer/hls/TsChunk.java b/library/src/main/java/com/google/android/exoplayer/hls/TsChunk.java index 9da8eb7459..ee872bef83 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/TsChunk.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/TsChunk.java @@ -15,12 +15,8 @@ */ package com.google.android.exoplayer.hls; -import com.google.android.exoplayer.MediaFormat; -import com.google.android.exoplayer.SampleHolder; -import com.google.android.exoplayer.parser.ts.TsExtractor; import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DataSpec; -import com.google.android.exoplayer.upstream.NonBlockingInputStream; /** * A MPEG2TS chunk. @@ -44,82 +40,42 @@ public final class TsChunk extends HlsChunk { */ private final boolean discontinuity; - private final TsExtractor extractor; - private boolean pendingDiscontinuity; /** * @param dataSource A {@link DataSource} for loading the data. * @param dataSpec Defines the data to be loaded. - * @param extractor The extractor that will be used to extract the samples. * @param trigger The reason for this chunk being selected. * @param startTimeUs The start time of the media contained by the chunk, in microseconds. * @param endTimeUs The end time of the media contained by the chunk, in microseconds. * @param nextChunkIndex The index of the next chunk, or -1 if this is the last chunk. * @param discontinuity The encoding discontinuity indicator. */ - public TsChunk(DataSource dataSource, DataSpec dataSpec, int trigger, TsExtractor extractor, - long startTimeUs, long endTimeUs, int nextChunkIndex, boolean discontinuity) { + public TsChunk(DataSource dataSource, DataSpec dataSpec, int trigger, long startTimeUs, + long endTimeUs, int nextChunkIndex, boolean discontinuity) { super(dataSource, dataSpec, trigger); this.startTimeUs = startTimeUs; this.endTimeUs = endTimeUs; this.nextChunkIndex = nextChunkIndex; - this.extractor = extractor; this.discontinuity = discontinuity; this.pendingDiscontinuity = discontinuity; } - public boolean readDiscontinuity() { - if (pendingDiscontinuity) { - extractor.reset(); - pendingDiscontinuity = false; - return true; - } - return false; - } - - public boolean prepare() { - return extractor.prepare(getNonBlockingInputStream()); - } - - public int getTrackCount() { - return extractor.getTrackCount(); - } - - public boolean sampleAvailable() { - // TODO: Maybe optimize this to not require looping over the tracks. - if (!prepare()) { - return false; - } - // TODO: Optimize this to not require looping over the tracks. - NonBlockingInputStream inputStream = getNonBlockingInputStream(); - int trackCount = extractor.getTrackCount(); - for (int i = 0; i < trackCount; i++) { - int result = extractor.read(inputStream, i, null); - if ((result & TsExtractor.RESULT_NEED_SAMPLE_HOLDER) != 0) { - return true; - } - } - return false; - } - - public boolean read(int track, SampleHolder holder) { - int result = extractor.read(getNonBlockingInputStream(), track, holder); - return (result & TsExtractor.RESULT_READ_SAMPLE) != 0; - } - - public void reset() { - extractor.reset(); - pendingDiscontinuity = discontinuity; - resetReadPosition(); - } - - public MediaFormat getMediaFormat(int track) { - return extractor.getFormat(track); - } - public boolean isLastChunk() { return nextChunkIndex == -1; } + public void reset() { + resetReadPosition(); + pendingDiscontinuity = discontinuity; + } + + public boolean hasPendingDiscontinuity() { + return pendingDiscontinuity; + } + + public void clearPendingDiscontinuity() { + pendingDiscontinuity = false; + } + } diff --git a/library/src/main/java/com/google/android/exoplayer/parser/ts/TsExtractor.java b/library/src/main/java/com/google/android/exoplayer/parser/ts/TsExtractor.java index 8b4099ef77..29fe07c1bf 100644 --- a/library/src/main/java/com/google/android/exoplayer/parser/ts/TsExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer/parser/ts/TsExtractor.java @@ -37,19 +37,6 @@ import java.util.Queue; */ public final class TsExtractor { - /** - * An attempt to read from the input stream returned insufficient data. - */ - public static final int RESULT_NEED_MORE_DATA = 1; - /** - * A media sample was read. - */ - public static final int RESULT_READ_SAMPLE = 2; - /** - * The next thing to be read is a sample, but a {@link SampleHolder} was not supplied. - */ - public static final int RESULT_NEED_SAMPLE_HOLDER = 4; - private static final String TAG = "TsExtractor"; private static final int TS_PACKET_SIZE = 188; @@ -69,12 +56,18 @@ public final class TsExtractor { private boolean prepared; + private boolean pendingTimestampOffsetUpdate; + private long pendingTimestampOffsetUs; + private long sampleTimestampOffsetUs; + private long largestParsedTimestampUs; + public TsExtractor() { tsPacketBuffer = new BitsArray(); pesPayloadReaders = new SparseArray(); tsPayloadReaders = new SparseArray(); tsPayloadReaders.put(TS_PAT_PID, new PatReader()); samplesPool = new LinkedList(); + largestParsedTimestampUs = Long.MIN_VALUE; } /** @@ -102,10 +95,19 @@ public final class TsExtractor { return pesPayloadReaders.valueAt(track).getMediaFormat(); } + /** + * Whether the extractor is prepared. + * + * @return True if the extractor is prepared. False otherwise. + */ + public boolean isPrepared() { + return prepared; + } + /** * Resets the extractor's internal state. */ - public void reset() { + public void reset(long nextSampleTimestampUs) { prepared = false; tsPacketBuffer.reset(); tsPayloadReaders.clear(); @@ -115,27 +117,67 @@ public final class TsExtractor { pesPayloadReaders.valueAt(i).clear(); } pesPayloadReaders.clear(); + // Configure for subsequent read operations. + pendingTimestampOffsetUpdate = true; + pendingTimestampOffsetUs = nextSampleTimestampUs; + largestParsedTimestampUs = Long.MIN_VALUE; } /** - * Attempts to prepare the extractor. The extractor is prepared once it has read sufficient data - * to have established the available tracks and their corresponding media formats. + * Consumes data from a {@link NonBlockingInputStream}. *

- * Calling this method is a no-op if the extractor is already prepared. + * The read terminates if the end of the input stream is reached, if insufficient data is + * available to read a sample, or if the extractor has consumed up to the specified target + * timestamp. * - * @param inputStream The input stream from which data can be read. - * @return True if the extractor was prepared. False if more data is required. + * @param inputStream The input stream from which data should be read. + * @param targetTimestampUs A target timestamp to consume up to. + * @return True if the target timestamp was reached. False otherwise. */ - public boolean prepare(NonBlockingInputStream inputStream) { - while (!prepared) { - if (readTSPacket(inputStream) == -1) { - return false; - } + public boolean consumeUntil(NonBlockingInputStream inputStream, long targetTimestampUs) { + while (largestParsedTimestampUs < targetTimestampUs && readTSPacket(inputStream) != -1) { + // Carry on. + } + if (!prepared) { prepared = checkPrepared(); } + return largestParsedTimestampUs >= targetTimestampUs; + } + + /** + * Gets the next sample for the specified track. + * + * @param track The track from which to read. + * @param out A {@link SampleHolder} into which the next sample should be read. + * @return True if a sample was read. False otherwise. + */ + public boolean getSample(int track, SampleHolder out) { + Assertions.checkState(prepared); + Queue queue = pesPayloadReaders.valueAt(track).samplesQueue; + if (queue.isEmpty()) { + return false; + } + Sample sample = queue.remove(); + convert(sample, out); + recycleSample(sample); return true; } + /** + * Whether samples are available for reading from {@link #getSample(int, SampleHolder)}. + * + * @return True if samples are available for reading from {@link #getSample(int, SampleHolder)}. + * False otherwise. + */ + public boolean hasSamples() { + for (int i = 0; i < pesPayloadReaders.size(); i++) { + if (!pesPayloadReaders.valueAt(i).samplesQueue.isEmpty()) { + return true; + } + } + return false; + } + private boolean checkPrepared() { int pesPayloadReaderCount = pesPayloadReaders.size(); if (pesPayloadReaderCount == 0) { @@ -149,40 +191,6 @@ public final class TsExtractor { return true; } - /** - * Consumes data from a {@link NonBlockingInputStream}. - *

- * The read terminates if the end of the input stream is reached, if insufficient data is - * available to read a sample, or if a sample is read. The returned flags indicate - * both the reason for termination and data that was parsed during the read. - * - * @param inputStream The input stream from which data should be read. - * @param track The track from which to read. - * @param out A {@link SampleHolder} into which the next sample should be read. If null then - * {@link #RESULT_NEED_SAMPLE_HOLDER} will be returned once a sample has been reached. - * @return One or more of the {@code RESULT_*} flags defined in this class. - */ - public int read(NonBlockingInputStream inputStream, int track, SampleHolder out) { - Assertions.checkState(prepared); - Queue queue = pesPayloadReaders.valueAt(track).samplesQueue; - - // Keep reading if the buffer is empty. - while (queue.isEmpty()) { - if (readTSPacket(inputStream) == -1) { - return RESULT_NEED_MORE_DATA; - } - } - - if (!queue.isEmpty() && out == null) { - return RESULT_NEED_SAMPLE_HOLDER; - } - - Sample sample = queue.remove(); - convert(sample, out); - recycleSample(sample); - return RESULT_READ_SAMPLE; - } - /** * Read a single TS packet. */ @@ -506,6 +514,12 @@ public final class TsExtractor { addToSample(sample, buffer, sampleSize); sample.flags = flags; sample.timeUs = sampleTimeUs; + addSample(sample); + } + + protected void addSample(Sample sample) { + adjustTimestamp(sample); + largestParsedTimestampUs = Math.max(largestParsedTimestampUs, sample.timeUs); samplesQueue.add(sample); } @@ -517,6 +531,14 @@ public final class TsExtractor { sample.size += size; } + private void adjustTimestamp(Sample sample) { + if (pendingTimestampOffsetUpdate) { + sampleTimestampOffsetUs = pendingTimestampOffsetUs - sample.timeUs; + pendingTimestampOffsetUpdate = false; + } + sample.timeUs += sampleTimestampOffsetUs; + } + } /** @@ -549,7 +571,7 @@ public final class TsExtractor { // Single PES packet should contain only one new H.264 frame. if (currentSample != null) { - samplesQueue.add(currentSample); + addSample(currentSample); } currentSample = getSample(); pesPayloadSize -= readOneH264Frame(pesBuffer, false); From 4c146ee28d6a867a08b850f10d2be0b6051b1298 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Tue, 28 Oct 2014 19:48:54 +0000 Subject: [PATCH 08/55] Neaten TsExtractor a little. - Add a readBit method to BitsArray for reading a boolean flag. - Make things accessed from inner classes package visibility to avoid the compiler generating thunk methods. --- .../exoplayer/parser/ts/BitsArray.java | 8 +++ .../exoplayer/parser/ts/TsExtractor.java | 61 ++++++++----------- 2 files changed, 35 insertions(+), 34 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/parser/ts/BitsArray.java b/library/src/main/java/com/google/android/exoplayer/parser/ts/BitsArray.java index 862305f4b3..efd43dbec3 100644 --- a/library/src/main/java/com/google/android/exoplayer/parser/ts/BitsArray.java +++ b/library/src/main/java/com/google/android/exoplayer/parser/ts/BitsArray.java @@ -137,6 +137,14 @@ public final class BitsArray { return b & 0xFF; } + /** + * Reads a single bit. + * + * @return True if the bit is set. False otherwise. + */ + public boolean readBit() { + return readBits(1) == 1; + } /** * Reads up to 32 bits. diff --git a/library/src/main/java/com/google/android/exoplayer/parser/ts/TsExtractor.java b/library/src/main/java/com/google/android/exoplayer/parser/ts/TsExtractor.java index 29fe07c1bf..c0419d6499 100644 --- a/library/src/main/java/com/google/android/exoplayer/parser/ts/TsExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer/parser/ts/TsExtractor.java @@ -56,10 +56,10 @@ public final class TsExtractor { private boolean prepared; - private boolean pendingTimestampOffsetUpdate; - private long pendingTimestampOffsetUs; - private long sampleTimestampOffsetUs; - private long largestParsedTimestampUs; + /* package */ boolean pendingTimestampOffsetUpdate; + /* package */ long pendingTimestampOffsetUs; + /* package */ long sampleTimestampOffsetUs; + /* package */ long largestParsedTimestampUs; public TsExtractor() { tsPacketBuffer = new BitsArray(); @@ -201,11 +201,7 @@ public final class TsExtractor { } tsPacketBuffer.reset(); - - int bytesRead = tsPacketBuffer.append(inputStream, TS_PACKET_SIZE); - if (bytesRead != TS_PACKET_SIZE) { - return -1; - } + tsPacketBuffer.append(inputStream, TS_PACKET_SIZE); // Parse TS header. // Check sync byte. @@ -215,25 +211,25 @@ public final class TsExtractor { } // Skip transportErrorIndicator. tsPacketBuffer.skipBits(1); - int payloadUnitStartIndicator = tsPacketBuffer.readBits(1); + boolean payloadUnitStartIndicator = tsPacketBuffer.readBit(); // Skip transportPriority. tsPacketBuffer.skipBits(1); int pid = tsPacketBuffer.readBits(13); // Skip transport_scrambling_control. tsPacketBuffer.skipBits(2); - int adaptationFieldExist = tsPacketBuffer.readBits(1); - int payloadExist = tsPacketBuffer.readBits(1); + boolean adaptationFieldExists = tsPacketBuffer.readBit(); + boolean payloadExists = tsPacketBuffer.readBit(); // Skip continuityCounter. tsPacketBuffer.skipBits(4); - // Read Adaptation Field. - if (adaptationFieldExist == 1) { - int afLength = tsPacketBuffer.readBits(8); - tsPacketBuffer.skipBytes(afLength); + // Read the adaptation field. + if (adaptationFieldExists) { + int adaptationFieldLength = tsPacketBuffer.readBits(8); + tsPacketBuffer.skipBytes(adaptationFieldLength); } // Read Payload. - if (payloadExist == 1) { + if (payloadExists) { TsPayloadReader payloadReader = tsPayloadReaders.get(pid); if (payloadReader == null) { return 0; @@ -255,14 +251,14 @@ public final class TsExtractor { out.timeUs = in.timeUs; } - private Sample getSample() { + /* package */ Sample getSample() { if (samplesPool.isEmpty()) { return new Sample(DEFAULT_BUFFER_SEGMENT_SIZE); } return samplesPool.remove(); } - private void recycleSample(Sample sample) { + /* package */ void recycleSample(Sample sample) { sample.reset(); samplesPool.add(sample); } @@ -271,7 +267,7 @@ public final class TsExtractor { * Parses payload data. */ private abstract static class TsPayloadReader { - public abstract void read(BitsArray tsBuffer, int payloadUnitStartIndicator); + public abstract void read(BitsArray tsBuffer, boolean payloadUnitStartIndicator); } /** @@ -280,9 +276,9 @@ public final class TsExtractor { private class PatReader extends TsPayloadReader { @Override - public void read(BitsArray tsBuffer, int payloadUnitStartIndicator) { + public void read(BitsArray tsBuffer, boolean payloadUnitStartIndicator) { // Skip pointer. - if (payloadUnitStartIndicator == 1) { + if (payloadUnitStartIndicator) { int pointerField = tsBuffer.readBits(8); tsBuffer.skipBytes(pointerField); } @@ -313,9 +309,9 @@ public final class TsExtractor { private class PmtReader extends TsPayloadReader { @Override - public void read(BitsArray tsBuffer, int payloadUnitStartIndicator) { + public void read(BitsArray tsBuffer, boolean payloadUnitStartIndicator) { // Skip pointer. - if (payloadUnitStartIndicator == 1) { + if (payloadUnitStartIndicator) { int pointerField = tsBuffer.readBits(8); tsBuffer.skipBytes(pointerField); } @@ -399,8 +395,8 @@ public final class TsExtractor { } @Override - public void read(BitsArray tsBuffer, int payloadUnitStartIndicator) { - if (payloadUnitStartIndicator == 1 && !pesBuffer.isEmpty()) { + public void read(BitsArray tsBuffer, boolean payloadUnitStartIndicator) { + if (payloadUnitStartIndicator && !pesBuffer.isEmpty()) { readPES(); } pesBuffer.append(tsBuffer, tsBuffer.bytesLeft()); @@ -414,6 +410,7 @@ public final class TsExtractor { if (packetStartCodePrefix != 0x000001) { // Error. } + // TODO: Read and use stream_id. // Skip stream_id. pesBuffer.skipBits(8); @@ -422,7 +419,7 @@ public final class TsExtractor { // Skip some fields/flags. // TODO: might need to use data_alignment_indicator. pesBuffer.skipBits(8); // 2+2+1+1+1+1 - int ptsFlag = pesBuffer.readBits(1); + boolean ptsFlag = pesBuffer.readBit(); // Skip DTS flag. pesBuffer.skipBits(1); // Skip some fields/flags. @@ -434,8 +431,7 @@ public final class TsExtractor { } long timeUs = 0; - - if (ptsFlag == 1) { + if (ptsFlag) { // Skip prefix. pesBuffer.skipBits(4); long pts = pesBuffer.readBitsLong(3) << 30; @@ -444,9 +440,7 @@ public final class TsExtractor { pesBuffer.skipBits(1); pts |= pesBuffer.readBitsLong(15); pesBuffer.skipBits(1); - timeUs = pts * 1000000 / 90000; - // Skip the rest of the header. pesBuffer.skipBytes(pesHeaderDataLength - 5); } else { @@ -463,7 +457,6 @@ public final class TsExtractor { } pesPayloadReader.read(pesBuffer, payloadSize, timeUs); - pesBuffer.reset(); } @@ -662,7 +655,7 @@ public final class TsExtractor { } adtsBuffer.skipBits(15); - int hasCRC = adtsBuffer.readBits(1); + boolean hasCRC = !adtsBuffer.readBit(); if (!hasMediaFormat()) { int audioObjectType = adtsBuffer.readBits(2) + 1; @@ -688,7 +681,7 @@ public final class TsExtractor { adtsBuffer.skipBits(13); // Decrement frame size by ADTS header size and CRC. - if (hasCRC == 0) { + if (hasCRC) { // Skip CRC. adtsBuffer.skipBytes(2); frameSize -= 9; From a76addba5d59637dec40653698e7dc3551534ced Mon Sep 17 00:00:00 2001 From: Andrey Udovenko Date: Tue, 4 Nov 2014 13:38:22 -0500 Subject: [PATCH 09/55] Add AES-128 encryption support for HLS #69 and parsing logic for CODECS and RESOLUTION attributes. --- .../demo/full/player/HlsRendererBuilder.java | 2 +- .../demo/simple/HlsRendererBuilder.java | 2 +- .../android/exoplayer/hls/HlsChunkSource.java | 75 +++++++++++++-- .../exoplayer/hls/HlsMasterPlaylist.java | 8 +- .../hls/HlsMasterPlaylistParser.java | 32 ++++++- .../exoplayer/hls/HlsMediaPlaylist.java | 12 ++- .../exoplayer/hls/HlsMediaPlaylistParser.java | 37 ++++++- .../android/exoplayer/hls/HlsParserUtil.java | 8 ++ .../exoplayer/hls/HlsSampleSource.java | 10 +- .../exoplayer/upstream/Aes128DataSource.java | 96 +++++++++++++++++++ 10 files changed, 267 insertions(+), 15 deletions(-) create mode 100644 library/src/main/java/com/google/android/exoplayer/upstream/Aes128DataSource.java diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/HlsRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/HlsRendererBuilder.java index dd85f933c8..5306dedd2c 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/HlsRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/HlsRendererBuilder.java @@ -111,7 +111,7 @@ public class HlsRendererBuilder implements RendererBuilder, ManifestCallback variants = new ArrayList(); int bandwidth = 0; + String[] codecs = null; + int width = -1; + int height = -1; + String line; while ((line = reader.readLine()) != null) { line = line.trim(); @@ -60,9 +70,29 @@ public final class HlsMasterPlaylistParser implements ManifestParser Date: Tue, 4 Nov 2014 14:24:13 -0500 Subject: [PATCH 10/55] Specify UserAgent for ManifestFetcher in HlsRendererBuilder. --- .../android/exoplayer/demo/full/player/HlsRendererBuilder.java | 2 +- .../android/exoplayer/demo/simple/HlsRendererBuilder.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/HlsRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/HlsRendererBuilder.java index 5306dedd2c..26313d57c6 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/HlsRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/HlsRendererBuilder.java @@ -73,7 +73,7 @@ public class HlsRendererBuilder implements RendererBuilder, ManifestCallback mediaPlaylistFetcher = - new ManifestFetcher(parser, contentId, url); + new ManifestFetcher(parser, contentId, url, userAgent); mediaPlaylistFetcher.singleLoad(player.getMainHandler().getLooper(), this); break; case DemoUtil.TYPE_HLS_MEDIA: diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/simple/HlsRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/simple/HlsRendererBuilder.java index 4845444fbb..078cfad763 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/simple/HlsRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/simple/HlsRendererBuilder.java @@ -74,7 +74,7 @@ import java.util.Collections; case DemoUtil.TYPE_HLS_MASTER: HlsMasterPlaylistParser parser = new HlsMasterPlaylistParser(); ManifestFetcher mediaPlaylistFetcher = - new ManifestFetcher(parser, contentId, url); + new ManifestFetcher(parser, contentId, url, userAgent); mediaPlaylistFetcher.singleLoad(playerActivity.getMainLooper(), this); break; case DemoUtil.TYPE_HLS_MEDIA: From 71f918c01b3318f42cc9b2d7f83b4976e7c20bbb Mon Sep 17 00:00:00 2001 From: Andrey Udovenko Date: Wed, 5 Nov 2014 11:54:45 -0500 Subject: [PATCH 11/55] ID3 refactoring to match apple's player behavior #67 --- .../demo/full/FullPlayerActivity.java | 13 ++++++++----- .../demo/full/player/DemoPlayer.java | 7 +++---- .../demo/full/player/HlsRendererBuilder.java | 2 +- .../android/exoplayer/metadata/Id3Parser.java | 19 +++++++++++-------- .../exoplayer/metadata/MetadataParser.java | 8 ++++---- .../metadata/MetadataTrackRenderer.java | 12 ++++++------ .../{Metadata.java => TxxxMetadata.java} | 13 ++++++++----- 7 files changed, 41 insertions(+), 33 deletions(-) rename library/src/main/java/com/google/android/exoplayer/metadata/{Metadata.java => TxxxMetadata.java} (69%) diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/FullPlayerActivity.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/FullPlayerActivity.java index 1fbc317ed7..03cf5b4e2c 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/FullPlayerActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/FullPlayerActivity.java @@ -25,7 +25,7 @@ import com.google.android.exoplayer.demo.full.player.DemoPlayer; import com.google.android.exoplayer.demo.full.player.DemoPlayer.RendererBuilder; import com.google.android.exoplayer.demo.full.player.HlsRendererBuilder; import com.google.android.exoplayer.demo.full.player.SmoothStreamingRendererBuilder; -import com.google.android.exoplayer.metadata.Metadata; +import com.google.android.exoplayer.metadata.TxxxMetadata; import com.google.android.exoplayer.text.CaptionStyleCompat; import com.google.android.exoplayer.text.SubtitleView; import com.google.android.exoplayer.util.Util; @@ -56,7 +56,7 @@ import android.widget.PopupMenu; import android.widget.PopupMenu.OnMenuItemClickListener; import android.widget.TextView; -import java.util.List; +import java.util.Map; /** * An activity that plays media using {@link DemoPlayer}. @@ -415,10 +415,13 @@ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callba // DemoPlayer.MetadataListener implementation @Override - public void onMetadata(List metadata) { + public void onMetadata(Map metadata) { for (int i = 0; i < metadata.size(); i++) { - Metadata next = metadata.get(i); - Log.i(TAG, "ID3 TimedMetadata: key=" + next.key + ", value=" + next.value); + if (metadata.containsKey(TxxxMetadata.TYPE)) { + TxxxMetadata txxxMetadata = (TxxxMetadata) metadata.get(TxxxMetadata.TYPE); + Log.i(TAG, String.format("ID3 TimedMetadata: description=%s, value=%s", + txxxMetadata.description, txxxMetadata.value)); + } } } diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DemoPlayer.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DemoPlayer.java index b6a68bc98f..ad046fed00 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DemoPlayer.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DemoPlayer.java @@ -26,7 +26,6 @@ import com.google.android.exoplayer.TrackRenderer; import com.google.android.exoplayer.chunk.ChunkSampleSource; import com.google.android.exoplayer.chunk.MultiTrackChunkSource; import com.google.android.exoplayer.drm.StreamingDrmSessionManager; -import com.google.android.exoplayer.metadata.Metadata; import com.google.android.exoplayer.metadata.MetadataTrackRenderer; import com.google.android.exoplayer.text.TextTrackRenderer; import com.google.android.exoplayer.upstream.DefaultBandwidthMeter; @@ -38,7 +37,7 @@ import android.os.Looper; import android.view.Surface; import java.io.IOException; -import java.util.List; +import java.util.Map; import java.util.concurrent.CopyOnWriteArrayList; /** @@ -141,7 +140,7 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi * A listener for receiving metadata parsed from the media stream. */ public interface MetadataListener { - void onMetadata(List metadata); + void onMetadata(Map metadata); } // Constants pulled into this class for convenience. @@ -475,7 +474,7 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi } @Override - public void onMetadata(List metadata) { + public void onMetadata(Map metadata) { if (metadataListener != null) { metadataListener.onMetadata(metadata); } diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/HlsRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/HlsRendererBuilder.java index 26313d57c6..778a026228 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/HlsRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/HlsRendererBuilder.java @@ -94,7 +94,7 @@ public class HlsRendererBuilder implements RendererBuilder, ManifestCallback parse(byte[] data, int size) + public Map parse(byte[] data, int size) throws UnsupportedEncodingException, ParserException { BitsArray id3Buffer = new BitsArray(data, size); int id3Size = parseId3Header(id3Buffer); - List metadata = new ArrayList(); + Map metadata = new HashMap(); while (id3Size > 0) { int frameId0 = id3Buffer.readUnsignedByte(); @@ -63,20 +63,23 @@ public class Id3Parser implements MetadataParser { id3Buffer.readBytes(frame, 0, frameSize - 1); int firstZeroIndex = indexOf(frame, 0, (byte) 0); - String key = new String(frame, 0, firstZeroIndex, charset); + String description = new String(frame, 0, firstZeroIndex, charset); int valueStartIndex = indexOfNot(frame, firstZeroIndex, (byte) 0); int valueEndIndex = indexOf(frame, valueStartIndex, (byte) 0); String value = new String(frame, valueStartIndex, valueEndIndex - valueStartIndex, charset); - metadata.add(new Metadata(key, value)); + metadata.put(TxxxMetadata.TYPE, new TxxxMetadata(description, value)); } else { - id3Buffer.skipBytes(frameSize); + String type = String.format("%c%c%c%c", frameId0, frameId1, frameId2, frameId3); + byte[] frame = new byte[frameSize]; + id3Buffer.readBytes(frame, 0, frameSize); + metadata.put(type, frame); } id3Size -= frameSize + 10 /* header size */; } - return Collections.unmodifiableList(metadata); + return Collections.unmodifiableMap(metadata); } private static int indexOf(byte[] data, int fromIndex, byte key) { diff --git a/library/src/main/java/com/google/android/exoplayer/metadata/MetadataParser.java b/library/src/main/java/com/google/android/exoplayer/metadata/MetadataParser.java index 46d2b1179a..1f9b0dcaef 100644 --- a/library/src/main/java/com/google/android/exoplayer/metadata/MetadataParser.java +++ b/library/src/main/java/com/google/android/exoplayer/metadata/MetadataParser.java @@ -16,7 +16,7 @@ package com.google.android.exoplayer.metadata; import java.io.IOException; -import java.util.List; +import java.util.Map; /** * Parses {@link Metadata}s from binary data. @@ -32,14 +32,14 @@ public interface MetadataParser { public boolean canParse(String mimeType); /** - * Parses a list of {@link Metadata} objects from the provided binary data. + * Parses a map of metadata type to metadata objects from the provided binary data. * * @param data The raw binary data from which to parse the metadata. * @param size The size of the input data. - * @return A parsed {@link List} of {@link Metadata} objects. + * @return A parsed {@link Map} of metadata type to metadata objects. * @throws IOException If a problem occurred parsing the data. */ - public List parse(byte[] data, int size) + public Map parse(byte[] data, int size) throws IOException; } diff --git a/library/src/main/java/com/google/android/exoplayer/metadata/MetadataTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/metadata/MetadataTrackRenderer.java index 5d5ad0c50d..48ed78f20c 100644 --- a/library/src/main/java/com/google/android/exoplayer/metadata/MetadataTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/metadata/MetadataTrackRenderer.java @@ -28,7 +28,7 @@ import android.os.Looper; import android.os.Message; import java.io.IOException; -import java.util.List; +import java.util.Map; /** * A {@link TrackRenderer} for metadata embedded in a media stream. @@ -45,7 +45,7 @@ public class MetadataTrackRenderer extends TrackRenderer implements Callback { * * @param metadata The metadata to process. */ - void onMetadata(List metadata); + void onMetadata(Map metadata); } @@ -63,7 +63,7 @@ public class MetadataTrackRenderer extends TrackRenderer implements Callback { private boolean inputStreamEnded; private long pendingMetadataTimestamp; - private List pendingMetadata; + private Map pendingMetadata; /** * @param source A source from which samples containing metadata can be read. @@ -185,7 +185,7 @@ public class MetadataTrackRenderer extends TrackRenderer implements Callback { return true; } - private void invokeRenderer(List metadata) { + private void invokeRenderer(Map metadata) { if (metadataHandler != null) { metadataHandler.obtainMessage(MSG_INVOKE_RENDERER, metadata).sendToTarget(); } else { @@ -198,13 +198,13 @@ public class MetadataTrackRenderer extends TrackRenderer implements Callback { public boolean handleMessage(Message msg) { switch (msg.what) { case MSG_INVOKE_RENDERER: - invokeRendererInternal((List) msg.obj); + invokeRendererInternal((Map) msg.obj); return true; } return false; } - private void invokeRendererInternal(List metadata) { + private void invokeRendererInternal(Map metadata) { metadataRenderer.onMetadata(metadata); } diff --git a/library/src/main/java/com/google/android/exoplayer/metadata/Metadata.java b/library/src/main/java/com/google/android/exoplayer/metadata/TxxxMetadata.java similarity index 69% rename from library/src/main/java/com/google/android/exoplayer/metadata/Metadata.java rename to library/src/main/java/com/google/android/exoplayer/metadata/TxxxMetadata.java index d89b02f9bd..c455bb825d 100644 --- a/library/src/main/java/com/google/android/exoplayer/metadata/Metadata.java +++ b/library/src/main/java/com/google/android/exoplayer/metadata/TxxxMetadata.java @@ -16,15 +16,18 @@ package com.google.android.exoplayer.metadata; /** - * A metadata that contains textual data associated with time indices. + * A metadata that contains parsed ID3 TXXX (User defined text information) frame data associated + * with time indices. */ -public class Metadata { +public class TxxxMetadata { - public final String key; + public static final String TYPE = "TXXX"; + + public final String description; public final String value; - public Metadata(String key, String value) { - this.key = key; + public TxxxMetadata(String description, String value) { + this.description = description; this.value = value; } From 55b4272a466a42e5a441746bb8f02380de0cefdf Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Wed, 5 Nov 2014 17:25:02 +0000 Subject: [PATCH 12/55] Pro-actively parse the final PES packet. --- .../exoplayer/parser/ts/TsExtractor.java | 49 +++++++++++++------ 1 file changed, 33 insertions(+), 16 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/parser/ts/TsExtractor.java b/library/src/main/java/com/google/android/exoplayer/parser/ts/TsExtractor.java index c0419d6499..3a95798903 100644 --- a/library/src/main/java/com/google/android/exoplayer/parser/ts/TsExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer/parser/ts/TsExtractor.java @@ -267,7 +267,9 @@ public final class TsExtractor { * Parses payload data. */ private abstract static class TsPayloadReader { + public abstract void read(BitsArray tsBuffer, boolean payloadUnitStartIndicator); + } /** @@ -389,33 +391,47 @@ public final class TsExtractor { // Parses PES payload and extracts individual samples. private final PesPayloadReader pesPayloadReader; + private int packetLength; + public PesReader(PesPayloadReader pesPayloadReader) { this.pesPayloadReader = pesPayloadReader; + this.packetLength = -1; pesBuffer = new BitsArray(); } @Override public void read(BitsArray tsBuffer, boolean payloadUnitStartIndicator) { if (payloadUnitStartIndicator && !pesBuffer.isEmpty()) { - readPES(); + // We've encountered the start of the next packet, but haven't yet read the body. Read it. + // Note that this should only happen if the packet length was unspecified. + Assertions.checkState(packetLength == 0); + readPacketBody(); } + pesBuffer.append(tsBuffer, tsBuffer.bytesLeft()); + + if (packetLength == -1 && pesBuffer.bytesLeft() >= 6) { + // We haven't read the start of the packet, but have enough data to do so. + readPacketStart(); + } + if (packetLength > 0 && pesBuffer.bytesLeft() >= packetLength) { + // The packet length was specified and we now have the whole packet. Read it. + readPacketBody(); + } } - /** - * Parses completed PES data. - */ - private void readPES() { - int packetStartCodePrefix = pesBuffer.readBits(24); - if (packetStartCodePrefix != 0x000001) { + private void readPacketStart() { + int startCodePrefix = pesBuffer.readBits(24); + if (startCodePrefix != 0x000001) { // Error. } - // TODO: Read and use stream_id. // Skip stream_id. pesBuffer.skipBits(8); - int pesPacketLength = pesBuffer.readBits(16); + packetLength = pesBuffer.readBits(16); + } + private void readPacketBody() { // Skip some fields/flags. // TODO: might need to use data_alignment_indicator. pesBuffer.skipBits(8); // 2+2+1+1+1+1 @@ -425,9 +441,9 @@ public final class TsExtractor { // Skip some fields/flags. pesBuffer.skipBits(6); // 1+1+1+1+1+1 - int pesHeaderDataLength = pesBuffer.readBits(8); - if (pesHeaderDataLength == 0) { - pesHeaderDataLength = pesBuffer.bytesLeft(); + int headerDataLength = pesBuffer.readBits(8); + if (headerDataLength == 0) { + headerDataLength = pesBuffer.bytesLeft(); } long timeUs = 0; @@ -442,22 +458,23 @@ public final class TsExtractor { pesBuffer.skipBits(1); timeUs = pts * 1000000 / 90000; // Skip the rest of the header. - pesBuffer.skipBytes(pesHeaderDataLength - 5); + pesBuffer.skipBytes(headerDataLength - 5); } else { // Skip the rest of the header. - pesBuffer.skipBytes(pesHeaderDataLength); + pesBuffer.skipBytes(headerDataLength); } int payloadSize; - if (pesPacketLength == 0) { + if (packetLength == 0) { // If pesPacketLength is not specified read all available data. payloadSize = pesBuffer.bytesLeft(); } else { - payloadSize = pesPacketLength - pesHeaderDataLength - 3; + payloadSize = packetLength - headerDataLength - 3; } pesPayloadReader.read(pesBuffer, payloadSize, timeUs); pesBuffer.reset(); + packetLength = -1; } } From 9790430a626c8b1a0b894529df95ad32cffe5510 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 6 Nov 2014 19:17:22 +0000 Subject: [PATCH 13/55] Trim whitespace from codecs --- .../google/android/exoplayer/hls/HlsMasterPlaylistParser.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/src/main/java/com/google/android/exoplayer/hls/HlsMasterPlaylistParser.java b/library/src/main/java/com/google/android/exoplayer/hls/HlsMasterPlaylistParser.java index 191ec06996..d0cb58baa9 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/HlsMasterPlaylistParser.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/HlsMasterPlaylistParser.java @@ -73,7 +73,7 @@ public final class HlsMasterPlaylistParser implements ManifestParser Date: Thu, 6 Nov 2014 19:22:14 +0000 Subject: [PATCH 14/55] HLS improvements + steps towards ABR. --- .../android/exoplayer/hls/HlsChunkSource.java | 17 +- .../exoplayer/hls/HlsSampleSource.java | 97 ++++++++---- .../google/android/exoplayer/hls/TsChunk.java | 36 ++--- .../exoplayer/parser/ts/TsExtractor.java | 145 ++++++++++++------ 4 files changed, 182 insertions(+), 113 deletions(-) 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 8ea6b27e29..2dc1158cd5 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 @@ -45,7 +45,6 @@ public class HlsChunkSource { private final HlsMasterPlaylist masterPlaylist; private final HlsMediaPlaylistParser mediaPlaylistParser; - private long liveStartTimeUs; /* package */ HlsMediaPlaylist mediaPlaylist; /* package */ boolean mediaPlaylistWasLive; /* package */ long lastMediaPlaylistLoadTimeMs; @@ -168,25 +167,22 @@ public class HlsChunkSource { DataSpec dataSpec = new DataSpec(chunkUri, 0, C.LENGTH_UNBOUNDED, null); - long startTimeUs = segment.startTimeUs; - long endTimeUs = startTimeUs + (long) (segment.durationSecs * 1000000); + long startTimeUs; int nextChunkMediaSequence = chunkMediaSequence + 1; - if (mediaPlaylistWasLive) { if (queue.isEmpty()) { - liveStartTimeUs = startTimeUs; startTimeUs = 0; - endTimeUs -= liveStartTimeUs; } else { - startTimeUs -= liveStartTimeUs; - endTimeUs -= liveStartTimeUs; + startTimeUs = queue.get(queue.size() - 1).endTimeUs; } } else { // Not live. + startTimeUs = segment.startTimeUs; if (chunkIndex == mediaPlaylist.segments.size() - 1) { nextChunkMediaSequence = -1; } } + long endTimeUs = startTimeUs + (long) (segment.durationSecs * 1000000); DataSource dataSource; if (encryptedDataSource != null) { @@ -194,9 +190,8 @@ public class HlsChunkSource { } else { dataSource = upstreamDataSource; } - - out.chunk = new TsChunk(dataSource, dataSpec, 0, startTimeUs, endTimeUs, - nextChunkMediaSequence, segment.discontinuity); + out.chunk = new TsChunk(dataSource, dataSpec, 0, 0, startTimeUs, endTimeUs, + nextChunkMediaSequence, segment.discontinuity, false); } private boolean shouldRerequestMediaPlaylist() { 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 f05d9cb8ec..7482904986 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 @@ -48,10 +48,11 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { private static final long MAX_SAMPLE_INTERLEAVING_OFFSET_US = 5000000; private static final int NO_RESET_PENDING = -1; - private final TsExtractor extractor; + private final TsExtractor.SamplePool samplePool; private final LoadControl loadControl; private final HlsChunkSource chunkSource; private final HlsChunkOperationHolder currentLoadableHolder; + private final LinkedList extractors; private final LinkedList mediaChunks; private final List readOnlyHlsChunks; private final int bufferSizeContribution; @@ -84,7 +85,8 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { this.bufferSizeContribution = bufferSizeContribution; this.frameAccurateSeeking = frameAccurateSeeking; this.remainingReleaseCount = downstreamRendererCount; - extractor = new TsExtractor(); + samplePool = new TsExtractor.SamplePool(); + extractors = new LinkedList(); currentLoadableHolder = new HlsChunkOperationHolder(); mediaChunks = new LinkedList(); readOnlyHlsChunks = Collections.unmodifiableList(mediaChunks); @@ -100,6 +102,10 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { loadControl.register(this, bufferSizeContribution); } continueBufferingInternal(); + if (extractors.isEmpty()) { + return false; + } + TsExtractor extractor = extractors.get(0); if (extractor.isPrepared()) { trackCount = extractor.getTrackCount(); trackEnabledStates = new boolean[trackCount]; @@ -171,39 +177,38 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { } TsChunk mediaChunk = mediaChunks.getFirst(); + int currentVariant = mediaChunk.variantIndex; + + TsExtractor extractor; + if (extractors.isEmpty()) { + extractor = new TsExtractor(mediaChunk.startTimeUs, samplePool); + extractors.addLast(extractor); + if (mediaChunk.discardFromFirstKeyframes) { + extractor.discardFromNextKeyframes(); + } + } else { + extractor = extractors.getLast(); + } + if (mediaChunk.isReadFinished() && mediaChunks.size() > 1) { discardDownstreamHlsChunk(); mediaChunk = mediaChunks.getFirst(); - } - - boolean haveSufficientSamples = false; - if (mediaChunk.hasPendingDiscontinuity()) { - if (extractor.hasSamples()) { - // There are samples from before the discontinuity yet to be read from the extractor, so - // we don't want to reset the extractor yet. - haveSufficientSamples = true; - } else { - extractor.reset(mediaChunk.startTimeUs); - mediaChunk.clearPendingDiscontinuity(); - if (pendingDiscontinuities == null) { - // We're not prepared yet. - } else { - for (int i = 0; i < pendingDiscontinuities.length; i++) { - pendingDiscontinuities[i] = true; - } - } + if (mediaChunk.discontinuity || mediaChunk.variantIndex != currentVariant) { + extractor = new TsExtractor(mediaChunk.startTimeUs, samplePool); + extractors.addLast(extractor); + } + if (mediaChunk.discardFromFirstKeyframes) { + extractor.discardFromNextKeyframes(); } } - if (!mediaChunk.hasPendingDiscontinuity()) { - // Allow the extractor to consume from the current chunk. - NonBlockingInputStream inputStream = mediaChunk.getNonBlockingInputStream(); - haveSufficientSamples = extractor.consumeUntil(inputStream, - downstreamPositionUs + MAX_SAMPLE_INTERLEAVING_OFFSET_US); + // Allow the extractor to consume from the current chunk. + NonBlockingInputStream inputStream = mediaChunk.getNonBlockingInputStream(); + boolean haveSufficientSamples = extractor.consumeUntil(inputStream, + downstreamPositionUs + MAX_SAMPLE_INTERLEAVING_OFFSET_US); + if (!haveSufficientSamples) { // If we can't read any more, then we always say we have sufficient samples. - if (!haveSufficientSamples) { - haveSufficientSamples = mediaChunk.isLastChunk() && mediaChunk.isReadFinished(); - } + haveSufficientSamples = mediaChunk.isLastChunk() && mediaChunk.isReadFinished(); } if (!haveSufficientSamples && currentLoadableException != null) { @@ -223,7 +228,28 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { return DISCONTINUITY_READ; } - if (onlyReadDiscontinuity || isPendingReset() || !extractor.isPrepared()) { + if (onlyReadDiscontinuity || isPendingReset()) { + return NOTHING_READ; + } + + if (extractors.isEmpty()) { + return NOTHING_READ; + } + + TsExtractor extractor = extractors.getFirst(); + while (extractors.size() > 1 && !extractor.hasSamples()) { + // We're finished reading from the extractor for all tracks, and so can discard it. + extractors.removeFirst().clear(); + extractor = extractors.getFirst(); + } + int extractorIndex = 0; + while (extractors.size() > extractorIndex + 1 && !extractor.hasSamples(track)) { + // We're finished reading from the extractor for this particular track, so advance to the + // next one for the current read. + extractor = extractors.get(++extractorIndex); + } + + if (!extractor.isPrepared()) { return NOTHING_READ; } @@ -265,9 +291,9 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { if (mediaChunk == null) { restartFrom(positionUs); } else { + discardExtractors(); discardDownstreamHlsChunks(mediaChunk); - mediaChunk.reset(); - extractor.reset(mediaChunk.startTimeUs); + mediaChunk.resetReadPosition(); updateLoadControl(); } } @@ -494,13 +520,20 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { TsChunk mediaChunk = (TsChunk) currentLoadable; mediaChunks.add(mediaChunk); if (isPendingReset()) { - extractor.reset(mediaChunk.startTimeUs); + discardExtractors(); pendingResetPositionUs = NO_RESET_PENDING; } } loader.startLoading(currentLoadable, this); } + private void discardExtractors() { + for (int i = 0; i < extractors.size(); i++) { + extractors.get(i).clear(); + } + extractors.clear(); + } + /** * Discards downstream media chunks until {@code untilChunk} if found. {@code untilChunk} is not * itself discarded. Null can be passed to discard all media chunks. diff --git a/library/src/main/java/com/google/android/exoplayer/hls/TsChunk.java b/library/src/main/java/com/google/android/exoplayer/hls/TsChunk.java index ee872bef83..e69b8c88e0 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/TsChunk.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/TsChunk.java @@ -23,6 +23,10 @@ import com.google.android.exoplayer.upstream.DataSpec; */ public final class TsChunk extends HlsChunk { + /** + * The index of the variant in the master playlist. + */ + public final int variantIndex; /** * The start time of the media contained by the chunk. */ @@ -38,44 +42,38 @@ public final class TsChunk extends HlsChunk { /** * The encoding discontinuity indicator. */ - private final boolean discontinuity; - - private boolean pendingDiscontinuity; + public final boolean discontinuity; + /** + * For each track, whether samples from the first keyframe (inclusive) should be discarded. + */ + public final boolean discardFromFirstKeyframes; /** * @param dataSource A {@link DataSource} for loading the data. * @param dataSpec Defines the data to be loaded. * @param trigger The reason for this chunk being selected. + * @param variantIndex The index of the variant in the master playlist. * @param startTimeUs The start time of the media contained by the chunk, in microseconds. * @param endTimeUs The end time of the media contained by the chunk, in microseconds. * @param nextChunkIndex The index of the next chunk, or -1 if this is the last chunk. * @param discontinuity The encoding discontinuity indicator. + * @param discardFromFirstKeyframes For each contained media stream, whether samples from the + * first keyframe (inclusive) should be discarded. */ - public TsChunk(DataSource dataSource, DataSpec dataSpec, int trigger, long startTimeUs, - long endTimeUs, int nextChunkIndex, boolean discontinuity) { + public TsChunk(DataSource dataSource, DataSpec dataSpec, int trigger, int variantIndex, + long startTimeUs, long endTimeUs, int nextChunkIndex, boolean discontinuity, + boolean discardFromFirstKeyframes) { super(dataSource, dataSpec, trigger); + this.variantIndex = variantIndex; this.startTimeUs = startTimeUs; this.endTimeUs = endTimeUs; this.nextChunkIndex = nextChunkIndex; this.discontinuity = discontinuity; - this.pendingDiscontinuity = discontinuity; + this.discardFromFirstKeyframes = discardFromFirstKeyframes; } public boolean isLastChunk() { return nextChunkIndex == -1; } - public void reset() { - resetReadPosition(); - pendingDiscontinuity = discontinuity; - } - - public boolean hasPendingDiscontinuity() { - return pendingDiscontinuity; - } - - public void clearPendingDiscontinuity() { - pendingDiscontinuity = false; - } - } diff --git a/library/src/main/java/com/google/android/exoplayer/parser/ts/TsExtractor.java b/library/src/main/java/com/google/android/exoplayer/parser/ts/TsExtractor.java index 3a95798903..d305aa4f31 100644 --- a/library/src/main/java/com/google/android/exoplayer/parser/ts/TsExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer/parser/ts/TsExtractor.java @@ -23,11 +23,13 @@ import com.google.android.exoplayer.util.CodecSpecificDataUtil; import com.google.android.exoplayer.util.MimeTypes; import android.annotation.SuppressLint; +import android.media.MediaCodec; import android.media.MediaExtractor; import android.util.Log; import android.util.Pair; import android.util.SparseArray; +import java.util.ArrayList; import java.util.Collections; import java.util.LinkedList; import java.util.Queue; @@ -47,26 +49,27 @@ public final class TsExtractor { private static final int TS_STREAM_TYPE_H264 = 0x1B; private static final int TS_STREAM_TYPE_ID3 = 0x15; - private static final int DEFAULT_BUFFER_SEGMENT_SIZE = 64 * 1024; - private final BitsArray tsPacketBuffer; private final SparseArray pesPayloadReaders; // Indexed by streamType private final SparseArray tsPayloadReaders; // Indexed by pid - private final Queue samplesPool; + private final SamplePool samplePool; private boolean prepared; - /* package */ boolean pendingTimestampOffsetUpdate; - /* package */ long pendingTimestampOffsetUs; + /* package */ boolean pendingFirstSampleTimestampAdjustment; + /* package */ long firstSampleTimestamp; /* package */ long sampleTimestampOffsetUs; /* package */ long largestParsedTimestampUs; + /* package */ boolean discardFromNextKeyframes; - public TsExtractor() { + public TsExtractor(long firstSampleTimestamp, SamplePool samplePool) { + this.firstSampleTimestamp = firstSampleTimestamp; + this.samplePool = samplePool; + pendingFirstSampleTimestampAdjustment = true; tsPacketBuffer = new BitsArray(); pesPayloadReaders = new SparseArray(); tsPayloadReaders = new SparseArray(); tsPayloadReaders.put(TS_PAT_PID, new PatReader()); - samplesPool = new LinkedList(); largestParsedTimestampUs = Long.MIN_VALUE; } @@ -105,22 +108,19 @@ public final class TsExtractor { } /** - * Resets the extractor's internal state. + * Flushes any pending or incomplete samples, returning them to the sample pool. */ - public void reset(long nextSampleTimestampUs) { - prepared = false; - tsPacketBuffer.reset(); - tsPayloadReaders.clear(); - tsPayloadReaders.put(TS_PAT_PID, new PatReader()); - // Clear each reader before discarding it, so as to recycle any queued Sample objects. + public void clear() { for (int i = 0; i < pesPayloadReaders.size(); i++) { pesPayloadReaders.valueAt(i).clear(); } - pesPayloadReaders.clear(); - // Configure for subsequent read operations. - pendingTimestampOffsetUpdate = true; - pendingTimestampOffsetUs = nextSampleTimestampUs; - largestParsedTimestampUs = Long.MIN_VALUE; + } + + /** + * For each track, whether to discard samples from the next keyframe (inclusive). + */ + public void discardFromNextKeyframes() { + discardFromNextKeyframes = true; } /** @@ -153,31 +153,43 @@ public final class TsExtractor { */ public boolean getSample(int track, SampleHolder out) { Assertions.checkState(prepared); - Queue queue = pesPayloadReaders.valueAt(track).samplesQueue; + Queue queue = pesPayloadReaders.valueAt(track).sampleQueue; if (queue.isEmpty()) { return false; } Sample sample = queue.remove(); convert(sample, out); - recycleSample(sample); + samplePool.recycle(sample); return true; } /** - * Whether samples are available for reading from {@link #getSample(int, SampleHolder)}. + * Whether samples are available for reading from {@link #getSample(int, SampleHolder)} for any + * track. * - * @return True if samples are available for reading from {@link #getSample(int, SampleHolder)}. - * False otherwise. + * @return True if samples are available for reading from {@link #getSample(int, SampleHolder)} + * for any track. False otherwise. */ public boolean hasSamples() { for (int i = 0; i < pesPayloadReaders.size(); i++) { - if (!pesPayloadReaders.valueAt(i).samplesQueue.isEmpty()) { + if (hasSamples(i)) { return true; } } return false; } + /** + * Whether samples are available for reading from {@link #getSample(int, SampleHolder)} for the + * specified track. + * + * @return True if samples are available for reading from {@link #getSample(int, SampleHolder)} + * for the specified track. False otherwise. + */ + public boolean hasSamples(int track) { + return !pesPayloadReaders.valueAt(track).sampleQueue.isEmpty(); + } + private boolean checkPrepared() { int pesPayloadReaderCount = pesPayloadReaders.size(); if (pesPayloadReaderCount == 0) { @@ -251,18 +263,6 @@ public final class TsExtractor { out.timeUs = in.timeUs; } - /* package */ Sample getSample() { - if (samplesPool.isEmpty()) { - return new Sample(DEFAULT_BUFFER_SEGMENT_SIZE); - } - return samplesPool.remove(); - } - - /* package */ void recycleSample(Sample sample) { - sample.reset(); - samplesPool.add(sample); - } - /** * Parses payload data. */ @@ -484,12 +484,14 @@ public final class TsExtractor { */ private abstract class PesPayloadReader { - public final Queue samplesQueue; + public final Queue sampleQueue; private MediaFormat mediaFormat; + private boolean foundFirstKeyframe; + private boolean foundLastKeyframe; protected PesPayloadReader() { - this.samplesQueue = new LinkedList(); + this.sampleQueue = new LinkedList(); } public boolean hasMediaFormat() { @@ -507,8 +509,8 @@ public final class TsExtractor { public abstract void read(BitsArray pesBuffer, int pesPayloadSize, long pesTimeUs); public void clear() { - while (!samplesQueue.isEmpty()) { - recycleSample(samplesQueue.remove()); + while (!sampleQueue.isEmpty()) { + samplePool.recycle(sampleQueue.remove()); } } @@ -520,17 +522,31 @@ public final class TsExtractor { * @param sampleTimeUs The sample time stamp. */ protected void addSample(BitsArray buffer, int sampleSize, long sampleTimeUs, int flags) { - Sample sample = getSample(); + Sample sample = samplePool.get(); addToSample(sample, buffer, sampleSize); sample.flags = flags; sample.timeUs = sampleTimeUs; addSample(sample); } + @SuppressLint("InlinedApi") protected void addSample(Sample sample) { + boolean isKeyframe = (sample.flags & MediaCodec.BUFFER_FLAG_SYNC_FRAME) != 0; + if (isKeyframe) { + if (!foundFirstKeyframe) { + foundFirstKeyframe = true; + } + if (discardFromNextKeyframes) { + foundLastKeyframe = true; + } + } adjustTimestamp(sample); - largestParsedTimestampUs = Math.max(largestParsedTimestampUs, sample.timeUs); - samplesQueue.add(sample); + if (foundFirstKeyframe && !foundLastKeyframe) { + largestParsedTimestampUs = Math.max(largestParsedTimestampUs, sample.timeUs); + sampleQueue.add(sample); + } else { + samplePool.recycle(sample); + } } protected void addToSample(Sample sample, BitsArray buffer, int size) { @@ -542,9 +558,9 @@ public final class TsExtractor { } private void adjustTimestamp(Sample sample) { - if (pendingTimestampOffsetUpdate) { - sampleTimestampOffsetUs = pendingTimestampOffsetUs - sample.timeUs; - pendingTimestampOffsetUpdate = false; + if (pendingFirstSampleTimestampAdjustment) { + sampleTimestampOffsetUs = firstSampleTimestamp - sample.timeUs; + pendingFirstSampleTimestampAdjustment = false; } sample.timeUs += sampleTimestampOffsetUs; } @@ -583,7 +599,7 @@ public final class TsExtractor { if (currentSample != null) { addSample(currentSample); } - currentSample = getSample(); + currentSample = samplePool.get(); pesPayloadSize -= readOneH264Frame(pesBuffer, false); currentSample.timeUs = pesTimeUs; @@ -615,7 +631,7 @@ public final class TsExtractor { public void clear() { super.clear(); if (currentSample != null) { - recycleSample(currentSample); + samplePool.recycle(currentSample); currentSample = null; } } @@ -742,8 +758,35 @@ public final class TsExtractor { } /** - * Simplified version of SampleHolder for internal buffering. - */ + * A pool from which the extractor can obtain sample objects for internal use. + */ + public static class SamplePool { + + private static final int DEFAULT_BUFFER_SEGMENT_SIZE = 64 * 1024; + + private final ArrayList samples; + + public SamplePool() { + samples = new ArrayList(); + } + + /* package */ Sample get() { + if (samples.isEmpty()) { + return new Sample(DEFAULT_BUFFER_SEGMENT_SIZE); + } + return samples.remove(samples.size() - 1); + } + + /* package */ void recycle(Sample sample) { + sample.reset(); + samples.add(sample); + } + + } + + /** + * Simplified version of SampleHolder for internal buffering. + */ private static class Sample { public byte[] data; From 6c6ba900a9da4abea158920323ac6b18d78d58f9 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 13 Nov 2014 16:23:44 +0000 Subject: [PATCH 15/55] Fix the build. --- .../src/main/java/com/google/android/exoplayer/MediaFormat.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/src/main/java/com/google/android/exoplayer/MediaFormat.java b/library/src/main/java/com/google/android/exoplayer/MediaFormat.java index b147e9150e..7a79e1d5b7 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaFormat.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaFormat.java @@ -89,7 +89,7 @@ public class MediaFormat { public static MediaFormat createId3Format() { return new MediaFormat(MimeTypes.APPLICATION_ID3, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, - NO_VALUE, NO_VALUE, null); + NO_VALUE, NO_VALUE, NO_VALUE, null); } @TargetApi(16) From fd519016209391698d4a5a828f6f7deac73e88ff Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 13 Nov 2014 16:32:10 +0000 Subject: [PATCH 16/55] Big HLS update. Add start of adaptive support, but leave disabled for now. --- .../demo/full/player/HlsRendererBuilder.java | 21 +- .../demo/simple/HlsRendererBuilder.java | 22 +- .../android/exoplayer/hls/BitArrayChunk.java | 100 +++++ .../android/exoplayer/hls/HlsChunk.java | 137 +------ .../hls/HlsChunkOperationHolder.java | 37 -- .../android/exoplayer/hls/HlsChunkSource.java | 299 ++++++++++----- .../exoplayer/hls/HlsMasterPlaylist.java | 19 - .../hls/HlsMasterPlaylistParser.java | 11 +- .../exoplayer/hls/HlsMediaPlaylistParser.java | 3 +- .../android/exoplayer/hls/HlsParserUtil.java | 2 +- .../exoplayer/hls/HlsSampleSource.java | 343 ++++-------------- .../google/android/exoplayer/hls/TsChunk.java | 78 +++- .../{parser/ts => hls}/TsExtractor.java | 290 +++++++++------ .../google/android/exoplayer/hls/Variant.java | 56 +++ .../android/exoplayer/metadata/Id3Parser.java | 8 +- .../exoplayer/metadata/MetadataParser.java | 2 +- .../ts/BitsArray.java => util/BitArray.java} | 72 +++- 17 files changed, 776 insertions(+), 724 deletions(-) create mode 100644 library/src/main/java/com/google/android/exoplayer/hls/BitArrayChunk.java delete mode 100644 library/src/main/java/com/google/android/exoplayer/hls/HlsChunkOperationHolder.java rename library/src/main/java/com/google/android/exoplayer/{parser/ts => hls}/TsExtractor.java (72%) create mode 100644 library/src/main/java/com/google/android/exoplayer/hls/Variant.java rename library/src/main/java/com/google/android/exoplayer/{parser/ts/BitsArray.java => util/BitArray.java} (80%) diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/HlsRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/HlsRendererBuilder.java index 778a026228..9451add3cb 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/HlsRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/HlsRendererBuilder.java @@ -15,8 +15,6 @@ */ package com.google.android.exoplayer.demo.full.player; -import com.google.android.exoplayer.DefaultLoadControl; -import com.google.android.exoplayer.LoadControl; import com.google.android.exoplayer.MediaCodecAudioTrackRenderer; import com.google.android.exoplayer.MediaCodecVideoTrackRenderer; import com.google.android.exoplayer.TrackRenderer; @@ -25,13 +23,13 @@ import com.google.android.exoplayer.demo.full.player.DemoPlayer.RendererBuilder; import com.google.android.exoplayer.demo.full.player.DemoPlayer.RendererBuilderCallback; import com.google.android.exoplayer.hls.HlsChunkSource; import com.google.android.exoplayer.hls.HlsMasterPlaylist; -import com.google.android.exoplayer.hls.HlsMasterPlaylist.Variant; import com.google.android.exoplayer.hls.HlsMasterPlaylistParser; import com.google.android.exoplayer.hls.HlsSampleSource; +import com.google.android.exoplayer.hls.Variant; import com.google.android.exoplayer.metadata.Id3Parser; import com.google.android.exoplayer.metadata.MetadataTrackRenderer; -import com.google.android.exoplayer.upstream.BufferPool; import com.google.android.exoplayer.upstream.DataSource; +import com.google.android.exoplayer.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer.upstream.UriDataSource; import com.google.android.exoplayer.util.ManifestFetcher; import com.google.android.exoplayer.util.ManifestFetcher.ManifestCallback; @@ -47,9 +45,6 @@ import java.util.Collections; */ public class HlsRendererBuilder implements RendererBuilder, ManifestCallback { - private static final int BUFFER_SEGMENT_SIZE = 64 * 1024; - private static final int VIDEO_BUFFER_SEGMENTS = 200; - private final String userAgent; private final String url; private final String contentId; @@ -89,12 +84,12 @@ public class HlsRendererBuilder implements RendererBuilder, ManifestCallback { - private static final int BUFFER_SEGMENT_SIZE = 64 * 1024; - private static final int VIDEO_BUFFER_SEGMENTS = 200; - private final SimplePlayerActivity playerActivity; private final String userAgent; private final String url; @@ -90,12 +85,11 @@ import java.util.Collections; @Override public void onManifest(String contentId, HlsMasterPlaylist manifest) { - LoadControl loadControl = new DefaultLoadControl(new BufferPool(BUFFER_SEGMENT_SIZE)); - - DataSource dataSource = new UriDataSource(userAgent, null); - HlsChunkSource chunkSource = new HlsChunkSource(dataSource, manifest); - HlsSampleSource sampleSource = new HlsSampleSource(chunkSource, loadControl, - VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true, 2); + DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(); + DataSource dataSource = new UriDataSource(userAgent, bandwidthMeter); + HlsChunkSource chunkSource = new HlsChunkSource(dataSource, manifest, bandwidthMeter, null, + false); + HlsSampleSource sampleSource = new HlsSampleSource(chunkSource, true, 2); MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(sampleSource, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 0, playerActivity.getMainHandler(), playerActivity, 50); @@ -109,7 +103,7 @@ import java.util.Collections; private HlsMasterPlaylist newSimpleMasterPlaylist(String mediaPlaylistUrl) { return new HlsMasterPlaylist(Uri.parse(""), - Collections.singletonList(new Variant(mediaPlaylistUrl, 0, null, -1, -1))); + Collections.singletonList(new Variant(0, mediaPlaylistUrl, 0, null, -1, -1))); } } diff --git a/library/src/main/java/com/google/android/exoplayer/hls/BitArrayChunk.java b/library/src/main/java/com/google/android/exoplayer/hls/BitArrayChunk.java new file mode 100644 index 0000000000..cc56691441 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/hls/BitArrayChunk.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer.hls; + +import com.google.android.exoplayer.upstream.DataSource; +import com.google.android.exoplayer.upstream.DataSpec; +import com.google.android.exoplayer.util.BitArray; + +import java.io.IOException; + +/** + * An abstract base class for {@link HlsChunk} implementations where the data should be loaded into + * a {@link BitArray} and subsequently consumed. + */ +public abstract class BitArrayChunk extends HlsChunk { + + private static final int READ_GRANULARITY = 16 * 1024; + + private final BitArray bitArray; + private volatile boolean loadFinished; + private volatile boolean loadCanceled; + + /** + * @param dataSource The source from which the data should be loaded. + * @param dataSpec Defines the data to be loaded. {@code dataSpec.length} must not exceed + * {@link Integer#MAX_VALUE}. If {@code dataSpec.length == C.LENGTH_UNBOUNDED} then + * the length resolved by {@code dataSource.open(dataSpec)} must not exceed + * {@link Integer#MAX_VALUE}. + * @param bitArray The {@link BitArray} into which the data should be loaded. + */ + public BitArrayChunk(DataSource dataSource, DataSpec dataSpec, BitArray bitArray) { + super(dataSource, dataSpec); + this.bitArray = bitArray; + } + + @Override + public void consume() throws IOException { + consume(bitArray); + } + + /** + * Invoked by {@link #consume()}. Implementations should override this method to consume the + * loaded data. + * + * @param bitArray The {@link BitArray} containing the loaded data. + * @throws IOException If an error occurs consuming the loaded data. + */ + protected abstract void consume(BitArray bitArray) throws IOException; + + /** + * Whether the whole of the chunk has been loaded. + * + * @return True if the whole of the chunk has been loaded. False otherwise. + */ + @Override + public boolean isLoadFinished() { + return loadFinished; + } + + // Loadable implementation + + @Override + public final void cancelLoad() { + loadCanceled = true; + } + + @Override + public final boolean isLoadCanceled() { + return loadCanceled; + } + + @Override + public final void load() throws IOException, InterruptedException { + try { + bitArray.reset(); + dataSource.open(dataSpec); + int bytesRead = 0; + while (bytesRead != -1 && !loadCanceled) { + bytesRead = bitArray.append(dataSource, READ_GRANULARITY); + } + loadFinished = !loadCanceled; + } finally { + dataSource.close(); + } + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/hls/HlsChunk.java b/library/src/main/java/com/google/android/exoplayer/hls/HlsChunk.java index 256921f9ea..4fe1a18646 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/HlsChunk.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/HlsChunk.java @@ -15,38 +15,21 @@ */ package com.google.android.exoplayer.hls; -import com.google.android.exoplayer.C; -import com.google.android.exoplayer.upstream.Allocation; -import com.google.android.exoplayer.upstream.Allocator; import com.google.android.exoplayer.upstream.DataSource; -import com.google.android.exoplayer.upstream.DataSourceStream; import com.google.android.exoplayer.upstream.DataSpec; import com.google.android.exoplayer.upstream.Loader.Loadable; -import com.google.android.exoplayer.upstream.NonBlockingInputStream; import com.google.android.exoplayer.util.Assertions; import java.io.IOException; /** * An abstract base class for {@link Loadable} implementations that load chunks of data required - * for the playback of streams. - *

- * TODO: Figure out whether this should merge with the chunk package, or whether the hls - * implementation is going to naturally diverge. + * for the playback of HLS streams. */ public abstract class HlsChunk implements Loadable { - /** - * The reason for a {@link HlsChunkSource} having generated this chunk. For reporting only. - * Possible values for this variable are defined by the specific {@link HlsChunkSource} - * implementations. - */ - public final int trigger; - - private final DataSource dataSource; - private final DataSpec dataSpec; - - private DataSourceStream dataSourceStream; + protected final DataSource dataSource; + protected final DataSpec dataSpec; /** * @param dataSource The source from which the data should be loaded. @@ -54,123 +37,15 @@ public abstract class HlsChunk implements Loadable { * {@link Integer#MAX_VALUE}. If {@code dataSpec.length == C.LENGTH_UNBOUNDED} then * the length resolved by {@code dataSource.open(dataSpec)} must not exceed * {@link Integer#MAX_VALUE}. - * @param trigger See {@link #trigger}. */ - public HlsChunk(DataSource dataSource, DataSpec dataSpec, int trigger) { + public HlsChunk(DataSource dataSource, DataSpec dataSpec) { Assertions.checkState(dataSpec.length <= Integer.MAX_VALUE); this.dataSource = Assertions.checkNotNull(dataSource); this.dataSpec = Assertions.checkNotNull(dataSpec); - this.trigger = trigger; } - /** - * Initializes the {@link HlsChunk}. - * - * @param allocator An {@link Allocator} from which the {@link Allocation} needed to contain the - * data can be obtained. - */ - public final void init(Allocator allocator) { - Assertions.checkState(dataSourceStream == null); - dataSourceStream = new DataSourceStream(dataSource, dataSpec, allocator); - } + public abstract void consume() throws IOException; - /** - * Releases the {@link HlsChunk}, releasing any backing {@link Allocation}s. - */ - public final void release() { - if (dataSourceStream != null) { - dataSourceStream.close(); - dataSourceStream = null; - } - } - - /** - * Gets the length of the chunk in bytes. - * - * @return The length of the chunk in bytes, or {@link C#LENGTH_UNBOUNDED} if the length has yet - * to be determined. - */ - public final long getLength() { - return dataSourceStream.getLength(); - } - - /** - * Whether the whole of the data has been consumed. - * - * @return True if the whole of the data has been consumed. False otherwise. - */ - public final boolean isReadFinished() { - return dataSourceStream.isEndOfStream(); - } - - /** - * Whether the whole of the chunk has been loaded. - * - * @return True if the whole of the chunk has been loaded. False otherwise. - */ - public final boolean isLoadFinished() { - return dataSourceStream.isLoadFinished(); - } - - /** - * Gets the number of bytes that have been loaded. - * - * @return The number of bytes that have been loaded. - */ - public final long bytesLoaded() { - return dataSourceStream.getLoadPosition(); - } - - /** - * Causes loaded data to be consumed. - * - * @throws IOException If an error occurs consuming the loaded data. - */ - public final void consume() throws IOException { - Assertions.checkState(dataSourceStream != null); - consumeStream(dataSourceStream); - } - - /** - * Invoked by {@link #consume()}. Implementations may override this method if they wish to - * consume the loaded data at this point. - *

- * The default implementation is a no-op. - * - * @param stream The stream of loaded data. - * @throws IOException If an error occurs consuming the loaded data. - */ - protected void consumeStream(NonBlockingInputStream stream) throws IOException { - // Do nothing. - } - - protected final NonBlockingInputStream getNonBlockingInputStream() { - return dataSourceStream; - } - - protected final void resetReadPosition() { - if (dataSourceStream != null) { - dataSourceStream.resetReadPosition(); - } else { - // We haven't been initialized yet, so the read position must already be 0. - } - } - - // Loadable implementation - - @Override - public final void cancelLoad() { - dataSourceStream.cancelLoad(); - } - - @Override - public final boolean isLoadCanceled() { - return dataSourceStream.isLoadCanceled(); - } - - @Override - public final void load() throws IOException, InterruptedException { - dataSourceStream.load(); - } + public abstract boolean isLoadFinished(); } diff --git a/library/src/main/java/com/google/android/exoplayer/hls/HlsChunkOperationHolder.java b/library/src/main/java/com/google/android/exoplayer/hls/HlsChunkOperationHolder.java deleted file mode 100644 index 27b11c2ebd..0000000000 --- a/library/src/main/java/com/google/android/exoplayer/hls/HlsChunkOperationHolder.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (C) 2014 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer.hls; - -/** - * Holds a hls chunk operation, which consists of a {@link HlsChunk} to load together with the - * number of {@link TsChunk}s that should be retained on the queue. - *

- * TODO: Figure out whether this should merge with the chunk package, or whether the hls - * implementation is going to naturally diverge. - */ -public final class HlsChunkOperationHolder { - - /** - * The number of {@link TsChunk}s to retain in a queue. - */ - public int queueSize; - - /** - * The chunk. - */ - public HlsChunk chunk; - -} 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 2dc1158cd5..06cf2a68d2 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,10 +18,12 @@ package com.google.android.exoplayer.hls; import com.google.android.exoplayer.C; import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.TrackRenderer; +import com.google.android.exoplayer.hls.TsExtractor.SamplePool; import com.google.android.exoplayer.upstream.Aes128DataSource; +import com.google.android.exoplayer.upstream.BandwidthMeter; import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DataSpec; -import com.google.android.exoplayer.upstream.NonBlockingInputStream; +import com.google.android.exoplayer.util.BitArray; import com.google.android.exoplayer.util.Util; import android.net.Uri; @@ -30,6 +32,8 @@ import android.os.SystemClock; import java.io.ByteArrayInputStream; import java.io.IOException; import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Locale; @@ -41,26 +45,66 @@ import java.util.Locale; */ public class HlsChunkSource { + private static final float BANDWIDTH_FRACTION = 0.8f; + private static final long MIN_BUFFER_TO_SWITCH_UP_US = 5000000; + private static final long MAX_BUFFER_TO_SWITCH_DOWN_US = 15000000; + + private final SamplePool samplePool = new TsExtractor.SamplePool(); private final DataSource upstreamDataSource; - private final HlsMasterPlaylist masterPlaylist; private final HlsMediaPlaylistParser mediaPlaylistParser; + private final Variant[] enabledVariants; + private final BandwidthMeter bandwidthMeter; + private final BitArray bitArray; + private final boolean enableAdaptive; + private final Uri baseUri; + private final int maxWidth; + private final int maxHeight; - /* package */ HlsMediaPlaylist mediaPlaylist; - /* package */ boolean mediaPlaylistWasLive; - /* package */ long lastMediaPlaylistLoadTimeMs; + /* package */ final HlsMediaPlaylist[] mediaPlaylists; + /* package */ final long[] lastMediaPlaylistLoadTimesMs; + /* package */ boolean live; + /* package */ long durationUs; + private int variantIndex; private DataSource encryptedDataSource; private String encryptionKeyUri; - // TODO: Once proper m3u8 parsing is in place, actually use the url! - public HlsChunkSource(DataSource dataSource, HlsMasterPlaylist masterPlaylist) { + /** + * @param dataSource A {@link DataSource} suitable for loading the media data. + * @param masterPlaylist The master playlist. + * @param variantIndices A subset of variant indices to consider, or null to consider all of the + * variants in the master playlist. + */ + public HlsChunkSource(DataSource dataSource, HlsMasterPlaylist masterPlaylist, + BandwidthMeter bandwidthMeter, int[] variantIndices, boolean enableAdaptive) { this.upstreamDataSource = dataSource; - this.masterPlaylist = masterPlaylist; + this.bandwidthMeter = bandwidthMeter; + this.enableAdaptive = enableAdaptive; + baseUri = masterPlaylist.baseUri; + bitArray = new BitArray(); mediaPlaylistParser = new HlsMediaPlaylistParser(); + enabledVariants = filterVariants(masterPlaylist, variantIndices); + lastMediaPlaylistLoadTimesMs = new long[enabledVariants.length]; + mediaPlaylists = new HlsMediaPlaylist[enabledVariants.length]; + int maxWidth = -1; + int maxHeight = -1; + // Select the first variant from the master playlist that's enabled. + long minOriginalVariantIndex = Integer.MAX_VALUE; + for (int i = 0; i < enabledVariants.length; i++) { + if (enabledVariants[i].index < minOriginalVariantIndex) { + minOriginalVariantIndex = enabledVariants[i].index; + variantIndex = i; + } + maxWidth = Math.max(enabledVariants[i].width, maxWidth); + maxHeight = Math.max(enabledVariants[i].width, maxHeight); + } + // TODO: We should allow the default values to be passed through the constructor. + this.maxWidth = maxWidth > 0 ? maxWidth : 1920; + this.maxHeight = maxHeight > 0 ? maxHeight : 1080; } public long getDurationUs() { - return mediaPlaylistWasLive ? TrackRenderer.UNKNOWN_TIME_US : mediaPlaylist.durationUs; + return live ? TrackRenderer.UNKNOWN_TIME_US : durationUs; } /** @@ -72,49 +116,33 @@ public class HlsChunkSource { * @param out The {@link MediaFormat} on which the maximum video dimensions should be set. */ public void getMaxVideoDimensions(MediaFormat out) { - // TODO: Implement this. + out.setMaxVideoDimensions(maxWidth, maxHeight); } /** - * Updates the provided {@link HlsChunkOperationHolder} to contain the next operation that should - * be performed by the calling {@link HlsSampleSource}. - *

- * The next operation comprises of a possibly shortened queue length (shortened if the - * implementation wishes for the caller to discard {@link TsChunk}s from the queue), together - * with the next {@link HlsChunk} to load. The next chunk may be a {@link TsChunk} to be added to - * the queue, or another {@link HlsChunk} type (e.g. to load initialization data), or null if the - * source is not able to provide a chunk in its current state. + * Returns the next {@link HlsChunk} that should be loaded. * - * @param queue A representation of the currently buffered {@link TsChunk}s. - * @param seekPositionUs If the queue is empty, this parameter must specify the seek position. If - * the queue is non-empty then this parameter is ignored. + * @param previousTsChunk The previously loaded chunk that the next chunk should follow. + * @param seekPositionUs If there is no previous chunk, this parameter must specify the seek + * position. If there is a previous chunk then this parameter is ignored. * @param playbackPositionUs The current playback position. - * @param out A holder for the next operation, whose {@link HlsChunkOperationHolder#queueSize} is - * initially equal to the length of the queue, and whose {@linkHls ChunkOperationHolder#chunk} - * is initially equal to null or a {@link TsChunk} previously supplied by the - * {@link HlsChunkSource} that the caller has not yet finished loading. In the latter case the - * chunk can either be replaced or left unchanged. Note that leaving the chunk unchanged is - * both preferred and more efficient than replacing it with a new but identical chunk. + * @return The next chunk to load. */ - public void getChunkOperation(List queue, long seekPositionUs, long playbackPositionUs, - HlsChunkOperationHolder out) { - if (out.chunk != null) { - // We already have a chunk. Keep it. - return; - } + public HlsChunk getChunkOperation(TsChunk previousTsChunk, long seekPositionUs, + long playbackPositionUs) { + HlsMediaPlaylist mediaPlaylist = mediaPlaylists[variantIndex]; if (mediaPlaylist == null) { - out.chunk = newMediaPlaylistChunk(); - return; + return newMediaPlaylistChunk(); } int chunkMediaSequence = 0; - if (mediaPlaylistWasLive) { - if (queue.isEmpty()) { + if (live) { + if (previousTsChunk == null) { chunkMediaSequence = getLiveStartChunkMediaSequence(); } else { // For live nextChunkIndex contains chunk media sequence number. - chunkMediaSequence = queue.get(queue.size() - 1).nextChunkIndex; + chunkMediaSequence = previousTsChunk.nextChunkIndex; // If the updated playlist is far ahead and doesn't even have the last chunk from the // queue, then try to catch up, skip a few chunks and start as if it was a new playlist. if (chunkMediaSequence < mediaPlaylist.mediaSequence) { @@ -124,28 +152,26 @@ public class HlsChunkSource { } } else { // Not live. - if (queue.isEmpty()) { + if (previousTsChunk == null) { chunkMediaSequence = Util.binarySearchFloor(mediaPlaylist.segments, seekPositionUs, true, true) + mediaPlaylist.mediaSequence; } else { - chunkMediaSequence = queue.get(queue.size() - 1).nextChunkIndex; + chunkMediaSequence = previousTsChunk.nextChunkIndex; } } if (chunkMediaSequence == -1) { - out.chunk = null; - return; + // We've reached the end of the stream. + return null; } int chunkIndex = chunkMediaSequence - mediaPlaylist.mediaSequence; - // If the end of the playlist is reached. if (chunkIndex >= mediaPlaylist.segments.size()) { if (mediaPlaylist.live && shouldRerequestMediaPlaylist()) { - out.chunk = newMediaPlaylistChunk(); + return newMediaPlaylistChunk(); } else { - out.chunk = null; + return null; } - return; } HlsMediaPlaylist.Segment segment = mediaPlaylist.segments.get(chunkIndex); @@ -156,97 +182,204 @@ public class HlsChunkSource { if (!segment.encryptionKeyUri.equals(encryptionKeyUri)) { // Encryption is specified and the key has changed. Uri keyUri = Util.getMergedUri(mediaPlaylist.baseUri, segment.encryptionKeyUri); - out.chunk = newEncryptionKeyChunk(keyUri, segment.encryptionIV); + HlsChunk toReturn = newEncryptionKeyChunk(keyUri, segment.encryptionIV); encryptionKeyUri = segment.encryptionKeyUri; - return; + return toReturn; } } else { encryptedDataSource = null; encryptionKeyUri = null; } - DataSpec dataSpec = new DataSpec(chunkUri, 0, C.LENGTH_UNBOUNDED, null); - long startTimeUs; + boolean splicingIn = previousTsChunk != null && previousTsChunk.splicingOut; int nextChunkMediaSequence = chunkMediaSequence + 1; - if (mediaPlaylistWasLive) { - if (queue.isEmpty()) { + if (live) { + if (previousTsChunk == null) { startTimeUs = 0; + } else if (splicingIn) { + startTimeUs = previousTsChunk.startTimeUs; } else { - startTimeUs = queue.get(queue.size() - 1).endTimeUs; + startTimeUs = previousTsChunk.endTimeUs; } } else { // Not live. startTimeUs = segment.startTimeUs; - if (chunkIndex == mediaPlaylist.segments.size() - 1) { - nextChunkMediaSequence = -1; - } } + if (!mediaPlaylist.live && chunkIndex == mediaPlaylist.segments.size() - 1) { + nextChunkMediaSequence = -1; + } + long endTimeUs = startTimeUs + (long) (segment.durationSecs * 1000000); + int currentVariantIndex = variantIndex; + boolean splicingOut = false; + if (splicingIn) { + // Do nothing. + } else if (enableAdaptive && nextChunkMediaSequence != -1) { + int idealVariantIndex = getVariantIndexForBandwdith( + (int) (bandwidthMeter.getBitrateEstimate() * BANDWIDTH_FRACTION)); + long bufferedUs = startTimeUs - playbackPositionUs; + if ((idealVariantIndex > currentVariantIndex && bufferedUs < MAX_BUFFER_TO_SWITCH_DOWN_US) + || (idealVariantIndex < currentVariantIndex && bufferedUs > MIN_BUFFER_TO_SWITCH_UP_US)) { + variantIndex = idealVariantIndex; + } + splicingOut = variantIndex != currentVariantIndex; + if (splicingOut) { + // If we're splicing out, we want to load the same chunk again next time, but for a + // different variant. + nextChunkMediaSequence = chunkMediaSequence; + } + } + + // Configure the datasource for loading the chunk. DataSource dataSource; if (encryptedDataSource != null) { dataSource = encryptedDataSource; } else { dataSource = upstreamDataSource; } - out.chunk = new TsChunk(dataSource, dataSpec, 0, 0, startTimeUs, endTimeUs, - nextChunkMediaSequence, segment.discontinuity, false); + DataSpec dataSpec = new DataSpec(chunkUri, 0, C.LENGTH_UNBOUNDED, null); + + // Configure the extractor that will read the chunk. + TsExtractor extractor; + if (previousTsChunk == null || splicingIn || segment.discontinuity) { + extractor = new TsExtractor(startTimeUs, samplePool); + } else { + extractor = previousTsChunk.extractor; + } + if (splicingOut) { + extractor.discardFromNextKeyframes(); + } + + return new TsChunk(dataSource, dataSpec, extractor, enabledVariants[currentVariantIndex].index, + startTimeUs, endTimeUs, nextChunkMediaSequence, splicingOut); + } + + private int getVariantIndexForBandwdith(int bandwidth) { + for (int i = 0; i < enabledVariants.length - 1; i++) { + if (enabledVariants[i].bandwidth <= bandwidth) { + return i; + } + } + return enabledVariants.length - 1; } private boolean shouldRerequestMediaPlaylist() { // Don't re-request media playlist more often than one-half of the target duration. + HlsMediaPlaylist mediaPlaylist = mediaPlaylists[variantIndex]; long timeSinceLastMediaPlaylistLoadMs = - SystemClock.elapsedRealtime() - lastMediaPlaylistLoadTimeMs; + SystemClock.elapsedRealtime() - lastMediaPlaylistLoadTimesMs[variantIndex]; return timeSinceLastMediaPlaylistLoadMs >= (mediaPlaylist.targetDurationSecs * 1000) / 2; } private int getLiveStartChunkMediaSequence() { // For live start playback from the third chunk from the end. + HlsMediaPlaylist mediaPlaylist = mediaPlaylists[variantIndex]; int chunkIndex = mediaPlaylist.segments.size() > 3 ? mediaPlaylist.segments.size() - 3 : 0; return chunkIndex + mediaPlaylist.mediaSequence; } private MediaPlaylistChunk newMediaPlaylistChunk() { - Uri mediaPlaylistUri = Util.getMergedUri(masterPlaylist.baseUri, - masterPlaylist.variants.get(0).url); + Uri mediaPlaylistUri = Util.getMergedUri(baseUri, enabledVariants[variantIndex].url); DataSpec dataSpec = new DataSpec(mediaPlaylistUri, 0, C.LENGTH_UNBOUNDED, null); - Uri mediaPlaylistBaseUri = Util.parseBaseUri(mediaPlaylistUri.toString()); - return new MediaPlaylistChunk(upstreamDataSource, dataSpec, 0, mediaPlaylistBaseUri); + Uri baseUri = Util.parseBaseUri(mediaPlaylistUri.toString()); + return new MediaPlaylistChunk(variantIndex, upstreamDataSource, dataSpec, baseUri); } private EncryptionKeyChunk newEncryptionKeyChunk(Uri keyUri, String iv) { DataSpec dataSpec = new DataSpec(keyUri, 0, C.LENGTH_UNBOUNDED, null); - return new EncryptionKeyChunk(upstreamDataSource, dataSpec, 0, iv); + return new EncryptionKeyChunk(upstreamDataSource, dataSpec, iv); } - private class MediaPlaylistChunk extends HlsChunk { + private static Variant[] filterVariants(HlsMasterPlaylist masterPlaylist, int[] variantIndices) { + List masterVariants = masterPlaylist.variants; + ArrayList enabledVariants = new ArrayList(); + if (variantIndices != null) { + for (int i = 0; i < variantIndices.length; i++) { + enabledVariants.add(masterVariants.get(variantIndices[i])); + } + } else { + // If variantIndices is null then all variants are initially considered. + enabledVariants.addAll(masterVariants); + } - private final Uri baseUri; + ArrayList definiteVideoVariants = new ArrayList(); + ArrayList definiteAudioOnlyVariants = new ArrayList(); + for (int i = 0; i < enabledVariants.size(); i++) { + Variant variant = enabledVariants.get(i); + if (variant.height > 0 || variantHasExplicitCodecWithPrefix(variant, "avc")) { + definiteVideoVariants.add(variant); + } else if (variantHasExplicitCodecWithPrefix(variant, "mp4a")) { + definiteAudioOnlyVariants.add(variant); + } + } - public MediaPlaylistChunk(DataSource dataSource, DataSpec dataSpec, int trigger, Uri baseUri) { - super(dataSource, dataSpec, trigger); - this.baseUri = baseUri; + if (!definiteVideoVariants.isEmpty()) { + // We've identified some variants as definitely containing video. Assume variants within the + // master playlist are marked consistently, and hence that we have the full set. Filter out + // any other variants, which are likely to be audio only. + enabledVariants = definiteVideoVariants; + } else if (definiteAudioOnlyVariants.size() < enabledVariants.size()) { + // We've identified some variants, but not all, as being audio only. Filter them out to leave + // the remaining variants, which are likely to contain video. + enabledVariants.removeAll(definiteAudioOnlyVariants); + } else { + // Leave the enabled variants unchanged. They're likely either all video or all audio. + } + + Collections.sort(enabledVariants, new Variant.DecreasingBandwidthComparator()); + + Variant[] enabledVariantsArray = new Variant[enabledVariants.size()]; + enabledVariants.toArray(enabledVariantsArray); + return enabledVariantsArray; + } + + private static boolean variantHasExplicitCodecWithPrefix(Variant variant, String prefix) { + String[] codecs = variant.codecs; + if (codecs == null) { + return false; + } + for (int i = 0; i < codecs.length; i++) { + if (codecs[i].startsWith(prefix)) { + return true; + } + } + return false; + } + + private class MediaPlaylistChunk extends BitArrayChunk { + + @SuppressWarnings("hiding") + private final int variantIndex; + private final Uri playlistBaseUri; + + public MediaPlaylistChunk(int variantIndex, DataSource dataSource, DataSpec dataSpec, + Uri playlistBaseUri) { + super(dataSource, dataSpec, bitArray); + this.variantIndex = variantIndex; + this.playlistBaseUri = playlistBaseUri; } @Override - protected void consumeStream(NonBlockingInputStream stream) throws IOException { - byte[] data = new byte[(int) stream.getAvailableByteCount()]; - stream.read(data, 0, data.length); - lastMediaPlaylistLoadTimeMs = SystemClock.elapsedRealtime(); - mediaPlaylist = mediaPlaylistParser.parse( - new ByteArrayInputStream(data), null, null, baseUri); - mediaPlaylistWasLive |= mediaPlaylist.live; + protected void consume(BitArray data) throws IOException { + HlsMediaPlaylist mediaPlaylist = mediaPlaylistParser.parse( + new ByteArrayInputStream(data.getData(), 0, data.bytesLeft()), null, null, + playlistBaseUri); + mediaPlaylists[variantIndex] = mediaPlaylist; + lastMediaPlaylistLoadTimesMs[variantIndex] = SystemClock.elapsedRealtime(); + live |= mediaPlaylist.live; + durationUs = mediaPlaylist.durationUs; } } - private class EncryptionKeyChunk extends HlsChunk { + private class EncryptionKeyChunk extends BitArrayChunk { private final String iv; - public EncryptionKeyChunk(DataSource dataSource, DataSpec dataSpec, int trigger, String iv) { - super(dataSource, dataSpec, trigger); + public EncryptionKeyChunk(DataSource dataSource, DataSpec dataSpec, String iv) { + super(dataSource, dataSpec, bitArray); if (iv.toLowerCase(Locale.getDefault()).startsWith("0x")) { this.iv = iv.substring(2); } else { @@ -255,9 +388,9 @@ public class HlsChunkSource { } @Override - protected void consumeStream(NonBlockingInputStream stream) throws IOException { - byte[] keyData = new byte[(int) stream.getAvailableByteCount()]; - stream.read(keyData, 0, keyData.length); + protected void consume(BitArray data) throws IOException { + byte[] secretKey = new byte[data.bytesLeft()]; + data.readBytes(secretKey, 0, secretKey.length); int ivParsed = Integer.parseInt(iv, 16); String iv = String.format("%032X", ivParsed); @@ -267,7 +400,7 @@ public class HlsChunkSource { System.arraycopy(ivData, 0, ivDataWithPadding, ivDataWithPadding.length - ivData.length, ivData.length); - encryptedDataSource = new Aes128DataSource(keyData, ivDataWithPadding, upstreamDataSource); + encryptedDataSource = new Aes128DataSource(secretKey, ivDataWithPadding, upstreamDataSource); } } diff --git a/library/src/main/java/com/google/android/exoplayer/hls/HlsMasterPlaylist.java b/library/src/main/java/com/google/android/exoplayer/hls/HlsMasterPlaylist.java index 8d7ff1bc61..ade40ebb3a 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/HlsMasterPlaylist.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/HlsMasterPlaylist.java @@ -24,25 +24,6 @@ import java.util.List; */ public final class HlsMasterPlaylist { - /** - * Variant stream reference. - */ - public static final class Variant { - public final int bandwidth; - public final String url; - public final String[] codecs; - public final int width; - public final int height; - - public Variant(String url, int bandwidth, String[] codecs, int width, int height) { - this.bandwidth = bandwidth; - this.url = url; - this.codecs = codecs; - this.width = width; - this.height = height; - } - } - public final Uri baseUri; public final List variants; diff --git a/library/src/main/java/com/google/android/exoplayer/hls/HlsMasterPlaylistParser.java b/library/src/main/java/com/google/android/exoplayer/hls/HlsMasterPlaylistParser.java index d0cb58baa9..3b2ed82b7c 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/HlsMasterPlaylistParser.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/HlsMasterPlaylistParser.java @@ -15,7 +15,6 @@ */ package com.google.android.exoplayer.hls; -import com.google.android.exoplayer.hls.HlsMasterPlaylist.Variant; import com.google.android.exoplayer.util.ManifestParser; import android.net.Uri; @@ -61,6 +60,7 @@ public final class HlsMasterPlaylistParser implements ManifestParser - * TODO: Figure out whether this should merge with the chunk package, or whether the hls - * implementation is going to naturally diverge. */ public class HlsSampleSource implements SampleSource, Loader.Callback { - private static final long MAX_SAMPLE_INTERLEAVING_OFFSET_US = 5000000; + private static final long BUFFER_DURATION_US = 20000000; private static final int NO_RESET_PENDING = -1; - private final TsExtractor.SamplePool samplePool; - private final LoadControl loadControl; private final HlsChunkSource chunkSource; - private final HlsChunkOperationHolder currentLoadableHolder; private final LinkedList extractors; - private final LinkedList mediaChunks; - private final List readOnlyHlsChunks; - private final int bufferSizeContribution; private final boolean frameAccurateSeeking; private int remainingReleaseCount; @@ -70,7 +54,10 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { private long downstreamPositionUs; private long lastSeekPositionUs; private long pendingResetPositionUs; - private long lastPerformedBufferOperation; + + private TsChunk previousTsLoadable; + private HlsChunk currentLoadable; + private boolean loadingFinished; private Loader loader; private IOException currentLoadableException; @@ -78,18 +65,12 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { private int currentLoadableExceptionCount; private long currentLoadableExceptionTimestamp; - public HlsSampleSource(HlsChunkSource chunkSource, LoadControl loadControl, - int bufferSizeContribution, boolean frameAccurateSeeking, int downstreamRendererCount) { + public HlsSampleSource(HlsChunkSource chunkSource, + boolean frameAccurateSeeking, int downstreamRendererCount) { this.chunkSource = chunkSource; - this.loadControl = loadControl; - this.bufferSizeContribution = bufferSizeContribution; this.frameAccurateSeeking = frameAccurateSeeking; this.remainingReleaseCount = downstreamRendererCount; - samplePool = new TsExtractor.SamplePool(); extractors = new LinkedList(); - currentLoadableHolder = new HlsChunkOperationHolder(); - mediaChunks = new LinkedList(); - readOnlyHlsChunks = Collections.unmodifiableList(mediaChunks); } @Override @@ -99,13 +80,12 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { } if (loader == null) { loader = new Loader("Loader:HLS"); - loadControl.register(this, bufferSizeContribution); } continueBufferingInternal(); if (extractors.isEmpty()) { return false; } - TsExtractor extractor = extractors.get(0); + TsExtractor extractor = extractors.getFirst(); if (extractor.isPrepared()) { trackCount = extractor.getTrackCount(); trackEnabledStates = new boolean[trackCount]; @@ -156,8 +136,9 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { if (loader.isLoading()) { loader.cancelLoading(); } else { - clearHlsChunks(); + discardExtractors(); clearCurrentLoadable(); + previousTsLoadable = null; } } } @@ -171,50 +152,15 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { } private boolean continueBufferingInternal() throws IOException { - updateLoadControl(); - if (isPendingReset()) { + maybeStartLoading(); + if (isPendingReset() || extractors.isEmpty()) { return false; } - - TsChunk mediaChunk = mediaChunks.getFirst(); - int currentVariant = mediaChunk.variantIndex; - - TsExtractor extractor; - if (extractors.isEmpty()) { - extractor = new TsExtractor(mediaChunk.startTimeUs, samplePool); - extractors.addLast(extractor); - if (mediaChunk.discardFromFirstKeyframes) { - extractor.discardFromNextKeyframes(); - } - } else { - extractor = extractors.getLast(); - } - - if (mediaChunk.isReadFinished() && mediaChunks.size() > 1) { - discardDownstreamHlsChunk(); - mediaChunk = mediaChunks.getFirst(); - if (mediaChunk.discontinuity || mediaChunk.variantIndex != currentVariant) { - extractor = new TsExtractor(mediaChunk.startTimeUs, samplePool); - extractors.addLast(extractor); - } - if (mediaChunk.discardFromFirstKeyframes) { - extractor.discardFromNextKeyframes(); - } - } - - // Allow the extractor to consume from the current chunk. - NonBlockingInputStream inputStream = mediaChunk.getNonBlockingInputStream(); - boolean haveSufficientSamples = extractor.consumeUntil(inputStream, - downstreamPositionUs + MAX_SAMPLE_INTERLEAVING_OFFSET_US); - if (!haveSufficientSamples) { - // If we can't read any more, then we always say we have sufficient samples. - haveSufficientSamples = mediaChunk.isLastChunk() && mediaChunk.isReadFinished(); - } - - if (!haveSufficientSamples && currentLoadableException != null) { + boolean haveSamples = extractors.getFirst().hasSamples(); + if (!haveSamples && currentLoadableException != null) { throw currentLoadableException; } - return haveSufficientSamples; + return haveSamples; } @Override @@ -228,11 +174,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { return DISCONTINUITY_READ; } - if (onlyReadDiscontinuity || isPendingReset()) { - return NOTHING_READ; - } - - if (extractors.isEmpty()) { + if (onlyReadDiscontinuity || isPendingReset() || extractors.isEmpty()) { return NOTHING_READ; } @@ -266,12 +208,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { return SAMPLE_READ; } - TsChunk mediaChunk = mediaChunks.getFirst(); - if (mediaChunk.isLastChunk() && mediaChunk.isReadFinished()) { - return END_OF_STREAM; - } - - return NOTHING_READ; + return loadingFinished ? END_OF_STREAM : NOTHING_READ; } @Override @@ -283,32 +220,10 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { if (pendingResetPositionUs == positionUs) { return; } - for (int i = 0; i < pendingDiscontinuities.length; i++) { pendingDiscontinuities[i] = true; } - TsChunk mediaChunk = getHlsChunk(positionUs); - if (mediaChunk == null) { - restartFrom(positionUs); - } else { - discardExtractors(); - discardDownstreamHlsChunks(mediaChunk); - mediaChunk.resetReadPosition(); - updateLoadControl(); - } - } - - private TsChunk getHlsChunk(long positionUs) { - Iterator mediaChunkIterator = mediaChunks.iterator(); - while (mediaChunkIterator.hasNext()) { - TsChunk mediaChunk = mediaChunkIterator.next(); - if (positionUs < mediaChunk.startTimeUs) { - return null; - } else if (mediaChunk.isLastChunk() || positionUs < mediaChunk.endTimeUs) { - return mediaChunk; - } - } - return null; + restartFrom(positionUs); } @Override @@ -317,22 +232,10 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { Assertions.checkState(enabledTrackCount > 0); if (isPendingReset()) { return pendingResetPositionUs; - } - TsChunk mediaChunk = mediaChunks.getLast(); - HlsChunk currentLoadable = currentLoadableHolder.chunk; - if (currentLoadable != null && mediaChunk == currentLoadable) { - // Linearly interpolate partially-fetched chunk times. - long chunkLength = mediaChunk.getLength(); - if (chunkLength != C.LENGTH_UNBOUNDED) { - return mediaChunk.startTimeUs + ((mediaChunk.endTimeUs - mediaChunk.startTimeUs) - * mediaChunk.bytesLoaded()) / chunkLength; - } else { - return mediaChunk.startTimeUs; - } - } else if (mediaChunk.isLastChunk()) { + } else if (loadingFinished) { return TrackRenderer.END_OF_TRACK_US; } else { - return mediaChunk.endTimeUs; + return extractors.getLast().getLargestSampleTimestamp(); } } @@ -340,7 +243,6 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { public void release() { Assertions.checkState(remainingReleaseCount > 0); if (--remainingReleaseCount == 0 && loader != null) { - loadControl.unregister(this); loader.release(); loader = null; } @@ -348,7 +250,6 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { @Override public void onLoadCompleted(Loadable loadable) { - HlsChunk currentLoadable = currentLoadableHolder.chunk; try { currentLoadable.consume(); } catch (IOException e) { @@ -357,28 +258,24 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { currentLoadableExceptionTimestamp = SystemClock.elapsedRealtime(); currentLoadableExceptionFatal = true; } finally { - if (!isTsChunk(currentLoadable)) { - currentLoadable.release(); + if (isTsChunk(currentLoadable)) { + TsChunk tsChunk = (TsChunk) loadable; + loadingFinished = tsChunk.isLastChunk(); } if (!currentLoadableExceptionFatal) { clearCurrentLoadable(); } - updateLoadControl(); + maybeStartLoading(); } } @Override public void onLoadCanceled(Loadable loadable) { - HlsChunk currentLoadable = currentLoadableHolder.chunk; - if (!isTsChunk(currentLoadable)) { - currentLoadable.release(); - } clearCurrentLoadable(); if (enabledTrackCount > 0) { restartFrom(pendingResetPositionUs); } else { - clearHlsChunks(); - loadControl.trimAllocator(); + previousTsLoadable = null; } } @@ -387,142 +284,65 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { currentLoadableException = e; currentLoadableExceptionCount++; currentLoadableExceptionTimestamp = SystemClock.elapsedRealtime(); - updateLoadControl(); + maybeStartLoading(); } private void restartFrom(long positionUs) { pendingResetPositionUs = positionUs; + previousTsLoadable = null; + loadingFinished = false; + discardExtractors(); if (loader.isLoading()) { loader.cancelLoading(); } else { - clearHlsChunks(); - clearCurrentLoadable(); - updateLoadControl(); - } - } - - private void clearHlsChunks() { - discardDownstreamHlsChunks(null); - } - - private void clearCurrentLoadable() { - currentLoadableHolder.chunk = null; - currentLoadableException = null; - currentLoadableExceptionCount = 0; - currentLoadableExceptionFatal = false; - } - - private void updateLoadControl() { - if (currentLoadableExceptionFatal) { - // We've failed, but we still need to update the control with our current state. - loadControl.update(this, downstreamPositionUs, -1, false, true); - return; - } - - long loadPositionUs; - if (isPendingReset()) { - loadPositionUs = pendingResetPositionUs; - } else { - TsChunk lastHlsChunk = mediaChunks.getLast(); - loadPositionUs = lastHlsChunk.nextChunkIndex == -1 ? -1 : lastHlsChunk.endTimeUs; - } - - boolean isBackedOff = currentLoadableException != null; - boolean nextLoader = loadControl.update(this, downstreamPositionUs, loadPositionUs, - isBackedOff || loader.isLoading(), false); - - long now = SystemClock.elapsedRealtime(); - - if (isBackedOff) { - long elapsedMillis = now - currentLoadableExceptionTimestamp; - if (elapsedMillis >= getRetryDelayMillis(currentLoadableExceptionCount)) { - resumeFromBackOff(); - } - return; - } - - if (!loader.isLoading()) { - if (currentLoadableHolder.chunk == null || now - lastPerformedBufferOperation > 1000) { - lastPerformedBufferOperation = now; - currentLoadableHolder.queueSize = readOnlyHlsChunks.size(); - chunkSource.getChunkOperation(readOnlyHlsChunks, pendingResetPositionUs, - downstreamPositionUs, currentLoadableHolder); - discardUpstreamHlsChunks(currentLoadableHolder.queueSize); - } - if (nextLoader) { - maybeStartLoading(); - } - } - } - - /** - * Resumes loading. - *

- * If the {@link HlsChunkSource} returns a chunk equivalent to the backed off chunk B, then the - * loading of B will be resumed. In all other cases B will be discarded and the new chunk will - * be loaded. - */ - private void resumeFromBackOff() { - currentLoadableException = null; - - HlsChunk backedOffChunk = currentLoadableHolder.chunk; - if (!isTsChunk(backedOffChunk)) { - currentLoadableHolder.queueSize = readOnlyHlsChunks.size(); - chunkSource.getChunkOperation(readOnlyHlsChunks, pendingResetPositionUs, downstreamPositionUs, - currentLoadableHolder); - discardUpstreamHlsChunks(currentLoadableHolder.queueSize); - if (currentLoadableHolder.chunk == backedOffChunk) { - // HlsChunk was unchanged. Resume loading. - loader.startLoading(backedOffChunk, this); - } else { - backedOffChunk.release(); - maybeStartLoading(); - } - return; - } - - if (backedOffChunk == mediaChunks.getFirst()) { - // We're not able to clear the first media chunk, so we have no choice but to continue - // loading it. - loader.startLoading(backedOffChunk, this); - return; - } - - // The current loadable is the last media chunk. Remove it before we invoke the chunk source, - // and add it back again afterwards. - TsChunk removedChunk = mediaChunks.removeLast(); - Assertions.checkState(backedOffChunk == removedChunk); - currentLoadableHolder.queueSize = readOnlyHlsChunks.size(); - chunkSource.getChunkOperation(readOnlyHlsChunks, pendingResetPositionUs, downstreamPositionUs, - currentLoadableHolder); - mediaChunks.add(removedChunk); - - if (currentLoadableHolder.chunk == backedOffChunk) { - // HlsChunk was unchanged. Resume loading. - loader.startLoading(backedOffChunk, this); - } else { - // This call will remove and release at least one chunk from the end of mediaChunks. Since - // the current loadable is the last media chunk, it is guaranteed to be removed. - discardUpstreamHlsChunks(currentLoadableHolder.queueSize); clearCurrentLoadable(); maybeStartLoading(); } } + private void clearCurrentLoadable() { + currentLoadable = null; + currentLoadableException = null; + currentLoadableExceptionCount = 0; + currentLoadableExceptionFatal = false; + } + private void maybeStartLoading() { - HlsChunk currentLoadable = currentLoadableHolder.chunk; - if (currentLoadable == null) { - // Nothing to load. + if (currentLoadableExceptionFatal || loadingFinished) { return; } - currentLoadable.init(loadControl.getAllocator()); + + boolean isBackedOff = currentLoadableException != null; + if (isBackedOff) { + long elapsedMillis = SystemClock.elapsedRealtime() - currentLoadableExceptionTimestamp; + if (elapsedMillis >= getRetryDelayMillis(currentLoadableExceptionCount)) { + currentLoadableException = null; + loader.startLoading(currentLoadable, this); + } + return; + } + + boolean bufferFull = !extractors.isEmpty() && (extractors.getLast().getLargestSampleTimestamp() + - downstreamPositionUs) >= BUFFER_DURATION_US; + if (loader.isLoading() || bufferFull) { + return; + } + + HlsChunk nextLoadable = chunkSource.getChunkOperation(previousTsLoadable, + pendingResetPositionUs, downstreamPositionUs); + if (nextLoadable == null) { + return; + } + + currentLoadable = nextLoadable; if (isTsChunk(currentLoadable)) { - TsChunk mediaChunk = (TsChunk) currentLoadable; - mediaChunks.add(mediaChunk); + previousTsLoadable = (TsChunk) currentLoadable; if (isPendingReset()) { - discardExtractors(); pendingResetPositionUs = NO_RESET_PENDING; } + if (extractors.isEmpty() || extractors.getLast() != previousTsLoadable.extractor) { + extractors.addLast(previousTsLoadable.extractor); + } } loader.startLoading(currentLoadable, this); } @@ -534,39 +354,6 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { extractors.clear(); } - /** - * Discards downstream media chunks until {@code untilChunk} if found. {@code untilChunk} is not - * itself discarded. Null can be passed to discard all media chunks. - * - * @param untilChunk The first media chunk to keep, or null to discard all media chunks. - */ - private void discardDownstreamHlsChunks(TsChunk untilChunk) { - if (mediaChunks.isEmpty() || untilChunk == mediaChunks.getFirst()) { - return; - } - while (!mediaChunks.isEmpty() && untilChunk != mediaChunks.getFirst()) { - mediaChunks.removeFirst().release(); - } - } - - /** - * Discards the first downstream media chunk. - */ - private void discardDownstreamHlsChunk() { - mediaChunks.removeFirst().release(); - } - - /** - * Discard upstream media chunks until the queue length is equal to the length specified. - * - * @param queueLength The desired length of the queue. - */ - private void discardUpstreamHlsChunks(int queueLength) { - while (mediaChunks.size() > queueLength) { - mediaChunks.removeLast().release(); - } - } - private boolean isTsChunk(HlsChunk chunk) { return chunk instanceof TsChunk; } diff --git a/library/src/main/java/com/google/android/exoplayer/hls/TsChunk.java b/library/src/main/java/com/google/android/exoplayer/hls/TsChunk.java index e69b8c88e0..9222317839 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/TsChunk.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/TsChunk.java @@ -15,9 +15,12 @@ */ package com.google.android.exoplayer.hls; +import com.google.android.exoplayer.C; import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DataSpec; +import java.io.IOException; + /** * A MPEG2TS chunk. */ @@ -40,40 +43,87 @@ public final class TsChunk extends HlsChunk { */ public final int nextChunkIndex; /** - * The encoding discontinuity indicator. + * True if this is the final chunk being loaded for the current variant, as we splice to another + * one. False otherwise. */ - public final boolean discontinuity; + public final boolean splicingOut; /** - * For each track, whether samples from the first keyframe (inclusive) should be discarded. + * The extractor into which this chunk is being consumed. */ - public final boolean discardFromFirstKeyframes; + public final TsExtractor extractor; + + private volatile int loadPosition; + private volatile boolean loadFinished; + private volatile boolean loadCanceled; /** * @param dataSource A {@link DataSource} for loading the data. * @param dataSpec Defines the data to be loaded. - * @param trigger The reason for this chunk being selected. * @param variantIndex The index of the variant in the master playlist. * @param startTimeUs The start time of the media contained by the chunk, in microseconds. * @param endTimeUs The end time of the media contained by the chunk, in microseconds. * @param nextChunkIndex The index of the next chunk, or -1 if this is the last chunk. - * @param discontinuity The encoding discontinuity indicator. - * @param discardFromFirstKeyframes For each contained media stream, whether samples from the - * first keyframe (inclusive) should be discarded. + * @param splicingOut True if this is the final chunk being loaded for the current variant, as we + * splice to another one. False otherwise. */ - public TsChunk(DataSource dataSource, DataSpec dataSpec, int trigger, int variantIndex, - long startTimeUs, long endTimeUs, int nextChunkIndex, boolean discontinuity, - boolean discardFromFirstKeyframes) { - super(dataSource, dataSpec, trigger); + public TsChunk(DataSource dataSource, DataSpec dataSpec, TsExtractor tsExtractor, + int variantIndex, long startTimeUs, long endTimeUs, int nextChunkIndex, boolean splicingOut) { + super(dataSource, dataSpec); + this.extractor = tsExtractor; this.variantIndex = variantIndex; this.startTimeUs = startTimeUs; this.endTimeUs = endTimeUs; this.nextChunkIndex = nextChunkIndex; - this.discontinuity = discontinuity; - this.discardFromFirstKeyframes = discardFromFirstKeyframes; + this.splicingOut = splicingOut; + } + + @Override + public void consume() throws IOException { + // Do nothing. } public boolean isLastChunk() { return nextChunkIndex == -1; } + @Override + public boolean isLoadFinished() { + return loadFinished; + } + + // Loadable implementation + + @Override + public void cancelLoad() { + loadCanceled = true; + } + + @Override + public boolean isLoadCanceled() { + return loadCanceled; + } + + @Override + public void load() throws IOException, InterruptedException { + DataSpec loadDataSpec; + if (loadPosition == 0) { + loadDataSpec = dataSpec; + } else { + long remainingLength = dataSpec.length != C.LENGTH_UNBOUNDED + ? dataSpec.length - loadPosition : C.LENGTH_UNBOUNDED; + loadDataSpec = new DataSpec(dataSpec.uri, dataSpec.position + loadPosition, + remainingLength, dataSpec.key); + } + try { + dataSource.open(loadDataSpec); + int bytesRead = 0; + while (bytesRead != -1 && !loadCanceled) { + bytesRead = extractor.read(dataSource); + } + loadFinished = !loadCanceled; + } finally { + dataSource.close(); + } + } + } diff --git a/library/src/main/java/com/google/android/exoplayer/parser/ts/TsExtractor.java b/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java similarity index 72% rename from library/src/main/java/com/google/android/exoplayer/parser/ts/TsExtractor.java rename to library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java index d305aa4f31..e2ccabf569 100644 --- a/library/src/main/java/com/google/android/exoplayer/parser/ts/TsExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java @@ -13,25 +13,27 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer.parser.ts; +package com.google.android.exoplayer.hls; import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.SampleHolder; -import com.google.android.exoplayer.upstream.NonBlockingInputStream; +import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.util.Assertions; +import com.google.android.exoplayer.util.BitArray; import com.google.android.exoplayer.util.CodecSpecificDataUtil; import com.google.android.exoplayer.util.MimeTypes; import android.annotation.SuppressLint; -import android.media.MediaCodec; import android.media.MediaExtractor; import android.util.Log; import android.util.Pair; import android.util.SparseArray; +import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedList; +import java.util.List; import java.util.Queue; /** @@ -49,15 +51,15 @@ public final class TsExtractor { private static final int TS_STREAM_TYPE_H264 = 0x1B; private static final int TS_STREAM_TYPE_ID3 = 0x15; - private final BitsArray tsPacketBuffer; + private final BitArray tsPacketBuffer; private final SparseArray pesPayloadReaders; // Indexed by streamType private final SparseArray tsPayloadReaders; // Indexed by pid private final SamplePool samplePool; + /* package */ final long firstSampleTimestamp; private boolean prepared; /* package */ boolean pendingFirstSampleTimestampAdjustment; - /* package */ long firstSampleTimestamp; /* package */ long sampleTimestampOffsetUs; /* package */ long largestParsedTimestampUs; /* package */ boolean discardFromNextKeyframes; @@ -66,7 +68,7 @@ public final class TsExtractor { this.firstSampleTimestamp = firstSampleTimestamp; this.samplePool = samplePool; pendingFirstSampleTimestampAdjustment = true; - tsPacketBuffer = new BitsArray(); + tsPacketBuffer = new BitArray(); pesPayloadReaders = new SparseArray(); tsPayloadReaders = new SparseArray(); tsPayloadReaders.put(TS_PAT_PID, new PatReader()); @@ -117,31 +119,19 @@ public final class TsExtractor { } /** - * For each track, whether to discard samples from the next keyframe (inclusive). + * For each track, discards samples from the next keyframe (inclusive). */ public void discardFromNextKeyframes() { discardFromNextKeyframes = true; } /** - * Consumes data from a {@link NonBlockingInputStream}. - *

- * The read terminates if the end of the input stream is reached, if insufficient data is - * available to read a sample, or if the extractor has consumed up to the specified target - * timestamp. + * Gets the largest timestamp of any sample parsed by the extractor. * - * @param inputStream The input stream from which data should be read. - * @param targetTimestampUs A target timestamp to consume up to. - * @return True if the target timestamp was reached. False otherwise. + * @return The largest timestamp, or {@link Long#MIN_VALUE} if no samples have been parsed. */ - public boolean consumeUntil(NonBlockingInputStream inputStream, long targetTimestampUs) { - while (largestParsedTimestampUs < targetTimestampUs && readTSPacket(inputStream) != -1) { - // Carry on. - } - if (!prepared) { - prepared = checkPrepared(); - } - return largestParsedTimestampUs >= targetTimestampUs; + public long getLargestSampleTimestamp() { + return largestParsedTimestampUs; } /** @@ -204,23 +194,29 @@ public final class TsExtractor { } /** - * Read a single TS packet. + * Reads up to a single TS packet. + * + * @param dataSource The {@link DataSource} from which to read. + * @throws IOException If an error occurred reading from the source. + * @return The number of bytes read from the source. */ - private int readTSPacket(NonBlockingInputStream inputStream) { - // Read entire single TS packet. - if (inputStream.getAvailableByteCount() < TS_PACKET_SIZE) { + public int read(DataSource dataSource) throws IOException { + int read = tsPacketBuffer.append(dataSource, TS_PACKET_SIZE - tsPacketBuffer.bytesLeft()); + if (read == -1) { return -1; } - tsPacketBuffer.reset(); - tsPacketBuffer.append(inputStream, TS_PACKET_SIZE); + if (tsPacketBuffer.bytesLeft() != TS_PACKET_SIZE) { + return read; + } // Parse TS header. // Check sync byte. int syncByte = tsPacketBuffer.readUnsignedByte(); if (syncByte != TS_SYNC_BYTE) { - return 0; + return read; } + // Skip transportErrorIndicator. tsPacketBuffer.skipBits(1); boolean payloadUnitStartIndicator = tsPacketBuffer.readBit(); @@ -243,12 +239,17 @@ public final class TsExtractor { // Read Payload. if (payloadExists) { TsPayloadReader payloadReader = tsPayloadReaders.get(pid); - if (payloadReader == null) { - return 0; + if (payloadReader != null) { + payloadReader.read(tsPacketBuffer, payloadUnitStartIndicator); } - payloadReader.read(tsPacketBuffer, payloadUnitStartIndicator); } - return 0; + + if (!prepared) { + prepared = checkPrepared(); + } + + tsPacketBuffer.reset(); + return read; } private void convert(Sample in, SampleHolder out) { @@ -268,7 +269,7 @@ public final class TsExtractor { */ private abstract static class TsPayloadReader { - public abstract void read(BitsArray tsBuffer, boolean payloadUnitStartIndicator); + public abstract void read(BitArray tsBuffer, boolean payloadUnitStartIndicator); } @@ -278,7 +279,7 @@ public final class TsExtractor { private class PatReader extends TsPayloadReader { @Override - public void read(BitsArray tsBuffer, boolean payloadUnitStartIndicator) { + public void read(BitArray tsBuffer, boolean payloadUnitStartIndicator) { // Skip pointer. if (payloadUnitStartIndicator) { int pointerField = tsBuffer.readBits(8); @@ -311,7 +312,7 @@ public final class TsExtractor { private class PmtReader extends TsPayloadReader { @Override - public void read(BitsArray tsBuffer, boolean payloadUnitStartIndicator) { + public void read(BitArray tsBuffer, boolean payloadUnitStartIndicator) { // Skip pointer. if (payloadUnitStartIndicator) { int pointerField = tsBuffer.readBits(8); @@ -323,10 +324,10 @@ public final class TsExtractor { int sectionLength = tsBuffer.readBits(12); // Skip the rest of the PMT header. tsBuffer.skipBits(60); // 16+2+5+1+8+8+3+13+4 - int programInfoLength = tsBuffer.readBits(12); - // Read descriptors. - readDescriptors(tsBuffer, programInfoLength); + int programInfoLength = tsBuffer.readBits(12); + // Skip the descriptors. + tsBuffer.skipBytes(programInfoLength); int entriesSize = sectionLength - 9 /* size of the rest of the fields before descriptors */ - programInfoLength - 4 /* CRC size */; @@ -335,9 +336,10 @@ public final class TsExtractor { tsBuffer.skipBits(3); int elementaryPid = tsBuffer.readBits(13); tsBuffer.skipBits(4); - int esInfoLength = tsBuffer.readBits(12); - readDescriptors(tsBuffer, esInfoLength); + int esInfoLength = tsBuffer.readBits(12); + // Skip the descriptors. + tsBuffer.skipBytes(esInfoLength); entriesSize -= esInfoLength + 5; if (pesPayloadReaders.get(streamType) != null) { @@ -366,19 +368,6 @@ public final class TsExtractor { // Skip CRC_32. } - private void readDescriptors(BitsArray tsBuffer, int descriptorsSize) { - while (descriptorsSize > 0) { - // Skip tag. - tsBuffer.skipBits(8); - int descriptorsLength = tsBuffer.readBits(8); - if (descriptorsLength > 0) { - // Skip entire descriptor data. - tsBuffer.skipBytes(descriptorsLength); - } - descriptorsSize -= descriptorsSize + 2; - } - } - } /** @@ -387,7 +376,7 @@ public final class TsExtractor { private class PesReader extends TsPayloadReader { // Reusable buffer for incomplete PES data. - private final BitsArray pesBuffer; + private final BitArray pesBuffer; // Parses PES payload and extracts individual samples. private final PesPayloadReader pesPayloadReader; @@ -396,11 +385,11 @@ public final class TsExtractor { public PesReader(PesPayloadReader pesPayloadReader) { this.pesPayloadReader = pesPayloadReader; this.packetLength = -1; - pesBuffer = new BitsArray(); + pesBuffer = new BitArray(); } @Override - public void read(BitsArray tsBuffer, boolean payloadUnitStartIndicator) { + public void read(BitArray tsBuffer, boolean payloadUnitStartIndicator) { if (payloadUnitStartIndicator && !pesBuffer.isEmpty()) { // We've encountered the start of the next packet, but haven't yet read the body. Read it. // Note that this should only happen if the packet length was unspecified. @@ -484,7 +473,7 @@ public final class TsExtractor { */ private abstract class PesPayloadReader { - public final Queue sampleQueue; + public final LinkedList sampleQueue; private MediaFormat mediaFormat; private boolean foundFirstKeyframe; @@ -506,7 +495,7 @@ public final class TsExtractor { this.mediaFormat = mediaFormat; } - public abstract void read(BitsArray pesBuffer, int pesPayloadSize, long pesTimeUs); + public abstract void read(BitArray pesBuffer, int pesPayloadSize, long pesTimeUs); public void clear() { while (!sampleQueue.isEmpty()) { @@ -521,7 +510,7 @@ public final class TsExtractor { * @param sampleSize The size of the sample data. * @param sampleTimeUs The sample time stamp. */ - protected void addSample(BitsArray buffer, int sampleSize, long sampleTimeUs, int flags) { + protected void addSample(BitArray buffer, int sampleSize, long sampleTimeUs, int flags) { Sample sample = samplePool.get(); addToSample(sample, buffer, sampleSize); sample.flags = flags; @@ -531,7 +520,7 @@ public final class TsExtractor { @SuppressLint("InlinedApi") protected void addSample(Sample sample) { - boolean isKeyframe = (sample.flags & MediaCodec.BUFFER_FLAG_SYNC_FRAME) != 0; + boolean isKeyframe = (sample.flags & MediaExtractor.SAMPLE_FLAG_SYNC) != 0; if (isKeyframe) { if (!foundFirstKeyframe) { foundFirstKeyframe = true; @@ -549,7 +538,7 @@ public final class TsExtractor { } } - protected void addToSample(Sample sample, BitsArray buffer, int size) { + protected void addToSample(Sample sample, BitArray buffer, int size) { if (sample.data.length - sample.size < size) { sample.expand(size - sample.data.length + sample.size); } @@ -572,22 +561,24 @@ public final class TsExtractor { */ private class H264Reader extends PesPayloadReader { - // IDR picture. private static final int NAL_UNIT_TYPE_IDR = 5; - // Access unit delimiter. private static final int NAL_UNIT_TYPE_AUD = 9; + private static final int NAL_UNIT_TYPE_SPS = 7; // Used to store uncompleted sample data. private Sample currentSample; - public H264Reader() { - // TODO: Parse the format from the stream. - setMediaFormat(MediaFormat.createVideoFormat(MimeTypes.VIDEO_H264, MediaFormat.NO_VALUE, - 1920, 1080, null)); + @Override + public void clear() { + super.clear(); + if (currentSample != null) { + samplePool.recycle(currentSample); + currentSample = null; + } } @Override - public void read(BitsArray pesBuffer, int pesPayloadSize, long pesTimeUs) { + public void read(BitArray pesBuffer, int pesPayloadSize, long pesTimeUs) { // Read leftover frame data from previous PES packet. pesPayloadSize -= readOneH264Frame(pesBuffer, true); @@ -597,6 +588,9 @@ public final class TsExtractor { // Single PES packet should contain only one new H.264 frame. if (currentSample != null) { + if (!hasMediaFormat() && (currentSample.flags & MediaExtractor.SAMPLE_FLAG_SYNC) != 0) { + parseMediaFormat(currentSample); + } addSample(currentSample); } currentSample = samplePool.get(); @@ -609,32 +603,120 @@ public final class TsExtractor { } @SuppressLint("InlinedApi") - private int readOneH264Frame(BitsArray pesBuffer, boolean remainderOnly) { + private int readOneH264Frame(BitArray pesBuffer, boolean remainderOnly) { int offset = remainderOnly ? 0 : 3; int audStart = pesBuffer.findNextNalUnit(NAL_UNIT_TYPE_AUD, offset); - int idrStart = pesBuffer.findNextNalUnit(NAL_UNIT_TYPE_IDR, offset); - if (audStart > 0) { - if (currentSample != null) { - addToSample(currentSample, pesBuffer, audStart); - if (idrStart < audStart) { - currentSample.flags = MediaExtractor.SAMPLE_FLAG_SYNC; - } - } else { - pesBuffer.skipBytes(audStart); + if (currentSample != null) { + int idrStart = pesBuffer.findNextNalUnit(NAL_UNIT_TYPE_IDR, offset); + if (idrStart < audStart) { + currentSample.flags = MediaExtractor.SAMPLE_FLAG_SYNC; } - return audStart; + addToSample(currentSample, pesBuffer, audStart); + } else { + pesBuffer.skipBytes(audStart); } - return 0; + return audStart; } - @Override - public void clear() { - super.clear(); - if (currentSample != null) { - samplePool.recycle(currentSample); - currentSample = null; + private void parseMediaFormat(Sample sample) { + BitArray bitArray = new BitArray(sample.data, sample.size); + // Locate the SPS unit. + int spsOffset = bitArray.findNextNalUnit(NAL_UNIT_TYPE_SPS, 0); + if (spsOffset == bitArray.bytesLeft()) { + return; } + int nextNalOffset = bitArray.findNextNalUnit(-1, spsOffset + 3); + + // Unescape the SPS unit. + byte[] unescapedSps = unescapeStream(bitArray.getData(), spsOffset, nextNalOffset); + bitArray.reset(unescapedSps, unescapedSps.length); + + // Parse the SPS unit + // Skip the NAL header. + bitArray.skipBytes(4); + // TODO: Handle different profiles properly. + bitArray.skipBytes(1); + // Skip 6 constraint bits, 2 reserved bits and level_idc. + bitArray.skipBytes(2); + // Skip seq_parameter_set_id. + bitArray.readExpGolombCodedInt(); + // Skip log2_max_frame_num_minus4 + bitArray.readExpGolombCodedInt(); + long picOrderCntType = bitArray.readExpGolombCodedInt(); + if (picOrderCntType == 0) { + // Skip log2_max_pic_order_cnt_lsb_minus4 + bitArray.readExpGolombCodedInt(); + } else if (picOrderCntType == 1) { + // Skip delta_pic_order_always_zero_flag + bitArray.skipBits(1); + // Skip offset_for_non_ref_pic (actually a signed value, but for skipping we can read it + // as though it were unsigned). + bitArray.readExpGolombCodedInt(); + // Skip offset_for_top_to_bottom_field (actually a signed value, but for skipping we can + // read it as though it were unsigned). + bitArray.readExpGolombCodedInt(); + long numRefFramesInPicOrderCntCycle = bitArray.readExpGolombCodedInt(); + for (int i = 0; i < numRefFramesInPicOrderCntCycle; i++) { + // Skip offset_for_ref_frame[i] + bitArray.readExpGolombCodedInt(); + } + } + // Skip max_num_ref_frames + bitArray.readExpGolombCodedInt(); + // Skip gaps_in_frame_num_value_allowed_flag + bitArray.skipBits(1); + int picWidthInMbs = bitArray.readExpGolombCodedInt() + 1; + int picHeightInMapUnits = bitArray.readExpGolombCodedInt() + 1; + boolean frameMbsOnlyFlag = bitArray.readBit(); + int frameHeightInMbs = (2 - (frameMbsOnlyFlag ? 1 : 0)) * picHeightInMapUnits; + + // Set the format. + setMediaFormat(MediaFormat.createVideoFormat(MimeTypes.VIDEO_H264, MediaFormat.NO_VALUE, + picWidthInMbs * 16, frameHeightInMbs * 16, null)); } + + /** + * Replaces occurrences of [0, 0, 3] with [0, 0]. + *

+ * See ISO/IEC 14496-10:2005(E) page 36 for more information. + */ + private byte[] unescapeStream(byte[] data, int offset, int limit) { + int position = offset; + List escapePositions = new ArrayList(); + while (position < limit) { + position = findNextUnescapeIndex(data, position, limit); + if (position < limit) { + escapePositions.add(position); + position += 3; + } + } + + int escapeCount = escapePositions.size(); + int escapedPosition = offset; // The position being read from. + int unescapedPosition = 0; // The position being written to. + byte[] unescapedData = new byte[limit - offset - escapeCount]; + for (int i = 0; i < escapeCount; i++) { + int nextEscapePosition = escapePositions.get(i); + int copyLength = nextEscapePosition - escapedPosition; + System.arraycopy(data, escapedPosition, unescapedData, unescapedPosition, copyLength); + escapedPosition += copyLength + 3; + unescapedPosition += copyLength + 2; + } + + int remainingLength = unescapedData.length - unescapedPosition; + System.arraycopy(data, escapedPosition, unescapedData, unescapedPosition, remainingLength); + return unescapedData; + } + + private int findNextUnescapeIndex(byte[] bytes, int offset, int limit) { + for (int i = offset; i < limit - 2; i++) { + if (bytes[i] == 0x00 && bytes[i + 1] == 0x00 && bytes[i + 2] == 0x03) { + return i; + } + } + return limit; + } + } /** @@ -642,15 +724,15 @@ public final class TsExtractor { */ private class AdtsReader extends PesPayloadReader { - private final BitsArray adtsBuffer; + private final BitArray adtsBuffer; private long timeUs; public AdtsReader() { - adtsBuffer = new BitsArray(); + adtsBuffer = new BitArray(); } @Override - public void read(BitsArray pesBuffer, int pesPayloadSize, long pesTimeUs) { + public void read(BitArray pesBuffer, int pesPayloadSize, long pesTimeUs) { boolean needToProcessLeftOvers = !adtsBuffer.isEmpty(); adtsBuffer.append(pesBuffer, pesPayloadSize); // If there are leftovers from previous PES packet, process it with last calculated timeUs. @@ -751,7 +833,7 @@ public final class TsExtractor { @SuppressLint("InlinedApi") @Override - public void read(BitsArray pesBuffer, int pesPayloadSize, long pesTimeUs) { + public void read(BitArray pesBuffer, int pesPayloadSize, long pesTimeUs) { addSample(pesBuffer, pesPayloadSize, pesTimeUs, MediaExtractor.SAMPLE_FLAG_SYNC); } @@ -764,31 +846,33 @@ public final class TsExtractor { private static final int DEFAULT_BUFFER_SEGMENT_SIZE = 64 * 1024; - private final ArrayList samples; + private Sample firstInPool; - public SamplePool() { - samples = new ArrayList(); - } - - /* package */ Sample get() { - if (samples.isEmpty()) { + /* package */ synchronized Sample get() { + if (firstInPool == null) { return new Sample(DEFAULT_BUFFER_SEGMENT_SIZE); } - return samples.remove(samples.size() - 1); + Sample sample = firstInPool; + firstInPool = sample.nextInPool; + sample.nextInPool = null; + return sample; } - /* package */ void recycle(Sample sample) { + /* package */ synchronized void recycle(Sample sample) { sample.reset(); - samples.add(sample); + sample.nextInPool = firstInPool; + firstInPool = sample; } } /** - * Simplified version of SampleHolder for internal buffering. + * An internal variant of {@link SampleHolder} for internal pooling and buffering. */ private static class Sample { + public Sample nextInPool; + public byte[] data; public int flags; public int size; diff --git a/library/src/main/java/com/google/android/exoplayer/hls/Variant.java b/library/src/main/java/com/google/android/exoplayer/hls/Variant.java new file mode 100644 index 0000000000..47d0a450a9 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/hls/Variant.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer.hls; + +import java.util.Comparator; + +/** + * Variant stream reference. + */ +public final class Variant { + + /** + * Sorts {@link Variant} objects in order of decreasing bandwidth. + *

+ * When two {@link Variant}s have the same bandwidth, the one with the lowest index comes first. + */ + public static final class DecreasingBandwidthComparator implements Comparator { + + @Override + public int compare(Variant a, Variant b) { + int bandwidthDifference = b.bandwidth - a.bandwidth; + return bandwidthDifference != 0 ? bandwidthDifference : a.index - b.index; + } + + } + + public final int index; + public final int bandwidth; + public final String url; + public final String[] codecs; + public final int width; + public final int height; + + public Variant(int index, String url, int bandwidth, String[] codecs, int width, int height) { + this.index = index; + this.bandwidth = bandwidth; + this.url = url; + this.codecs = codecs; + this.width = width; + this.height = height; + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/metadata/Id3Parser.java b/library/src/main/java/com/google/android/exoplayer/metadata/Id3Parser.java index 518ee0c2bc..ea68984e37 100644 --- a/library/src/main/java/com/google/android/exoplayer/metadata/Id3Parser.java +++ b/library/src/main/java/com/google/android/exoplayer/metadata/Id3Parser.java @@ -16,7 +16,7 @@ package com.google.android.exoplayer.metadata; import com.google.android.exoplayer.ParserException; -import com.google.android.exoplayer.parser.ts.BitsArray; +import com.google.android.exoplayer.util.BitArray; import com.google.android.exoplayer.util.MimeTypes; import java.io.UnsupportedEncodingException; @@ -37,7 +37,7 @@ public class Id3Parser implements MetadataParser { @Override public Map parse(byte[] data, int size) throws UnsupportedEncodingException, ParserException { - BitsArray id3Buffer = new BitsArray(data, size); + BitArray id3Buffer = new BitArray(data, size); int id3Size = parseId3Header(id3Buffer); Map metadata = new HashMap(); @@ -102,11 +102,11 @@ public class Id3Parser implements MetadataParser { /** * Parses ID3 header. - * @param id3Buffer A {@link BitsArray} with raw ID3 data. + * @param id3Buffer A {@link BitArray} with raw ID3 data. * @return The size of data that contains ID3 frames without header and footer. * @throws ParserException If ID3 file identifier != "ID3". */ - private static int parseId3Header(BitsArray id3Buffer) throws ParserException { + private static int parseId3Header(BitArray id3Buffer) throws ParserException { int id1 = id3Buffer.readUnsignedByte(); int id2 = id3Buffer.readUnsignedByte(); int id3 = id3Buffer.readUnsignedByte(); diff --git a/library/src/main/java/com/google/android/exoplayer/metadata/MetadataParser.java b/library/src/main/java/com/google/android/exoplayer/metadata/MetadataParser.java index 1f9b0dcaef..8bfaca8b77 100644 --- a/library/src/main/java/com/google/android/exoplayer/metadata/MetadataParser.java +++ b/library/src/main/java/com/google/android/exoplayer/metadata/MetadataParser.java @@ -19,7 +19,7 @@ import java.io.IOException; import java.util.Map; /** - * Parses {@link Metadata}s from binary data. + * Parses metadata objects from binary data. */ public interface MetadataParser { diff --git a/library/src/main/java/com/google/android/exoplayer/parser/ts/BitsArray.java b/library/src/main/java/com/google/android/exoplayer/util/BitArray.java similarity index 80% rename from library/src/main/java/com/google/android/exoplayer/parser/ts/BitsArray.java rename to library/src/main/java/com/google/android/exoplayer/util/BitArray.java index efd43dbec3..d5c26c0a2c 100644 --- a/library/src/main/java/com/google/android/exoplayer/parser/ts/BitsArray.java +++ b/library/src/main/java/com/google/android/exoplayer/util/BitArray.java @@ -13,15 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer.parser.ts; +package com.google.android.exoplayer.util; -import com.google.android.exoplayer.upstream.NonBlockingInputStream; -import com.google.android.exoplayer.util.Assertions; +import com.google.android.exoplayer.upstream.DataSource; + +import java.io.IOException; /** * Wraps a byte array, providing methods that allow it to be read as a bitstream. */ -public final class BitsArray { +public final class BitArray { private byte[] data; @@ -33,16 +34,16 @@ public final class BitsArray { private int byteOffset; private int bitOffset; - public BitsArray() { + public BitArray() { } - public BitsArray(byte[] data, int limit) { + public BitArray(byte[] data, int limit) { this.data = data; this.limit = limit; } /** - * Resets the state. + * Clears all data, setting the offset and limit to zero. */ public void reset() { byteOffset = 0; @@ -50,6 +51,28 @@ public final class BitsArray { limit = 0; } + /** + * Resets to wrap the specified data, setting the offset to zero. + * + * @param data The data to wrap. + * @param limit The limit to set. + */ + public void reset(byte[] data, int limit) { + this.data = data; + this.limit = limit; + byteOffset = 0; + bitOffset = 0; + } + + /** + * Gets the backing byte array. + * + * @return The backing byte array. + */ + public byte[] getData() { + return data; + } + /** * Gets the current byte offset. * @@ -69,16 +92,16 @@ public final class BitsArray { } /** - * Appends data from a {@link NonBlockingInputStream}. + * Appends data from a {@link DataSource}. * - * @param inputStream The {@link NonBlockingInputStream} whose data should be appended. + * @param dataSource The {@link DataSource} from which to read. * @param length The maximum number of bytes to read and append. - * @return The number of bytes that were read and appended. May be 0 if no data was available - * from the stream. -1 is returned if the end of the stream has been reached. + * @return The number of bytes that were read and appended, or -1 if no more data is available. + * @throws IOException If an error occurs reading from the source. */ - public int append(NonBlockingInputStream inputStream, int length) { + public int append(DataSource dataSource, int length) throws IOException { expand(length); - int bytesRead = inputStream.read(data, limit, length); + int bytesRead = dataSource.read(data, limit, length); if (bytesRead == -1) { return -1; } @@ -87,12 +110,12 @@ public final class BitsArray { } /** - * Appends data from another {@link BitsArray}. + * Appends data from another {@link BitArray}. * - * @param bitsArray The {@link BitsArray} whose data should be appended. + * @param bitsArray The {@link BitArray} whose data should be appended. * @param length The number of bytes to read and append. */ - public void append(BitsArray bitsArray, int length) { + public void append(BitArray bitsArray, int length) { expand(length); bitsArray.readBytes(data, limit, length); limit += length; @@ -256,6 +279,19 @@ public final class BitsArray { return limit == 0; } + /** + * Reads an Exp-Golomb-coded format integer. + * + * @return The value of the parsed Exp-Golomb-coded integer. + */ + public int readExpGolombCodedInt() { + int leadingZeros = 0; + while (!readBit()) { + leadingZeros++; + } + return (1 << leadingZeros) - 1 + (leadingZeros > 0 ? readBits(leadingZeros) : 0); + } + /** * Reads a Synchsafe integer. * Synchsafe integers are integers that keep the highest bit of every byte zeroed. @@ -293,7 +329,7 @@ public final class BitsArray { /** * Finds the next NAL unit. * - * @param nalUnitType The type of the NAL unit to search for. + * @param nalUnitType The type of the NAL unit to search for, or -1 for any NAL unit. * @param offset The additional offset in the data to start the search from. * @return The offset from the current position to the start of the NAL unit. If a NAL unit is * not found, then the offset to the end of the data is returned. @@ -302,7 +338,7 @@ public final class BitsArray { for (int i = byteOffset + offset; i < limit - 3; i++) { // Check for NAL unit start code prefix == 0x000001. if ((data[i] == 0 && data[i + 1] == 0 && data[i + 2] == 1) - && (nalUnitType == (data[i + 3] & 0x1F))) { + && (nalUnitType == -1 || (nalUnitType == (data[i + 3] & 0x1F)))) { return i - byteOffset; } } From 643f33f7e97fb638ba0b779e5faf547a14b55e6a Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 14 Nov 2014 16:31:23 +0000 Subject: [PATCH 17/55] Properly handle different profiles (skipping SPS data as needed). --- .../android/exoplayer/hls/TsExtractor.java | 69 ++++++++++++++----- .../android/exoplayer/util/BitArray.java | 18 ++++- 2 files changed, 68 insertions(+), 19 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java b/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java index e2ccabf569..12ff31539b 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java @@ -634,39 +634,62 @@ public final class TsExtractor { // Parse the SPS unit // Skip the NAL header. bitArray.skipBytes(4); - // TODO: Handle different profiles properly. - bitArray.skipBytes(1); + int profileIdc = bitArray.readBits(8); // Skip 6 constraint bits, 2 reserved bits and level_idc. bitArray.skipBytes(2); // Skip seq_parameter_set_id. - bitArray.readExpGolombCodedInt(); + bitArray.readUnsignedExpGolombCodedInt(); + + if (profileIdc == 100 || profileIdc == 110 || profileIdc == 122 || profileIdc == 244 + || profileIdc == 44 || profileIdc == 83 || profileIdc == 86 || profileIdc == 118 + || profileIdc == 128 || profileIdc == 138) { + int chromaFormatIdc = bitArray.readUnsignedExpGolombCodedInt(); + if (chromaFormatIdc == 3) { + // Skip separate_colour_plane_flag + bitArray.skipBits(1); + } + // Skip bit_depth_luma_minus8 + bitArray.readUnsignedExpGolombCodedInt(); + // Skip bit_depth_chroma_minus8 + bitArray.readUnsignedExpGolombCodedInt(); + // Skip qpprime_y_zero_transform_bypass_flag + bitArray.skipBits(1); + boolean seqScalingMatrixPresentFlag = bitArray.readBit(); + if (seqScalingMatrixPresentFlag) { + int limit = (chromaFormatIdc != 3) ? 8 : 12; + for (int i = 0; i < limit; i++) { + boolean seqScalingListPresentFlag = bitArray.readBit(); + if (seqScalingListPresentFlag) { + skipScalingList(bitArray, i < 6 ? 16 : 64); + } + } + } + } // Skip log2_max_frame_num_minus4 - bitArray.readExpGolombCodedInt(); - long picOrderCntType = bitArray.readExpGolombCodedInt(); + bitArray.readUnsignedExpGolombCodedInt(); + long picOrderCntType = bitArray.readUnsignedExpGolombCodedInt(); if (picOrderCntType == 0) { // Skip log2_max_pic_order_cnt_lsb_minus4 - bitArray.readExpGolombCodedInt(); + bitArray.readUnsignedExpGolombCodedInt(); } else if (picOrderCntType == 1) { // Skip delta_pic_order_always_zero_flag bitArray.skipBits(1); - // Skip offset_for_non_ref_pic (actually a signed value, but for skipping we can read it - // as though it were unsigned). - bitArray.readExpGolombCodedInt(); - // Skip offset_for_top_to_bottom_field (actually a signed value, but for skipping we can - // read it as though it were unsigned). - bitArray.readExpGolombCodedInt(); - long numRefFramesInPicOrderCntCycle = bitArray.readExpGolombCodedInt(); + // Skip offset_for_non_ref_pic + bitArray.readSignedExpGolombCodedInt(); + // Skip offset_for_top_to_bottom_field + bitArray.readSignedExpGolombCodedInt(); + long numRefFramesInPicOrderCntCycle = bitArray.readUnsignedExpGolombCodedInt(); for (int i = 0; i < numRefFramesInPicOrderCntCycle; i++) { // Skip offset_for_ref_frame[i] - bitArray.readExpGolombCodedInt(); + bitArray.readUnsignedExpGolombCodedInt(); } } // Skip max_num_ref_frames - bitArray.readExpGolombCodedInt(); + bitArray.readUnsignedExpGolombCodedInt(); // Skip gaps_in_frame_num_value_allowed_flag bitArray.skipBits(1); - int picWidthInMbs = bitArray.readExpGolombCodedInt() + 1; - int picHeightInMapUnits = bitArray.readExpGolombCodedInt() + 1; + int picWidthInMbs = bitArray.readUnsignedExpGolombCodedInt() + 1; + int picHeightInMapUnits = bitArray.readUnsignedExpGolombCodedInt() + 1; boolean frameMbsOnlyFlag = bitArray.readBit(); int frameHeightInMbs = (2 - (frameMbsOnlyFlag ? 1 : 0)) * picHeightInMapUnits; @@ -675,6 +698,18 @@ public final class TsExtractor { picWidthInMbs * 16, frameHeightInMbs * 16, null)); } + private void skipScalingList(BitArray bitArray, int size) { + int lastScale = 8; + int nextScale = 8; + for (int i = 0; i < size; i++) { + if (nextScale != 0) { + int deltaScale = bitArray.readSignedExpGolombCodedInt(); + nextScale = (lastScale + deltaScale + 256) % 256; + } + lastScale = (nextScale == 0) ? lastScale : nextScale; + } + } + /** * Replaces occurrences of [0, 0, 3] with [0, 0]. *

diff --git a/library/src/main/java/com/google/android/exoplayer/util/BitArray.java b/library/src/main/java/com/google/android/exoplayer/util/BitArray.java index d5c26c0a2c..23cd760b6d 100644 --- a/library/src/main/java/com/google/android/exoplayer/util/BitArray.java +++ b/library/src/main/java/com/google/android/exoplayer/util/BitArray.java @@ -280,11 +280,25 @@ public final class BitArray { } /** - * Reads an Exp-Golomb-coded format integer. + * Reads an unsigned Exp-Golomb-coded format integer. * * @return The value of the parsed Exp-Golomb-coded integer. */ - public int readExpGolombCodedInt() { + public int readUnsignedExpGolombCodedInt() { + return readExpGolombCodeNum(); + } + + /** + * Reads an signed Exp-Golomb-coded format integer. + * + * @return The value of the parsed Exp-Golomb-coded integer. + */ + public int readSignedExpGolombCodedInt() { + int codeNum = readExpGolombCodeNum(); + return ((codeNum % 2) == 0 ? -1 : 1) * ((codeNum + 1) / 2); + } + + private int readExpGolombCodeNum() { int leadingZeros = 0; while (!readBit()) { leadingZeros++; From 6b123590cab361ecee7ebef3b010eff043d6c7a2 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 14 Nov 2014 16:31:47 +0000 Subject: [PATCH 18/55] Correctly propagate errors --- .../android/exoplayer/hls/HlsSampleSource.java | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) 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 c358d25142..fea2fa1a74 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 @@ -165,7 +165,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { @Override public int readData(int track, long playbackPositionUs, MediaFormatHolder formatHolder, - SampleHolder sampleHolder, boolean onlyReadDiscontinuity) { + SampleHolder sampleHolder, boolean onlyReadDiscontinuity) throws IOException { Assertions.checkState(prepared); downstreamPositionUs = playbackPositionUs; @@ -175,6 +175,9 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { } if (onlyReadDiscontinuity || isPendingReset() || extractors.isEmpty()) { + if (currentLoadableException != null) { + throw currentLoadableException; + } return NOTHING_READ; } @@ -192,6 +195,9 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { } if (!extractor.isPrepared()) { + if (currentLoadableException != null) { + throw currentLoadableException; + } return NOTHING_READ; } @@ -208,7 +214,14 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { return SAMPLE_READ; } - return loadingFinished ? END_OF_STREAM : NOTHING_READ; + if (loadingFinished) { + return END_OF_STREAM; + } + + if (currentLoadableException != null) { + throw currentLoadableException; + } + return NOTHING_READ; } @Override From eb1210d410988492865ef1f025c449f256e3eabc Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 14 Nov 2014 18:31:55 +0000 Subject: [PATCH 19/55] Make sampleQueue thread safe --- .../android/exoplayer/hls/TsExtractor.java | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java b/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java index 12ff31539b..d92c69c87e 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java @@ -32,9 +32,9 @@ import android.util.SparseArray; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; -import java.util.LinkedList; import java.util.List; import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; /** * Facilitates the extraction of data from the MPEG-2 TS container format. @@ -144,10 +144,10 @@ public final class TsExtractor { public boolean getSample(int track, SampleHolder out) { Assertions.checkState(prepared); Queue queue = pesPayloadReaders.valueAt(track).sampleQueue; - if (queue.isEmpty()) { + Sample sample = queue.poll(); + if (sample == null) { return false; } - Sample sample = queue.remove(); convert(sample, out); samplePool.recycle(sample); return true; @@ -473,14 +473,14 @@ public final class TsExtractor { */ private abstract class PesPayloadReader { - public final LinkedList sampleQueue; + public final ConcurrentLinkedQueue sampleQueue; private MediaFormat mediaFormat; private boolean foundFirstKeyframe; private boolean foundLastKeyframe; protected PesPayloadReader() { - this.sampleQueue = new LinkedList(); + this.sampleQueue = new ConcurrentLinkedQueue(); } public boolean hasMediaFormat() { @@ -498,8 +498,10 @@ public final class TsExtractor { public abstract void read(BitArray pesBuffer, int pesPayloadSize, long pesTimeUs); public void clear() { - while (!sampleQueue.isEmpty()) { - samplePool.recycle(sampleQueue.remove()); + Sample toRecycle = sampleQueue.poll(); + while (toRecycle != null) { + samplePool.recycle(toRecycle); + toRecycle = sampleQueue.poll(); } } From 360d452dade2dda732bb49d9642787f5b12b0775 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 14 Nov 2014 18:32:22 +0000 Subject: [PATCH 20/55] Avoid seeking if seekPosition==currentPosition --- .../com/google/android/exoplayer/hls/HlsSampleSource.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 fea2fa1a74..19c7f18a0a 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 @@ -228,11 +228,12 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { public void seekToUs(long positionUs) { Assertions.checkState(prepared); Assertions.checkState(enabledTrackCount > 0); - downstreamPositionUs = positionUs; lastSeekPositionUs = positionUs; - if (pendingResetPositionUs == positionUs) { + if (pendingResetPositionUs == positionUs || downstreamPositionUs == positionUs) { + downstreamPositionUs = positionUs; return; } + downstreamPositionUs = positionUs; for (int i = 0; i < pendingDiscontinuities.length; i++) { pendingDiscontinuities[i] = true; } From 467f19568b73b4fa236d8c97ff2fc583d08bf05d Mon Sep 17 00:00:00 2001 From: Jonas Larsson Date: Fri, 14 Nov 2014 14:34:00 -0800 Subject: [PATCH 21/55] TsExtractor: Account for frame cropping when parsing SPS Passing uncropped dimensions to certain decoders will make them output frames without proper cropping set. Signed-off-by: Jonas Larsson --- .../android/exoplayer/hls/TsExtractor.java | 32 +++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java b/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java index d92c69c87e..fc07d9bcc1 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java @@ -642,10 +642,11 @@ public final class TsExtractor { // Skip seq_parameter_set_id. bitArray.readUnsignedExpGolombCodedInt(); + int chromaFormatIdc = 1; // Default is 4:2:0 if (profileIdc == 100 || profileIdc == 110 || profileIdc == 122 || profileIdc == 244 || profileIdc == 44 || profileIdc == 83 || profileIdc == 86 || profileIdc == 118 || profileIdc == 128 || profileIdc == 138) { - int chromaFormatIdc = bitArray.readUnsignedExpGolombCodedInt(); + chromaFormatIdc = bitArray.readUnsignedExpGolombCodedInt(); if (chromaFormatIdc == 3) { // Skip separate_colour_plane_flag bitArray.skipBits(1); @@ -694,10 +695,37 @@ public final class TsExtractor { int picHeightInMapUnits = bitArray.readUnsignedExpGolombCodedInt() + 1; boolean frameMbsOnlyFlag = bitArray.readBit(); int frameHeightInMbs = (2 - (frameMbsOnlyFlag ? 1 : 0)) * picHeightInMapUnits; + if (!frameMbsOnlyFlag) { + // Skip mb_adaptive_frame_field_flag + bitArray.skipBits(1); + } + // Skip direct_8x8_inference_flag + bitArray.skipBits(1); + int frameWidth = picWidthInMbs * 16; + int frameHeight = frameHeightInMbs * 16; + boolean frameCroppingFlag = bitArray.readBit(); + if (frameCroppingFlag) { + int frameCropLeftOffset = bitArray.readUnsignedExpGolombCodedInt(); + int frameCropRightOffset = bitArray.readUnsignedExpGolombCodedInt(); + int frameCropTopOffset = bitArray.readUnsignedExpGolombCodedInt(); + int frameCropBottomOffset = bitArray.readUnsignedExpGolombCodedInt(); + int cropUnitX, cropUnitY; + if (chromaFormatIdc == 0) { + cropUnitX = 1; + cropUnitY = 2 - (frameMbsOnlyFlag ? 1 : 0); + } else { + int subWidthC = (chromaFormatIdc == 3) ? 1 : 2; + int subHeightC = (chromaFormatIdc == 1) ? 2 : 1; + cropUnitX = subWidthC; + cropUnitY = subHeightC * (2 - (frameMbsOnlyFlag ? 1 : 0)); + } + frameWidth -= (frameCropLeftOffset + frameCropRightOffset) * cropUnitX; + frameHeight -= (frameCropTopOffset + frameCropBottomOffset) * cropUnitY; + } // Set the format. setMediaFormat(MediaFormat.createVideoFormat(MimeTypes.VIDEO_H264, MediaFormat.NO_VALUE, - picWidthInMbs * 16, frameHeightInMbs * 16, null)); + frameWidth, frameHeight, null)); } private void skipScalingList(BitArray bitArray, int size) { From 3abcefa00cdc8432a4a79af7bf568d753a9c3ce1 Mon Sep 17 00:00:00 2001 From: Andrey Udovenko Date: Tue, 18 Nov 2014 14:05:34 -0500 Subject: [PATCH 22/55] Add EXT-X-BYTERANGE support #139 --- .../android/exoplayer/demo/Samples.java | 3 +++ .../android/exoplayer/hls/HlsChunkSource.java | 3 ++- .../exoplayer/hls/HlsMediaPlaylist.java | 7 +++++- .../exoplayer/hls/HlsMediaPlaylistParser.java | 22 ++++++++++++++++++- 4 files changed, 32 insertions(+), 3 deletions(-) diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/Samples.java b/demo/src/main/java/com/google/android/exoplayer/demo/Samples.java index 8b1336142c..ee2c3f1cb6 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/Samples.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/Samples.java @@ -138,6 +138,9 @@ package com.google.android.exoplayer.demo; new Sample("Apple master playlist", "uid:hls:applemaster", "https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/" + "bipbop_4x3_variant.m3u8", DemoUtil.TYPE_HLS_MASTER, false, true), + new Sample("Apple master playlist advanced", "uid:hls:applemasteradvanced", + "https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_16x9/" + + "bipbop_16x9_variant.m3u8", DemoUtil.TYPE_HLS_MASTER, false, true), new Sample("Apple single media playlist", "uid:hls:applesinglemedia", "https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/gear1/" + "prog_index.m3u8", DemoUtil.TYPE_HLS_MEDIA, false, true), 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 06cf2a68d2..b58350024f 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 @@ -239,7 +239,8 @@ public class HlsChunkSource { } else { dataSource = upstreamDataSource; } - DataSpec dataSpec = new DataSpec(chunkUri, 0, C.LENGTH_UNBOUNDED, null); + DataSpec dataSpec = new DataSpec(chunkUri, segment.byterangeOffset, segment.byterangeLength, + null); // Configure the extractor that will read the chunk. TsExtractor extractor; 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 7e8136cab3..29df5562df 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 @@ -35,9 +35,12 @@ public final class HlsMediaPlaylist { public final String encryptionMethod; public final String encryptionKeyUri; public final String encryptionIV; + public final int byterangeOffset; + public final int byterangeLength; public Segment(String uri, double durationSecs, boolean discontinuity, long startTimeUs, - String encryptionMethod, String encryptionKeyUri, String encryptionIV) { + String encryptionMethod, String encryptionKeyUri, String encryptionIV, + int byterangeOffset, int byterangeLength) { this.url = uri; this.durationSecs = durationSecs; this.discontinuity = discontinuity; @@ -45,6 +48,8 @@ public final class HlsMediaPlaylist { this.encryptionMethod = encryptionMethod; this.encryptionKeyUri = encryptionKeyUri; this.encryptionIV = encryptionIV; + this.byterangeOffset = byterangeOffset; + this.byterangeLength = byterangeLength; } @Override diff --git a/library/src/main/java/com/google/android/exoplayer/hls/HlsMediaPlaylistParser.java b/library/src/main/java/com/google/android/exoplayer/hls/HlsMediaPlaylistParser.java index 319bf15485..abae63301a 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/HlsMediaPlaylistParser.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/HlsMediaPlaylistParser.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer.hls; +import com.google.android.exoplayer.C; import com.google.android.exoplayer.hls.HlsMediaPlaylist.Segment; import com.google.android.exoplayer.util.ManifestParser; @@ -41,6 +42,7 @@ public final class HlsMediaPlaylistParser implements ManifestParser 1) { + segmentByterangeOffset = Integer.parseInt(splitByteRange[1]); + } } else if (line.equals(DISCONTINUITY_TAG)) { segmentDiscontinuity = true; } else if (!line.startsWith("#")) { segmentMediaSequence++; + if (segmentByterangeLength == C.LENGTH_UNBOUNDED) { + segmentByterangeOffset = 0; + } segments.add(new Segment(line, segmentDurationSecs, segmentDiscontinuity, segmentStartTimeUs, segmentEncryptionMethod, segmentEncryptionKeyUri, - segmentEncryptionIV)); + segmentEncryptionIV, segmentByterangeOffset, segmentByterangeLength)); segmentStartTimeUs += (long) (segmentDurationSecs * 1000000); segmentDiscontinuity = false; segmentDurationSecs = 0.0; + if (segmentByterangeLength != C.LENGTH_UNBOUNDED) { + segmentByterangeOffset += segmentByterangeLength; + } + segmentByterangeLength = C.LENGTH_UNBOUNDED; } else if (line.equals(ENDLIST_TAG)) { live = false; break; From c57484f90a554cc86af5ab7351e8a4569970214c Mon Sep 17 00:00:00 2001 From: Andrey Udovenko Date: Tue, 18 Nov 2014 14:11:02 -0500 Subject: [PATCH 23/55] Fix for IV bigger than 32 bits #145 --- .../com/google/android/exoplayer/hls/HlsChunkSource.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) 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 b58350024f..88c729f495 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 @@ -393,11 +393,8 @@ public class HlsChunkSource { byte[] secretKey = new byte[data.bytesLeft()]; data.readBytes(secretKey, 0, secretKey.length); - int ivParsed = Integer.parseInt(iv, 16); - String iv = String.format("%032X", ivParsed); - byte[] ivData = new BigInteger(iv, 16).toByteArray(); - byte[] ivDataWithPadding = new byte[iv.length() / 2]; + byte[] ivDataWithPadding = new byte[16]; System.arraycopy(ivData, 0, ivDataWithPadding, ivDataWithPadding.length - ivData.length, ivData.length); From 15d3df6a5852e2ba483364d03f9031f106974b5f Mon Sep 17 00:00:00 2001 From: Andrey Udovenko Date: Tue, 18 Nov 2014 14:48:40 -0500 Subject: [PATCH 24/55] Add EIA-608 (CEA-608) Closed Captioning support for HLS #68 --- .../demo/full/FullPlayerActivity.java | 33 +++- .../demo/full/player/DemoPlayer.java | 62 +++++-- .../demo/full/player/HlsRendererBuilder.java | 16 +- .../google/android/exoplayer/MediaFormat.java | 5 + .../android/exoplayer/hls/TsExtractor.java | 111 ++++++++--- .../exoplayer/metadata/ClosedCaption.java | 48 +++++ .../exoplayer/metadata/Eia608Parser.java | 173 ++++++++++++++++++ .../android/exoplayer/metadata/Id3Parser.java | 2 +- .../exoplayer/metadata/MetadataParser.java | 16 +- .../metadata/MetadataTrackRenderer.java | 27 +-- .../android/exoplayer/util/MimeTypes.java | 1 + 11 files changed, 428 insertions(+), 66 deletions(-) create mode 100644 library/src/main/java/com/google/android/exoplayer/metadata/ClosedCaption.java create mode 100644 library/src/main/java/com/google/android/exoplayer/metadata/Eia608Parser.java diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/FullPlayerActivity.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/FullPlayerActivity.java index 03cf5b4e2c..b5ca2e5b49 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/FullPlayerActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/FullPlayerActivity.java @@ -25,6 +25,7 @@ import com.google.android.exoplayer.demo.full.player.DemoPlayer; import com.google.android.exoplayer.demo.full.player.DemoPlayer.RendererBuilder; import com.google.android.exoplayer.demo.full.player.HlsRendererBuilder; import com.google.android.exoplayer.demo.full.player.SmoothStreamingRendererBuilder; +import com.google.android.exoplayer.metadata.ClosedCaption; import com.google.android.exoplayer.metadata.TxxxMetadata; import com.google.android.exoplayer.text.CaptionStyleCompat; import com.google.android.exoplayer.text.SubtitleView; @@ -56,13 +57,15 @@ import android.widget.PopupMenu; import android.widget.PopupMenu.OnMenuItemClickListener; import android.widget.TextView; +import java.util.List; import java.util.Map; /** * An activity that plays media using {@link DemoPlayer}. */ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callback, OnClickListener, - DemoPlayer.Listener, DemoPlayer.TextListener, DemoPlayer.MetadataListener { + DemoPlayer.Listener, DemoPlayer.TextListener, DemoPlayer.Id3MetadataListener, + DemoPlayer.ClosedCaptionListener { private static final String TAG = "FullPlayerActivity"; @@ -196,6 +199,7 @@ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callba player.addListener(this); player.setTextListener(this); player.setMetadataListener(this); + player.setClosedCaptionListener(this); player.seekTo(playerPosition); playerNeedsPrepare = true; mediaController.setMediaPlayer(player.getPlayerControl()); @@ -415,7 +419,7 @@ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callba // DemoPlayer.MetadataListener implementation @Override - public void onMetadata(Map metadata) { + public void onId3Metadata(Map metadata) { for (int i = 0; i < metadata.size(); i++) { if (metadata.containsKey(TxxxMetadata.TYPE)) { TxxxMetadata txxxMetadata = (TxxxMetadata) metadata.get(TxxxMetadata.TYPE); @@ -425,6 +429,31 @@ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callba } } + //DemoPlayer.ClosedCaptioListener implementation + + @Override + public void onClosedCaption(List closedCaptions) { + StringBuilder stringBuilder = new StringBuilder(); + for (ClosedCaption caption : closedCaptions) { + // Ignore control characters and just insert a new line in between words. + if (caption.type == ClosedCaption.TYPE_CTRL) { + if (stringBuilder.length() > 0 + && stringBuilder.charAt(stringBuilder.length() - 1) != '\n') { + stringBuilder.append('\n'); + } + } else if (caption.type == ClosedCaption.TYPE_TEXT) { + stringBuilder.append(caption.text); + } + } + if (stringBuilder.length() > 0 && stringBuilder.charAt(stringBuilder.length() - 1) == '\n') { + stringBuilder.deleteCharAt(stringBuilder.length() - 1); + } + if (stringBuilder.length() > 0) { + subtitleView.setVisibility(View.VISIBLE); + subtitleView.setText(stringBuilder.toString()); + } + } + // SurfaceHolder.Callback implementation @Override diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DemoPlayer.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DemoPlayer.java index ad046fed00..42c10d9a52 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DemoPlayer.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DemoPlayer.java @@ -26,6 +26,7 @@ import com.google.android.exoplayer.TrackRenderer; import com.google.android.exoplayer.chunk.ChunkSampleSource; import com.google.android.exoplayer.chunk.MultiTrackChunkSource; import com.google.android.exoplayer.drm.StreamingDrmSessionManager; +import com.google.android.exoplayer.metadata.ClosedCaption; import com.google.android.exoplayer.metadata.MetadataTrackRenderer; import com.google.android.exoplayer.text.TextTrackRenderer; import com.google.android.exoplayer.upstream.DefaultBandwidthMeter; @@ -37,6 +38,7 @@ import android.os.Looper; import android.view.Surface; import java.io.IOException; +import java.util.List; import java.util.Map; import java.util.concurrent.CopyOnWriteArrayList; @@ -48,7 +50,7 @@ import java.util.concurrent.CopyOnWriteArrayList; public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventListener, DefaultBandwidthMeter.EventListener, MediaCodecVideoTrackRenderer.EventListener, MediaCodecAudioTrackRenderer.EventListener, TextTrackRenderer.TextRenderer, - MetadataTrackRenderer.MetadataRenderer, StreamingDrmSessionManager.EventListener { + StreamingDrmSessionManager.EventListener { /** * Builds renderers for the player. @@ -137,10 +139,17 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi } /** - * A listener for receiving metadata parsed from the media stream. + * A listener for receiving ID3 metadata parsed from the media stream. */ - public interface MetadataListener { - void onMetadata(Map metadata); + public interface Id3MetadataListener { + void onId3Metadata(Map metadata); + } + + /** + * A listener for receiving closed captions parsed from the media stream. + */ + public interface ClosedCaptionListener { + void onClosedCaption(List closedCaptions); } // Constants pulled into this class for convenience. @@ -158,7 +167,8 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi public static final int TYPE_AUDIO = 1; public static final int TYPE_TEXT = 2; public static final int TYPE_TIMED_METADATA = 3; - public static final int TYPE_DEBUG = 4; + public static final int TYPE_CLOSED_CAPTIONS = 4; + public static final int TYPE_DEBUG = 5; private static final int RENDERER_BUILDING_STATE_IDLE = 1; private static final int RENDERER_BUILDING_STATE_BUILDING = 2; @@ -183,7 +193,8 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi private int[] selectedTracks; private TextListener textListener; - private MetadataListener metadataListener; + private Id3MetadataListener id3MetadataListener; + private ClosedCaptionListener closedCaptionListener; private InternalErrorListener internalErrorListener; private InfoListener infoListener; @@ -225,8 +236,12 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi textListener = listener; } - public void setMetadataListener(MetadataListener listener) { - metadataListener = listener; + public void setMetadataListener(Id3MetadataListener listener) { + id3MetadataListener = listener; + } + + public void setClosedCaptionListener(ClosedCaptionListener listener) { + closedCaptionListener = listener; } public void setSurface(Surface surface) { @@ -473,11 +488,32 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi } } - @Override - public void onMetadata(Map metadata) { - if (metadataListener != null) { - metadataListener.onMetadata(metadata); - } + /* package */ MetadataTrackRenderer.MetadataRenderer> + getId3MetadataRenderer() { + return new MetadataTrackRenderer.MetadataRenderer>() { + + @Override + public void onMetadata(Map metadata) { + if (id3MetadataListener != null) { + id3MetadataListener.onId3Metadata(metadata); + } + } + + }; + } + + /* package */ MetadataTrackRenderer.MetadataRenderer> + getClosedCaptionMetadataRenderer() { + return new MetadataTrackRenderer.MetadataRenderer>() { + + @Override + public void onMetadata(List metadata) { + if (closedCaptionListener != null) { + closedCaptionListener.onClosedCaption(metadata); + } + } + + }; } @Override diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/HlsRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/HlsRendererBuilder.java index 9451add3cb..9d1267d9cd 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/HlsRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/HlsRendererBuilder.java @@ -26,6 +26,8 @@ import com.google.android.exoplayer.hls.HlsMasterPlaylist; import com.google.android.exoplayer.hls.HlsMasterPlaylistParser; import com.google.android.exoplayer.hls.HlsSampleSource; import com.google.android.exoplayer.hls.Variant; +import com.google.android.exoplayer.metadata.ClosedCaption; +import com.google.android.exoplayer.metadata.Eia608Parser; import com.google.android.exoplayer.metadata.Id3Parser; import com.google.android.exoplayer.metadata.MetadataTrackRenderer; import com.google.android.exoplayer.upstream.DataSource; @@ -39,6 +41,8 @@ import android.net.Uri; import java.io.IOException; import java.util.Collections; +import java.util.List; +import java.util.Map; /** * A {@link RendererBuilder} for HLS. @@ -94,13 +98,19 @@ public class HlsRendererBuilder implements RendererBuilder, ManifestCallback> id3Renderer = + new MetadataTrackRenderer>(sampleSource, new Id3Parser(), + player.getId3MetadataRenderer(), player.getMainHandler().getLooper()); + + MetadataTrackRenderer> closedCaptionRenderer = + new MetadataTrackRenderer>(sampleSource, new Eia608Parser(), + player.getClosedCaptionMetadataRenderer(), player.getMainHandler().getLooper()); TrackRenderer[] renderers = new TrackRenderer[DemoPlayer.RENDERER_COUNT]; renderers[DemoPlayer.TYPE_VIDEO] = videoRenderer; renderers[DemoPlayer.TYPE_AUDIO] = audioRenderer; - renderers[DemoPlayer.TYPE_TIMED_METADATA] = metadataRenderer; + renderers[DemoPlayer.TYPE_TIMED_METADATA] = id3Renderer; + renderers[DemoPlayer.TYPE_CLOSED_CAPTIONS] = closedCaptionRenderer; callback.onRenderers(null, null, renderers); } diff --git a/library/src/main/java/com/google/android/exoplayer/MediaFormat.java b/library/src/main/java/com/google/android/exoplayer/MediaFormat.java index 7a79e1d5b7..bc1f1670af 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaFormat.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaFormat.java @@ -92,6 +92,11 @@ public class MediaFormat { NO_VALUE, NO_VALUE, NO_VALUE, null); } + public static MediaFormat createEia608Format() { + return new MediaFormat(MimeTypes.APPLICATION_EIA608, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, + NO_VALUE, NO_VALUE, NO_VALUE, null); + } + @TargetApi(16) private MediaFormat(android.media.MediaFormat format) { this.frameworkMediaFormat = format; diff --git a/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java b/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java index d92c69c87e..0e0ac7b445 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer.hls; import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.SampleHolder; +import com.google.android.exoplayer.metadata.Eia608Parser; import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.BitArray; @@ -33,7 +34,6 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; /** @@ -50,9 +50,10 @@ public final class TsExtractor { private static final int TS_STREAM_TYPE_AAC = 0x0F; private static final int TS_STREAM_TYPE_H264 = 0x1B; private static final int TS_STREAM_TYPE_ID3 = 0x15; + private static final int TS_STREAM_TYPE_EIA608 = 0x100; // 0xFF + 1 private final BitArray tsPacketBuffer; - private final SparseArray pesPayloadReaders; // Indexed by streamType + private final SparseArray sampleQueues; // Indexed by streamType private final SparseArray tsPayloadReaders; // Indexed by pid private final SamplePool samplePool; /* package */ final long firstSampleTimestamp; @@ -69,7 +70,7 @@ public final class TsExtractor { this.samplePool = samplePool; pendingFirstSampleTimestampAdjustment = true; tsPacketBuffer = new BitArray(); - pesPayloadReaders = new SparseArray(); + sampleQueues = new SparseArray(); tsPayloadReaders = new SparseArray(); tsPayloadReaders.put(TS_PAT_PID, new PatReader()); largestParsedTimestampUs = Long.MIN_VALUE; @@ -84,7 +85,7 @@ public final class TsExtractor { */ public int getTrackCount() { Assertions.checkState(prepared); - return pesPayloadReaders.size(); + return sampleQueues.size(); } /** @@ -97,7 +98,7 @@ public final class TsExtractor { */ public MediaFormat getFormat(int track) { Assertions.checkState(prepared); - return pesPayloadReaders.valueAt(track).getMediaFormat(); + return sampleQueues.valueAt(track).getMediaFormat(); } /** @@ -113,13 +114,13 @@ public final class TsExtractor { * Flushes any pending or incomplete samples, returning them to the sample pool. */ public void clear() { - for (int i = 0; i < pesPayloadReaders.size(); i++) { - pesPayloadReaders.valueAt(i).clear(); + for (int i = 0; i < sampleQueues.size(); i++) { + sampleQueues.valueAt(i).clear(); } } /** - * For each track, discards samples from the next keyframe (inclusive). + * For each track, discards samples from the next key frame (inclusive). */ public void discardFromNextKeyframes() { discardFromNextKeyframes = true; @@ -143,8 +144,7 @@ public final class TsExtractor { */ public boolean getSample(int track, SampleHolder out) { Assertions.checkState(prepared); - Queue queue = pesPayloadReaders.valueAt(track).sampleQueue; - Sample sample = queue.poll(); + Sample sample = sampleQueues.valueAt(track).poll(); if (sample == null) { return false; } @@ -161,7 +161,7 @@ public final class TsExtractor { * for any track. False otherwise. */ public boolean hasSamples() { - for (int i = 0; i < pesPayloadReaders.size(); i++) { + for (int i = 0; i < sampleQueues.size(); i++) { if (hasSamples(i)) { return true; } @@ -177,16 +177,16 @@ public final class TsExtractor { * for the specified track. False otherwise. */ public boolean hasSamples(int track) { - return !pesPayloadReaders.valueAt(track).sampleQueue.isEmpty(); + return !sampleQueues.valueAt(track).isEmpty(); } private boolean checkPrepared() { - int pesPayloadReaderCount = pesPayloadReaders.size(); + int pesPayloadReaderCount = sampleQueues.size(); if (pesPayloadReaderCount == 0) { return false; } for (int i = 0; i < pesPayloadReaderCount; i++) { - if (!pesPayloadReaders.valueAt(i).hasMediaFormat()) { + if (!sampleQueues.valueAt(i).hasMediaFormat()) { return false; } } @@ -342,7 +342,7 @@ public final class TsExtractor { tsBuffer.skipBytes(esInfoLength); entriesSize -= esInfoLength + 5; - if (pesPayloadReaders.get(streamType) != null) { + if (sampleQueues.get(streamType) != null) { continue; } @@ -352,7 +352,9 @@ public final class TsExtractor { pesPayloadReader = new AdtsReader(); break; case TS_STREAM_TYPE_H264: - pesPayloadReader = new H264Reader(); + SeiReader seiReader = new SeiReader(); + sampleQueues.put(TS_STREAM_TYPE_EIA608, seiReader); + pesPayloadReader = new H264Reader(seiReader); break; case TS_STREAM_TYPE_ID3: pesPayloadReader = new Id3Reader(); @@ -360,7 +362,7 @@ public final class TsExtractor { } if (pesPayloadReader != null) { - pesPayloadReaders.put(streamType, pesPayloadReader); + sampleQueues.put(streamType, pesPayloadReader); tsPayloadReaders.put(elementaryPid, new PesReader(pesPayloadReader)); } } @@ -469,18 +471,18 @@ public final class TsExtractor { } /** - * Extracts individual samples from continuous byte stream. + * A collection of extracted samples. */ - private abstract class PesPayloadReader { + private abstract class SampleQueue { - public final ConcurrentLinkedQueue sampleQueue; + private final ConcurrentLinkedQueue queue; private MediaFormat mediaFormat; private boolean foundFirstKeyframe; private boolean foundLastKeyframe; - protected PesPayloadReader() { - this.sampleQueue = new ConcurrentLinkedQueue(); + protected SampleQueue() { + this.queue = new ConcurrentLinkedQueue(); } public boolean hasMediaFormat() { @@ -495,16 +497,22 @@ public final class TsExtractor { this.mediaFormat = mediaFormat; } - public abstract void read(BitArray pesBuffer, int pesPayloadSize, long pesTimeUs); - public void clear() { - Sample toRecycle = sampleQueue.poll(); + Sample toRecycle = queue.poll(); while (toRecycle != null) { samplePool.recycle(toRecycle); - toRecycle = sampleQueue.poll(); + toRecycle = queue.poll(); } } + public Sample poll() { + return queue.poll(); + } + + public boolean isEmpty() { + return queue.isEmpty(); + } + /** * Creates a new Sample and adds it to the queue. * @@ -534,7 +542,7 @@ public final class TsExtractor { adjustTimestamp(sample); if (foundFirstKeyframe && !foundLastKeyframe) { largestParsedTimestampUs = Math.max(largestParsedTimestampUs, sample.timeUs); - sampleQueue.add(sample); + queue.add(sample); } else { samplePool.recycle(sample); } @@ -558,6 +566,15 @@ public final class TsExtractor { } + /** + * Extracts individual samples from continuous byte stream. + */ + private abstract class PesPayloadReader extends SampleQueue { + + public abstract void read(BitArray pesBuffer, int pesPayloadSize, long pesTimeUs); + + } + /** * Parses a continuous H264 byte stream and extracts individual frames. */ @@ -567,9 +584,15 @@ public final class TsExtractor { private static final int NAL_UNIT_TYPE_AUD = 9; private static final int NAL_UNIT_TYPE_SPS = 7; + public final SeiReader seiReader; + // Used to store uncompleted sample data. private Sample currentSample; + public H264Reader(SeiReader seiReader) { + this.seiReader = seiReader; + } + @Override public void clear() { super.clear(); @@ -593,6 +616,7 @@ public final class TsExtractor { if (!hasMediaFormat() && (currentSample.flags & MediaExtractor.SAMPLE_FLAG_SYNC) != 0) { parseMediaFormat(currentSample); } + seiReader.read(currentSample.data, currentSample.size, pesTimeUs); addSample(currentSample); } currentSample = samplePool.get(); @@ -756,6 +780,39 @@ public final class TsExtractor { } + /** + * Parses a SEI data from H.264 frames and extracts samples with closed captions data. + */ + private class SeiReader extends SampleQueue { + + // SEI data, used for Closed Captions. + private static final int NAL_UNIT_TYPE_SEI = 6; + + private final BitArray seiBuffer; + + public SeiReader() { + setMediaFormat(MediaFormat.createEia608Format()); + seiBuffer = new BitArray(); + } + + @SuppressLint("InlinedApi") + public void read(byte[] data, int size, long pesTimeUs) { + seiBuffer.reset(data, size); + while (seiBuffer.bytesLeft() > 0) { + int seiStart = seiBuffer.findNextNalUnit(NAL_UNIT_TYPE_SEI, 0); + if (seiStart == seiBuffer.bytesLeft()) { + return; + } + seiBuffer.skipBytes(seiStart + 4); + int ccDataSize = Eia608Parser.parseHeader(seiBuffer); + if (ccDataSize > 0) { + addSample(seiBuffer, ccDataSize, pesTimeUs, MediaExtractor.SAMPLE_FLAG_SYNC); + } + } + } + + } + /** * Parses a continuous ADTS byte stream and extracts individual frames. */ diff --git a/library/src/main/java/com/google/android/exoplayer/metadata/ClosedCaption.java b/library/src/main/java/com/google/android/exoplayer/metadata/ClosedCaption.java new file mode 100644 index 0000000000..bcaa6bb8bb --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/metadata/ClosedCaption.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer.metadata; + +/** + * A Closed Caption that contains textual data associated with time indices. + */ +public final class ClosedCaption { + + /** + * Identifies closed captions with control characters. + */ + public static final int TYPE_CTRL = 0; + /** + * Identifies closed captions with textual information. + */ + public static final int TYPE_TEXT = 1; + + /** + * The type of the closed caption data. If equals to {@link #TYPE_TEXT} the {@link #text} field + * has the textual data, if equals to {@link #TYPE_CTRL} the {@link #text} field has two control + * characters (C1, C2). + */ + public final int type; + /** + * Contains text or two control characters. + */ + public final String text; + + public ClosedCaption(int type, String text) { + this.type = type; + this.text = text; + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/metadata/Eia608Parser.java b/library/src/main/java/com/google/android/exoplayer/metadata/Eia608Parser.java new file mode 100644 index 0000000000..8166fe610e --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/metadata/Eia608Parser.java @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer.metadata; + +import com.google.android.exoplayer.util.BitArray; +import com.google.android.exoplayer.util.MimeTypes; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Facilitates the extraction and parsing of EIA-608 (a.k.a. "line 21 captions" and "CEA-608") + * Closed Captions from the SEI data block from H.264. + */ +public class Eia608Parser implements MetadataParser> { + + private static final int PAYLOAD_TYPE_CC = 4; + private static final int COUNTRY_CODE = 0xB5; + private static final int PROVIDER_CODE = 0x31; + private static final int USER_ID = 0x47413934; // "GA94" + private static final int USER_DATA_TYPE_CODE = 0x3; + + private static final int[] SPECIAL_CHARACTER_SET = new int[] { + 0xAE, // 30: 174 '®' "Registered Sign" - registered trademark symbol + 0xB0, // 31: 176 '°' "Degree Sign" + 0xBD, // 32: 189 '½' "Vulgar Fraction One Half" (1/2 symbol) + 0xBF, // 33: 191 '¿' "Inverted Question Mark" + 0x2122, // 34: "Trade Mark Sign" (tm superscript) + 0xA2, // 35: 162 '¢' "Cent Sign" + 0xA3, // 36: 163 '£' "Pound Sign" - pounds sterling + 0x266A, // 37: "Eighth Note" - music note + 0xE0, // 38: 224 'à' "Latin small letter A with grave" + 0x20, // 39: TRANSPARENT SPACE - for now use ordinary space + 0xE8, // 3A: 232 'è' "Latin small letter E with grave" + 0xE2, // 3B: 226 'â' "Latin small letter A with circumflex" + 0xEA, // 3C: 234 'ê' "Latin small letter E with circumflex" + 0xEE, // 3D: 238 'î' "Latin small letter I with circumflex" + 0xF4, // 3E: 244 'ô' "Latin small letter O with circumflex" + 0xFB // 3F: 251 'û' "Latin small letter U with circumflex" + }; + + @Override + public boolean canParse(String mimeType) { + return mimeType.equals(MimeTypes.APPLICATION_EIA608); + } + + @Override + public List parse(byte[] data, int size) throws IOException { + if (size <= 0) { + return null; + } + BitArray seiBuffer = new BitArray(data, size); + seiBuffer.skipBits(3); // reserved + process_cc_data_flag + zero_bit + int ccCount = seiBuffer.readBits(5); + seiBuffer.skipBytes(1); + + List captions = new ArrayList(); + + StringBuilder stringBuilder = new StringBuilder(); + for (int i = 0; i < ccCount; i++) { + seiBuffer.skipBits(5); // one_bit + reserved + boolean ccValid = seiBuffer.readBit(); + if (!ccValid) { + seiBuffer.skipBits(18); + continue; + } + int ccType = seiBuffer.readBits(2); + if (ccType != 0 && ccType != 1) { + // Not EIA-608 captions. + seiBuffer.skipBits(16); + continue; + } + seiBuffer.skipBits(1); + byte ccData1 = (byte) seiBuffer.readBits(7); + seiBuffer.skipBits(1); + byte ccData2 = (byte) seiBuffer.readBits(7); + + if ((ccData1 == 0x11) && ((ccData2 & 0x70) == 0x30)) { + ccData2 &= 0xF; + stringBuilder.append((char) SPECIAL_CHARACTER_SET[ccData2]); + continue; + } + + // Control character. + if (ccData1 < 0x20) { + if (stringBuilder.length() > 0) { + captions.add(new ClosedCaption(ClosedCaption.TYPE_TEXT, stringBuilder.toString())); + stringBuilder.setLength(0); + } + captions.add(new ClosedCaption(ClosedCaption.TYPE_CTRL, + new String(new char[]{(char) ccData1, (char) ccData2}))); + continue; + } + + stringBuilder.append((char) ccData1); + if (ccData2 != 0) { + stringBuilder.append((char) ccData2); + } + + } + + if (stringBuilder.length() > 0) { + captions.add(new ClosedCaption(ClosedCaption.TYPE_TEXT, stringBuilder.toString())); + } + + return Collections.unmodifiableList(captions); + } + + /** + * Parses the beginning of SEI data and returns the size of underlying contains closed captions + * data following the header. Returns 0 if the SEI doesn't contain any closed captions data. + * + * @param seiBuffer The buffer to read from. + * @return The size of closed captions data. + */ + public static int parseHeader(BitArray seiBuffer) { + int b = 0; + int payloadType = 0; + + do { + b = seiBuffer.readUnsignedByte(); + payloadType += b; + } while (b == 0xFF); + + if (payloadType != PAYLOAD_TYPE_CC) { + return 0; + } + + int payloadSize = 0; + do { + b = seiBuffer.readUnsignedByte(); + payloadSize += b; + } while (b == 0xFF); + + if (payloadSize <= 0) { + return 0; + } + + int countryCode = seiBuffer.readUnsignedByte(); + if (countryCode != COUNTRY_CODE) { + return 0; + } + int providerCode = seiBuffer.readBits(16); + if (providerCode != PROVIDER_CODE) { + return 0; + } + int userIdentifier = seiBuffer.readBits(32); + if (userIdentifier != USER_ID) { + return 0; + } + int userDataTypeCode = seiBuffer.readUnsignedByte(); + if (userDataTypeCode != USER_DATA_TYPE_CODE) { + return 0; + } + return payloadSize; + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/metadata/Id3Parser.java b/library/src/main/java/com/google/android/exoplayer/metadata/Id3Parser.java index ea68984e37..efa3b66147 100644 --- a/library/src/main/java/com/google/android/exoplayer/metadata/Id3Parser.java +++ b/library/src/main/java/com/google/android/exoplayer/metadata/Id3Parser.java @@ -27,7 +27,7 @@ import java.util.Map; /** * Extracts individual TXXX text frames from raw ID3 data. */ -public class Id3Parser implements MetadataParser { +public class Id3Parser implements MetadataParser> { @Override public boolean canParse(String mimeType) { diff --git a/library/src/main/java/com/google/android/exoplayer/metadata/MetadataParser.java b/library/src/main/java/com/google/android/exoplayer/metadata/MetadataParser.java index 8bfaca8b77..654f549b18 100644 --- a/library/src/main/java/com/google/android/exoplayer/metadata/MetadataParser.java +++ b/library/src/main/java/com/google/android/exoplayer/metadata/MetadataParser.java @@ -16,30 +16,30 @@ package com.google.android.exoplayer.metadata; import java.io.IOException; -import java.util.Map; /** - * Parses metadata objects from binary data. + * Parses objects of type from binary data. + * + * @param The type of the metadata. */ -public interface MetadataParser { +public interface MetadataParser { /** * Checks whether the parser supports a given mime type. * - * @param mimeType A subtitle mime type. + * @param mimeType A metadata mime type. * @return Whether the mime type is supported. */ public boolean canParse(String mimeType); /** - * Parses a map of metadata type to metadata objects from the provided binary data. + * Parses metadata objects of type from the provided binary data. * * @param data The raw binary data from which to parse the metadata. * @param size The size of the input data. - * @return A parsed {@link Map} of metadata type to metadata objects. + * @return @return A parsed metadata object of type . * @throws IOException If a problem occurred parsing the data. */ - public Map parse(byte[] data, int size) - throws IOException; + public T parse(byte[] data, int size) throws IOException; } diff --git a/library/src/main/java/com/google/android/exoplayer/metadata/MetadataTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/metadata/MetadataTrackRenderer.java index 48ed78f20c..147a222c4f 100644 --- a/library/src/main/java/com/google/android/exoplayer/metadata/MetadataTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/metadata/MetadataTrackRenderer.java @@ -28,32 +28,35 @@ import android.os.Looper; import android.os.Message; import java.io.IOException; -import java.util.Map; /** * A {@link TrackRenderer} for metadata embedded in a media stream. + * + * @param The type of the metadata. */ -public class MetadataTrackRenderer extends TrackRenderer implements Callback { +public class MetadataTrackRenderer extends TrackRenderer implements Callback { /** * An interface for components that process metadata. + * + * @param The type of the metadata. */ - public interface MetadataRenderer { + public interface MetadataRenderer { /** * Invoked each time there is a metadata associated with current playback time. * * @param metadata The metadata to process. */ - void onMetadata(Map metadata); + void onMetadata(T metadata); } private static final int MSG_INVOKE_RENDERER = 0; private final SampleSource source; - private final MetadataParser metadataParser; - private final MetadataRenderer metadataRenderer; + private final MetadataParser metadataParser; + private final MetadataRenderer metadataRenderer; private final Handler metadataHandler; private final MediaFormatHolder formatHolder; private final SampleHolder sampleHolder; @@ -63,7 +66,7 @@ public class MetadataTrackRenderer extends TrackRenderer implements Callback { private boolean inputStreamEnded; private long pendingMetadataTimestamp; - private Map pendingMetadata; + private T pendingMetadata; /** * @param source A source from which samples containing metadata can be read. @@ -75,8 +78,8 @@ public class MetadataTrackRenderer extends TrackRenderer implements Callback { * obtained using {@link android.app.Activity#getMainLooper()}. Null may be passed if the * renderer should be invoked directly on the player's internal rendering thread. */ - public MetadataTrackRenderer(SampleSource source, MetadataParser metadataParser, - MetadataRenderer metadataRenderer, Looper metadataRendererLooper) { + public MetadataTrackRenderer(SampleSource source, MetadataParser metadataParser, + MetadataRenderer metadataRenderer, Looper metadataRendererLooper) { this.source = Assertions.checkNotNull(source); this.metadataParser = Assertions.checkNotNull(metadataParser); this.metadataRenderer = Assertions.checkNotNull(metadataRenderer); @@ -185,7 +188,7 @@ public class MetadataTrackRenderer extends TrackRenderer implements Callback { return true; } - private void invokeRenderer(Map metadata) { + private void invokeRenderer(T metadata) { if (metadataHandler != null) { metadataHandler.obtainMessage(MSG_INVOKE_RENDERER, metadata).sendToTarget(); } else { @@ -198,13 +201,13 @@ public class MetadataTrackRenderer extends TrackRenderer implements Callback { public boolean handleMessage(Message msg) { switch (msg.what) { case MSG_INVOKE_RENDERER: - invokeRendererInternal((Map) msg.obj); + invokeRendererInternal((T) msg.obj); return true; } return false; } - private void invokeRendererInternal(Map metadata) { + private void invokeRendererInternal(T metadata) { metadataRenderer.onMetadata(metadata); } diff --git a/library/src/main/java/com/google/android/exoplayer/util/MimeTypes.java b/library/src/main/java/com/google/android/exoplayer/util/MimeTypes.java index 55a59d1867..dd9d700b02 100644 --- a/library/src/main/java/com/google/android/exoplayer/util/MimeTypes.java +++ b/library/src/main/java/com/google/android/exoplayer/util/MimeTypes.java @@ -38,6 +38,7 @@ public class MimeTypes { public static final String TEXT_VTT = BASE_TYPE_TEXT + "/vtt"; public static final String APPLICATION_ID3 = BASE_TYPE_APPLICATION + "/id3"; + public static final String APPLICATION_EIA608 = BASE_TYPE_APPLICATION + "/eia-608"; public static final String APPLICATION_TTML = BASE_TYPE_APPLICATION + "/ttml+xml"; private MimeTypes() {} From 3cfe894b93bbf2ff7dc1e48bb8b3d8d228fa0550 Mon Sep 17 00:00:00 2001 From: Andrey Udovenko Date: Tue, 18 Nov 2014 19:36:44 -0500 Subject: [PATCH 25/55] Additional IV fix. Trim sign bit from BigInteger.toByteArray() output, if it creates a 17th byte for it. #145 --- .../com/google/android/exoplayer/hls/HlsChunkSource.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 88c729f495..c50a7aad65 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 @@ -395,8 +395,9 @@ public class HlsChunkSource { byte[] ivData = new BigInteger(iv, 16).toByteArray(); byte[] ivDataWithPadding = new byte[16]; - System.arraycopy(ivData, 0, ivDataWithPadding, ivDataWithPadding.length - ivData.length, - ivData.length); + int offset = ivData.length > 16 ? ivData.length - 16 : 0; + System.arraycopy(ivData, offset, ivDataWithPadding, ivDataWithPadding.length - ivData.length + + offset, ivData.length - offset); encryptedDataSource = new Aes128DataSource(secretKey, ivDataWithPadding, upstreamDataSource); } From 4280511a33e961ad18670cb99432cc617ce32dfe Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Wed, 19 Nov 2014 10:34:49 +0000 Subject: [PATCH 26/55] Seamless splicing for adaptive HLS. --- .../demo/full/player/HlsRendererBuilder.java | 4 +- .../demo/simple/HlsRendererBuilder.java | 4 +- .../android/exoplayer/hls/HlsChunkSource.java | 3 - .../exoplayer/hls/HlsSampleSource.java | 7 + .../android/exoplayer/hls/TsExtractor.java | 235 +++++++++++++----- 5 files changed, 191 insertions(+), 62 deletions(-) diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/HlsRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/HlsRendererBuilder.java index 9d1267d9cd..5bacf381f7 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/HlsRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/HlsRendererBuilder.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer.demo.full.player; import com.google.android.exoplayer.MediaCodecAudioTrackRenderer; +import com.google.android.exoplayer.MediaCodecUtil; import com.google.android.exoplayer.MediaCodecVideoTrackRenderer; import com.google.android.exoplayer.TrackRenderer; import com.google.android.exoplayer.demo.DemoUtil; @@ -35,6 +36,7 @@ import com.google.android.exoplayer.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer.upstream.UriDataSource; import com.google.android.exoplayer.util.ManifestFetcher; import com.google.android.exoplayer.util.ManifestFetcher.ManifestCallback; +import com.google.android.exoplayer.util.MimeTypes; import android.media.MediaCodec; import android.net.Uri; @@ -92,7 +94,7 @@ public class HlsRendererBuilder implements RendererBuilder, ManifestCallback 1) { + // If there's more than one extractor, attempt to configure a seamless splice from the + // current one to the next one. + extractor.configureSpliceTo(extractors.get(1)); + } + int extractorIndex = 0; while (extractors.size() > extractorIndex + 1 && !extractor.hasSamples(track)) { // We're finished reading from the extractor for this particular track, so advance to the diff --git a/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java b/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java index 0e0ac7b445..51948c334d 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java @@ -59,11 +59,11 @@ public final class TsExtractor { /* package */ final long firstSampleTimestamp; private boolean prepared; + private boolean spliceConfigured; /* package */ boolean pendingFirstSampleTimestampAdjustment; /* package */ long sampleTimestampOffsetUs; /* package */ long largestParsedTimestampUs; - /* package */ boolean discardFromNextKeyframes; public TsExtractor(long firstSampleTimestamp, SamplePool samplePool) { this.firstSampleTimestamp = firstSampleTimestamp; @@ -120,10 +120,33 @@ public final class TsExtractor { } /** - * For each track, discards samples from the next key frame (inclusive). + * Attempts to configure a splice from this extractor to the next. + *

+ * The splice is performed such that for each track the samples read from the next extractor + * start with a keyframe, and continue from where the samples read from this extractor finish. + * A successful splice may discard samples from either or both extractors. + *

+ * Splice configuration may fail if the next extractor is not yet in a state that allows the + * splice to be performed. Calling this method is a noop if the splice has already been + * configured. Hence this method should be called repeatedly during the window within which a + * splice can be performed. + * + * @param nextExtractor The extractor being spliced to. */ - public void discardFromNextKeyframes() { - discardFromNextKeyframes = true; + public void configureSpliceTo(TsExtractor nextExtractor) { + Assertions.checkState(prepared); + if (spliceConfigured || !nextExtractor.isPrepared()) { + // The splice is already configured or the next extractor isn't ready to be spliced in. + // Already configured, or too early to splice. + return; + } + boolean spliceConfigured = true; + for (int i = 0; i < sampleQueues.size(); i++) { + spliceConfigured &= sampleQueues.valueAt(i).configureSpliceTo( + nextExtractor.sampleQueues.valueAt(i)); + } + this.spliceConfigured = spliceConfigured; + return; } /** @@ -144,12 +167,13 @@ public final class TsExtractor { */ public boolean getSample(int track, SampleHolder out) { Assertions.checkState(prepared); - Sample sample = sampleQueues.valueAt(track).poll(); + SampleQueue sampleQueue = sampleQueues.valueAt(track); + Sample sample = sampleQueue.poll(); if (sample == null) { return false; } convert(sample, out); - samplePool.recycle(sample); + sampleQueue.recycle(sample); return true; } @@ -177,7 +201,7 @@ public final class TsExtractor { * for the specified track. False otherwise. */ public boolean hasSamples(int track) { - return !sampleQueues.valueAt(track).isEmpty(); + return sampleQueues.valueAt(track).peek() != null; } private boolean checkPrepared() { @@ -252,6 +276,7 @@ public final class TsExtractor { return read; } + @SuppressLint("InlinedApi") private void convert(Sample in, SampleHolder out) { if (out.data == null || out.data.capacity() < in.size) { out.replaceBuffer(in.size); @@ -260,7 +285,7 @@ public final class TsExtractor { out.data.put(in.data, 0, in.size); } out.size = in.size; - out.flags = in.flags; + out.flags = in.isKeyframe ? MediaExtractor.SAMPLE_FLAG_SYNC : 0; out.timeUs = in.timeUs; } @@ -349,15 +374,15 @@ public final class TsExtractor { PesPayloadReader pesPayloadReader = null; switch (streamType) { case TS_STREAM_TYPE_AAC: - pesPayloadReader = new AdtsReader(); + pesPayloadReader = new AdtsReader(samplePool); break; case TS_STREAM_TYPE_H264: - SeiReader seiReader = new SeiReader(); + SeiReader seiReader = new SeiReader(samplePool); sampleQueues.put(TS_STREAM_TYPE_EIA608, seiReader); - pesPayloadReader = new H264Reader(seiReader); + pesPayloadReader = new H264Reader(samplePool, seiReader); break; case TS_STREAM_TYPE_ID3: - pesPayloadReader = new Id3Reader(); + pesPayloadReader = new Id3Reader(samplePool); break; } @@ -471,18 +496,24 @@ public final class TsExtractor { } /** - * A collection of extracted samples. + * A queue of extracted samples together with their corresponding {@link MediaFormat}. */ private abstract class SampleQueue { - private final ConcurrentLinkedQueue queue; + @SuppressWarnings("hiding") + private final SamplePool samplePool; + private final ConcurrentLinkedQueue internalQueue; private MediaFormat mediaFormat; - private boolean foundFirstKeyframe; - private boolean foundLastKeyframe; + private long spliceOutTimeUs; + private long lastParsedTimestampUs; + private boolean readFirstFrame; - protected SampleQueue() { - this.queue = new ConcurrentLinkedQueue(); + protected SampleQueue(SamplePool samplePool) { + this.samplePool = samplePool; + internalQueue = new ConcurrentLinkedQueue(); + spliceOutTimeUs = Long.MIN_VALUE; + lastParsedTimestampUs = Long.MIN_VALUE; } public boolean hasMediaFormat() { @@ -497,20 +528,113 @@ public final class TsExtractor { this.mediaFormat = mediaFormat; } + /** + * Removes and returns the next sample from the queue. + *

+ * The first sample returned is guaranteed to be a keyframe, since any non-keyframe samples + * queued prior to the first keyframe are discarded. + * + * @return The next sample from the queue, or null if a sample isn't available. + */ + public Sample poll() { + Sample head = peek(); + if (head != null) { + internalQueue.remove(); + readFirstFrame = true; + } + return head; + } + + /** + * Like {@link #poll()}, except the returned sample is not removed from the queue. + * + * @return The next sample from the queue, or null if a sample isn't available. + */ + public Sample peek() { + Sample head = internalQueue.peek(); + if (!readFirstFrame) { + // Peeking discard of samples until we find a keyframe or run out of available samples. + while (head != null && !head.isKeyframe) { + recycle(head); + internalQueue.remove(); + head = internalQueue.peek(); + } + } + if (head == null) { + return null; + } + if (spliceOutTimeUs != Long.MIN_VALUE && head.timeUs >= spliceOutTimeUs) { + // The sample is later than the time this queue is spliced out. + recycle(head); + internalQueue.remove(); + return null; + } + return head; + } + + /** + * Clears the queue. + */ public void clear() { - Sample toRecycle = queue.poll(); + Sample toRecycle = internalQueue.poll(); while (toRecycle != null) { - samplePool.recycle(toRecycle); - toRecycle = queue.poll(); + recycle(toRecycle); + toRecycle = internalQueue.poll(); } } - public Sample poll() { - return queue.poll(); + /** + * Recycles a sample. + * + * @param sample The sample to recycle. + */ + public void recycle(Sample sample) { + samplePool.recycle(sample); } - public boolean isEmpty() { - return queue.isEmpty(); + /** + * Attempts to configure a splice from this queue to the next. + * + * @param nextQueue The queue being spliced to. + * @return Whether the splice was configured successfully. + */ + public boolean configureSpliceTo(SampleQueue nextQueue) { + if (spliceOutTimeUs != Long.MIN_VALUE) { + // We've already configured the splice. + return true; + } + long firstPossibleSpliceTime; + Sample nextSample = internalQueue.peek(); + if (nextSample != null) { + firstPossibleSpliceTime = nextSample.timeUs; + } else { + firstPossibleSpliceTime = lastParsedTimestampUs + 1; + } + ConcurrentLinkedQueue nextInternalQueue = nextQueue.internalQueue; + Sample nextQueueSample = nextInternalQueue.peek(); + while (nextQueueSample != null + && (nextQueueSample.timeUs < firstPossibleSpliceTime || !nextQueueSample.isKeyframe)) { + // Discard samples from the next queue for as long as they are before the earliest possible + // splice time, or not keyframes. + nextQueue.internalQueue.remove(); + nextQueueSample = nextQueue.internalQueue.peek(); + } + if (nextQueueSample != null) { + // We've found a keyframe in the next queue that can serve as the splice point. Set the + // splice point now. + spliceOutTimeUs = nextQueueSample.timeUs; + return true; + } + return false; + } + + /** + * Obtains a Sample object to use. + * + * @return The sample. + */ + protected Sample getSample() { + return samplePool.get(); } /** @@ -519,33 +643,22 @@ public final class TsExtractor { * @param buffer The buffer to read sample data. * @param sampleSize The size of the sample data. * @param sampleTimeUs The sample time stamp. + * @param isKeyframe True if the sample is a keyframe. False otherwise. */ - protected void addSample(BitArray buffer, int sampleSize, long sampleTimeUs, int flags) { - Sample sample = samplePool.get(); + protected void addSample(BitArray buffer, int sampleSize, long sampleTimeUs, + boolean isKeyframe) { + Sample sample = getSample(); addToSample(sample, buffer, sampleSize); - sample.flags = flags; + sample.isKeyframe = isKeyframe; sample.timeUs = sampleTimeUs; addSample(sample); } - @SuppressLint("InlinedApi") protected void addSample(Sample sample) { - boolean isKeyframe = (sample.flags & MediaExtractor.SAMPLE_FLAG_SYNC) != 0; - if (isKeyframe) { - if (!foundFirstKeyframe) { - foundFirstKeyframe = true; - } - if (discardFromNextKeyframes) { - foundLastKeyframe = true; - } - } adjustTimestamp(sample); - if (foundFirstKeyframe && !foundLastKeyframe) { - largestParsedTimestampUs = Math.max(largestParsedTimestampUs, sample.timeUs); - queue.add(sample); - } else { - samplePool.recycle(sample); - } + lastParsedTimestampUs = sample.timeUs; + largestParsedTimestampUs = Math.max(largestParsedTimestampUs, sample.timeUs); + internalQueue.add(sample); } protected void addToSample(Sample sample, BitArray buffer, int size) { @@ -571,6 +684,10 @@ public final class TsExtractor { */ private abstract class PesPayloadReader extends SampleQueue { + protected PesPayloadReader(SamplePool samplePool) { + super(samplePool); + } + public abstract void read(BitArray pesBuffer, int pesPayloadSize, long pesTimeUs); } @@ -589,7 +706,8 @@ public final class TsExtractor { // Used to store uncompleted sample data. private Sample currentSample; - public H264Reader(SeiReader seiReader) { + public H264Reader(SamplePool samplePool, SeiReader seiReader) { + super(samplePool); this.seiReader = seiReader; } @@ -597,7 +715,7 @@ public final class TsExtractor { public void clear() { super.clear(); if (currentSample != null) { - samplePool.recycle(currentSample); + recycle(currentSample); currentSample = null; } } @@ -613,13 +731,13 @@ public final class TsExtractor { // Single PES packet should contain only one new H.264 frame. if (currentSample != null) { - if (!hasMediaFormat() && (currentSample.flags & MediaExtractor.SAMPLE_FLAG_SYNC) != 0) { + if (!hasMediaFormat() && currentSample.isKeyframe) { parseMediaFormat(currentSample); } seiReader.read(currentSample.data, currentSample.size, pesTimeUs); addSample(currentSample); } - currentSample = samplePool.get(); + currentSample = getSample(); pesPayloadSize -= readOneH264Frame(pesBuffer, false); currentSample.timeUs = pesTimeUs; @@ -635,7 +753,7 @@ public final class TsExtractor { if (currentSample != null) { int idrStart = pesBuffer.findNextNalUnit(NAL_UNIT_TYPE_IDR, offset); if (idrStart < audStart) { - currentSample.flags = MediaExtractor.SAMPLE_FLAG_SYNC; + currentSample.isKeyframe = true; } addToSample(currentSample, pesBuffer, audStart); } else { @@ -790,7 +908,8 @@ public final class TsExtractor { private final BitArray seiBuffer; - public SeiReader() { + public SeiReader(SamplePool samplePool) { + super(samplePool); setMediaFormat(MediaFormat.createEia608Format()); seiBuffer = new BitArray(); } @@ -806,7 +925,7 @@ public final class TsExtractor { seiBuffer.skipBytes(seiStart + 4); int ccDataSize = Eia608Parser.parseHeader(seiBuffer); if (ccDataSize > 0) { - addSample(seiBuffer, ccDataSize, pesTimeUs, MediaExtractor.SAMPLE_FLAG_SYNC); + addSample(seiBuffer, ccDataSize, pesTimeUs, true); } } } @@ -821,7 +940,8 @@ public final class TsExtractor { private final BitArray adtsBuffer; private long timeUs; - public AdtsReader() { + public AdtsReader(SamplePool samplePool) { + super(samplePool); adtsBuffer = new BitArray(); } @@ -904,7 +1024,7 @@ public final class TsExtractor { return false; } - addSample(adtsBuffer, frameSize, timeUs, MediaExtractor.SAMPLE_FLAG_SYNC); + addSample(adtsBuffer, frameSize, timeUs, true); return true; } @@ -921,14 +1041,15 @@ public final class TsExtractor { */ private class Id3Reader extends PesPayloadReader { - public Id3Reader() { + public Id3Reader(SamplePool samplePool) { + super(samplePool); setMediaFormat(MediaFormat.createId3Format()); } @SuppressLint("InlinedApi") @Override public void read(BitArray pesBuffer, int pesPayloadSize, long pesTimeUs) { - addSample(pesBuffer, pesPayloadSize, pesTimeUs, MediaExtractor.SAMPLE_FLAG_SYNC); + addSample(pesBuffer, pesPayloadSize, pesTimeUs, true); } } @@ -968,7 +1089,7 @@ public final class TsExtractor { public Sample nextInPool; public byte[] data; - public int flags; + public boolean isKeyframe; public int size; public long timeUs; @@ -983,7 +1104,7 @@ public final class TsExtractor { } public void reset() { - flags = 0; + isKeyframe = false; size = 0; timeUs = 0; } From 826c73a16bfb7212524d563420b5ed2ff79a0873 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Wed, 19 Nov 2014 16:06:33 +0000 Subject: [PATCH 27/55] Fix renderer count. Issue: #153 --- .../google/android/exoplayer/demo/full/player/DemoPlayer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DemoPlayer.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DemoPlayer.java index b127188557..cdd1445a34 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DemoPlayer.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DemoPlayer.java @@ -162,7 +162,7 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi public static final int DISABLED_TRACK = -1; public static final int PRIMARY_TRACK = 0; - public static final int RENDERER_COUNT = 5; + public static final int RENDERER_COUNT = 6; public static final int TYPE_VIDEO = 0; public static final int TYPE_AUDIO = 1; public static final int TYPE_TEXT = 2; From 8c07847b00552247540536a11e0cacc30482ba6f Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 20 Nov 2014 14:58:06 +0000 Subject: [PATCH 28/55] Properly propagate errors that occur during preparation. --- .../exoplayer/hls/HlsSampleSource.java | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) 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 b78029d5c5..cad1cdc03d 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 @@ -82,21 +82,23 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { loader = new Loader("Loader:HLS"); } continueBufferingInternal(); - if (extractors.isEmpty()) { - return false; - } - TsExtractor extractor = extractors.getFirst(); - if (extractor.isPrepared()) { - trackCount = extractor.getTrackCount(); - trackEnabledStates = new boolean[trackCount]; - pendingDiscontinuities = new boolean[trackCount]; - downstreamMediaFormats = new MediaFormat[trackCount]; - trackInfos = new TrackInfo[trackCount]; - for (int i = 0; i < trackCount; i++) { - MediaFormat format = extractor.getFormat(i); - trackInfos[i] = new TrackInfo(format.mimeType, chunkSource.getDurationUs()); + if (!extractors.isEmpty()) { + TsExtractor extractor = extractors.getFirst(); + if (extractor.isPrepared()) { + trackCount = extractor.getTrackCount(); + trackEnabledStates = new boolean[trackCount]; + pendingDiscontinuities = new boolean[trackCount]; + downstreamMediaFormats = new MediaFormat[trackCount]; + trackInfos = new TrackInfo[trackCount]; + for (int i = 0; i < trackCount; i++) { + MediaFormat format = extractor.getFormat(i); + trackInfos[i] = new TrackInfo(format.mimeType, chunkSource.getDurationUs()); + } + prepared = true; } - prepared = true; + } + if (!prepared && currentLoadableException != null) { + throw currentLoadableException; } return prepared; } From 81e2c9f0d3ea0fd934fbb4f01ae20bb81c7adf66 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 20 Nov 2014 14:59:22 +0000 Subject: [PATCH 29/55] Fix https://github.com/google/ExoPlayer/issues/159. The actual fix here is to not call discardExtractors in HlsSampleSource whilst the loading thread that's pushing data into it is still running. It's required to wait for that thread to have exited before doing this. Issue: #159 --- .../exoplayer/hls/HlsSampleSource.java | 29 ++++++------- .../android/exoplayer/hls/TsExtractor.java | 43 +++++++++++-------- 2 files changed, 39 insertions(+), 33 deletions(-) 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 cad1cdc03d..693af382f1 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 @@ -138,9 +138,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { if (loader.isLoading()) { loader.cancelLoading(); } else { - discardExtractors(); - clearCurrentLoadable(); - previousTsLoadable = null; + clearState(); } } } @@ -186,7 +184,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { TsExtractor extractor = extractors.getFirst(); while (extractors.size() > 1 && !extractor.hasSamples()) { // We're finished reading from the extractor for all tracks, and so can discard it. - extractors.removeFirst().clear(); + extractors.removeFirst().release(); extractor = extractors.getFirst(); } @@ -294,11 +292,10 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { @Override public void onLoadCanceled(Loadable loadable) { - clearCurrentLoadable(); if (enabledTrackCount > 0) { restartFrom(pendingResetPositionUs); } else { - previousTsLoadable = null; + clearState(); } } @@ -312,17 +309,24 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { private void restartFrom(long positionUs) { pendingResetPositionUs = positionUs; - previousTsLoadable = null; loadingFinished = false; - discardExtractors(); if (loader.isLoading()) { loader.cancelLoading(); } else { - clearCurrentLoadable(); + clearState(); maybeStartLoading(); } } + private void clearState() { + for (int i = 0; i < extractors.size(); i++) { + extractors.get(i).release(); + } + extractors.clear(); + clearCurrentLoadable(); + previousTsLoadable = null; + } + private void clearCurrentLoadable() { currentLoadable = null; currentLoadableException = null; @@ -370,13 +374,6 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { loader.startLoading(currentLoadable, this); } - private void discardExtractors() { - for (int i = 0; i < extractors.size(); i++) { - extractors.get(i).clear(); - } - extractors.clear(); - } - private boolean isTsChunk(HlsChunk chunk) { return chunk instanceof TsChunk; } diff --git a/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java b/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java index 6a00873eae..d574078162 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java @@ -58,12 +58,16 @@ public final class TsExtractor { private final SamplePool samplePool; /* package */ final long firstSampleTimestamp; - private boolean prepared; + // Accessed only by the consuming thread. private boolean spliceConfigured; + // Accessed only by the loading thread. /* package */ boolean pendingFirstSampleTimestampAdjustment; /* package */ long sampleTimestampOffsetUs; - /* package */ long largestParsedTimestampUs; + + // Accessed by both the loading and consuming threads. + private volatile boolean prepared; + /* package */ volatile long largestParsedTimestampUs; public TsExtractor(long firstSampleTimestamp, SamplePool samplePool) { this.firstSampleTimestamp = firstSampleTimestamp; @@ -111,11 +115,13 @@ public final class TsExtractor { } /** - * Flushes any pending or incomplete samples, returning them to the sample pool. + * Releases the extractor, recycling any pending or incomplete samples to the sample pool. + *

+ * This method should not be called whilst {@link #read(DataSource)} is also being invoked. */ - public void clear() { + public void release() { for (int i = 0; i < sampleQueues.size(); i++) { - sampleQueues.valueAt(i).clear(); + sampleQueues.valueAt(i).release(); } } @@ -504,16 +510,19 @@ public final class TsExtractor { private final SamplePool samplePool; private final ConcurrentLinkedQueue internalQueue; - private MediaFormat mediaFormat; - private long spliceOutTimeUs; - private long lastParsedTimestampUs; + // Accessed only by the consuming thread. private boolean readFirstFrame; + private long lastReadTimeUs; + private long spliceOutTimeUs; + + // Accessed by both the loading and consuming threads. + private volatile MediaFormat mediaFormat; protected SampleQueue(SamplePool samplePool) { this.samplePool = samplePool; internalQueue = new ConcurrentLinkedQueue(); spliceOutTimeUs = Long.MIN_VALUE; - lastParsedTimestampUs = Long.MIN_VALUE; + lastReadTimeUs = Long.MIN_VALUE; } public boolean hasMediaFormat() { @@ -541,6 +550,7 @@ public final class TsExtractor { if (head != null) { internalQueue.remove(); readFirstFrame = true; + lastReadTimeUs = head.timeUs; } return head; } @@ -575,7 +585,7 @@ public final class TsExtractor { /** * Clears the queue. */ - public void clear() { + public void release() { Sample toRecycle = internalQueue.poll(); while (toRecycle != null) { recycle(toRecycle); @@ -608,7 +618,7 @@ public final class TsExtractor { if (nextSample != null) { firstPossibleSpliceTime = nextSample.timeUs; } else { - firstPossibleSpliceTime = lastParsedTimestampUs + 1; + firstPossibleSpliceTime = lastReadTimeUs + 1; } ConcurrentLinkedQueue nextInternalQueue = nextQueue.internalQueue; Sample nextQueueSample = nextInternalQueue.peek(); @@ -656,7 +666,6 @@ public final class TsExtractor { protected void addSample(Sample sample) { adjustTimestamp(sample); - lastParsedTimestampUs = sample.timeUs; largestParsedTimestampUs = Math.max(largestParsedTimestampUs, sample.timeUs); internalQueue.add(sample); } @@ -712,8 +721,8 @@ public final class TsExtractor { } @Override - public void clear() { - super.clear(); + public void release() { + super.release(); if (currentSample != null) { recycle(currentSample); currentSample = null; @@ -861,7 +870,7 @@ public final class TsExtractor { cropUnitX = subWidthC; cropUnitY = subHeightC * (2 - (frameMbsOnlyFlag ? 1 : 0)); } - frameWidth -= (frameCropLeftOffset + frameCropRightOffset) * cropUnitX; + frameWidth -= (frameCropLeftOffset + frameCropRightOffset) * cropUnitX; frameHeight -= (frameCropTopOffset + frameCropBottomOffset) * cropUnitY; } @@ -1057,8 +1066,8 @@ public final class TsExtractor { } @Override - public void clear() { - super.clear(); + public void release() { + super.release(); adtsBuffer.reset(); } From 03e859d7745e720e2c4242ec9f66bd625dc91870 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 20 Nov 2014 17:11:02 +0000 Subject: [PATCH 30/55] Fix handling of encrypted media if IV changes. 1. Correctly replace the AES data source if IV changes. 2. Check the largest timestamp for being equal to MIN_VALUE, and handle this case properly. 3. Clean up AES data source a little. Issue: #162 --- .../android/exoplayer/hls/HlsChunkSource.java | 57 ++++++++++++------- .../exoplayer/hls/HlsSampleSource.java | 13 ++++- .../exoplayer/upstream/Aes128DataSource.java | 3 +- 3 files changed, 50 insertions(+), 23 deletions(-) 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 c7eede8c7a..b422db2d5e 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 @@ -67,7 +67,9 @@ public class HlsChunkSource { private int variantIndex; private DataSource encryptedDataSource; - private String encryptionKeyUri; + private Uri encryptionKeyUri; + private String encryptedDataSourceIv; + private byte[] encryptedDataSourceSecretKey; /** * @param dataSource A {@link DataSource} suitable for loading the media data. @@ -179,16 +181,17 @@ public class HlsChunkSource { // Check if encryption is specified. if (HlsMediaPlaylist.ENCRYPTION_METHOD_AES_128.equals(segment.encryptionMethod)) { - if (!segment.encryptionKeyUri.equals(encryptionKeyUri)) { + Uri keyUri = Util.getMergedUri(mediaPlaylist.baseUri, segment.encryptionKeyUri); + if (!keyUri.equals(encryptionKeyUri)) { // Encryption is specified and the key has changed. - Uri keyUri = Util.getMergedUri(mediaPlaylist.baseUri, segment.encryptionKeyUri); HlsChunk toReturn = newEncryptionKeyChunk(keyUri, segment.encryptionIV); - encryptionKeyUri = segment.encryptionKeyUri; return toReturn; } + if (!Util.areEqual(segment.encryptionIV, encryptedDataSourceIv)) { + initEncryptedDataSource(keyUri, segment.encryptionIV, encryptedDataSourceSecretKey); + } } else { - encryptedDataSource = null; - encryptionKeyUri = null; + clearEncryptedDataSource(); } long startTimeUs; @@ -290,6 +293,33 @@ public class HlsChunkSource { return new EncryptionKeyChunk(upstreamDataSource, dataSpec, iv); } + /* package */ void initEncryptedDataSource(Uri keyUri, String iv, byte[] secretKey) { + String trimmedIv; + if (iv.toLowerCase(Locale.getDefault()).startsWith("0x")) { + trimmedIv = iv.substring(2); + } else { + trimmedIv = iv; + } + + byte[] ivData = new BigInteger(trimmedIv, 16).toByteArray(); + byte[] ivDataWithPadding = new byte[16]; + int offset = ivData.length > 16 ? ivData.length - 16 : 0; + System.arraycopy(ivData, offset, ivDataWithPadding, ivDataWithPadding.length - ivData.length + + offset, ivData.length - offset); + + encryptedDataSource = new Aes128DataSource(secretKey, ivDataWithPadding, upstreamDataSource); + encryptionKeyUri = keyUri; + encryptedDataSourceIv = iv; + encryptedDataSourceSecretKey = secretKey; + } + + private void clearEncryptedDataSource() { + encryptionKeyUri = null; + encryptedDataSource = null; + encryptedDataSourceIv = null; + encryptedDataSourceSecretKey = null; + } + private static Variant[] filterVariants(HlsMasterPlaylist masterPlaylist, int[] variantIndices) { List masterVariants = masterPlaylist.variants; ArrayList enabledVariants = new ArrayList(); @@ -378,25 +408,14 @@ public class HlsChunkSource { public EncryptionKeyChunk(DataSource dataSource, DataSpec dataSpec, String iv) { super(dataSource, dataSpec, bitArray); - if (iv.toLowerCase(Locale.getDefault()).startsWith("0x")) { - this.iv = iv.substring(2); - } else { - this.iv = iv; - } + this.iv = iv; } @Override protected void consume(BitArray data) throws IOException { byte[] secretKey = new byte[data.bytesLeft()]; data.readBytes(secretKey, 0, secretKey.length); - - byte[] ivData = new BigInteger(iv, 16).toByteArray(); - byte[] ivDataWithPadding = new byte[16]; - int offset = ivData.length > 16 ? ivData.length - 16 : 0; - System.arraycopy(ivData, offset, ivDataWithPadding, ivDataWithPadding.length - ivData.length - + offset, ivData.length - offset); - - encryptedDataSource = new Aes128DataSource(secretKey, ivDataWithPadding, upstreamDataSource); + initEncryptedDataSource(dataSpec.uri, iv, secretKey); } } 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 693af382f1..4982ca894c 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 @@ -256,7 +256,9 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { } else if (loadingFinished) { return TrackRenderer.END_OF_TRACK_US; } else { - return extractors.getLast().getLargestSampleTimestamp(); + long largestSampleTimestamp = extractors.getLast().getLargestSampleTimestamp(); + return largestSampleTimestamp == Long.MIN_VALUE ? downstreamPositionUs + : largestSampleTimestamp; } } @@ -349,8 +351,13 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { return; } - boolean bufferFull = !extractors.isEmpty() && (extractors.getLast().getLargestSampleTimestamp() - - downstreamPositionUs) >= BUFFER_DURATION_US; + boolean bufferFull = false; + if (!extractors.isEmpty()) { + long largestSampleTimestamp = extractors.getLast().getLargestSampleTimestamp(); + bufferFull = largestSampleTimestamp != Long.MIN_VALUE + && (largestSampleTimestamp - downstreamPositionUs) >= BUFFER_DURATION_US; + } + if (loader.isLoading() || bufferFull) { return; } diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/Aes128DataSource.java b/library/src/main/java/com/google/android/exoplayer/upstream/Aes128DataSource.java index 1e76d898b8..938dd70ef1 100644 --- a/library/src/main/java/com/google/android/exoplayer/upstream/Aes128DataSource.java +++ b/library/src/main/java/com/google/android/exoplayer/upstream/Aes128DataSource.java @@ -42,7 +42,6 @@ public class Aes128DataSource implements DataSource { private final byte[] secretKey; private final byte[] iv; - private Cipher cipher; private CipherInputStream cipherInputStream; public Aes128DataSource(byte[] secretKey, byte[] iv, DataSource upstream) { @@ -53,6 +52,7 @@ public class Aes128DataSource implements DataSource { @Override public long open(DataSpec dataSpec) throws IOException { + Cipher cipher; try { cipher = Cipher.getInstance("AES/CBC/PKCS7Padding"); } catch (NoSuchAlgorithmException e) { @@ -80,6 +80,7 @@ public class Aes128DataSource implements DataSource { @Override public void close() throws IOException { + cipherInputStream = null; upstream.close(); } From 410fcdeb87084af7b5338c068fdde4163ed055e7 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Wed, 26 Nov 2014 12:01:36 +0000 Subject: [PATCH 31/55] Merge HLS playlist parsers, make a single parser identify the playlist type (master or media). Issue: #155 --- .../android/exoplayer/demo/DemoUtil.java | 5 +- .../android/exoplayer/demo/Samples.java | 8 +- .../demo/full/FullPlayerActivity.java | 7 +- .../demo/full/player/HlsRendererBuilder.java | 38 ++--- .../demo/simple/HlsRendererBuilder.java | 39 ++--- .../demo/simple/SimplePlayerActivity.java | 6 +- .../android/exoplayer/hls/HlsChunkSource.java | 30 +++- .../exoplayer/hls/HlsMasterPlaylist.java | 5 +- .../hls/HlsMasterPlaylistParser.java | 100 ------------ .../exoplayer/hls/HlsMediaPlaylist.java | 5 +- .../android/exoplayer/hls/HlsPlaylist.java | 36 +++++ ...listParser.java => HlsPlaylistParser.java} | 149 ++++++++++++++++-- 12 files changed, 230 insertions(+), 198 deletions(-) delete mode 100644 library/src/main/java/com/google/android/exoplayer/hls/HlsMasterPlaylistParser.java create mode 100644 library/src/main/java/com/google/android/exoplayer/hls/HlsPlaylist.java rename library/src/main/java/com/google/android/exoplayer/hls/{HlsMediaPlaylistParser.java => HlsPlaylistParser.java} (58%) diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/DemoUtil.java b/demo/src/main/java/com/google/android/exoplayer/demo/DemoUtil.java index 4d1031b0ab..d8a6b27291 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/DemoUtil.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/DemoUtil.java @@ -50,8 +50,9 @@ public class DemoUtil { public static final int TYPE_DASH_VOD = 0; public static final int TYPE_SS = 1; public static final int TYPE_OTHER = 2; - public static final int TYPE_HLS_MASTER = 3; - public static final int TYPE_HLS_MEDIA = 4; + public static final int TYPE_DASH_LIVE = 3; + public static final int TYPE_DASH_LIVE_DVR = 4; + public static final int TYPE_HLS = 5; public static final boolean EXPOSE_EXPERIMENTAL_FEATURES = false; diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/Samples.java b/demo/src/main/java/com/google/android/exoplayer/demo/Samples.java index ee2c3f1cb6..9c5f048116 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/Samples.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/Samples.java @@ -59,7 +59,7 @@ package com.google.android.exoplayer.demo; DemoUtil.TYPE_SS, false, false), new Sample("Apple master playlist (HLS)", "uid:hls:applemaster", "https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/" - + "bipbop_4x3_variant.m3u8", DemoUtil.TYPE_HLS_MASTER, false, false), + + "bipbop_4x3_variant.m3u8", DemoUtil.TYPE_HLS, false, false), new Sample("Dizzy (Misc)", "uid:misc:dizzy", "http://html5demos.com/assets/dizzy.mp4", DemoUtil.TYPE_OTHER, false, false), }; @@ -137,13 +137,13 @@ package com.google.android.exoplayer.demo; public static final Sample[] HLS = new Sample[] { new Sample("Apple master playlist", "uid:hls:applemaster", "https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/" - + "bipbop_4x3_variant.m3u8", DemoUtil.TYPE_HLS_MASTER, false, true), + + "bipbop_4x3_variant.m3u8", DemoUtil.TYPE_HLS, false, true), new Sample("Apple master playlist advanced", "uid:hls:applemasteradvanced", "https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_16x9/" - + "bipbop_16x9_variant.m3u8", DemoUtil.TYPE_HLS_MASTER, false, true), + + "bipbop_16x9_variant.m3u8", DemoUtil.TYPE_HLS, false, true), new Sample("Apple single media playlist", "uid:hls:applesinglemedia", "https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/gear1/" - + "prog_index.m3u8", DemoUtil.TYPE_HLS_MEDIA, false, true), + + "prog_index.m3u8", DemoUtil.TYPE_HLS, false, true), }; public static final Sample[] MISC = new Sample[] { diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/FullPlayerActivity.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/FullPlayerActivity.java index b5ca2e5b49..0a4f2b626f 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/FullPlayerActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/FullPlayerActivity.java @@ -185,9 +185,8 @@ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callba case DemoUtil.TYPE_DASH_VOD: return new DashVodRendererBuilder(userAgent, contentUri.toString(), contentId, new WidevineTestMediaDrmCallback(contentId), debugTextView); - case DemoUtil.TYPE_HLS_MASTER: - case DemoUtil.TYPE_HLS_MEDIA: - return new HlsRendererBuilder(userAgent, contentUri.toString(), contentId, contentType); + case DemoUtil.TYPE_HLS: + return new HlsRendererBuilder(userAgent, contentUri.toString(), contentId); default: return new DefaultRendererBuilder(this, contentUri, debugTextView); } @@ -429,7 +428,7 @@ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callba } } - //DemoPlayer.ClosedCaptioListener implementation + // DemoPlayer.ClosedCaptioListener implementation @Override public void onClosedCaption(List closedCaptions) { diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/HlsRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/HlsRendererBuilder.java index 5bacf381f7..79db5d6566 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/HlsRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/HlsRendererBuilder.java @@ -19,14 +19,12 @@ import com.google.android.exoplayer.MediaCodecAudioTrackRenderer; import com.google.android.exoplayer.MediaCodecUtil; import com.google.android.exoplayer.MediaCodecVideoTrackRenderer; import com.google.android.exoplayer.TrackRenderer; -import com.google.android.exoplayer.demo.DemoUtil; import com.google.android.exoplayer.demo.full.player.DemoPlayer.RendererBuilder; import com.google.android.exoplayer.demo.full.player.DemoPlayer.RendererBuilderCallback; import com.google.android.exoplayer.hls.HlsChunkSource; -import com.google.android.exoplayer.hls.HlsMasterPlaylist; -import com.google.android.exoplayer.hls.HlsMasterPlaylistParser; +import com.google.android.exoplayer.hls.HlsPlaylist; +import com.google.android.exoplayer.hls.HlsPlaylistParser; import com.google.android.exoplayer.hls.HlsSampleSource; -import com.google.android.exoplayer.hls.Variant; import com.google.android.exoplayer.metadata.ClosedCaption; import com.google.android.exoplayer.metadata.Eia608Parser; import com.google.android.exoplayer.metadata.Id3Parser; @@ -39,48 +37,37 @@ import com.google.android.exoplayer.util.ManifestFetcher.ManifestCallback; import com.google.android.exoplayer.util.MimeTypes; import android.media.MediaCodec; -import android.net.Uri; import java.io.IOException; -import java.util.Collections; import java.util.List; import java.util.Map; /** * A {@link RendererBuilder} for HLS. */ -public class HlsRendererBuilder implements RendererBuilder, ManifestCallback { +public class HlsRendererBuilder implements RendererBuilder, ManifestCallback { private final String userAgent; private final String url; private final String contentId; - private final int contentType; private DemoPlayer player; private RendererBuilderCallback callback; - public HlsRendererBuilder(String userAgent, String url, String contentId, int contentType) { + public HlsRendererBuilder(String userAgent, String url, String contentId) { this.userAgent = userAgent; this.url = url; this.contentId = contentId; - this.contentType = contentType; } @Override public void buildRenderers(DemoPlayer player, RendererBuilderCallback callback) { this.player = player; this.callback = callback; - switch (contentType) { - case DemoUtil.TYPE_HLS_MASTER: - HlsMasterPlaylistParser parser = new HlsMasterPlaylistParser(); - ManifestFetcher mediaPlaylistFetcher = - new ManifestFetcher(parser, contentId, url, userAgent); - mediaPlaylistFetcher.singleLoad(player.getMainHandler().getLooper(), this); - break; - case DemoUtil.TYPE_HLS_MEDIA: - onManifest(contentId, newSimpleMasterPlaylist(url)); - break; - } + HlsPlaylistParser parser = new HlsPlaylistParser(); + ManifestFetcher playlistFetcher = + new ManifestFetcher(parser, contentId, url, userAgent); + playlistFetcher.singleLoad(player.getMainHandler().getLooper(), this); } @Override @@ -89,11 +76,11 @@ public class HlsRendererBuilder implements RendererBuilder, ManifestCallback { + ManifestCallback { private final SimplePlayerActivity playerActivity; private final String userAgent; private final String url; private final String contentId; - private final int playlistType; private RendererBuilderCallback callback; public HlsRendererBuilder(SimplePlayerActivity playerActivity, String userAgent, String url, - String contentId, int playlistType) { + String contentId) { this.playerActivity = playerActivity; this.userAgent = userAgent; this.url = url; this.contentId = contentId; - this.playlistType = playlistType; } @Override public void buildRenderers(RendererBuilderCallback callback) { this.callback = callback; - switch (playlistType) { - case DemoUtil.TYPE_HLS_MASTER: - HlsMasterPlaylistParser parser = new HlsMasterPlaylistParser(); - ManifestFetcher mediaPlaylistFetcher = - new ManifestFetcher(parser, contentId, url, userAgent); - mediaPlaylistFetcher.singleLoad(playerActivity.getMainLooper(), this); - break; - case DemoUtil.TYPE_HLS_MEDIA: - onManifest(contentId, newSimpleMasterPlaylist(url)); - break; - } + HlsPlaylistParser parser = new HlsPlaylistParser(); + ManifestFetcher playlistFetcher = + new ManifestFetcher(parser, contentId, url, userAgent); + playlistFetcher.singleLoad(playerActivity.getMainLooper(), this); } @Override @@ -86,10 +73,11 @@ import java.util.Collections; } @Override - public void onManifest(String contentId, HlsMasterPlaylist manifest) { + public void onManifest(String contentId, HlsPlaylist manifest) { DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(); + DataSource dataSource = new UriDataSource(userAgent, bandwidthMeter); - HlsChunkSource chunkSource = new HlsChunkSource(dataSource, manifest, bandwidthMeter, null, + HlsChunkSource chunkSource = new HlsChunkSource(dataSource, url, manifest, bandwidthMeter, null, MediaCodecUtil.getDecoderInfo(MimeTypes.VIDEO_H264, false).adaptive); HlsSampleSource sampleSource = new HlsSampleSource(chunkSource, true, 2); MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(sampleSource, @@ -103,9 +91,4 @@ import java.util.Collections; callback.onRenderers(videoRenderer, audioRenderer); } - private HlsMasterPlaylist newSimpleMasterPlaylist(String mediaPlaylistUrl) { - return new HlsMasterPlaylist(Uri.parse(""), - Collections.singletonList(new Variant(0, mediaPlaylistUrl, 0, null, -1, -1))); - } - } diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/simple/SimplePlayerActivity.java b/demo/src/main/java/com/google/android/exoplayer/demo/simple/SimplePlayerActivity.java index f2b929d9fb..fb7766baf3 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/simple/SimplePlayerActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/simple/SimplePlayerActivity.java @@ -166,10 +166,8 @@ public class SimplePlayerActivity extends Activity implements SurfaceHolder.Call contentId); case DemoUtil.TYPE_DASH_VOD: return new DashVodRendererBuilder(this, userAgent, contentUri.toString(), contentId); - case DemoUtil.TYPE_HLS_MASTER: - case DemoUtil.TYPE_HLS_MEDIA: - return new HlsRendererBuilder(this, userAgent, contentUri.toString(), contentId, - contentType); + case DemoUtil.TYPE_HLS: + return new HlsRendererBuilder(this, userAgent, contentUri.toString(), contentId); default: return new DefaultRendererBuilder(this, contentUri); } 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 b422db2d5e..bfc33c25e7 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 @@ -23,6 +23,7 @@ import com.google.android.exoplayer.upstream.Aes128DataSource; import com.google.android.exoplayer.upstream.BandwidthMeter; import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DataSpec; +import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.BitArray; import com.google.android.exoplayer.util.Util; @@ -51,7 +52,7 @@ public class HlsChunkSource { private final SamplePool samplePool = new TsExtractor.SamplePool(); private final DataSource upstreamDataSource; - private final HlsMediaPlaylistParser mediaPlaylistParser; + private final HlsPlaylistParser playlistParser; private final Variant[] enabledVariants; private final BandwidthMeter bandwidthMeter; private final BitArray bitArray; @@ -73,21 +74,32 @@ public class HlsChunkSource { /** * @param dataSource A {@link DataSource} suitable for loading the media data. - * @param masterPlaylist The master playlist. + * @param playlistUrl The playlist URL. + * @param playlist The hls playlist. + * @param bandwidthMeter provides an estimate of the currently available bandwidth. * @param variantIndices A subset of variant indices to consider, or null to consider all of the * variants in the master playlist. */ - public HlsChunkSource(DataSource dataSource, HlsMasterPlaylist masterPlaylist, + public HlsChunkSource(DataSource dataSource, String playlistUrl, HlsPlaylist playlist, BandwidthMeter bandwidthMeter, int[] variantIndices, boolean enableAdaptive) { this.upstreamDataSource = dataSource; this.bandwidthMeter = bandwidthMeter; this.enableAdaptive = enableAdaptive; - baseUri = masterPlaylist.baseUri; + baseUri = playlist.baseUri; bitArray = new BitArray(); - mediaPlaylistParser = new HlsMediaPlaylistParser(); - enabledVariants = filterVariants(masterPlaylist, variantIndices); + playlistParser = new HlsPlaylistParser(); + + if (playlist.type == HlsPlaylist.TYPE_MEDIA) { + enabledVariants = new Variant[] {new Variant(0, playlistUrl, 0, null, -1, -1)}; + mediaPlaylists = new HlsMediaPlaylist[] {(HlsMediaPlaylist) playlist}; + } else { + Assertions.checkState(playlist.type == HlsPlaylist.TYPE_MASTER); + enabledVariants = filterVariants((HlsMasterPlaylist) playlist, variantIndices); + mediaPlaylists = new HlsMediaPlaylist[enabledVariants.length]; + } + lastMediaPlaylistLoadTimesMs = new long[enabledVariants.length]; - mediaPlaylists = new HlsMediaPlaylist[enabledVariants.length]; + int maxWidth = -1; int maxHeight = -1; // Select the first variant from the master playlist that's enabled. @@ -391,9 +403,11 @@ public class HlsChunkSource { @Override protected void consume(BitArray data) throws IOException { - HlsMediaPlaylist mediaPlaylist = mediaPlaylistParser.parse( + HlsPlaylist playlist = playlistParser.parse( new ByteArrayInputStream(data.getData(), 0, data.bytesLeft()), null, null, playlistBaseUri); + Assertions.checkState(playlist.type == HlsPlaylist.TYPE_MEDIA); + HlsMediaPlaylist mediaPlaylist = (HlsMediaPlaylist) playlist; mediaPlaylists[variantIndex] = mediaPlaylist; lastMediaPlaylistLoadTimesMs[variantIndex] = SystemClock.elapsedRealtime(); live |= mediaPlaylist.live; diff --git a/library/src/main/java/com/google/android/exoplayer/hls/HlsMasterPlaylist.java b/library/src/main/java/com/google/android/exoplayer/hls/HlsMasterPlaylist.java index ade40ebb3a..7ce299df0d 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/HlsMasterPlaylist.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/HlsMasterPlaylist.java @@ -22,13 +22,12 @@ import java.util.List; /** * Represents an HLS master playlist. */ -public final class HlsMasterPlaylist { +public final class HlsMasterPlaylist extends HlsPlaylist { - public final Uri baseUri; public final List variants; public HlsMasterPlaylist(Uri baseUri, List variants) { - this.baseUri = baseUri; + super(baseUri, HlsPlaylist.TYPE_MASTER); this.variants = variants; } diff --git a/library/src/main/java/com/google/android/exoplayer/hls/HlsMasterPlaylistParser.java b/library/src/main/java/com/google/android/exoplayer/hls/HlsMasterPlaylistParser.java deleted file mode 100644 index 3b2ed82b7c..0000000000 --- a/library/src/main/java/com/google/android/exoplayer/hls/HlsMasterPlaylistParser.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright (C) 2014 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer.hls; - -import com.google.android.exoplayer.util.ManifestParser; - -import android.net.Uri; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.regex.Pattern; - -/** - * HLS Master playlists parsing logic. - */ -public final class HlsMasterPlaylistParser implements ManifestParser { - - private static final String STREAM_INF_TAG = "#EXT-X-STREAM-INF"; - private static final String BANDWIDTH_ATTR = "BANDWIDTH"; - private static final String CODECS_ATTR = "CODECS"; - private static final String RESOLUTION_ATTR = "RESOLUTION"; - - private static final Pattern BANDWIDTH_ATTR_REGEX = - Pattern.compile(BANDWIDTH_ATTR + "=(\\d+)\\b"); - private static final Pattern CODECS_ATTR_REGEX = - Pattern.compile(CODECS_ATTR + "=\"(.+)\""); - private static final Pattern RESOLUTION_ATTR_REGEX = - Pattern.compile(RESOLUTION_ATTR + "=(\\d+x\\d+)"); - - @Override - public HlsMasterPlaylist parse(InputStream inputStream, String inputEncoding, - String contentId, Uri baseUri) throws IOException { - return parseMasterPlaylist(inputStream, inputEncoding, baseUri); - } - - private static HlsMasterPlaylist parseMasterPlaylist(InputStream inputStream, - String inputEncoding, Uri baseUri) throws IOException { - BufferedReader reader = new BufferedReader((inputEncoding == null) - ? new InputStreamReader(inputStream) : new InputStreamReader(inputStream, inputEncoding)); - List variants = new ArrayList(); - int bandwidth = 0; - String[] codecs = null; - int width = -1; - int height = -1; - int variantIndex = 0; - - String line; - while ((line = reader.readLine()) != null) { - line = line.trim(); - if (line.isEmpty()) { - continue; - } - if (line.startsWith(STREAM_INF_TAG)) { - bandwidth = HlsParserUtil.parseIntAttr(line, BANDWIDTH_ATTR_REGEX, BANDWIDTH_ATTR); - String codecsString = HlsParserUtil.parseOptionalStringAttr(line, CODECS_ATTR_REGEX); - if (codecsString != null) { - codecs = codecsString.split("(\\s*,\\s*)|(\\s*$)"); - } else { - codecs = null; - } - String resolutionString = HlsParserUtil.parseOptionalStringAttr(line, - RESOLUTION_ATTR_REGEX); - if (resolutionString != null) { - String[] widthAndHeight = resolutionString.split("x"); - width = Integer.parseInt(widthAndHeight[0]); - height = Integer.parseInt(widthAndHeight[1]); - } else { - width = -1; - height = -1; - } - } else if (!line.startsWith("#")) { - variants.add(new Variant(variantIndex++, line, bandwidth, codecs, width, height)); - bandwidth = 0; - codecs = null; - width = -1; - height = -1; - } - } - return new HlsMasterPlaylist(baseUri, Collections.unmodifiableList(variants)); - } - -} 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 29df5562df..70192c87c4 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 @@ -22,7 +22,7 @@ import java.util.List; /** * Represents an HLS media playlist. */ -public final class HlsMediaPlaylist { +public final class HlsMediaPlaylist extends HlsPlaylist { /** * Media segment reference. @@ -61,7 +61,6 @@ public final class HlsMediaPlaylist { public static final String ENCRYPTION_METHOD_NONE = "NONE"; public static final String ENCRYPTION_METHOD_AES_128 = "AES-128"; - public final Uri baseUri; public final int mediaSequence; public final int targetDurationSecs; public final int version; @@ -71,7 +70,7 @@ public final class HlsMediaPlaylist { public HlsMediaPlaylist(Uri baseUri, int mediaSequence, int targetDurationSecs, int version, boolean live, List segments) { - this.baseUri = baseUri; + super(baseUri, HlsPlaylist.TYPE_MEDIA); this.mediaSequence = mediaSequence; this.targetDurationSecs = targetDurationSecs; this.version = version; diff --git a/library/src/main/java/com/google/android/exoplayer/hls/HlsPlaylist.java b/library/src/main/java/com/google/android/exoplayer/hls/HlsPlaylist.java new file mode 100644 index 0000000000..3c86328ba6 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/hls/HlsPlaylist.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer.hls; + +import android.net.Uri; + +/** + * Represents an HLS playlist. + */ +public abstract class HlsPlaylist { + + public final static int TYPE_MASTER = 0; + public final static int TYPE_MEDIA = 1; + + public final Uri baseUri; + public final int type; + + protected HlsPlaylist(Uri baseUri, int type) { + this.baseUri = baseUri; + this.type = type; + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/hls/HlsMediaPlaylistParser.java b/library/src/main/java/com/google/android/exoplayer/hls/HlsPlaylistParser.java similarity index 58% rename from library/src/main/java/com/google/android/exoplayer/hls/HlsMediaPlaylistParser.java rename to library/src/main/java/com/google/android/exoplayer/hls/HlsPlaylistParser.java index abae63301a..8ee093c91d 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/HlsMediaPlaylistParser.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/HlsPlaylistParser.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer.hls; import com.google.android.exoplayer.C; +import com.google.android.exoplayer.ParserException; import com.google.android.exoplayer.hls.HlsMediaPlaylist.Segment; import com.google.android.exoplayer.util.ManifestParser; @@ -27,19 +28,27 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.Collections; +import java.util.LinkedList; import java.util.List; +import java.util.Queue; import java.util.regex.Pattern; /** - * HLS Media playlists parsing logic. + * HLS playlists parsing logic. */ -public final class HlsMediaPlaylistParser implements ManifestParser { +public final class HlsPlaylistParser implements ManifestParser { + + private static final String VERSION_TAG = "#EXT-X-VERSION"; + + private static final String STREAM_INF_TAG = "#EXT-X-STREAM-INF"; + private static final String BANDWIDTH_ATTR = "BANDWIDTH"; + private static final String CODECS_ATTR = "CODECS"; + private static final String RESOLUTION_ATTR = "RESOLUTION"; private static final String DISCONTINUITY_TAG = "#EXT-X-DISCONTINUITY"; private static final String MEDIA_DURATION_TAG = "#EXTINF"; private static final String MEDIA_SEQUENCE_TAG = "#EXT-X-MEDIA-SEQUENCE"; private static final String TARGET_DURATION_TAG = "#EXT-X-TARGETDURATION"; - private static final String VERSION_TAG = "#EXT-X-VERSION"; 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"; @@ -48,6 +57,13 @@ public final class HlsMediaPlaylistParser implements ManifestParser extraLines = new LinkedList(); + String line; + try { + while ((line = reader.readLine()) != null) { + line = line.trim(); + if (line.isEmpty()) { + // Do nothing. + } else if (line.startsWith(STREAM_INF_TAG)) { + extraLines.add(line); + return parseMasterPlaylist(new LineIterator(extraLines, reader), baseUri); + } else if (line.startsWith(TARGET_DURATION_TAG) + || line.startsWith(MEDIA_SEQUENCE_TAG) + || line.startsWith(MEDIA_DURATION_TAG) + || line.startsWith(KEY_TAG) + || line.startsWith(BYTERANGE_TAG) + || line.equals(DISCONTINUITY_TAG) + || line.equals(ENDLIST_TAG)) { + extraLines.add(line); + return parseMediaPlaylist(new LineIterator(extraLines, reader), baseUri); + } else if (line.startsWith(VERSION_TAG)) { + extraLines.add(line); + } else if (!line.startsWith("#")) { + throw new ParserException("Missing a tag before URL."); + } + } + } finally { + reader.close(); + } + throw new ParserException("Failed to parse the playlist, could not identify any tags."); + } + private static HlsMasterPlaylist parseMasterPlaylist(LineIterator iterator, Uri baseUri) + throws IOException { + List variants = new ArrayList(); + int bandwidth = 0; + String[] codecs = null; + int width = -1; + int height = -1; + int variantIndex = 0; + + String line; + while (iterator.hasNext()) { + line = iterator.next(); + if (line.startsWith(STREAM_INF_TAG)) { + bandwidth = HlsParserUtil.parseIntAttr(line, BANDWIDTH_ATTR_REGEX, BANDWIDTH_ATTR); + String codecsString = HlsParserUtil.parseOptionalStringAttr(line, CODECS_ATTR_REGEX); + if (codecsString != null) { + codecs = codecsString.split("(\\s*,\\s*)|(\\s*$)"); + } else { + codecs = null; + } + String resolutionString = HlsParserUtil.parseOptionalStringAttr(line, + RESOLUTION_ATTR_REGEX); + if (resolutionString != null) { + String[] widthAndHeight = resolutionString.split("x"); + width = Integer.parseInt(widthAndHeight[0]); + height = Integer.parseInt(widthAndHeight[1]); + } else { + width = -1; + height = -1; + } + } else if (!line.startsWith("#")) { + variants.add(new Variant(variantIndex++, line, bandwidth, codecs, width, height)); + bandwidth = 0; + codecs = null; + width = -1; + height = -1; + } + } + return new HlsMasterPlaylist(baseUri, Collections.unmodifiableList(variants)); + } + + private static HlsMediaPlaylist parseMediaPlaylist(LineIterator iterator, Uri baseUri) + throws IOException { int mediaSequence = 0; int targetDurationSecs = 0; int version = 1; // Default version == 1. @@ -95,11 +179,8 @@ public final class HlsMediaPlaylistParser implements ManifestParser extraLines; + + private String next; + + public LineIterator(Queue extraLines, BufferedReader reader) { + this.extraLines = extraLines; + this.reader = reader; + } + + public boolean hasNext() throws IOException { + if (next != null) { + return true; + } + if (!extraLines.isEmpty()) { + next = extraLines.poll(); + return true; + } + while ((next = reader.readLine()) != null) { + next = next.trim(); + if (!next.isEmpty()) { + return true; + } + } + return false; + } + + public String next() throws IOException { + String result = null; + if (hasNext()) { + result = next; + next = null; + } + return result; + } + + } + } From 8e2801ce9b4ab0091ec1681832f9de4c1acdbdc8 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Wed, 26 Nov 2014 12:08:46 +0000 Subject: [PATCH 32/55] Improve HLS ABR. - Add options to switch abruptly at segment boundaries. Third parties who guarantee keyframes at the start of segments will want this, because it makes switching more efficient and hence rebuffering less likely. - Switch quality faster when performing a splicing switch (when we detect that we need to switch variant, we now immediately request the same segment as we did last time for the new variant, rather than requesting one more segment for the old variant before doing this. --- .../demo/full/player/HlsRendererBuilder.java | 3 +- .../demo/simple/HlsRendererBuilder.java | 3 +- .../android/exoplayer/hls/HlsChunkSource.java | 197 +++++++++++------- .../exoplayer/hls/HlsSampleSource.java | 45 ++-- .../google/android/exoplayer/hls/TsChunk.java | 24 +-- .../android/exoplayer/hls/TsExtractor.java | 10 +- 6 files changed, 170 insertions(+), 112 deletions(-) diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/HlsRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/HlsRendererBuilder.java index 79db5d6566..5a3a3535e9 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/HlsRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/HlsRendererBuilder.java @@ -80,8 +80,9 @@ public class HlsRendererBuilder implements RendererBuilder, ManifestCallback + * The initially selected variant will be used throughout playback. + */ + public static final int ADAPTIVE_MODE_NONE = 0; + + /** + * Adaptive switches splice overlapping segments of the old and new variants. + *

+ * When performing a switch from one variant to another, overlapping segments will be requested + * from both the old and new variants. These segments will then be spliced together, allowing + * a seamless switch from one variant to another even if keyframes are misaligned or if keyframes + * are not positioned at the start of each segment. + *

+ * Note that where it can be guaranteed that the source content has keyframes positioned at the + * start of each segment, {@link #ADAPTIVE_MODE_ABRUPT} should always be used in preference to + * this mode. + */ + public static final int ADAPTIVE_MODE_SPLICE = 1; + + /** + * Adaptive switches are performed at segment boundaries. + *

+ * For this mode to perform seamless switches, the source content is required to have keyframes + * positioned at the start of each segment. If this is not the case a visual discontinuity may + * be experienced when switching from one variant to another. + *

+ * Note that where it can be guaranteed that the source content does have keyframes positioned at + * the start of each segment, this mode should always be used in preference to + * {@link #ADAPTIVE_MODE_SPLICE} because it requires fetching less data. + */ + public static final int ADAPTIVE_MODE_ABRUPT = 3; + private static final float BANDWIDTH_FRACTION = 0.8f; private static final long MIN_BUFFER_TO_SWITCH_UP_US = 5000000; private static final long MAX_BUFFER_TO_SWITCH_DOWN_US = 15000000; @@ -56,7 +90,7 @@ public class HlsChunkSource { private final Variant[] enabledVariants; private final BandwidthMeter bandwidthMeter; private final BitArray bitArray; - private final boolean enableAdaptive; + private final int adaptiveMode; private final Uri baseUri; private final int maxWidth; private final int maxHeight; @@ -79,27 +113,31 @@ public class HlsChunkSource { * @param bandwidthMeter provides an estimate of the currently available bandwidth. * @param variantIndices A subset of variant indices to consider, or null to consider all of the * variants in the master playlist. + * @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}. */ public HlsChunkSource(DataSource dataSource, String playlistUrl, HlsPlaylist playlist, - BandwidthMeter bandwidthMeter, int[] variantIndices, boolean enableAdaptive) { + BandwidthMeter bandwidthMeter, int[] variantIndices, int adaptiveMode) { this.upstreamDataSource = dataSource; this.bandwidthMeter = bandwidthMeter; - this.enableAdaptive = enableAdaptive; + this.adaptiveMode = adaptiveMode; baseUri = playlist.baseUri; bitArray = new BitArray(); playlistParser = new HlsPlaylistParser(); if (playlist.type == HlsPlaylist.TYPE_MEDIA) { enabledVariants = new Variant[] {new Variant(0, playlistUrl, 0, null, -1, -1)}; - mediaPlaylists = new HlsMediaPlaylist[] {(HlsMediaPlaylist) playlist}; + mediaPlaylists = new HlsMediaPlaylist[1]; + lastMediaPlaylistLoadTimesMs = new long[1]; + setMediaPlaylist(0, (HlsMediaPlaylist) playlist); } else { Assertions.checkState(playlist.type == HlsPlaylist.TYPE_MASTER); enabledVariants = filterVariants((HlsMasterPlaylist) playlist, variantIndices); mediaPlaylists = new HlsMediaPlaylist[enabledVariants.length]; + lastMediaPlaylistLoadTimesMs = new long[enabledVariants.length]; } - lastMediaPlaylistLoadTimesMs = new long[enabledVariants.length]; - int maxWidth = -1; int maxHeight = -1; // Select the first variant from the master playlist that's enabled. @@ -144,24 +182,41 @@ public class HlsChunkSource { */ public HlsChunk getChunkOperation(TsChunk previousTsChunk, long seekPositionUs, long playbackPositionUs) { - - HlsMediaPlaylist mediaPlaylist = mediaPlaylists[variantIndex]; - if (mediaPlaylist == null) { - return newMediaPlaylistChunk(); + if (previousTsChunk != null && previousTsChunk.isLastChunk) { + // We're already finished. + return null; } + int nextVariantIndex = variantIndex; + boolean switchingVariant = false; + boolean switchingVariantSpliced = false; + if (adaptiveMode == ADAPTIVE_MODE_NONE) { + // Do nothing. + } else { + nextVariantIndex = getNextVariantIndex(previousTsChunk, playbackPositionUs); + switchingVariant = nextVariantIndex != variantIndex; + switchingVariantSpliced = switchingVariant && adaptiveMode == ADAPTIVE_MODE_SPLICE; + } + + HlsMediaPlaylist mediaPlaylist = mediaPlaylists[nextVariantIndex]; + if (mediaPlaylist == null) { + // We don't have the media playlist for the next variant. Request it now. + return newMediaPlaylistChunk(nextVariantIndex); + } + + variantIndex = nextVariantIndex; int chunkMediaSequence = 0; + boolean liveDiscontinuity = false; if (live) { if (previousTsChunk == null) { - chunkMediaSequence = getLiveStartChunkMediaSequence(); + chunkMediaSequence = getLiveStartChunkMediaSequence(variantIndex); } else { - // For live nextChunkIndex contains chunk media sequence number. - chunkMediaSequence = previousTsChunk.nextChunkIndex; - // If the updated playlist is far ahead and doesn't even have the last chunk from the - // queue, then try to catch up, skip a few chunks and start as if it was a new playlist. + chunkMediaSequence = switchingVariantSpliced + ? previousTsChunk.chunkIndex : previousTsChunk.chunkIndex + 1; if (chunkMediaSequence < mediaPlaylist.mediaSequence) { - // TODO: Trigger discontinuity in this case. - chunkMediaSequence = getLiveStartChunkMediaSequence(); + // If the chunk is no longer in the playlist. Skip ahead and start again. + chunkMediaSequence = getLiveStartChunkMediaSequence(variantIndex); + liveDiscontinuity = true; } } } else { @@ -170,19 +225,15 @@ public class HlsChunkSource { chunkMediaSequence = Util.binarySearchFloor(mediaPlaylist.segments, seekPositionUs, true, true) + mediaPlaylist.mediaSequence; } else { - chunkMediaSequence = previousTsChunk.nextChunkIndex; + chunkMediaSequence = switchingVariantSpliced + ? previousTsChunk.chunkIndex : previousTsChunk.chunkIndex + 1; } } - if (chunkMediaSequence == -1) { - // We've reached the end of the stream. - return null; - } - int chunkIndex = chunkMediaSequence - mediaPlaylist.mediaSequence; if (chunkIndex >= mediaPlaylist.segments.size()) { - if (mediaPlaylist.live && shouldRerequestMediaPlaylist()) { - return newMediaPlaylistChunk(); + if (mediaPlaylist.live && shouldRerequestMediaPlaylist(variantIndex)) { + return newMediaPlaylistChunk(variantIndex); } else { return null; } @@ -206,67 +257,59 @@ public class HlsChunkSource { clearEncryptedDataSource(); } + // Configure the data source and spec for the chunk. + DataSource dataSource = encryptedDataSource != null ? encryptedDataSource : upstreamDataSource; + DataSpec dataSpec = new DataSpec(chunkUri, segment.byterangeOffset, segment.byterangeLength, + null); + + // Compute start and end times, and the sequence number of the next chunk. long startTimeUs; - boolean splicingIn = previousTsChunk != null && previousTsChunk.splicingOut; - int nextChunkMediaSequence = chunkMediaSequence + 1; if (live) { if (previousTsChunk == null) { startTimeUs = 0; - } else if (splicingIn) { + } else if (switchingVariantSpliced) { startTimeUs = previousTsChunk.startTimeUs; } else { startTimeUs = previousTsChunk.endTimeUs; } - } else { - // Not live. + } else /* Not live */ { startTimeUs = segment.startTimeUs; } - if (!mediaPlaylist.live && chunkIndex == mediaPlaylist.segments.size() - 1) { - nextChunkMediaSequence = -1; - } - long endTimeUs = startTimeUs + (long) (segment.durationSecs * 1000000); - - int currentVariantIndex = variantIndex; - boolean splicingOut = false; - if (splicingIn) { - // Do nothing. - } else if (enableAdaptive && nextChunkMediaSequence != -1) { - int idealVariantIndex = getVariantIndexForBandwdith( - (int) (bandwidthMeter.getBitrateEstimate() * BANDWIDTH_FRACTION)); - long bufferedUs = startTimeUs - playbackPositionUs; - if ((idealVariantIndex > currentVariantIndex && bufferedUs < MAX_BUFFER_TO_SWITCH_DOWN_US) - || (idealVariantIndex < currentVariantIndex && bufferedUs > MIN_BUFFER_TO_SWITCH_UP_US)) { - variantIndex = idealVariantIndex; - } - splicingOut = variantIndex != currentVariantIndex; - if (splicingOut) { - // If we're splicing out, we want to load the same chunk again next time, but for a - // different variant. - nextChunkMediaSequence = chunkMediaSequence; - } - } - - // Configure the datasource for loading the chunk. - DataSource dataSource; - if (encryptedDataSource != null) { - dataSource = encryptedDataSource; - } else { - dataSource = upstreamDataSource; - } - DataSpec dataSpec = new DataSpec(chunkUri, segment.byterangeOffset, segment.byterangeLength, - null); + boolean isLastChunk = !mediaPlaylist.live && chunkIndex == mediaPlaylist.segments.size() - 1; // Configure the extractor that will read the chunk. TsExtractor extractor; - if (previousTsChunk == null || splicingIn || segment.discontinuity) { - extractor = new TsExtractor(startTimeUs, samplePool); + if (previousTsChunk == null || segment.discontinuity || switchingVariant || liveDiscontinuity) { + extractor = new TsExtractor(startTimeUs, samplePool, switchingVariantSpliced); } else { extractor = previousTsChunk.extractor; } - return new TsChunk(dataSource, dataSpec, extractor, enabledVariants[currentVariantIndex].index, - startTimeUs, endTimeUs, nextChunkMediaSequence, splicingOut); + return new TsChunk(dataSource, dataSpec, extractor, enabledVariants[variantIndex].index, + startTimeUs, endTimeUs, chunkMediaSequence, isLastChunk); + } + + private int getNextVariantIndex(TsChunk previousTsChunk, long playbackPositionUs) { + int idealVariantIndex = getVariantIndexForBandwdith( + (int) (bandwidthMeter.getBitrateEstimate() * BANDWIDTH_FRACTION)); + if (idealVariantIndex == variantIndex) { + // We're already using the ideal variant. + return variantIndex; + } + // We're not using the ideal variant for the available bandwidth, but only switch if the + // conditions are appropriate. + long bufferedPositionUs = previousTsChunk == null ? playbackPositionUs + : adaptiveMode == ADAPTIVE_MODE_SPLICE ? previousTsChunk.startTimeUs + : previousTsChunk.endTimeUs; + long bufferedUs = bufferedPositionUs - playbackPositionUs; + if ((idealVariantIndex > variantIndex && bufferedUs < MAX_BUFFER_TO_SWITCH_DOWN_US) + || (idealVariantIndex < variantIndex && bufferedUs > MIN_BUFFER_TO_SWITCH_UP_US)) { + // Switch variant. + return idealVariantIndex; + } + // Stick with the current variant for now. + return variantIndex; } private int getVariantIndexForBandwdith(int bandwidth) { @@ -278,7 +321,7 @@ public class HlsChunkSource { return enabledVariants.length - 1; } - private boolean shouldRerequestMediaPlaylist() { + private boolean shouldRerequestMediaPlaylist(int variantIndex) { // Don't re-request media playlist more often than one-half of the target duration. HlsMediaPlaylist mediaPlaylist = mediaPlaylists[variantIndex]; long timeSinceLastMediaPlaylistLoadMs = @@ -286,14 +329,14 @@ public class HlsChunkSource { return timeSinceLastMediaPlaylistLoadMs >= (mediaPlaylist.targetDurationSecs * 1000) / 2; } - private int getLiveStartChunkMediaSequence() { + private int getLiveStartChunkMediaSequence(int variantIndex) { // For live start playback from the third chunk from the end. HlsMediaPlaylist mediaPlaylist = mediaPlaylists[variantIndex]; int chunkIndex = mediaPlaylist.segments.size() > 3 ? mediaPlaylist.segments.size() - 3 : 0; return chunkIndex + mediaPlaylist.mediaSequence; } - private MediaPlaylistChunk newMediaPlaylistChunk() { + private MediaPlaylistChunk newMediaPlaylistChunk(int variantIndex) { Uri mediaPlaylistUri = Util.getMergedUri(baseUri, enabledVariants[variantIndex].url); DataSpec dataSpec = new DataSpec(mediaPlaylistUri, 0, C.LENGTH_UNBOUNDED, null); Uri baseUri = Util.parseBaseUri(mediaPlaylistUri.toString()); @@ -332,6 +375,13 @@ public class HlsChunkSource { encryptedDataSourceSecretKey = null; } + /* package */ void setMediaPlaylist(int variantIndex, HlsMediaPlaylist mediaPlaylist) { + lastMediaPlaylistLoadTimesMs[variantIndex] = SystemClock.elapsedRealtime(); + mediaPlaylists[variantIndex] = mediaPlaylist; + live |= mediaPlaylist.live; + durationUs = mediaPlaylist.durationUs; + } + private static Variant[] filterVariants(HlsMasterPlaylist masterPlaylist, int[] variantIndices) { List masterVariants = masterPlaylist.variants; ArrayList enabledVariants = new ArrayList(); @@ -408,10 +458,7 @@ public class HlsChunkSource { playlistBaseUri); Assertions.checkState(playlist.type == HlsPlaylist.TYPE_MEDIA); HlsMediaPlaylist mediaPlaylist = (HlsMediaPlaylist) playlist; - mediaPlaylists[variantIndex] = mediaPlaylist; - lastMediaPlaylistLoadTimesMs[variantIndex] = SystemClock.elapsedRealtime(); - live |= mediaPlaylist.live; - durationUs = mediaPlaylist.durationUs; + setMediaPlaylist(variantIndex, mediaPlaylist); } } 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 4982ca894c..e2ef450970 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 @@ -35,12 +35,18 @@ import java.util.LinkedList; */ public class HlsSampleSource implements SampleSource, Loader.Callback { + /** + * The default minimum number of times to retry loading data prior to failing. + */ + public static final int DEFAULT_MIN_LOADABLE_RETRY_COUNT = 1; + private static final long BUFFER_DURATION_US = 20000000; private static final int NO_RESET_PENDING = -1; private final HlsChunkSource chunkSource; private final LinkedList extractors; private final boolean frameAccurateSeeking; + private final int minLoadableRetryCount; private int remainingReleaseCount; private boolean prepared; @@ -65,11 +71,18 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { private int currentLoadableExceptionCount; private long currentLoadableExceptionTimestamp; - public HlsSampleSource(HlsChunkSource chunkSource, - boolean frameAccurateSeeking, int downstreamRendererCount) { + public HlsSampleSource(HlsChunkSource chunkSource, boolean frameAccurateSeeking, + int downstreamRendererCount) { + this(chunkSource, frameAccurateSeeking, downstreamRendererCount, + DEFAULT_MIN_LOADABLE_RETRY_COUNT); + } + + public HlsSampleSource(HlsChunkSource chunkSource, boolean frameAccurateSeeking, + int downstreamRendererCount, int minLoadableRetryCount) { this.chunkSource = chunkSource; this.frameAccurateSeeking = frameAccurateSeeking; this.remainingReleaseCount = downstreamRendererCount; + this.minLoadableRetryCount = minLoadableRetryCount; extractors = new LinkedList(); } @@ -97,8 +110,8 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { prepared = true; } } - if (!prepared && currentLoadableException != null) { - throw currentLoadableException; + if (!prepared) { + maybeThrowLoadableException(); } return prepared; } @@ -157,8 +170,8 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { return false; } boolean haveSamples = extractors.getFirst().hasSamples(); - if (!haveSamples && currentLoadableException != null) { - throw currentLoadableException; + if (!haveSamples) { + maybeThrowLoadableException(); } return haveSamples; } @@ -175,9 +188,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { } if (onlyReadDiscontinuity || isPendingReset() || extractors.isEmpty()) { - if (currentLoadableException != null) { - throw currentLoadableException; - } + maybeThrowLoadableException(); return NOTHING_READ; } @@ -202,9 +213,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { } if (!extractor.isPrepared()) { - if (currentLoadableException != null) { - throw currentLoadableException; - } + maybeThrowLoadableException(); return NOTHING_READ; } @@ -225,9 +234,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { return END_OF_STREAM; } - if (currentLoadableException != null) { - throw currentLoadableException; - } + maybeThrowLoadableException(); return NOTHING_READ; } @@ -283,7 +290,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { } finally { if (isTsChunk(currentLoadable)) { TsChunk tsChunk = (TsChunk) loadable; - loadingFinished = tsChunk.isLastChunk(); + loadingFinished = tsChunk.isLastChunk; } if (!currentLoadableExceptionFatal) { clearCurrentLoadable(); @@ -309,6 +316,12 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { maybeStartLoading(); } + private void maybeThrowLoadableException() throws IOException { + if (currentLoadableException != null && currentLoadableExceptionCount > minLoadableRetryCount) { + throw currentLoadableException; + } + } + private void restartFrom(long positionUs) { pendingResetPositionUs = positionUs; loadingFinished = false; diff --git a/library/src/main/java/com/google/android/exoplayer/hls/TsChunk.java b/library/src/main/java/com/google/android/exoplayer/hls/TsChunk.java index 9222317839..4261dbd4ca 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/TsChunk.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/TsChunk.java @@ -39,14 +39,13 @@ public final class TsChunk extends HlsChunk { */ public final long endTimeUs; /** - * The index of the next media chunk, or -1 if this is the last media chunk in the stream. + * The chunk index. */ - public final int nextChunkIndex; + public final int chunkIndex; /** - * True if this is the final chunk being loaded for the current variant, as we splice to another - * one. False otherwise. + * True if this is the last chunk in the media. False otherwise. */ - public final boolean splicingOut; + public final boolean isLastChunk; /** * The extractor into which this chunk is being consumed. */ @@ -62,19 +61,18 @@ public final class TsChunk extends HlsChunk { * @param variantIndex The index of the variant in the master playlist. * @param startTimeUs The start time of the media contained by the chunk, in microseconds. * @param endTimeUs The end time of the media contained by the chunk, in microseconds. - * @param nextChunkIndex The index of the next chunk, or -1 if this is the last chunk. - * @param splicingOut True if this is the final chunk being loaded for the current variant, as we - * splice to another one. False otherwise. + * @param chunkIndex The index of the chunk. + * @param isLastChunk True if this is the last chunk in the media. False otherwise. */ public TsChunk(DataSource dataSource, DataSpec dataSpec, TsExtractor tsExtractor, - int variantIndex, long startTimeUs, long endTimeUs, int nextChunkIndex, boolean splicingOut) { + int variantIndex, long startTimeUs, long endTimeUs, int chunkIndex, boolean isLastChunk) { super(dataSource, dataSpec); this.extractor = tsExtractor; this.variantIndex = variantIndex; this.startTimeUs = startTimeUs; this.endTimeUs = endTimeUs; - this.nextChunkIndex = nextChunkIndex; - this.splicingOut = splicingOut; + this.chunkIndex = chunkIndex; + this.isLastChunk = isLastChunk; } @Override @@ -82,10 +80,6 @@ public final class TsChunk extends HlsChunk { // Do nothing. } - public boolean isLastChunk() { - return nextChunkIndex == -1; - } - @Override public boolean isLoadFinished() { return loadFinished; diff --git a/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java b/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java index d574078162..64541a1492 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java @@ -56,6 +56,7 @@ public final class TsExtractor { private final SparseArray sampleQueues; // Indexed by streamType private final SparseArray tsPayloadReaders; // Indexed by pid private final SamplePool samplePool; + private final boolean shouldSpliceIn; /* package */ final long firstSampleTimestamp; // Accessed only by the consuming thread. @@ -69,9 +70,10 @@ public final class TsExtractor { private volatile boolean prepared; /* package */ volatile long largestParsedTimestampUs; - public TsExtractor(long firstSampleTimestamp, SamplePool samplePool) { + public TsExtractor(long firstSampleTimestamp, SamplePool samplePool, boolean shouldSpliceIn) { this.firstSampleTimestamp = firstSampleTimestamp; this.samplePool = samplePool; + this.shouldSpliceIn = shouldSpliceIn; pendingFirstSampleTimestampAdjustment = true; tsPacketBuffer = new BitArray(); sampleQueues = new SparseArray(); @@ -141,9 +143,9 @@ public final class TsExtractor { */ public void configureSpliceTo(TsExtractor nextExtractor) { Assertions.checkState(prepared); - if (spliceConfigured || !nextExtractor.isPrepared()) { - // The splice is already configured or the next extractor isn't ready to be spliced in. - // Already configured, or too early to splice. + if (spliceConfigured || !nextExtractor.shouldSpliceIn || !nextExtractor.isPrepared()) { + // The splice is already configured, or the next extractor doesn't want to be spliced in, or + // the next extractor isn't ready to be spliced in. return; } boolean spliceConfigured = true; From 40f3172237fcb0c58eb07c2766c989cd501e15ae Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Wed, 26 Nov 2014 17:21:41 +0000 Subject: [PATCH 33/55] HLS: More control over buffering + tweak caption impl. - Move all three buffering constants to a single class (the chunk source). - Increase the target buffer to 40s for increased robustness against temporary network blips. - Make values configurable via the chunk source constructor. - Treat captions as a text track for HLS. This allows them to be enabled/disabled through the demo app UI. Issue: #165 --- .../demo/full/FullPlayerActivity.java | 31 +-------- .../demo/full/player/DemoPlayer.java | 64 +++++++++++-------- .../demo/full/player/HlsRendererBuilder.java | 2 +- .../android/exoplayer/hls/HlsChunkSource.java | 52 +++++++++++++-- .../exoplayer/hls/HlsSampleSource.java | 14 +--- 5 files changed, 87 insertions(+), 76 deletions(-) diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/FullPlayerActivity.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/FullPlayerActivity.java index 0a4f2b626f..668b02cbf2 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/FullPlayerActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/FullPlayerActivity.java @@ -25,7 +25,6 @@ import com.google.android.exoplayer.demo.full.player.DemoPlayer; import com.google.android.exoplayer.demo.full.player.DemoPlayer.RendererBuilder; import com.google.android.exoplayer.demo.full.player.HlsRendererBuilder; import com.google.android.exoplayer.demo.full.player.SmoothStreamingRendererBuilder; -import com.google.android.exoplayer.metadata.ClosedCaption; import com.google.android.exoplayer.metadata.TxxxMetadata; import com.google.android.exoplayer.text.CaptionStyleCompat; import com.google.android.exoplayer.text.SubtitleView; @@ -57,15 +56,13 @@ import android.widget.PopupMenu; import android.widget.PopupMenu.OnMenuItemClickListener; import android.widget.TextView; -import java.util.List; import java.util.Map; /** * An activity that plays media using {@link DemoPlayer}. */ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callback, OnClickListener, - DemoPlayer.Listener, DemoPlayer.TextListener, DemoPlayer.Id3MetadataListener, - DemoPlayer.ClosedCaptionListener { + DemoPlayer.Listener, DemoPlayer.TextListener, DemoPlayer.Id3MetadataListener { private static final String TAG = "FullPlayerActivity"; @@ -198,7 +195,6 @@ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callba player.addListener(this); player.setTextListener(this); player.setMetadataListener(this); - player.setClosedCaptionListener(this); player.seekTo(playerPosition); playerNeedsPrepare = true; mediaController.setMediaPlayer(player.getPlayerControl()); @@ -428,31 +424,6 @@ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callba } } - // DemoPlayer.ClosedCaptioListener implementation - - @Override - public void onClosedCaption(List closedCaptions) { - StringBuilder stringBuilder = new StringBuilder(); - for (ClosedCaption caption : closedCaptions) { - // Ignore control characters and just insert a new line in between words. - if (caption.type == ClosedCaption.TYPE_CTRL) { - if (stringBuilder.length() > 0 - && stringBuilder.charAt(stringBuilder.length() - 1) != '\n') { - stringBuilder.append('\n'); - } - } else if (caption.type == ClosedCaption.TYPE_TEXT) { - stringBuilder.append(caption.text); - } - } - if (stringBuilder.length() > 0 && stringBuilder.charAt(stringBuilder.length() - 1) == '\n') { - stringBuilder.deleteCharAt(stringBuilder.length() - 1); - } - if (stringBuilder.length() > 0) { - subtitleView.setVisibility(View.VISIBLE); - subtitleView.setText(stringBuilder.toString()); - } - } - // SurfaceHolder.Callback implementation @Override diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DemoPlayer.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DemoPlayer.java index cdd1445a34..816312cee8 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DemoPlayer.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DemoPlayer.java @@ -145,13 +145,6 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi void onId3Metadata(Map metadata); } - /** - * A listener for receiving closed captions parsed from the media stream. - */ - public interface ClosedCaptionListener { - void onClosedCaption(List closedCaptions); - } - // Constants pulled into this class for convenience. public static final int STATE_IDLE = ExoPlayer.STATE_IDLE; public static final int STATE_PREPARING = ExoPlayer.STATE_PREPARING; @@ -162,13 +155,12 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi public static final int DISABLED_TRACK = -1; public static final int PRIMARY_TRACK = 0; - public static final int RENDERER_COUNT = 6; + public static final int RENDERER_COUNT = 5; public static final int TYPE_VIDEO = 0; public static final int TYPE_AUDIO = 1; public static final int TYPE_TEXT = 2; public static final int TYPE_TIMED_METADATA = 3; - public static final int TYPE_CLOSED_CAPTIONS = 4; - public static final int TYPE_DEBUG = 5; + public static final int TYPE_DEBUG = 4; private static final int RENDERER_BUILDING_STATE_IDLE = 1; private static final int RENDERER_BUILDING_STATE_BUILDING = 2; @@ -179,6 +171,7 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi private final PlayerControl playerControl; private final Handler mainHandler; private final CopyOnWriteArrayList listeners; + private final StringBuilder closedCaptionStringBuilder; private int rendererBuildingState; private int lastReportedPlaybackState; @@ -194,7 +187,6 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi private TextListener textListener; private Id3MetadataListener id3MetadataListener; - private ClosedCaptionListener closedCaptionListener; private InternalErrorListener internalErrorListener; private InfoListener infoListener; @@ -210,6 +202,7 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi selectedTracks = new int[RENDERER_COUNT]; // Disable text initially. selectedTracks[TYPE_TEXT] = DISABLED_TRACK; + closedCaptionStringBuilder = new StringBuilder(); } public PlayerControl getPlayerControl() { @@ -240,10 +233,6 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi id3MetadataListener = listener; } - public void setClosedCaptionListener(ClosedCaptionListener listener) { - closedCaptionListener = listener; - } - public void setSurface(Surface surface) { this.surface = surface; pushSurfaceAndVideoTrack(false); @@ -275,6 +264,9 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi pushSurfaceAndVideoTrack(false); } else { pushTrackSelection(type, true); + if (type == TYPE_TEXT && index == DISABLED_TRACK && textListener != null) { + textListener.onText(null); + } } } @@ -483,36 +475,28 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi @Override public void onText(String text) { - if (textListener != null) { - textListener.onText(text); - } + processText(text); } /* package */ MetadataTrackRenderer.MetadataRenderer> getId3MetadataRenderer() { return new MetadataTrackRenderer.MetadataRenderer>() { - @Override public void onMetadata(Map metadata) { if (id3MetadataListener != null) { id3MetadataListener.onId3Metadata(metadata); } } - }; } /* package */ MetadataTrackRenderer.MetadataRenderer> getClosedCaptionMetadataRenderer() { return new MetadataTrackRenderer.MetadataRenderer>() { - @Override public void onMetadata(List metadata) { - if (closedCaptionListener != null) { - closedCaptionListener.onClosedCaption(metadata); - } + processClosedCaption(metadata); } - }; } @@ -607,6 +591,36 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi } } + /* package */ void processText(String text) { + if (textListener == null || selectedTracks[TYPE_TEXT] == DISABLED_TRACK) { + return; + } + textListener.onText(text); + } + + /* package */ void processClosedCaption(List metadata) { + if (textListener == null || selectedTracks[TYPE_TEXT] == DISABLED_TRACK) { + return; + } + closedCaptionStringBuilder.setLength(0); + for (ClosedCaption caption : metadata) { + // Ignore control characters and just insert a new line in between words. + if (caption.type == ClosedCaption.TYPE_CTRL) { + if (closedCaptionStringBuilder.length() > 0 + && closedCaptionStringBuilder.charAt(closedCaptionStringBuilder.length() - 1) != '\n') { + closedCaptionStringBuilder.append('\n'); + } + } else if (caption.type == ClosedCaption.TYPE_TEXT) { + closedCaptionStringBuilder.append(caption.text); + } + } + if (closedCaptionStringBuilder.length() > 0 + && closedCaptionStringBuilder.charAt(closedCaptionStringBuilder.length() - 1) == '\n') { + closedCaptionStringBuilder.deleteCharAt(closedCaptionStringBuilder.length() - 1); + } + textListener.onText(closedCaptionStringBuilder.toString()); + } + private class InternalRendererBuilderCallback implements RendererBuilderCallback { private boolean canceled; diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/HlsRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/HlsRendererBuilder.java index 5a3a3535e9..9d7a06a554 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/HlsRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/HlsRendererBuilder.java @@ -100,7 +100,7 @@ public class HlsRendererBuilder implements RendererBuilder, ManifestCallback= targetBufferDurationUs)) { + // We're either finished, or we have the target amount of data buffered. return null; } @@ -303,8 +341,8 @@ public class HlsChunkSource { : adaptiveMode == ADAPTIVE_MODE_SPLICE ? previousTsChunk.startTimeUs : previousTsChunk.endTimeUs; long bufferedUs = bufferedPositionUs - playbackPositionUs; - if ((idealVariantIndex > variantIndex && bufferedUs < MAX_BUFFER_TO_SWITCH_DOWN_US) - || (idealVariantIndex < variantIndex && bufferedUs > MIN_BUFFER_TO_SWITCH_UP_US)) { + if ((idealVariantIndex > variantIndex && bufferedUs < maxBufferDurationToSwitchDownUs) + || (idealVariantIndex < variantIndex && bufferedUs > minBufferDurationToSwitchUpUs)) { // Switch variant. return idealVariantIndex; } 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 e2ef450970..00e4e4f513 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 @@ -40,7 +40,6 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { */ public static final int DEFAULT_MIN_LOADABLE_RETRY_COUNT = 1; - private static final long BUFFER_DURATION_US = 20000000; private static final int NO_RESET_PENDING = -1; private final HlsChunkSource chunkSource; @@ -350,7 +349,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { } private void maybeStartLoading() { - if (currentLoadableExceptionFatal || loadingFinished) { + if (currentLoadableExceptionFatal || loadingFinished || loader.isLoading()) { return; } @@ -364,17 +363,6 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { return; } - boolean bufferFull = false; - if (!extractors.isEmpty()) { - long largestSampleTimestamp = extractors.getLast().getLargestSampleTimestamp(); - bufferFull = largestSampleTimestamp != Long.MIN_VALUE - && (largestSampleTimestamp - downstreamPositionUs) >= BUFFER_DURATION_US; - } - - if (loader.isLoading() || bufferFull) { - return; - } - HlsChunk nextLoadable = chunkSource.getChunkOperation(previousTsLoadable, pendingResetPositionUs, downstreamPositionUs); if (nextLoadable == null) { From c2d55acab52941cb883c891e6973b0aa4457ec5b Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Wed, 3 Dec 2014 18:10:30 +0000 Subject: [PATCH 34/55] Get Exo+HLS memory usage more under control. - Split sample pools for video/audio/misc, since the typical required sample sizes are very different (and so it becomes inefficient to use a sample sized for video to hold audio). - Add TODO for further improvements. Issue: #174 --- .../android/exoplayer/hls/TsExtractor.java | 71 ++++++++++++------- 1 file changed, 45 insertions(+), 26 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java b/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java index 64541a1492..9b66dce6b7 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java @@ -643,23 +643,25 @@ public final class TsExtractor { /** * Obtains a Sample object to use. * + * @param type The type of the sample. * @return The sample. */ - protected Sample getSample() { - return samplePool.get(); + protected Sample getSample(int type) { + return samplePool.get(type); } /** * Creates a new Sample and adds it to the queue. * + * @param type The type of the sample. * @param buffer The buffer to read sample data. * @param sampleSize The size of the sample data. * @param sampleTimeUs The sample time stamp. * @param isKeyframe True if the sample is a keyframe. False otherwise. */ - protected void addSample(BitArray buffer, int sampleSize, long sampleTimeUs, + protected void addSample(int type, BitArray buffer, int sampleSize, long sampleTimeUs, boolean isKeyframe) { - Sample sample = getSample(); + Sample sample = getSample(type); addToSample(sample, buffer, sampleSize); sample.isKeyframe = isKeyframe; sample.timeUs = sampleTimeUs; @@ -748,7 +750,7 @@ public final class TsExtractor { seiReader.read(currentSample.data, currentSample.size, pesTimeUs); addSample(currentSample); } - currentSample = getSample(); + currentSample = getSample(Sample.TYPE_VIDEO); pesPayloadSize -= readOneH264Frame(pesBuffer, false); currentSample.timeUs = pesTimeUs; @@ -964,7 +966,7 @@ public final class TsExtractor { seiBuffer.skipBytes(seiStart + 4); int ccDataSize = Eia608Parser.parseHeader(seiBuffer); if (ccDataSize > 0) { - addSample(seiBuffer, ccDataSize, pesTimeUs, true); + addSample(Sample.TYPE_MISC, seiBuffer, ccDataSize, pesTimeUs, true); } } } @@ -978,6 +980,7 @@ public final class TsExtractor { private final BitArray adtsBuffer; private long timeUs; + private long frameDurationUs; public AdtsReader(SamplePool samplePool) { super(samplePool); @@ -994,14 +997,7 @@ public final class TsExtractor { } int frameIndex = 0; do { - long frameDuration = 0; - // If frameIndex > 0, audioMediaFormat should be already parsed. - // If frameIndex == 0, timeUs = pesTimeUs anyway. - if (hasMediaFormat()) { - frameDuration = 1000000L * 1024L / getMediaFormat().sampleRate; - } - timeUs = pesTimeUs + frameIndex * frameDuration; - frameIndex++; + timeUs = pesTimeUs + (frameDurationUs * frameIndex++); } while(readOneAacFrame(timeUs)); } @@ -1039,6 +1035,7 @@ public final class TsExtractor { MediaFormat mediaFormat = MediaFormat.createAudioFormat(MimeTypes.AUDIO_AAC, MediaFormat.NO_VALUE, audioParams.second, audioParams.first, Collections.singletonList(audioSpecificConfig)); + frameDurationUs = (1000000 * 1024L) / mediaFormat.sampleRate; setMediaFormat(mediaFormat); } else { adtsBuffer.skipBits(10); @@ -1063,7 +1060,7 @@ public final class TsExtractor { return false; } - addSample(adtsBuffer, frameSize, timeUs, true); + addSample(Sample.TYPE_AUDIO, adtsBuffer, frameSize, timeUs, true); return true; } @@ -1088,34 +1085,49 @@ public final class TsExtractor { @SuppressLint("InlinedApi") @Override public void read(BitArray pesBuffer, int pesPayloadSize, long pesTimeUs) { - addSample(pesBuffer, pesPayloadSize, pesTimeUs, true); + addSample(Sample.TYPE_MISC, pesBuffer, pesPayloadSize, pesTimeUs, true); } } /** * A pool from which the extractor can obtain sample objects for internal use. + * + * TODO: Over time the average size of a sample in the video pool will become larger, as the + * proportion of samples in the pool that have at some point held a keyframe grows. Currently + * this leads to inefficient memory usage, since samples large enough to hold keyframes end up + * being used to hold non-keyframes. We need to fix this. */ public static class SamplePool { - private static final int DEFAULT_BUFFER_SEGMENT_SIZE = 64 * 1024; + private static final int[] DEFAULT_SAMPLE_SIZES; + static { + DEFAULT_SAMPLE_SIZES = new int[Sample.TYPE_COUNT]; + DEFAULT_SAMPLE_SIZES[Sample.TYPE_VIDEO] = 10 * 1024; + DEFAULT_SAMPLE_SIZES[Sample.TYPE_AUDIO] = 512; + DEFAULT_SAMPLE_SIZES[Sample.TYPE_MISC] = 512; + } - private Sample firstInPool; + private final Sample[] pools; - /* package */ synchronized Sample get() { - if (firstInPool == null) { - return new Sample(DEFAULT_BUFFER_SEGMENT_SIZE); + public SamplePool() { + pools = new Sample[Sample.TYPE_COUNT]; + } + + /* package */ synchronized Sample get(int type) { + if (pools[type] == null) { + return new Sample(type, DEFAULT_SAMPLE_SIZES[type]); } - Sample sample = firstInPool; - firstInPool = sample.nextInPool; + Sample sample = pools[type]; + pools[type] = sample.nextInPool; sample.nextInPool = null; return sample; } /* package */ synchronized void recycle(Sample sample) { sample.reset(); - sample.nextInPool = firstInPool; - firstInPool = sample; + sample.nextInPool = pools[sample.type]; + pools[sample.type] = sample; } } @@ -1125,6 +1137,12 @@ public final class TsExtractor { */ private static class Sample { + public static final int TYPE_VIDEO = 0; + public static final int TYPE_AUDIO = 1; + public static final int TYPE_MISC = 2; + public static final int TYPE_COUNT = 3; + + public final int type; public Sample nextInPool; public byte[] data; @@ -1132,7 +1150,8 @@ public final class TsExtractor { public int size; public long timeUs; - public Sample(int length) { + public Sample(int type, int length) { + this.type = type; data = new byte[length]; } From 3d775c164106d6709c4238ea6a02e61ee20f7afa Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Wed, 3 Dec 2014 18:50:31 +0000 Subject: [PATCH 35/55] Use C constants + minor cleanup. --- .../com/google/android/exoplayer/demo/DemoUtil.java | 4 +--- .../google/android/exoplayer/hls/HlsChunkSource.java | 5 ++--- .../google/android/exoplayer/hls/HlsMediaPlaylist.java | 10 ++++++---- .../android/exoplayer/hls/HlsPlaylistParser.java | 2 +- .../com/google/android/exoplayer/hls/TsExtractor.java | 5 +++-- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/DemoUtil.java b/demo/src/main/java/com/google/android/exoplayer/demo/DemoUtil.java index aab66b59b5..a2915f53ff 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/DemoUtil.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/DemoUtil.java @@ -50,9 +50,7 @@ public class DemoUtil { public static final int TYPE_DASH = 0; public static final int TYPE_SS = 1; public static final int TYPE_OTHER = 2; - public static final int TYPE_DASH_LIVE = 3; - public static final int TYPE_DASH_LIVE_DVR = 4; - public static final int TYPE_HLS = 5; + public static final int TYPE_HLS = 3; public static final boolean EXPOSE_EXPERIMENTAL_FEATURES = false; 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 9306b5b218..0ca9ab4272 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 @@ -17,7 +17,6 @@ package com.google.android.exoplayer.hls; import com.google.android.exoplayer.C; import com.google.android.exoplayer.MediaFormat; -import com.google.android.exoplayer.TrackRenderer; import com.google.android.exoplayer.hls.TsExtractor.SamplePool; import com.google.android.exoplayer.upstream.Aes128DataSource; import com.google.android.exoplayer.upstream.BandwidthMeter; @@ -193,7 +192,7 @@ public class HlsChunkSource { } public long getDurationUs() { - return live ? TrackRenderer.UNKNOWN_TIME_US : durationUs; + return live ? C.UNKNOWN_TIME_US : durationUs; } /** @@ -313,7 +312,7 @@ public class HlsChunkSource { } else /* Not live */ { startTimeUs = segment.startTimeUs; } - long endTimeUs = startTimeUs + (long) (segment.durationSecs * 1000000); + long endTimeUs = startTimeUs + (long) (segment.durationSecs * C.MICROS_PER_SECOND); boolean isLastChunk = !mediaPlaylist.live && chunkIndex == mediaPlaylist.segments.size() - 1; // Configure the extractor that will read the chunk. 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 70192c87c4..3e9f151c08 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 @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer.hls; +import com.google.android.exoplayer.C; + import android.net.Uri; import java.util.List; @@ -77,11 +79,11 @@ public final class HlsMediaPlaylist extends HlsPlaylist { this.live = live; this.segments = segments; - if (this.segments.size() > 0) { - Segment lastSegment = segments.get(this.segments.size() - 1); - this.durationUs = lastSegment.startTimeUs + (long) (lastSegment.durationSecs * 1000000); + if (!segments.isEmpty()) { + Segment last = segments.get(segments.size() - 1); + durationUs = last.startTimeUs + (long) (last.durationSecs * C.MICROS_PER_SECOND); } else { - this.durationUs = 0; + durationUs = 0; } } 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 8ee093c91d..a2497e3218 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 @@ -223,7 +223,7 @@ public final class HlsPlaylistParser implements ManifestParser { segments.add(new Segment(line, segmentDurationSecs, segmentDiscontinuity, segmentStartTimeUs, segmentEncryptionMethod, segmentEncryptionKeyUri, segmentEncryptionIV, segmentByterangeOffset, segmentByterangeLength)); - segmentStartTimeUs += (long) (segmentDurationSecs * 1000000); + segmentStartTimeUs += (long) (segmentDurationSecs * C.MICROS_PER_SECOND); segmentDiscontinuity = false; segmentDurationSecs = 0.0; if (segmentByterangeLength != C.LENGTH_UNBOUNDED) { diff --git a/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java b/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java index 9b66dce6b7..c78402a5de 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer.hls; +import com.google.android.exoplayer.C; import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.SampleHolder; import com.google.android.exoplayer.metadata.Eia608Parser; @@ -480,7 +481,7 @@ public final class TsExtractor { pesBuffer.skipBits(1); pts |= pesBuffer.readBitsLong(15); pesBuffer.skipBits(1); - timeUs = pts * 1000000 / 90000; + timeUs = (pts * C.MICROS_PER_SECOND) / 90000; // Skip the rest of the header. pesBuffer.skipBytes(headerDataLength - 5); } else { @@ -1035,7 +1036,7 @@ public final class TsExtractor { MediaFormat mediaFormat = MediaFormat.createAudioFormat(MimeTypes.AUDIO_AAC, MediaFormat.NO_VALUE, audioParams.second, audioParams.first, Collections.singletonList(audioSpecificConfig)); - frameDurationUs = (1000000 * 1024L) / mediaFormat.sampleRate; + frameDurationUs = (C.MICROS_PER_SECOND * 1024L) / mediaFormat.sampleRate; setMediaFormat(mediaFormat); } else { adtsBuffer.skipBits(10); From 7ca1de22759ab507f5c0a378f71abe3beb885ec3 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 8 Dec 2014 11:36:51 +0000 Subject: [PATCH 36/55] Experiment to add SPS/PPS to the media format. --- .../android/exoplayer/hls/TsExtractor.java | 26 ++++++++++++++----- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java b/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java index c78402a5de..a87dd270d3 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java @@ -712,8 +712,9 @@ public final class TsExtractor { private class H264Reader extends PesPayloadReader { private static final int NAL_UNIT_TYPE_IDR = 5; - private static final int NAL_UNIT_TYPE_AUD = 9; private static final int NAL_UNIT_TYPE_SPS = 7; + private static final int NAL_UNIT_TYPE_PPS = 8; + private static final int NAL_UNIT_TYPE_AUD = 9; public final SeiReader seiReader; @@ -778,15 +779,26 @@ public final class TsExtractor { private void parseMediaFormat(Sample sample) { BitArray bitArray = new BitArray(sample.data, sample.size); - // Locate the SPS unit. + // Locate the SPS and PPS units. int spsOffset = bitArray.findNextNalUnit(NAL_UNIT_TYPE_SPS, 0); - if (spsOffset == bitArray.bytesLeft()) { + int ppsOffset = bitArray.findNextNalUnit(NAL_UNIT_TYPE_PPS, 0); + if (spsOffset == bitArray.bytesLeft() || ppsOffset == bitArray.bytesLeft()) { return; } - int nextNalOffset = bitArray.findNextNalUnit(-1, spsOffset + 3); + int spsLength = bitArray.findNextNalUnit(-1, spsOffset + 3) - spsOffset; + int ppsLength = bitArray.findNextNalUnit(-1, ppsOffset + 3) - ppsOffset; + + byte[] spsData = new byte[spsLength]; + byte[] ppsData = new byte[ppsLength]; + System.arraycopy(bitArray.getData(), spsOffset, spsData, 0, spsLength); + System.arraycopy(bitArray.getData(), ppsOffset, ppsData, 0, ppsLength); + + List initializationData = new ArrayList(); + initializationData.add(spsData); + initializationData.add(ppsData); // Unescape the SPS unit. - byte[] unescapedSps = unescapeStream(bitArray.getData(), spsOffset, nextNalOffset); + byte[] unescapedSps = unescapeData(spsData, 0, spsLength); bitArray.reset(unescapedSps, unescapedSps.length); // Parse the SPS unit @@ -881,7 +893,7 @@ public final class TsExtractor { // Set the format. setMediaFormat(MediaFormat.createVideoFormat(MimeTypes.VIDEO_H264, MediaFormat.NO_VALUE, - frameWidth, frameHeight, null)); + frameWidth, frameHeight, initializationData)); } private void skipScalingList(BitArray bitArray, int size) { @@ -901,7 +913,7 @@ public final class TsExtractor { *

* See ISO/IEC 14496-10:2005(E) page 36 for more information. */ - private byte[] unescapeStream(byte[] data, int offset, int limit) { + private byte[] unescapeData(byte[] data, int offset, int limit) { int position = offset; List escapePositions = new ArrayList(); while (position < limit) { From 87ca1b3465c35c96b68ca04d24a872cad71ae072 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 8 Dec 2014 20:18:10 +0000 Subject: [PATCH 37/55] Skip previously loaded HLS data after an error occurs. Github issue: #183 --- .../google/android/exoplayer/hls/TsChunk.java | 33 ++++++++++++------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/hls/TsChunk.java b/library/src/main/java/com/google/android/exoplayer/hls/TsChunk.java index 4261dbd4ca..04a3f9200f 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/TsChunk.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/TsChunk.java @@ -15,7 +15,6 @@ */ package com.google.android.exoplayer.hls; -import com.google.android.exoplayer.C; import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DataSpec; @@ -26,6 +25,8 @@ import java.io.IOException; */ public final class TsChunk extends HlsChunk { + private static final byte[] SCRATCH_SPACE = new byte[4096]; + /** * The index of the variant in the master playlist. */ @@ -51,7 +52,7 @@ public final class TsChunk extends HlsChunk { */ public final TsExtractor extractor; - private volatile int loadPosition; + private int loadPosition; private volatile boolean loadFinished; private volatile boolean loadCanceled; @@ -99,20 +100,28 @@ public final class TsChunk extends HlsChunk { @Override public void load() throws IOException, InterruptedException { - DataSpec loadDataSpec; - if (loadPosition == 0) { - loadDataSpec = dataSpec; - } else { - long remainingLength = dataSpec.length != C.LENGTH_UNBOUNDED - ? dataSpec.length - loadPosition : C.LENGTH_UNBOUNDED; - loadDataSpec = new DataSpec(dataSpec.uri, dataSpec.position + loadPosition, - remainingLength, dataSpec.key); - } try { - dataSource.open(loadDataSpec); + dataSource.open(dataSpec); int bytesRead = 0; + int bytesSkipped = 0; + // If we previously fed part of this chunk to the extractor, skip it this time. + // TODO: Ideally we'd construct a dataSpec that only loads the remainder of the data here, + // rather than loading the whole chunk again and then skipping data we previously loaded. To + // do this is straightforward for non-encrypted content, but more complicated for content + // encrypted with AES, for which we'll need to modify the way that decryption is performed. + while (bytesRead != -1 && !loadCanceled && bytesSkipped < loadPosition) { + int skipLength = Math.min(loadPosition - bytesSkipped, SCRATCH_SPACE.length); + bytesRead = dataSource.read(SCRATCH_SPACE, 0, skipLength); + if (bytesRead != -1) { + bytesSkipped += bytesRead; + } + } + // Feed the remaining data into the extractor. while (bytesRead != -1 && !loadCanceled) { bytesRead = extractor.read(dataSource); + if (bytesRead != -1) { + loadPosition += bytesRead; + } } loadFinished = !loadCanceled; } finally { From a25421889168df7c3ffc72842ee7c39a4da3025e Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 8 Dec 2014 20:19:24 +0000 Subject: [PATCH 38/55] No-op format changes. --- .../java/com/google/android/exoplayer/hls/TsExtractor.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java b/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java index a87dd270d3..ae4430cd5d 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java @@ -787,18 +787,16 @@ public final class TsExtractor { } int spsLength = bitArray.findNextNalUnit(-1, spsOffset + 3) - spsOffset; int ppsLength = bitArray.findNextNalUnit(-1, ppsOffset + 3) - ppsOffset; - byte[] spsData = new byte[spsLength]; byte[] ppsData = new byte[ppsLength]; System.arraycopy(bitArray.getData(), spsOffset, spsData, 0, spsLength); System.arraycopy(bitArray.getData(), ppsOffset, ppsData, 0, ppsLength); - List initializationData = new ArrayList(); initializationData.add(spsData); initializationData.add(ppsData); // Unescape the SPS unit. - byte[] unescapedSps = unescapeData(spsData, 0, spsLength); + byte[] unescapedSps = unescapeStream(spsData, 0, spsLength); bitArray.reset(unescapedSps, unescapedSps.length); // Parse the SPS unit @@ -913,7 +911,7 @@ public final class TsExtractor { *

* See ISO/IEC 14496-10:2005(E) page 36 for more information. */ - private byte[] unescapeData(byte[] data, int offset, int limit) { + private byte[] unescapeStream(byte[] data, int offset, int limit) { int position = offset; List escapePositions = new ArrayList(); while (position < limit) { From 1554db1673db2983566caa830dcc819ca8c1d1cb Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 11 Dec 2014 10:26:50 +0000 Subject: [PATCH 39/55] Skip bad PES packets Issue: #200 --- .../google/android/exoplayer/hls/TsExtractor.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java b/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java index ae4430cd5d..52a746ae27 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java @@ -448,12 +448,14 @@ public final class TsExtractor { private void readPacketStart() { int startCodePrefix = pesBuffer.readBits(24); if (startCodePrefix != 0x000001) { - // Error. + Log.e(TAG, "Unexpected start code prefix: " + startCodePrefix); + pesBuffer.reset(); + packetLength = -1; + } else { + // TODO: Read and use stream_id. + pesBuffer.skipBits(8); // Skip stream_id. + packetLength = pesBuffer.readBits(16); } - // TODO: Read and use stream_id. - // Skip stream_id. - pesBuffer.skipBits(8); - packetLength = pesBuffer.readBits(16); } private void readPacketBody() { From b80569237b575b7619e0eff73c305c116a782a91 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 12 Dec 2014 14:25:48 +0000 Subject: [PATCH 40/55] Correctly propagate fatal load error. --- .../java/com/google/android/exoplayer/hls/HlsSampleSource.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 00e4e4f513..1432212b86 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 @@ -316,7 +316,8 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { } private void maybeThrowLoadableException() throws IOException { - if (currentLoadableException != null && currentLoadableExceptionCount > minLoadableRetryCount) { + if (currentLoadableException != null && (currentLoadableExceptionFatal + || currentLoadableExceptionCount > minLoadableRetryCount)) { throw currentLoadableException; } } From 39c07d570c75f7cf11206f802ec807a770b4278c Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 15 Dec 2014 15:30:21 +0000 Subject: [PATCH 41/55] Adapt HLS on non-adaptive devices. This just brings HLS treatment in the demo app in line with DASH and SmoothStreaming. --- .../exoplayer/demo/full/player/HlsRendererBuilder.java | 5 +---- .../android/exoplayer/demo/simple/HlsRendererBuilder.java | 5 +---- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/HlsRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/HlsRendererBuilder.java index 9d7a06a554..9fe0a7e815 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/HlsRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/HlsRendererBuilder.java @@ -16,7 +16,6 @@ package com.google.android.exoplayer.demo.full.player; import com.google.android.exoplayer.MediaCodecAudioTrackRenderer; -import com.google.android.exoplayer.MediaCodecUtil; import com.google.android.exoplayer.MediaCodecVideoTrackRenderer; import com.google.android.exoplayer.TrackRenderer; import com.google.android.exoplayer.demo.full.player.DemoPlayer.RendererBuilder; @@ -34,7 +33,6 @@ import com.google.android.exoplayer.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer.upstream.UriDataSource; import com.google.android.exoplayer.util.ManifestFetcher; import com.google.android.exoplayer.util.ManifestFetcher.ManifestCallback; -import com.google.android.exoplayer.util.MimeTypes; import android.media.MediaCodec; @@ -80,9 +78,8 @@ public class HlsRendererBuilder implements RendererBuilder, ManifestCallback Date: Fri, 19 Dec 2014 12:10:16 +0000 Subject: [PATCH 42/55] Allow re-enabling video track without transition through buffering state. --- .../android/exoplayer/demo/full/player/HlsRendererBuilder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/HlsRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/HlsRendererBuilder.java index 9fe0a7e815..89e3bcc526 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/HlsRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/HlsRendererBuilder.java @@ -82,7 +82,7 @@ public class HlsRendererBuilder implements RendererBuilder, ManifestCallback> id3Renderer = From 0756c3d28c33026a90be6dff88a2b9806e16be30 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 19 Dec 2014 12:11:17 +0000 Subject: [PATCH 43/55] Relax assertion. We've seen a few streams where this assertion fails. If you just skip the packet, things appear to recover correctly in all cases I've seen, so replacing failure with a warning. --- .../android/exoplayer/hls/TsExtractor.java | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java b/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java index 52a746ae27..3429ec337b 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java @@ -427,10 +427,17 @@ public final class TsExtractor { @Override public void read(BitArray tsBuffer, boolean payloadUnitStartIndicator) { if (payloadUnitStartIndicator && !pesBuffer.isEmpty()) { - // We've encountered the start of the next packet, but haven't yet read the body. Read it. - // Note that this should only happen if the packet length was unspecified. - Assertions.checkState(packetLength == 0); - readPacketBody(); + if (packetLength == 0) { + // The length of the previous packet was unspecified. We've now seen the start of the + // next one, so consume the previous packet's body. + readPacketBody(); + } else { + // Either we didn't have enough data to read the length of the previous packet, or we + // did read the length but didn't receive that amount of data. Neither case is expected. + Log.w(TAG, "Unexpected packet fragment of length " + pesBuffer.bytesLeft()); + pesBuffer.reset(); + packetLength = -1; + } } pesBuffer.append(tsBuffer, tsBuffer.bytesLeft()); @@ -448,7 +455,7 @@ public final class TsExtractor { private void readPacketStart() { int startCodePrefix = pesBuffer.readBits(24); if (startCodePrefix != 0x000001) { - Log.e(TAG, "Unexpected start code prefix: " + startCodePrefix); + Log.w(TAG, "Unexpected start code prefix: " + startCodePrefix); pesBuffer.reset(); packetLength = -1; } else { From 1fce55f6fe15991106202c8054b51fa2c0b53195 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 19 Dec 2014 12:12:04 +0000 Subject: [PATCH 44/55] HLS: Consider all programs. I'm not sure exactly what the implications of this change are, but I'd really hope that only one program in each stream is carrying audio/video. For GoPro cameras, they expose the video stream in the second program, for some reason. Issue: #116 --- .../android/exoplayer/hls/TsExtractor.java | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java b/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java index 3429ec337b..e8a8f81fc0 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java @@ -320,21 +320,17 @@ public final class TsExtractor { tsBuffer.skipBytes(pointerField); } - // Skip PAT header. - tsBuffer.skipBits(64); // 8+1+1+2+12+16+2+5+1+8+8 + tsBuffer.skipBits(12); // 8+1+1+2 + int sectionLength = tsBuffer.readBits(12); + tsBuffer.skipBits(40); // 16+2+5+1+8+8 - // Only read the first program and take it. - - // Skip program_number. - tsBuffer.skipBits(16 + 3); - int pid = tsBuffer.readBits(13); - - // Pick the first program. - if (tsPayloadReaders.get(pid) == null) { + int programCount = (sectionLength - 9) / 4; + for (int i = 0; i < programCount; i++) { + tsBuffer.skipBits(19); + int pid = tsBuffer.readBits(13); tsPayloadReaders.put(pid, new PmtReader()); } - // Skip other programs if exist. // Skip CRC_32. } From c497b78ffe04752b3593e5cfc7a763438b907edb Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 19 Dec 2014 12:13:46 +0000 Subject: [PATCH 45/55] Fix memory leak in TsExtractor when not all tracks are enabled. Previously samples belonging to disabled tracks would just accumulate in an arbitrarily long queue in TsExtractor. We need to actively throw samples away from disabled tracks up to the current playback position, so as to prevent this. Issue: #174 --- .../exoplayer/hls/HlsSampleSource.java | 24 +++++++++- .../android/exoplayer/hls/TsExtractor.java | 44 ++++++++++++------- 2 files changed, 51 insertions(+), 17 deletions(-) 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 1432212b86..ed0573e58a 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 @@ -160,6 +160,9 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { Assertions.checkState(prepared); Assertions.checkState(enabledTrackCount > 0); downstreamPositionUs = playbackPositionUs; + if (!extractors.isEmpty()) { + discardSamplesForDisabledTracks(extractors.getFirst(), downstreamPositionUs); + } return continueBufferingInternal(); } @@ -168,7 +171,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { if (isPendingReset() || extractors.isEmpty()) { return false; } - boolean haveSamples = extractors.getFirst().hasSamples(); + boolean haveSamples = prepared && haveSamplesForEnabledTracks(extractors.getFirst()); if (!haveSamples) { maybeThrowLoadableException(); } @@ -192,7 +195,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { } TsExtractor extractor = extractors.getFirst(); - while (extractors.size() > 1 && !extractor.hasSamples()) { + while (extractors.size() > 1 && !haveSamplesForEnabledTracks(extractor)) { // We're finished reading from the extractor for all tracks, and so can discard it. extractors.removeFirst().release(); extractor = extractors.getFirst(); @@ -315,6 +318,23 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { maybeStartLoading(); } + private void discardSamplesForDisabledTracks(TsExtractor extractor, long timeUs) { + for (int i = 0; i < trackEnabledStates.length; i++) { + if (!trackEnabledStates[i]) { + extractor.discardUntil(i, timeUs); + } + } + } + + private boolean haveSamplesForEnabledTracks(TsExtractor extractor) { + for (int i = 0; i < trackEnabledStates.length; i++) { + if (trackEnabledStates[i] && extractor.hasSamples(i)) { + return true; + } + } + return false; + } + private void maybeThrowLoadableException() throws IOException { if (currentLoadableException != null && (currentLoadableExceptionFatal || currentLoadableExceptionCount > minLoadableRetryCount)) { diff --git a/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java b/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java index e8a8f81fc0..f59a5206f9 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java @@ -187,19 +187,14 @@ public final class TsExtractor { } /** - * Whether samples are available for reading from {@link #getSample(int, SampleHolder)} for any - * track. + * Discards samples for the specified track up to the specified time. * - * @return True if samples are available for reading from {@link #getSample(int, SampleHolder)} - * for any track. False otherwise. + * @param track The track from which samples should be discarded. + * @param timeUs The time up to which samples should be discarded, in microseconds. */ - public boolean hasSamples() { - for (int i = 0; i < sampleQueues.size(); i++) { - if (hasSamples(i)) { - return true; - } - } - return false; + public void discardUntil(int track, long timeUs) { + Assertions.checkState(prepared); + sampleQueues.valueAt(track).discardUntil(timeUs); } /** @@ -519,7 +514,7 @@ public final class TsExtractor { private final ConcurrentLinkedQueue internalQueue; // Accessed only by the consuming thread. - private boolean readFirstFrame; + private boolean needKeyframe; private long lastReadTimeUs; private long spliceOutTimeUs; @@ -529,8 +524,9 @@ public final class TsExtractor { protected SampleQueue(SamplePool samplePool) { this.samplePool = samplePool; internalQueue = new ConcurrentLinkedQueue(); - spliceOutTimeUs = Long.MIN_VALUE; + needKeyframe = true; lastReadTimeUs = Long.MIN_VALUE; + spliceOutTimeUs = Long.MIN_VALUE; } public boolean hasMediaFormat() { @@ -557,7 +553,7 @@ public final class TsExtractor { Sample head = peek(); if (head != null) { internalQueue.remove(); - readFirstFrame = true; + needKeyframe = false; lastReadTimeUs = head.timeUs; } return head; @@ -570,7 +566,7 @@ public final class TsExtractor { */ public Sample peek() { Sample head = internalQueue.peek(); - if (!readFirstFrame) { + if (needKeyframe) { // Peeking discard of samples until we find a keyframe or run out of available samples. while (head != null && !head.isKeyframe) { recycle(head); @@ -590,6 +586,24 @@ public final class TsExtractor { return head; } + /** + * Discards samples from the queue up to the specified time. + * + * @param timeUs The time up to which samples should be discarded, in microseconds. + */ + public void discardUntil(long timeUs) { + Sample head = peek(); + while (head != null && head.timeUs < timeUs) { + recycle(head); + internalQueue.remove(); + head = internalQueue.peek(); + // We're discarding at least one sample, so any subsequent read will need to start at + // a keyframe. + needKeyframe = true; + } + lastReadTimeUs = Long.MIN_VALUE; + } + /** * Clears the queue. */ From 0414b0d2f6cfda21c1db13166ff55d4b6cd20d2c Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Sat, 20 Dec 2014 11:57:05 +0000 Subject: [PATCH 46/55] Fix assertion fail on seek. Issue: #214 --- .../exoplayer/hls/HlsSampleSource.java | 36 ++++++++++++++----- .../android/exoplayer/hls/TsExtractor.java | 1 + 2 files changed, 29 insertions(+), 8 deletions(-) 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 ed0573e58a..8297c4aeb9 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 @@ -171,7 +171,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { if (isPendingReset() || extractors.isEmpty()) { return false; } - boolean haveSamples = prepared && haveSamplesForEnabledTracks(extractors.getFirst()); + boolean haveSamples = prepared && haveSamplesForEnabledTracks(getCurrentExtractor()); if (!haveSamples) { maybeThrowLoadableException(); } @@ -194,13 +194,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { return NOTHING_READ; } - TsExtractor extractor = extractors.getFirst(); - while (extractors.size() > 1 && !haveSamplesForEnabledTracks(extractor)) { - // We're finished reading from the extractor for all tracks, and so can discard it. - extractors.removeFirst().release(); - extractor = extractors.getFirst(); - } - + TsExtractor extractor = getCurrentExtractor(); if (extractors.size() > 1) { // If there's more than one extractor, attempt to configure a seamless splice from the // current one to the next one. @@ -318,7 +312,30 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { maybeStartLoading(); } + /** + * Gets the current extractor from which samples should be read. + *

+ * Calling this method discards extractors without any samples from the front of the queue. The + * last extractor is retained even if it doesn't have any samples. + *

+ * This method must not be called unless {@link #extractors} is non-empty. + * + * @return The current extractor from which samples should be read. Guaranteed to be non-null. + */ + private TsExtractor getCurrentExtractor() { + TsExtractor extractor = extractors.getFirst(); + while (extractors.size() > 1 && !haveSamplesForEnabledTracks(extractor)) { + // We're finished reading from the extractor for all tracks, and so can discard it. + extractors.removeFirst().release(); + extractor = extractors.getFirst(); + } + return extractor; + } + private void discardSamplesForDisabledTracks(TsExtractor extractor, long timeUs) { + if (!extractor.isPrepared()) { + return; + } for (int i = 0; i < trackEnabledStates.length; i++) { if (!trackEnabledStates[i]) { extractor.discardUntil(i, timeUs); @@ -327,6 +344,9 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { } private boolean haveSamplesForEnabledTracks(TsExtractor extractor) { + if (!extractor.isPrepared()) { + return false; + } for (int i = 0; i < trackEnabledStates.length; i++) { if (trackEnabledStates[i] && extractor.hasSamples(i)) { return true; diff --git a/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java b/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java index f59a5206f9..b8e6c6ef6e 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java @@ -205,6 +205,7 @@ public final class TsExtractor { * for the specified track. False otherwise. */ public boolean hasSamples(int track) { + Assertions.checkState(prepared); return sampleQueues.valueAt(track).peek() != null; } From fe43377104de176286a57ae05e399a32c55ac838 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 12 Jan 2015 17:44:42 +0000 Subject: [PATCH 47/55] Increase retry count to 3 --- .../java/com/google/android/exoplayer/hls/HlsSampleSource.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 8297c4aeb9..6ef2703a59 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 @@ -38,7 +38,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { /** * The default minimum number of times to retry loading data prior to failing. */ - public static final int DEFAULT_MIN_LOADABLE_RETRY_COUNT = 1; + public static final int DEFAULT_MIN_LOADABLE_RETRY_COUNT = 3; private static final int NO_RESET_PENDING = -1; From 4158ede6e312ae718f165b1b48c952189d67b27a Mon Sep 17 00:00:00 2001 From: Andrey Udovenko Date: Wed, 21 Jan 2015 11:54:23 -0500 Subject: [PATCH 48/55] Move Closed Captions processing to Eia608TrackRenderer. Use TextRenderer interface for captions. Sort captions based on video frames DTS. Add better control characters and special characters in basic North American character set support. Fixes #156 --- .../demo/full/player/DemoPlayer.java | 41 +-- .../demo/full/player/HlsRendererBuilder.java | 9 +- .../android/exoplayer/hls/TsExtractor.java | 4 +- .../android/exoplayer/text/TextRenderer.java | 30 ++ .../exoplayer/text/TextTrackRenderer.java | 14 - .../eia608}/ClosedCaption.java | 20 +- .../eia608}/Eia608Parser.java | 69 ++++- .../text/eia608/Eia608TrackRenderer.java | 260 ++++++++++++++++++ 8 files changed, 370 insertions(+), 77 deletions(-) create mode 100644 library/src/main/java/com/google/android/exoplayer/text/TextRenderer.java rename library/src/main/java/com/google/android/exoplayer/{metadata => text/eia608}/ClosedCaption.java (73%) rename library/src/main/java/com/google/android/exoplayer/{metadata => text/eia608}/Eia608Parser.java (64%) create mode 100644 library/src/main/java/com/google/android/exoplayer/text/eia608/Eia608TrackRenderer.java diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DemoPlayer.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DemoPlayer.java index 9fb7ee6d3b..a7ba111990 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DemoPlayer.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DemoPlayer.java @@ -27,9 +27,8 @@ import com.google.android.exoplayer.audio.AudioTrack; import com.google.android.exoplayer.chunk.ChunkSampleSource; import com.google.android.exoplayer.chunk.MultiTrackChunkSource; import com.google.android.exoplayer.drm.StreamingDrmSessionManager; -import com.google.android.exoplayer.metadata.ClosedCaption; import com.google.android.exoplayer.metadata.MetadataTrackRenderer; -import com.google.android.exoplayer.text.TextTrackRenderer; +import com.google.android.exoplayer.text.TextRenderer; import com.google.android.exoplayer.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer.util.PlayerControl; @@ -39,7 +38,6 @@ import android.os.Looper; import android.view.Surface; import java.io.IOException; -import java.util.List; import java.util.Map; import java.util.concurrent.CopyOnWriteArrayList; @@ -51,7 +49,7 @@ import java.util.concurrent.CopyOnWriteArrayList; public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventListener, DefaultBandwidthMeter.EventListener, MediaCodecVideoTrackRenderer.EventListener, MediaCodecAudioTrackRenderer.EventListener, Ac3PassthroughAudioTrackRenderer.EventListener, - TextTrackRenderer.TextRenderer, StreamingDrmSessionManager.EventListener { + TextRenderer, StreamingDrmSessionManager.EventListener { /** * Builds renderers for the player. @@ -173,7 +171,6 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi private final PlayerControl playerControl; private final Handler mainHandler; private final CopyOnWriteArrayList listeners; - private final StringBuilder closedCaptionStringBuilder; private int rendererBuildingState; private int lastReportedPlaybackState; @@ -204,7 +201,6 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi selectedTracks = new int[RENDERER_COUNT]; // Disable text initially. selectedTracks[TYPE_TEXT] = DISABLED_TRACK; - closedCaptionStringBuilder = new StringBuilder(); } public PlayerControl getPlayerControl() { @@ -499,16 +495,6 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi }; } - /* package */ MetadataTrackRenderer.MetadataRenderer> - getClosedCaptionMetadataRenderer() { - return new MetadataTrackRenderer.MetadataRenderer>() { - @Override - public void onMetadata(List metadata) { - processClosedCaption(metadata); - } - }; - } - @Override public void onPlayWhenReadyCommitted() { // Do nothing. @@ -607,29 +593,6 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi textListener.onText(text); } - /* package */ void processClosedCaption(List metadata) { - if (textListener == null || selectedTracks[TYPE_TEXT] == DISABLED_TRACK) { - return; - } - closedCaptionStringBuilder.setLength(0); - for (ClosedCaption caption : metadata) { - // Ignore control characters and just insert a new line in between words. - if (caption.type == ClosedCaption.TYPE_CTRL) { - if (closedCaptionStringBuilder.length() > 0 - && closedCaptionStringBuilder.charAt(closedCaptionStringBuilder.length() - 1) != '\n') { - closedCaptionStringBuilder.append('\n'); - } - } else if (caption.type == ClosedCaption.TYPE_TEXT) { - closedCaptionStringBuilder.append(caption.text); - } - } - if (closedCaptionStringBuilder.length() > 0 - && closedCaptionStringBuilder.charAt(closedCaptionStringBuilder.length() - 1) == '\n') { - closedCaptionStringBuilder.deleteCharAt(closedCaptionStringBuilder.length() - 1); - } - textListener.onText(closedCaptionStringBuilder.toString()); - } - private class InternalRendererBuilderCallback implements RendererBuilderCallback { private boolean canceled; diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/HlsRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/HlsRendererBuilder.java index 89e3bcc526..cab7ffcc33 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/HlsRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/HlsRendererBuilder.java @@ -24,10 +24,9 @@ import com.google.android.exoplayer.hls.HlsChunkSource; import com.google.android.exoplayer.hls.HlsPlaylist; import com.google.android.exoplayer.hls.HlsPlaylistParser; import com.google.android.exoplayer.hls.HlsSampleSource; -import com.google.android.exoplayer.metadata.ClosedCaption; -import com.google.android.exoplayer.metadata.Eia608Parser; import com.google.android.exoplayer.metadata.Id3Parser; import com.google.android.exoplayer.metadata.MetadataTrackRenderer; +import com.google.android.exoplayer.text.eia608.Eia608TrackRenderer; import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer.upstream.UriDataSource; @@ -37,7 +36,6 @@ import com.google.android.exoplayer.util.ManifestFetcher.ManifestCallback; import android.media.MediaCodec; import java.io.IOException; -import java.util.List; import java.util.Map; /** @@ -89,9 +87,8 @@ public class HlsRendererBuilder implements RendererBuilder, ManifestCallback>(sampleSource, new Id3Parser(), player.getId3MetadataRenderer(), player.getMainHandler().getLooper()); - MetadataTrackRenderer> closedCaptionRenderer = - new MetadataTrackRenderer>(sampleSource, new Eia608Parser(), - player.getClosedCaptionMetadataRenderer(), player.getMainHandler().getLooper()); + Eia608TrackRenderer closedCaptionRenderer = new Eia608TrackRenderer(sampleSource, player, + player.getMainHandler().getLooper()); TrackRenderer[] renderers = new TrackRenderer[DemoPlayer.RENDERER_COUNT]; renderers[DemoPlayer.TYPE_VIDEO] = videoRenderer; diff --git a/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java b/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java index b8e6c6ef6e..5347f7de5d 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java @@ -18,7 +18,7 @@ package com.google.android.exoplayer.hls; import com.google.android.exoplayer.C; import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.SampleHolder; -import com.google.android.exoplayer.metadata.Eia608Parser; +import com.google.android.exoplayer.text.eia608.Eia608Parser; import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.BitArray; @@ -769,7 +769,7 @@ public final class TsExtractor { if (!hasMediaFormat() && currentSample.isKeyframe) { parseMediaFormat(currentSample); } - seiReader.read(currentSample.data, currentSample.size, pesTimeUs); + seiReader.read(currentSample.data, currentSample.size, currentSample.timeUs); addSample(currentSample); } currentSample = getSample(Sample.TYPE_VIDEO); diff --git a/library/src/main/java/com/google/android/exoplayer/text/TextRenderer.java b/library/src/main/java/com/google/android/exoplayer/text/TextRenderer.java new file mode 100644 index 0000000000..8b0b1ae6dc --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/text/TextRenderer.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer.text; + +/** + * An interface for components that render text. + */ +public interface TextRenderer { + + /** + * Invoked each time there is a change in the text to be rendered. + * + * @param text The text to render, or null if no text is to be rendered. + */ + void onText(String text); + +} diff --git a/library/src/main/java/com/google/android/exoplayer/text/TextTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/text/TextTrackRenderer.java index c85eb469c1..7f3dba9654 100644 --- a/library/src/main/java/com/google/android/exoplayer/text/TextTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/text/TextTrackRenderer.java @@ -38,20 +38,6 @@ import java.io.IOException; @TargetApi(16) public class TextTrackRenderer extends TrackRenderer implements Callback { - /** - * An interface for components that render text. - */ - public interface TextRenderer { - - /** - * Invoked each time there is a change in the text to be rendered. - * - * @param text The text to render, or null if no text is to be rendered. - */ - void onText(String text); - - } - private static final int MSG_UPDATE_OVERLAY = 0; private final Handler textRendererHandler; diff --git a/library/src/main/java/com/google/android/exoplayer/metadata/ClosedCaption.java b/library/src/main/java/com/google/android/exoplayer/text/eia608/ClosedCaption.java similarity index 73% rename from library/src/main/java/com/google/android/exoplayer/metadata/ClosedCaption.java rename to library/src/main/java/com/google/android/exoplayer/text/eia608/ClosedCaption.java index bcaa6bb8bb..dad39fc359 100644 --- a/library/src/main/java/com/google/android/exoplayer/metadata/ClosedCaption.java +++ b/library/src/main/java/com/google/android/exoplayer/text/eia608/ClosedCaption.java @@ -13,12 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer.metadata; +package com.google.android.exoplayer.text.eia608; /** * A Closed Caption that contains textual data associated with time indices. */ -public final class ClosedCaption { +public final class ClosedCaption implements Comparable { /** * Identifies closed captions with control characters. @@ -39,10 +39,24 @@ public final class ClosedCaption { * Contains text or two control characters. */ public final String text; + /** + * Timestamp associated with the closed caption. + */ + public final long timeUs; - public ClosedCaption(int type, String text) { + public ClosedCaption(int type, String text, long timeUs) { this.type = type; this.text = text; + this.timeUs = timeUs; + } + + @Override + public int compareTo(ClosedCaption another) { + long delta = this.timeUs - another.timeUs; + if (delta == 0) { + return 0; + } + return delta > 0 ? 1 : -1; } } diff --git a/library/src/main/java/com/google/android/exoplayer/metadata/Eia608Parser.java b/library/src/main/java/com/google/android/exoplayer/text/eia608/Eia608Parser.java similarity index 64% rename from library/src/main/java/com/google/android/exoplayer/metadata/Eia608Parser.java rename to library/src/main/java/com/google/android/exoplayer/text/eia608/Eia608Parser.java index 8166fe610e..a9c145bbc3 100644 --- a/library/src/main/java/com/google/android/exoplayer/metadata/Eia608Parser.java +++ b/library/src/main/java/com/google/android/exoplayer/text/eia608/Eia608Parser.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer.metadata; +package com.google.android.exoplayer.text.eia608; import com.google.android.exoplayer.util.BitArray; import com.google.android.exoplayer.util.MimeTypes; @@ -27,7 +27,7 @@ import java.util.List; * Facilitates the extraction and parsing of EIA-608 (a.k.a. "line 21 captions" and "CEA-608") * Closed Captions from the SEI data block from H.264. */ -public class Eia608Parser implements MetadataParser> { +public class Eia608Parser { private static final int PAYLOAD_TYPE_CC = 4; private static final int COUNTRY_CODE = 0xB5; @@ -35,6 +35,35 @@ public class Eia608Parser implements MetadataParser> { private static final int USER_ID = 0x47413934; // "GA94" private static final int USER_DATA_TYPE_CODE = 0x3; + // Basic North American 608 CC char set, mostly ASCII. Indexed by (char-0x20). + private static final int[] BASIC_CHARACTER_SET = new int[] { + 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, // ! " # $ % & ' + 0x28, 0x29, // ( ) + 0xE1, // 2A: 225 'á' "Latin small letter A with acute" + 0x2B, 0x2C, 0x2D, 0x2E, 0x2F, // + , - . / + 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, // 0 1 2 3 4 5 6 7 + 0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F, // 8 9 : ; < = > ? + 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, // @ A B C D E F G + 0x48, 0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F, // H I J K L M N O + 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, // P Q R S T U V W + 0x58, 0x59, 0x5A, 0x5B, // X Y Z [ + 0xE9, // 5C: 233 'é' "Latin small letter E with acute" + 0x5D, // ] + 0xED, // 5E: 237 'í' "Latin small letter I with acute" + 0xF3, // 5F: 243 'ó' "Latin small letter O with acute" + 0xFA, // 60: 250 'ú' "Latin small letter U with acute" + 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, // a b c d e f g + 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, // h i j k l m n o + 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, // p q r s t u v w + 0x78, 0x79, 0x7A, // x y z + 0xE7, // 7B: 231 'ç' "Latin small letter C with cedilla" + 0xF7, // 7C: 247 '÷' "Division sign" + 0xD1, // 7D: 209 'Ñ' "Latin capital letter N with tilde" + 0xF1, // 7E: 241 'ñ' "Latin small letter N with tilde" + 0x25A0 // 7F: "Black Square" (NB: 2588 = Full Block) + }; + + // Special North American 608 CC char set. private static final int[] SPECIAL_CHARACTER_SET = new int[] { 0xAE, // 30: 174 '®' "Registered Sign" - registered trademark symbol 0xB0, // 31: 176 '°' "Degree Sign" @@ -54,13 +83,11 @@ public class Eia608Parser implements MetadataParser> { 0xFB // 3F: 251 'û' "Latin small letter U with circumflex" }; - @Override public boolean canParse(String mimeType) { return mimeType.equals(MimeTypes.APPLICATION_EIA608); } - @Override - public List parse(byte[] data, int size) throws IOException { + public List parse(byte[] data, int size, long timeUs) throws IOException { if (size <= 0) { return null; } @@ -90,37 +117,53 @@ public class Eia608Parser implements MetadataParser> { seiBuffer.skipBits(1); byte ccData2 = (byte) seiBuffer.readBits(7); + // Ignore empty captions. + if (ccData1 == 0 && ccData2 == 0) { + continue; + } + + // Special North American character set. if ((ccData1 == 0x11) && ((ccData2 & 0x70) == 0x30)) { - ccData2 &= 0xF; - stringBuilder.append((char) SPECIAL_CHARACTER_SET[ccData2]); + stringBuilder.append(getSpecialChar(ccData2)); continue; } // Control character. if (ccData1 < 0x20) { if (stringBuilder.length() > 0) { - captions.add(new ClosedCaption(ClosedCaption.TYPE_TEXT, stringBuilder.toString())); + captions.add(new ClosedCaption(ClosedCaption.TYPE_TEXT, stringBuilder.toString(), + timeUs)); stringBuilder.setLength(0); } captions.add(new ClosedCaption(ClosedCaption.TYPE_CTRL, - new String(new char[]{(char) ccData1, (char) ccData2}))); + new String(new char[] {(char) ccData1, (char) ccData2}), timeUs)); continue; } - stringBuilder.append((char) ccData1); + // Basic North American character set. + stringBuilder.append(getChar(ccData1)); if (ccData2 != 0) { - stringBuilder.append((char) ccData2); + stringBuilder.append(getChar(ccData2)); } - } if (stringBuilder.length() > 0) { - captions.add(new ClosedCaption(ClosedCaption.TYPE_TEXT, stringBuilder.toString())); + captions.add(new ClosedCaption(ClosedCaption.TYPE_TEXT, stringBuilder.toString(), timeUs)); } return Collections.unmodifiableList(captions); } + private static char getChar(byte ccData) { + int index = (ccData & 0x7F) - 0x20; + return (char) BASIC_CHARACTER_SET[index]; + } + + private static char getSpecialChar(byte ccData) { + int index = ccData & 0xF; + return (char) SPECIAL_CHARACTER_SET[index]; + } + /** * Parses the beginning of SEI data and returns the size of underlying contains closed captions * data following the header. Returns 0 if the SEI doesn't contain any closed captions data. diff --git a/library/src/main/java/com/google/android/exoplayer/text/eia608/Eia608TrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/text/eia608/Eia608TrackRenderer.java new file mode 100644 index 0000000000..d88e44d506 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/text/eia608/Eia608TrackRenderer.java @@ -0,0 +1,260 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer.text.eia608; + +import com.google.android.exoplayer.ExoPlaybackException; +import com.google.android.exoplayer.MediaFormatHolder; +import com.google.android.exoplayer.SampleHolder; +import com.google.android.exoplayer.SampleSource; +import com.google.android.exoplayer.TrackRenderer; +import com.google.android.exoplayer.text.TextRenderer; +import com.google.android.exoplayer.util.Assertions; + +import android.os.Handler; +import android.os.Handler.Callback; +import android.os.Looper; +import android.os.Message; + +import java.io.IOException; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Queue; + +/** + * A {@link TrackRenderer} for EIA-608 closed captions in a media stream. + */ +public class Eia608TrackRenderer extends TrackRenderer implements Callback { + + private static final int MSG_INVOKE_RENDERER = 0; + // The Number of closed captions text line to keep in memory. + private static final int ALLOWED_CAPTIONS_TEXT_LINES_COUNT = 4; + + private final SampleSource source; + private final Eia608Parser eia608Parser; + private final TextRenderer textRenderer; + private final Handler metadataHandler; + private final MediaFormatHolder formatHolder; + private final SampleHolder sampleHolder; + private final StringBuilder closedCaptionStringBuilder; + //Currently displayed captions. + private final List currentCaptions; + private final Queue newLineIndexes; + + private int trackIndex; + private long currentPositionUs; + private boolean inputStreamEnded; + + private long pendingCaptionsTimestamp; + private List pendingCaptions; + + /** + * @param source A source from which samples containing EIA-608 closed captions can be read. + * @param textRenderer The text renderer. + * @param textRendererLooper The looper associated with the thread on which textRenderer should be + * invoked. If the renderer makes use of standard Android UI components, then this should + * normally be the looper associated with the applications' main thread, which can be + * obtained using {@link android.app.Activity#getMainLooper()}. Null may be passed if the + * renderer should be invoked directly on the player's internal rendering thread. + */ + public Eia608TrackRenderer(SampleSource source, TextRenderer textRenderer, + Looper textRendererLooper) { + this.source = Assertions.checkNotNull(source); + this.textRenderer = Assertions.checkNotNull(textRenderer); + this.metadataHandler = textRendererLooper == null ? null + : new Handler(textRendererLooper, this); + eia608Parser = new Eia608Parser(); + formatHolder = new MediaFormatHolder(); + sampleHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_NORMAL); + closedCaptionStringBuilder = new StringBuilder(); + currentCaptions = new LinkedList(); + newLineIndexes = new LinkedList(); + } + + @Override + protected int doPrepare() throws ExoPlaybackException { + try { + boolean sourcePrepared = source.prepare(); + if (!sourcePrepared) { + return TrackRenderer.STATE_UNPREPARED; + } + } catch (IOException e) { + throw new ExoPlaybackException(e); + } + for (int i = 0; i < source.getTrackCount(); i++) { + if (eia608Parser.canParse(source.getTrackInfo(i).mimeType)) { + trackIndex = i; + return TrackRenderer.STATE_PREPARED; + } + } + return TrackRenderer.STATE_IGNORE; + } + + @Override + protected void onEnabled(long positionUs, boolean joining) { + source.enable(trackIndex, positionUs); + seekToInternal(positionUs); + } + + @Override + protected void seekTo(long positionUs) throws ExoPlaybackException { + source.seekToUs(positionUs); + seekToInternal(positionUs); + } + + private void seekToInternal(long positionUs) { + currentPositionUs = positionUs; + pendingCaptions = null; + inputStreamEnded = false; + // Clear displayed captions. + currentCaptions.clear(); + } + + @Override + protected void doSomeWork(long positionUs, long elapsedRealtimeUs) + throws ExoPlaybackException { + currentPositionUs = positionUs; + try { + source.continueBuffering(positionUs); + } catch (IOException e) { + throw new ExoPlaybackException(e); + } + + if (!inputStreamEnded && pendingCaptions == null) { + try { + int result = source.readData(trackIndex, positionUs, formatHolder, sampleHolder, false); + if (result == SampleSource.SAMPLE_READ) { + pendingCaptionsTimestamp = sampleHolder.timeUs; + pendingCaptions = eia608Parser.parse(sampleHolder.data.array(), sampleHolder.size, + sampleHolder.timeUs); + sampleHolder.data.clear(); + } else if (result == SampleSource.END_OF_STREAM) { + inputStreamEnded = true; + } + } catch (IOException e) { + throw new ExoPlaybackException(e); + } + } + + if (pendingCaptions != null && pendingCaptionsTimestamp <= currentPositionUs) { + invokeRenderer(pendingCaptions); + pendingCaptions = null; + } + } + + @Override + protected void onDisabled() { + pendingCaptions = null; + source.disable(trackIndex); + } + + @Override + protected long getDurationUs() { + return source.getTrackInfo(trackIndex).durationUs; + } + + @Override + protected long getCurrentPositionUs() { + return currentPositionUs; + } + + @Override + protected long getBufferedPositionUs() { + return TrackRenderer.END_OF_TRACK_US; + } + + @Override + protected boolean isEnded() { + return inputStreamEnded; + } + + @Override + protected boolean isReady() { + return true; + } + + private void invokeRenderer(List metadata) { + if (metadataHandler != null) { + metadataHandler.obtainMessage(MSG_INVOKE_RENDERER, metadata).sendToTarget(); + } else { + invokeRendererInternal(metadata); + } + } + + @SuppressWarnings("unchecked") + @Override + public boolean handleMessage(Message msg) { + switch (msg.what) { + case MSG_INVOKE_RENDERER: + invokeRendererInternal((List) msg.obj); + return true; + } + return false; + } + + private void invokeRendererInternal(List metadata) { + currentCaptions.addAll(metadata); + // Sort captions by the timestamp. + Collections.sort(currentCaptions); + closedCaptionStringBuilder.setLength(0); + + // After processing keep only captions after cutIndex. + int cutIndex = 0; + newLineIndexes.clear(); + for (int i = 0; i < currentCaptions.size(); i++) { + ClosedCaption caption = currentCaptions.get(i); + + if (caption.type == ClosedCaption.TYPE_CTRL) { + int cc2 = caption.text.codePointAt(1); + switch (cc2) { + case 0x2C: // Erase Displayed Memory. + closedCaptionStringBuilder.setLength(0); + cutIndex = i; + newLineIndexes.clear(); + break; + case 0x25: // Roll-Up. + case 0x26: + case 0x27: + default: + if (cc2 >= 0x20 && cc2 < 0x40) { + break; + } + if (closedCaptionStringBuilder.length() > 0 + && closedCaptionStringBuilder.charAt(closedCaptionStringBuilder.length() - 1) + != '\n') { + closedCaptionStringBuilder.append('\n'); + newLineIndexes.add(i); + if (newLineIndexes.size() >= ALLOWED_CAPTIONS_TEXT_LINES_COUNT) { + cutIndex = newLineIndexes.poll(); + } + } + break; + } + } else { + closedCaptionStringBuilder.append(caption.text); + } + } + + if (cutIndex > 0 && cutIndex < currentCaptions.size() - 1) { + for (int i = 0; i <= cutIndex; i++) { + currentCaptions.remove(0); + } + } + + textRenderer.onText(closedCaptionStringBuilder.toString()); + } + +} From b30f55f1470d49e47ce3e4fa79a15c77beb6b22a Mon Sep 17 00:00:00 2001 From: Andrey Udovenko Date: Thu, 22 Jan 2015 12:53:33 -0500 Subject: [PATCH 49/55] Process only type == 0 captions. #156 --- .../com/google/android/exoplayer/text/eia608/Eia608Parser.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/text/eia608/Eia608Parser.java b/library/src/main/java/com/google/android/exoplayer/text/eia608/Eia608Parser.java index a9c145bbc3..97773fb2d8 100644 --- a/library/src/main/java/com/google/android/exoplayer/text/eia608/Eia608Parser.java +++ b/library/src/main/java/com/google/android/exoplayer/text/eia608/Eia608Parser.java @@ -107,8 +107,7 @@ public class Eia608Parser { continue; } int ccType = seiBuffer.readBits(2); - if (ccType != 0 && ccType != 1) { - // Not EIA-608 captions. + if (ccType != 0) { seiBuffer.skipBits(16); continue; } From a9b2120fc961215ad09f3e52578964ae27caccde Mon Sep 17 00:00:00 2001 From: Lei YU Date: Fri, 23 Jan 2015 16:44:23 +0800 Subject: [PATCH 50/55] Fix an issue in BitArray.readUnsignedByte() returns incorrect value when bitOffset is not zero and data[byteOffset + 1] starts with bit 1. This is caused by signed right shift, the fix is simply to make it unsigned right shift. --- .../main/java/com/google/android/exoplayer/util/BitArray.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/src/main/java/com/google/android/exoplayer/util/BitArray.java b/library/src/main/java/com/google/android/exoplayer/util/BitArray.java index 23cd760b6d..ae3020904f 100644 --- a/library/src/main/java/com/google/android/exoplayer/util/BitArray.java +++ b/library/src/main/java/com/google/android/exoplayer/util/BitArray.java @@ -151,7 +151,7 @@ public final class BitArray { byte b; if (bitOffset != 0) { b = (byte) ((data[byteOffset] << bitOffset) - | (data[byteOffset + 1] >> (8 - bitOffset))); + | ((data[byteOffset + 1] & 0xFF) >> (8 - bitOffset))); } else { b = data[byteOffset]; } From 80602b16846da1c7a9e9df1b5eb051adb8498dfc Mon Sep 17 00:00:00 2001 From: Lei YU Date: Fri, 23 Jan 2015 23:52:46 +0800 Subject: [PATCH 51/55] Make BitArray.readUnsignedByte() a bit more clear by using int value instead of byte to prevent unnecessary convert from int to byte. --- .../com/google/android/exoplayer/util/BitArray.java | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/util/BitArray.java b/library/src/main/java/com/google/android/exoplayer/util/BitArray.java index ae3020904f..5f7ed4278f 100644 --- a/library/src/main/java/com/google/android/exoplayer/util/BitArray.java +++ b/library/src/main/java/com/google/android/exoplayer/util/BitArray.java @@ -148,16 +148,15 @@ public final class BitArray { * @return The value of the parsed byte. */ public int readUnsignedByte() { - byte b; + int value; if (bitOffset != 0) { - b = (byte) ((data[byteOffset] << bitOffset) - | ((data[byteOffset + 1] & 0xFF) >> (8 - bitOffset))); + value = (data[byteOffset] << bitOffset) + | ((data[byteOffset + 1] & 0xFF) >>> (8 - bitOffset)); } else { - b = data[byteOffset]; + value = data[byteOffset]; } byteOffset++; - // Converting a signed byte into unsigned. - return b & 0xFF; + return value & 0xFF; } /** From 6520557dc0e9e4b49907e0d0e3f8657dd0e9a4f0 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 23 Jan 2015 09:08:41 -0800 Subject: [PATCH 52/55] Add (not technically necessary 0xFF guard for clarity). Without this, the byte is cast as follows (in bits) if the top byte is set: 10000010 -> 1000000000000000000000000000010 This works because we then always shift at least one bit left, and only look at the bottom 8 bits of the result. It's confusing though. It's clearer if the cast to int gives just adds zeros to the front, like: 10000010 -> 0000000000000000000000010000010 --- .../main/java/com/google/android/exoplayer/util/BitArray.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/src/main/java/com/google/android/exoplayer/util/BitArray.java b/library/src/main/java/com/google/android/exoplayer/util/BitArray.java index 5f7ed4278f..45d7ec35d0 100644 --- a/library/src/main/java/com/google/android/exoplayer/util/BitArray.java +++ b/library/src/main/java/com/google/android/exoplayer/util/BitArray.java @@ -150,7 +150,7 @@ public final class BitArray { public int readUnsignedByte() { int value; if (bitOffset != 0) { - value = (data[byteOffset] << bitOffset) + value = ((data[byteOffset] & 0xFF) << bitOffset) | ((data[byteOffset + 1] & 0xFF) >>> (8 - bitOffset)); } else { value = data[byteOffset]; From 59b04df4c38092b6633abc5df3b1c330af63bbb6 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 23 Jan 2015 22:18:54 +0000 Subject: [PATCH 53/55] Fix incorrect max height calculation. --- .../java/com/google/android/exoplayer/hls/HlsChunkSource.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 0ca9ab4272..3c2c271167 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 @@ -184,7 +184,7 @@ public class HlsChunkSource { variantIndex = i; } maxWidth = Math.max(enabledVariants[i].width, maxWidth); - maxHeight = Math.max(enabledVariants[i].width, maxHeight); + maxHeight = Math.max(enabledVariants[i].height, maxHeight); } // TODO: We should allow the default values to be passed through the constructor. this.maxWidth = maxWidth > 0 ? maxWidth : 1920; From ee83468084f9fb1b53e6063481a0dff484e88922 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Tue, 27 Jan 2015 15:11:44 +0000 Subject: [PATCH 54/55] Blacklist playlists that 404/410. --- .../android/exoplayer/hls/HlsChunkSource.java | 64 +++++++++++++++++-- .../exoplayer/hls/HlsSampleSource.java | 11 +++- 2 files changed, 66 insertions(+), 9 deletions(-) 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 3c2c271167..584372c86a 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 @@ -22,12 +22,14 @@ import com.google.android.exoplayer.upstream.Aes128DataSource; import com.google.android.exoplayer.upstream.BandwidthMeter; import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DataSpec; +import com.google.android.exoplayer.upstream.HttpDataSource.InvalidResponseCodeException; import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.BitArray; import com.google.android.exoplayer.util.Util; import android.net.Uri; import android.os.SystemClock; +import android.util.Log; import java.io.ByteArrayInputStream; import java.io.IOException; @@ -96,6 +98,7 @@ public class HlsChunkSource { */ public static final long DEFAULT_MAX_BUFFER_TO_SWITCH_DOWN_MS = 20000; + private static final String TAG = "HlsChunkSource"; private static final float BANDWIDTH_FRACTION = 0.8f; private final SamplePool samplePool = new TsExtractor.SamplePool(); @@ -113,6 +116,7 @@ public class HlsChunkSource { private final long maxBufferDurationToSwitchDownUs; /* package */ final HlsMediaPlaylist[] mediaPlaylists; + /* package */ final boolean[] mediaPlaylistBlacklistFlags; /* package */ final long[] lastMediaPlaylistLoadTimesMs; /* package */ boolean live; /* package */ long durationUs; @@ -165,12 +169,14 @@ public class HlsChunkSource { if (playlist.type == HlsPlaylist.TYPE_MEDIA) { enabledVariants = new Variant[] {new Variant(0, playlistUrl, 0, null, -1, -1)}; mediaPlaylists = new HlsMediaPlaylist[1]; + mediaPlaylistBlacklistFlags = new boolean[1]; lastMediaPlaylistLoadTimesMs = new long[1]; setMediaPlaylist(0, (HlsMediaPlaylist) playlist); } else { Assertions.checkState(playlist.type == HlsPlaylist.TYPE_MASTER); enabledVariants = filterVariants((HlsMasterPlaylist) playlist, variantIndices); mediaPlaylists = new HlsMediaPlaylist[enabledVariants.length]; + mediaPlaylistBlacklistFlags = new boolean[enabledVariants.length]; lastMediaPlaylistLoadTimesMs = new long[enabledVariants.length]; } @@ -327,6 +333,37 @@ public class HlsChunkSource { startTimeUs, endTimeUs, chunkMediaSequence, isLastChunk); } + /** + * Invoked when an error occurs loading a chunk. + * + * @param chunk The chunk whose load failed. + * @param e The failure. + * @return True if the error was handled by the source. False otherwise. + */ + public boolean onLoadError(HlsChunk chunk, IOException e) { + if ((chunk instanceof MediaPlaylistChunk) && (e instanceof InvalidResponseCodeException)) { + InvalidResponseCodeException responseCodeException = (InvalidResponseCodeException) e; + int responseCode = responseCodeException.responseCode; + if (responseCode == 404 || responseCode == 410) { + MediaPlaylistChunk playlistChunk = (MediaPlaylistChunk) chunk; + mediaPlaylistBlacklistFlags[playlistChunk.variantIndex] = true; + if (!allPlaylistsBlacklisted()) { + // We've handled the 404/410 by blacklisting the playlist. + Log.w(TAG, "Blacklisted playlist (" + responseCode + "): " + + playlistChunk.dataSpec.uri); + return true; + } else { + // This was the last non-blacklisted playlist. Don't blacklist it. + Log.w(TAG, "Final playlist not blacklisted (" + responseCode + "): " + + playlistChunk.dataSpec.uri); + mediaPlaylistBlacklistFlags[playlistChunk.variantIndex] = false; + return false; + } + } + } + return false; + } + private int getNextVariantIndex(TsChunk previousTsChunk, long playbackPositionUs) { int idealVariantIndex = getVariantIndexForBandwdith( (int) (bandwidthMeter.getBitrateEstimate() * BANDWIDTH_FRACTION)); @@ -340,7 +377,8 @@ public class HlsChunkSource { : adaptiveMode == ADAPTIVE_MODE_SPLICE ? previousTsChunk.startTimeUs : previousTsChunk.endTimeUs; long bufferedUs = bufferedPositionUs - playbackPositionUs; - if ((idealVariantIndex > variantIndex && bufferedUs < maxBufferDurationToSwitchDownUs) + if (mediaPlaylistBlacklistFlags[variantIndex] + || (idealVariantIndex > variantIndex && bufferedUs < maxBufferDurationToSwitchDownUs) || (idealVariantIndex < variantIndex && bufferedUs > minBufferDurationToSwitchUpUs)) { // Switch variant. return idealVariantIndex; @@ -350,12 +388,16 @@ public class HlsChunkSource { } private int getVariantIndexForBandwdith(int bandwidth) { - for (int i = 0; i < enabledVariants.length - 1; i++) { - if (enabledVariants[i].bandwidth <= bandwidth) { - return i; + int lowestQualityEnabledVariant = 0; + for (int i = 0; i < enabledVariants.length; i++) { + if (!mediaPlaylistBlacklistFlags[i]) { + if (enabledVariants[i].bandwidth <= bandwidth) { + return i; + } + lowestQualityEnabledVariant = i; } } - return enabledVariants.length - 1; + return lowestQualityEnabledVariant; } private boolean shouldRerequestMediaPlaylist(int variantIndex) { @@ -475,10 +517,20 @@ public class HlsChunkSource { return false; } + private boolean allPlaylistsBlacklisted() { + for (int i = 0; i < mediaPlaylistBlacklistFlags.length; i++) { + if (!mediaPlaylistBlacklistFlags[i]) { + return false; + } + } + return true; + } + private class MediaPlaylistChunk extends BitArrayChunk { @SuppressWarnings("hiding") - private final int variantIndex; + /* package */ final int variantIndex; + private final Uri playlistBaseUri; public MediaPlaylistChunk(int variantIndex, DataSource dataSource, DataSpec dataSpec, 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 6ef2703a59..ea6fddd488 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 @@ -306,9 +306,14 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { @Override public void onLoadError(Loadable loadable, IOException e) { - currentLoadableException = e; - currentLoadableExceptionCount++; - currentLoadableExceptionTimestamp = SystemClock.elapsedRealtime(); + if (chunkSource.onLoadError(currentLoadable, e)) { + // Error handled by source. + clearCurrentLoadable(); + } else { + currentLoadableException = e; + currentLoadableExceptionCount++; + currentLoadableExceptionTimestamp = SystemClock.elapsedRealtime(); + } maybeStartLoading(); } From b7be7bc01b96f63be3892750ce0bc204e7100eb3 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Tue, 27 Jan 2015 15:12:33 +0000 Subject: [PATCH 55/55] Fix handling of PTS wraparound. --- .../android/exoplayer/hls/TsExtractor.java | 48 +++++++++++++------ 1 file changed, 34 insertions(+), 14 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java b/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java index 5347f7de5d..1c9dcc8804 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java @@ -53,19 +53,21 @@ public final class TsExtractor { private static final int TS_STREAM_TYPE_ID3 = 0x15; private static final int TS_STREAM_TYPE_EIA608 = 0x100; // 0xFF + 1 + private static final long MAX_PTS = 0x1FFFFFFFFL; + private final BitArray tsPacketBuffer; private final SparseArray sampleQueues; // Indexed by streamType private final SparseArray tsPayloadReaders; // Indexed by pid private final SamplePool samplePool; private final boolean shouldSpliceIn; - /* package */ final long firstSampleTimestamp; + private final long firstSampleTimestamp; // Accessed only by the consuming thread. private boolean spliceConfigured; // Accessed only by the loading thread. - /* package */ boolean pendingFirstSampleTimestampAdjustment; - /* package */ long sampleTimestampOffsetUs; + private long timestampOffsetUs; + private long lastPts; // Accessed by both the loading and consuming threads. private volatile boolean prepared; @@ -75,12 +77,12 @@ public final class TsExtractor { this.firstSampleTimestamp = firstSampleTimestamp; this.samplePool = samplePool; this.shouldSpliceIn = shouldSpliceIn; - pendingFirstSampleTimestampAdjustment = true; tsPacketBuffer = new BitArray(); sampleQueues = new SparseArray(); tsPayloadReaders = new SparseArray(); tsPayloadReaders.put(TS_PAT_PID, new PatReader()); largestParsedTimestampUs = Long.MIN_VALUE; + lastPts = Long.MIN_VALUE; } /** @@ -294,6 +296,33 @@ public final class TsExtractor { out.timeUs = in.timeUs; } + /** + * Adjusts a PTS value to the corresponding time in microseconds, accounting for PTS wraparound. + * + * @param pts The raw PTS value. + * @return The corresponding time in microseconds. + */ + /* package */ long ptsToTimeUs(long pts) { + if (lastPts != Long.MIN_VALUE) { + // The wrap count for the current PTS may be closestWrapCount or (closestWrapCount - 1), + // and we need to snap to the one closest to lastPts. + long closestWrapCount = (lastPts + (MAX_PTS / 2)) / MAX_PTS; + long ptsWrapBelow = pts + (MAX_PTS * (closestWrapCount - 1)); + long ptsWrapAbove = pts + (MAX_PTS * closestWrapCount); + pts = Math.abs(ptsWrapBelow - lastPts) < Math.abs(ptsWrapAbove - lastPts) + ? ptsWrapBelow : ptsWrapAbove; + } + // Calculate the corresponding timestamp. + long timeUs = (pts * C.MICROS_PER_SECOND) / 90000; + // If we haven't done the initial timestamp adjustment, do it now. + if (lastPts == Long.MIN_VALUE) { + timestampOffsetUs = firstSampleTimestamp - timeUs; + } + // Record the adjusted PTS to adjust for wraparound next time. + lastPts = pts; + return timeUs + timestampOffsetUs; + } + /** * Parses payload data. */ @@ -482,7 +511,7 @@ public final class TsExtractor { pesBuffer.skipBits(1); pts |= pesBuffer.readBitsLong(15); pesBuffer.skipBits(1); - timeUs = (pts * C.MICROS_PER_SECOND) / 90000; + timeUs = ptsToTimeUs(pts); // Skip the rest of the header. pesBuffer.skipBytes(headerDataLength - 5); } else { @@ -690,7 +719,6 @@ public final class TsExtractor { } protected void addSample(Sample sample) { - adjustTimestamp(sample); largestParsedTimestampUs = Math.max(largestParsedTimestampUs, sample.timeUs); internalQueue.add(sample); } @@ -703,14 +731,6 @@ public final class TsExtractor { sample.size += size; } - private void adjustTimestamp(Sample sample) { - if (pendingFirstSampleTimestampAdjustment) { - sampleTimestampOffsetUs = firstSampleTimestamp - sample.timeUs; - pendingFirstSampleTimestampAdjustment = false; - } - sample.timeUs += sampleTimestampOffsetUs; - } - } /**