Add basic HLS support (VOD and Live) with EXT-X-DISCONTINUITY.

This commit is contained in:
Andrey Udovenko 2014-10-01 16:54:50 -04:00
parent dd30632aa1
commit d64036c5ed
17 changed files with 2588 additions and 0 deletions

View file

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

View file

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

View file

@ -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),

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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