Commit d64036c5 by Andrey Udovenko

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

parent dd30632a
......@@ -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);
}
......
/*
* 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)));
}
}
/*
* 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();
}
}
/*
* 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;
}
/*
* 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;
}
}
}
/*
* 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;
}
}
/*
* 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));
}
}
/*
* 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;
}
}
}
/*
* 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));
}
}
/*
* 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));
}
}
/*
* 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;
}
}
/*
* 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;
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment