mirror of
https://github.com/samsonjs/media.git
synced 2026-03-26 09:35:47 +00:00
Add basic HLS support (VOD and Live) with EXT-X-DISCONTINUITY.
This commit is contained in:
parent
dd30632aa1
commit
d64036c5ed
17 changed files with 2588 additions and 0 deletions
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<HlsMasterPlaylist> {
|
||||
|
||||
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<HlsMasterPlaylist> mediaPlaylistFetcher =
|
||||
new ManifestFetcher<HlsMasterPlaylist>(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)));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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.
|
||||
* <p>
|
||||
* 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;
|
||||
|
||||
}
|
||||
|
|
@ -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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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}.
|
||||
* <p>
|
||||
* 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<TsChunk> 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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<Variant> variants;
|
||||
|
||||
public HlsMasterPlaylist(Uri baseUri, List<Variant> variants) {
|
||||
this.baseUri = baseUri;
|
||||
this.variants = variants;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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<HlsMasterPlaylist> {
|
||||
|
||||
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<Variant> variants = new ArrayList<Variant>();
|
||||
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));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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<Long> {
|
||||
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<Segment> segments;
|
||||
public final boolean live;
|
||||
public final long durationUs;
|
||||
|
||||
public HlsMediaPlaylist(Uri baseUri, int mediaSequence, int targetDurationSecs, int version,
|
||||
boolean live, List<Segment> 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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<HlsMediaPlaylist> {
|
||||
|
||||
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<Segment> segments = new ArrayList<Segment>();
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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.
|
||||
* <p>
|
||||
* 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<TsChunk> mediaChunks;
|
||||
private final List<TsChunk> 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<TsChunk>();
|
||||
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<TsChunk> 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.
|
||||
* <p>
|
||||
* 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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.
|
||||
* <p>
|
||||
* 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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<PesPayloadReader> pesPayloadReaders; // Indexed by streamType
|
||||
private final SparseArray<TsPayloadReader> tsPayloadReaders; // Indexed by pid
|
||||
private final Queue<Sample> samplesPool;
|
||||
|
||||
private boolean prepared;
|
||||
|
||||
public TsExtractor() {
|
||||
tsPacketBuffer = new BitsArray();
|
||||
pesPayloadReaders = new SparseArray<PesPayloadReader>();
|
||||
tsPayloadReaders = new SparseArray<TsPayloadReader>();
|
||||
tsPayloadReaders.put(TS_PAT_PID, new PatReader());
|
||||
samplesPool = new LinkedList<Sample>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the number of available tracks.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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}.
|
||||
* <p>
|
||||
* 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<Sample> 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<Sample> samplesQueue;
|
||||
|
||||
private MediaFormat mediaFormat;
|
||||
|
||||
protected PesPayloadReader() {
|
||||
this.samplesQueue = new LinkedList<Sample>();
|
||||
}
|
||||
|
||||
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<Integer, Integer> 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Reference in a new issue