Big HLS update. Add start of adaptive support, but leave disabled for now.
This commit is contained in:
parent
6c6ba900a9
commit
fd51901620
@ -15,8 +15,6 @@
|
|||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer.demo.full.player;
|
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.MediaCodecAudioTrackRenderer;
|
||||||
import com.google.android.exoplayer.MediaCodecVideoTrackRenderer;
|
import com.google.android.exoplayer.MediaCodecVideoTrackRenderer;
|
||||||
import com.google.android.exoplayer.TrackRenderer;
|
import com.google.android.exoplayer.TrackRenderer;
|
||||||
@ -25,13 +23,13 @@ import com.google.android.exoplayer.demo.full.player.DemoPlayer.RendererBuilder;
|
|||||||
import com.google.android.exoplayer.demo.full.player.DemoPlayer.RendererBuilderCallback;
|
import com.google.android.exoplayer.demo.full.player.DemoPlayer.RendererBuilderCallback;
|
||||||
import com.google.android.exoplayer.hls.HlsChunkSource;
|
import com.google.android.exoplayer.hls.HlsChunkSource;
|
||||||
import com.google.android.exoplayer.hls.HlsMasterPlaylist;
|
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.HlsMasterPlaylistParser;
|
||||||
import com.google.android.exoplayer.hls.HlsSampleSource;
|
import com.google.android.exoplayer.hls.HlsSampleSource;
|
||||||
|
import com.google.android.exoplayer.hls.Variant;
|
||||||
import com.google.android.exoplayer.metadata.Id3Parser;
|
import com.google.android.exoplayer.metadata.Id3Parser;
|
||||||
import com.google.android.exoplayer.metadata.MetadataTrackRenderer;
|
import com.google.android.exoplayer.metadata.MetadataTrackRenderer;
|
||||||
import com.google.android.exoplayer.upstream.BufferPool;
|
|
||||||
import com.google.android.exoplayer.upstream.DataSource;
|
import com.google.android.exoplayer.upstream.DataSource;
|
||||||
|
import com.google.android.exoplayer.upstream.DefaultBandwidthMeter;
|
||||||
import com.google.android.exoplayer.upstream.UriDataSource;
|
import com.google.android.exoplayer.upstream.UriDataSource;
|
||||||
import com.google.android.exoplayer.util.ManifestFetcher;
|
import com.google.android.exoplayer.util.ManifestFetcher;
|
||||||
import com.google.android.exoplayer.util.ManifestFetcher.ManifestCallback;
|
import com.google.android.exoplayer.util.ManifestFetcher.ManifestCallback;
|
||||||
@ -47,9 +45,6 @@ import java.util.Collections;
|
|||||||
*/
|
*/
|
||||||
public class HlsRendererBuilder implements RendererBuilder, ManifestCallback<HlsMasterPlaylist> {
|
public class HlsRendererBuilder implements RendererBuilder, ManifestCallback<HlsMasterPlaylist> {
|
||||||
|
|
||||||
private static final int BUFFER_SEGMENT_SIZE = 64 * 1024;
|
|
||||||
private static final int VIDEO_BUFFER_SEGMENTS = 200;
|
|
||||||
|
|
||||||
private final String userAgent;
|
private final String userAgent;
|
||||||
private final String url;
|
private final String url;
|
||||||
private final String contentId;
|
private final String contentId;
|
||||||
@ -89,12 +84,12 @@ public class HlsRendererBuilder implements RendererBuilder, ManifestCallback<Hls
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onManifest(String contentId, HlsMasterPlaylist manifest) {
|
public void onManifest(String contentId, HlsMasterPlaylist manifest) {
|
||||||
LoadControl loadControl = new DefaultLoadControl(new BufferPool(BUFFER_SEGMENT_SIZE));
|
DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();
|
||||||
|
|
||||||
DataSource dataSource = new UriDataSource(userAgent, null);
|
DataSource dataSource = new UriDataSource(userAgent, bandwidthMeter);
|
||||||
HlsChunkSource chunkSource = new HlsChunkSource(dataSource, manifest);
|
HlsChunkSource chunkSource = new HlsChunkSource(dataSource, manifest, bandwidthMeter, null,
|
||||||
HlsSampleSource sampleSource = new HlsSampleSource(chunkSource, loadControl,
|
false);
|
||||||
VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true, 3);
|
HlsSampleSource sampleSource = new HlsSampleSource(chunkSource, true, 3);
|
||||||
MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(sampleSource,
|
MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(sampleSource,
|
||||||
MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 0, player.getMainHandler(), player, 50);
|
MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 0, player.getMainHandler(), player, 50);
|
||||||
MediaCodecAudioTrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer(sampleSource);
|
MediaCodecAudioTrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer(sampleSource);
|
||||||
@ -111,7 +106,7 @@ public class HlsRendererBuilder implements RendererBuilder, ManifestCallback<Hls
|
|||||||
|
|
||||||
private HlsMasterPlaylist newSimpleMasterPlaylist(String mediaPlaylistUrl) {
|
private HlsMasterPlaylist newSimpleMasterPlaylist(String mediaPlaylistUrl) {
|
||||||
return new HlsMasterPlaylist(Uri.parse(""),
|
return new HlsMasterPlaylist(Uri.parse(""),
|
||||||
Collections.singletonList(new Variant(mediaPlaylistUrl, 0, null, -1, -1)));
|
Collections.singletonList(new Variant(0, mediaPlaylistUrl, 0, null, -1, -1)));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -15,8 +15,6 @@
|
|||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer.demo.simple;
|
package com.google.android.exoplayer.demo.simple;
|
||||||
|
|
||||||
import com.google.android.exoplayer.DefaultLoadControl;
|
|
||||||
import com.google.android.exoplayer.LoadControl;
|
|
||||||
import com.google.android.exoplayer.MediaCodecAudioTrackRenderer;
|
import com.google.android.exoplayer.MediaCodecAudioTrackRenderer;
|
||||||
import com.google.android.exoplayer.MediaCodecVideoTrackRenderer;
|
import com.google.android.exoplayer.MediaCodecVideoTrackRenderer;
|
||||||
import com.google.android.exoplayer.TrackRenderer;
|
import com.google.android.exoplayer.TrackRenderer;
|
||||||
@ -26,11 +24,11 @@ import com.google.android.exoplayer.demo.simple.SimplePlayerActivity.RendererBui
|
|||||||
import com.google.android.exoplayer.demo.simple.SimplePlayerActivity.RendererBuilderCallback;
|
import com.google.android.exoplayer.demo.simple.SimplePlayerActivity.RendererBuilderCallback;
|
||||||
import com.google.android.exoplayer.hls.HlsChunkSource;
|
import com.google.android.exoplayer.hls.HlsChunkSource;
|
||||||
import com.google.android.exoplayer.hls.HlsMasterPlaylist;
|
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.HlsMasterPlaylistParser;
|
||||||
import com.google.android.exoplayer.hls.HlsSampleSource;
|
import com.google.android.exoplayer.hls.HlsSampleSource;
|
||||||
import com.google.android.exoplayer.upstream.BufferPool;
|
import com.google.android.exoplayer.hls.Variant;
|
||||||
import com.google.android.exoplayer.upstream.DataSource;
|
import com.google.android.exoplayer.upstream.DataSource;
|
||||||
|
import com.google.android.exoplayer.upstream.DefaultBandwidthMeter;
|
||||||
import com.google.android.exoplayer.upstream.UriDataSource;
|
import com.google.android.exoplayer.upstream.UriDataSource;
|
||||||
import com.google.android.exoplayer.util.ManifestFetcher;
|
import com.google.android.exoplayer.util.ManifestFetcher;
|
||||||
import com.google.android.exoplayer.util.ManifestFetcher.ManifestCallback;
|
import com.google.android.exoplayer.util.ManifestFetcher.ManifestCallback;
|
||||||
@ -47,9 +45,6 @@ import java.util.Collections;
|
|||||||
/* package */ class HlsRendererBuilder implements RendererBuilder,
|
/* package */ class HlsRendererBuilder implements RendererBuilder,
|
||||||
ManifestCallback<HlsMasterPlaylist> {
|
ManifestCallback<HlsMasterPlaylist> {
|
||||||
|
|
||||||
private static final int BUFFER_SEGMENT_SIZE = 64 * 1024;
|
|
||||||
private static final int VIDEO_BUFFER_SEGMENTS = 200;
|
|
||||||
|
|
||||||
private final SimplePlayerActivity playerActivity;
|
private final SimplePlayerActivity playerActivity;
|
||||||
private final String userAgent;
|
private final String userAgent;
|
||||||
private final String url;
|
private final String url;
|
||||||
@ -90,12 +85,11 @@ import java.util.Collections;
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onManifest(String contentId, HlsMasterPlaylist manifest) {
|
public void onManifest(String contentId, HlsMasterPlaylist manifest) {
|
||||||
LoadControl loadControl = new DefaultLoadControl(new BufferPool(BUFFER_SEGMENT_SIZE));
|
DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();
|
||||||
|
DataSource dataSource = new UriDataSource(userAgent, bandwidthMeter);
|
||||||
DataSource dataSource = new UriDataSource(userAgent, null);
|
HlsChunkSource chunkSource = new HlsChunkSource(dataSource, manifest, bandwidthMeter, null,
|
||||||
HlsChunkSource chunkSource = new HlsChunkSource(dataSource, manifest);
|
false);
|
||||||
HlsSampleSource sampleSource = new HlsSampleSource(chunkSource, loadControl,
|
HlsSampleSource sampleSource = new HlsSampleSource(chunkSource, true, 2);
|
||||||
VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true, 2);
|
|
||||||
MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(sampleSource,
|
MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(sampleSource,
|
||||||
MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 0, playerActivity.getMainHandler(),
|
MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 0, playerActivity.getMainHandler(),
|
||||||
playerActivity, 50);
|
playerActivity, 50);
|
||||||
@ -109,7 +103,7 @@ import java.util.Collections;
|
|||||||
|
|
||||||
private HlsMasterPlaylist newSimpleMasterPlaylist(String mediaPlaylistUrl) {
|
private HlsMasterPlaylist newSimpleMasterPlaylist(String mediaPlaylistUrl) {
|
||||||
return new HlsMasterPlaylist(Uri.parse(""),
|
return new HlsMasterPlaylist(Uri.parse(""),
|
||||||
Collections.singletonList(new Variant(mediaPlaylistUrl, 0, null, -1, -1)));
|
Collections.singletonList(new Variant(0, mediaPlaylistUrl, 0, null, -1, -1)));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,100 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2014 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package com.google.android.exoplayer.hls;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer.upstream.DataSource;
|
||||||
|
import com.google.android.exoplayer.upstream.DataSpec;
|
||||||
|
import com.google.android.exoplayer.util.BitArray;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An abstract base class for {@link HlsChunk} implementations where the data should be loaded into
|
||||||
|
* a {@link BitArray} and subsequently consumed.
|
||||||
|
*/
|
||||||
|
public abstract class BitArrayChunk extends HlsChunk {
|
||||||
|
|
||||||
|
private static final int READ_GRANULARITY = 16 * 1024;
|
||||||
|
|
||||||
|
private final BitArray bitArray;
|
||||||
|
private volatile boolean loadFinished;
|
||||||
|
private volatile boolean loadCanceled;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param dataSource The source from which the data should be loaded.
|
||||||
|
* @param dataSpec Defines the data to be loaded. {@code dataSpec.length} must not exceed
|
||||||
|
* {@link Integer#MAX_VALUE}. If {@code dataSpec.length == C.LENGTH_UNBOUNDED} then
|
||||||
|
* the length resolved by {@code dataSource.open(dataSpec)} must not exceed
|
||||||
|
* {@link Integer#MAX_VALUE}.
|
||||||
|
* @param bitArray The {@link BitArray} into which the data should be loaded.
|
||||||
|
*/
|
||||||
|
public BitArrayChunk(DataSource dataSource, DataSpec dataSpec, BitArray bitArray) {
|
||||||
|
super(dataSource, dataSpec);
|
||||||
|
this.bitArray = bitArray;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void consume() throws IOException {
|
||||||
|
consume(bitArray);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoked by {@link #consume()}. Implementations should override this method to consume the
|
||||||
|
* loaded data.
|
||||||
|
*
|
||||||
|
* @param bitArray The {@link BitArray} containing the loaded data.
|
||||||
|
* @throws IOException If an error occurs consuming the loaded data.
|
||||||
|
*/
|
||||||
|
protected abstract void consume(BitArray bitArray) throws IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the whole of the chunk has been loaded.
|
||||||
|
*
|
||||||
|
* @return True if the whole of the chunk has been loaded. False otherwise.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean isLoadFinished() {
|
||||||
|
return loadFinished;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loadable implementation
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void cancelLoad() {
|
||||||
|
loadCanceled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final boolean isLoadCanceled() {
|
||||||
|
return loadCanceled;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void load() throws IOException, InterruptedException {
|
||||||
|
try {
|
||||||
|
bitArray.reset();
|
||||||
|
dataSource.open(dataSpec);
|
||||||
|
int bytesRead = 0;
|
||||||
|
while (bytesRead != -1 && !loadCanceled) {
|
||||||
|
bytesRead = bitArray.append(dataSource, READ_GRANULARITY);
|
||||||
|
}
|
||||||
|
loadFinished = !loadCanceled;
|
||||||
|
} finally {
|
||||||
|
dataSource.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -15,38 +15,21 @@
|
|||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer.hls;
|
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.DataSource;
|
||||||
import com.google.android.exoplayer.upstream.DataSourceStream;
|
|
||||||
import com.google.android.exoplayer.upstream.DataSpec;
|
import com.google.android.exoplayer.upstream.DataSpec;
|
||||||
import com.google.android.exoplayer.upstream.Loader.Loadable;
|
import com.google.android.exoplayer.upstream.Loader.Loadable;
|
||||||
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
|
|
||||||
import com.google.android.exoplayer.util.Assertions;
|
import com.google.android.exoplayer.util.Assertions;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An abstract base class for {@link Loadable} implementations that load chunks of data required
|
* An abstract base class for {@link Loadable} implementations that load chunks of data required
|
||||||
* for the playback of streams.
|
* for the playback of 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 abstract class HlsChunk implements Loadable {
|
public abstract class HlsChunk implements Loadable {
|
||||||
|
|
||||||
/**
|
protected final DataSource dataSource;
|
||||||
* The reason for a {@link HlsChunkSource} having generated this chunk. For reporting only.
|
protected final DataSpec dataSpec;
|
||||||
* 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 dataSource The source from which the data should be loaded.
|
||||||
@ -54,123 +37,15 @@ public abstract class HlsChunk implements Loadable {
|
|||||||
* {@link Integer#MAX_VALUE}. If {@code dataSpec.length == C.LENGTH_UNBOUNDED} then
|
* {@link Integer#MAX_VALUE}. If {@code dataSpec.length == C.LENGTH_UNBOUNDED} then
|
||||||
* the length resolved by {@code dataSource.open(dataSpec)} must not exceed
|
* the length resolved by {@code dataSource.open(dataSpec)} must not exceed
|
||||||
* {@link Integer#MAX_VALUE}.
|
* {@link Integer#MAX_VALUE}.
|
||||||
* @param trigger See {@link #trigger}.
|
|
||||||
*/
|
*/
|
||||||
public HlsChunk(DataSource dataSource, DataSpec dataSpec, int trigger) {
|
public HlsChunk(DataSource dataSource, DataSpec dataSpec) {
|
||||||
Assertions.checkState(dataSpec.length <= Integer.MAX_VALUE);
|
Assertions.checkState(dataSpec.length <= Integer.MAX_VALUE);
|
||||||
this.dataSource = Assertions.checkNotNull(dataSource);
|
this.dataSource = Assertions.checkNotNull(dataSource);
|
||||||
this.dataSpec = Assertions.checkNotNull(dataSpec);
|
this.dataSpec = Assertions.checkNotNull(dataSpec);
|
||||||
this.trigger = trigger;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public abstract void consume() throws IOException;
|
||||||
* Initializes the {@link HlsChunk}.
|
|
||||||
*
|
|
||||||
* @param allocator An {@link Allocator} from which the {@link Allocation} needed to contain the
|
|
||||||
* data can be obtained.
|
|
||||||
*/
|
|
||||||
public final void init(Allocator allocator) {
|
|
||||||
Assertions.checkState(dataSourceStream == null);
|
|
||||||
dataSourceStream = new DataSourceStream(dataSource, dataSpec, allocator);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
public abstract boolean isLoadFinished();
|
||||||
* 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();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,37 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2014 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package com.google.android.exoplayer.hls;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Holds a hls chunk operation, which consists of a {@link HlsChunk} to load together with the
|
|
||||||
* number of {@link TsChunk}s that should be retained on the queue.
|
|
||||||
* <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;
|
|
||||||
|
|
||||||
}
|
|
@ -18,10 +18,12 @@ package com.google.android.exoplayer.hls;
|
|||||||
import com.google.android.exoplayer.C;
|
import com.google.android.exoplayer.C;
|
||||||
import com.google.android.exoplayer.MediaFormat;
|
import com.google.android.exoplayer.MediaFormat;
|
||||||
import com.google.android.exoplayer.TrackRenderer;
|
import com.google.android.exoplayer.TrackRenderer;
|
||||||
|
import com.google.android.exoplayer.hls.TsExtractor.SamplePool;
|
||||||
import com.google.android.exoplayer.upstream.Aes128DataSource;
|
import com.google.android.exoplayer.upstream.Aes128DataSource;
|
||||||
|
import com.google.android.exoplayer.upstream.BandwidthMeter;
|
||||||
import com.google.android.exoplayer.upstream.DataSource;
|
import com.google.android.exoplayer.upstream.DataSource;
|
||||||
import com.google.android.exoplayer.upstream.DataSpec;
|
import com.google.android.exoplayer.upstream.DataSpec;
|
||||||
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
|
import com.google.android.exoplayer.util.BitArray;
|
||||||
import com.google.android.exoplayer.util.Util;
|
import com.google.android.exoplayer.util.Util;
|
||||||
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
@ -30,6 +32,8 @@ import android.os.SystemClock;
|
|||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
@ -41,26 +45,66 @@ import java.util.Locale;
|
|||||||
*/
|
*/
|
||||||
public class HlsChunkSource {
|
public class HlsChunkSource {
|
||||||
|
|
||||||
|
private static final float BANDWIDTH_FRACTION = 0.8f;
|
||||||
|
private static final long MIN_BUFFER_TO_SWITCH_UP_US = 5000000;
|
||||||
|
private static final long MAX_BUFFER_TO_SWITCH_DOWN_US = 15000000;
|
||||||
|
|
||||||
|
private final SamplePool samplePool = new TsExtractor.SamplePool();
|
||||||
private final DataSource upstreamDataSource;
|
private final DataSource upstreamDataSource;
|
||||||
private final HlsMasterPlaylist masterPlaylist;
|
|
||||||
private final HlsMediaPlaylistParser mediaPlaylistParser;
|
private final HlsMediaPlaylistParser mediaPlaylistParser;
|
||||||
|
private final Variant[] enabledVariants;
|
||||||
|
private final BandwidthMeter bandwidthMeter;
|
||||||
|
private final BitArray bitArray;
|
||||||
|
private final boolean enableAdaptive;
|
||||||
|
private final Uri baseUri;
|
||||||
|
private final int maxWidth;
|
||||||
|
private final int maxHeight;
|
||||||
|
|
||||||
/* package */ HlsMediaPlaylist mediaPlaylist;
|
/* package */ final HlsMediaPlaylist[] mediaPlaylists;
|
||||||
/* package */ boolean mediaPlaylistWasLive;
|
/* package */ final long[] lastMediaPlaylistLoadTimesMs;
|
||||||
/* package */ long lastMediaPlaylistLoadTimeMs;
|
/* package */ boolean live;
|
||||||
|
/* package */ long durationUs;
|
||||||
|
|
||||||
|
private int variantIndex;
|
||||||
private DataSource encryptedDataSource;
|
private DataSource encryptedDataSource;
|
||||||
private String encryptionKeyUri;
|
private String encryptionKeyUri;
|
||||||
|
|
||||||
// TODO: Once proper m3u8 parsing is in place, actually use the url!
|
/**
|
||||||
public HlsChunkSource(DataSource dataSource, HlsMasterPlaylist masterPlaylist) {
|
* @param dataSource A {@link DataSource} suitable for loading the media data.
|
||||||
|
* @param masterPlaylist The master playlist.
|
||||||
|
* @param variantIndices A subset of variant indices to consider, or null to consider all of the
|
||||||
|
* variants in the master playlist.
|
||||||
|
*/
|
||||||
|
public HlsChunkSource(DataSource dataSource, HlsMasterPlaylist masterPlaylist,
|
||||||
|
BandwidthMeter bandwidthMeter, int[] variantIndices, boolean enableAdaptive) {
|
||||||
this.upstreamDataSource = dataSource;
|
this.upstreamDataSource = dataSource;
|
||||||
this.masterPlaylist = masterPlaylist;
|
this.bandwidthMeter = bandwidthMeter;
|
||||||
|
this.enableAdaptive = enableAdaptive;
|
||||||
|
baseUri = masterPlaylist.baseUri;
|
||||||
|
bitArray = new BitArray();
|
||||||
mediaPlaylistParser = new HlsMediaPlaylistParser();
|
mediaPlaylistParser = new HlsMediaPlaylistParser();
|
||||||
|
enabledVariants = filterVariants(masterPlaylist, variantIndices);
|
||||||
|
lastMediaPlaylistLoadTimesMs = new long[enabledVariants.length];
|
||||||
|
mediaPlaylists = new HlsMediaPlaylist[enabledVariants.length];
|
||||||
|
int maxWidth = -1;
|
||||||
|
int maxHeight = -1;
|
||||||
|
// Select the first variant from the master playlist that's enabled.
|
||||||
|
long minOriginalVariantIndex = Integer.MAX_VALUE;
|
||||||
|
for (int i = 0; i < enabledVariants.length; i++) {
|
||||||
|
if (enabledVariants[i].index < minOriginalVariantIndex) {
|
||||||
|
minOriginalVariantIndex = enabledVariants[i].index;
|
||||||
|
variantIndex = i;
|
||||||
|
}
|
||||||
|
maxWidth = Math.max(enabledVariants[i].width, maxWidth);
|
||||||
|
maxHeight = Math.max(enabledVariants[i].width, maxHeight);
|
||||||
|
}
|
||||||
|
// TODO: We should allow the default values to be passed through the constructor.
|
||||||
|
this.maxWidth = maxWidth > 0 ? maxWidth : 1920;
|
||||||
|
this.maxHeight = maxHeight > 0 ? maxHeight : 1080;
|
||||||
}
|
}
|
||||||
|
|
||||||
public long getDurationUs() {
|
public long getDurationUs() {
|
||||||
return mediaPlaylistWasLive ? TrackRenderer.UNKNOWN_TIME_US : mediaPlaylist.durationUs;
|
return live ? TrackRenderer.UNKNOWN_TIME_US : durationUs;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -72,49 +116,33 @@ public class HlsChunkSource {
|
|||||||
* @param out The {@link MediaFormat} on which the maximum video dimensions should be set.
|
* @param out The {@link MediaFormat} on which the maximum video dimensions should be set.
|
||||||
*/
|
*/
|
||||||
public void getMaxVideoDimensions(MediaFormat out) {
|
public void getMaxVideoDimensions(MediaFormat out) {
|
||||||
// TODO: Implement this.
|
out.setMaxVideoDimensions(maxWidth, maxHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the provided {@link HlsChunkOperationHolder} to contain the next operation that should
|
* Returns the next {@link HlsChunk} that should be loaded.
|
||||||
* 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 previousTsChunk The previously loaded chunk that the next chunk should follow.
|
||||||
* @param seekPositionUs If the queue is empty, this parameter must specify the seek position. If
|
* @param seekPositionUs If there is no previous chunk, this parameter must specify the seek
|
||||||
* the queue is non-empty then this parameter is ignored.
|
* position. If there is a previous chunk then this parameter is ignored.
|
||||||
* @param playbackPositionUs The current playback position.
|
* @param playbackPositionUs The current playback position.
|
||||||
* @param out A holder for the next operation, whose {@link HlsChunkOperationHolder#queueSize} is
|
* @return The next chunk to load.
|
||||||
* 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,
|
public HlsChunk getChunkOperation(TsChunk previousTsChunk, long seekPositionUs,
|
||||||
HlsChunkOperationHolder out) {
|
long playbackPositionUs) {
|
||||||
if (out.chunk != null) {
|
|
||||||
// We already have a chunk. Keep it.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
HlsMediaPlaylist mediaPlaylist = mediaPlaylists[variantIndex];
|
||||||
if (mediaPlaylist == null) {
|
if (mediaPlaylist == null) {
|
||||||
out.chunk = newMediaPlaylistChunk();
|
return newMediaPlaylistChunk();
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int chunkMediaSequence = 0;
|
int chunkMediaSequence = 0;
|
||||||
if (mediaPlaylistWasLive) {
|
if (live) {
|
||||||
if (queue.isEmpty()) {
|
if (previousTsChunk == null) {
|
||||||
chunkMediaSequence = getLiveStartChunkMediaSequence();
|
chunkMediaSequence = getLiveStartChunkMediaSequence();
|
||||||
} else {
|
} else {
|
||||||
// For live nextChunkIndex contains chunk media sequence number.
|
// For live nextChunkIndex contains chunk media sequence number.
|
||||||
chunkMediaSequence = queue.get(queue.size() - 1).nextChunkIndex;
|
chunkMediaSequence = previousTsChunk.nextChunkIndex;
|
||||||
// If the updated playlist is far ahead and doesn't even have the last chunk from the
|
// 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.
|
// queue, then try to catch up, skip a few chunks and start as if it was a new playlist.
|
||||||
if (chunkMediaSequence < mediaPlaylist.mediaSequence) {
|
if (chunkMediaSequence < mediaPlaylist.mediaSequence) {
|
||||||
@ -124,28 +152,26 @@ public class HlsChunkSource {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Not live.
|
// Not live.
|
||||||
if (queue.isEmpty()) {
|
if (previousTsChunk == null) {
|
||||||
chunkMediaSequence = Util.binarySearchFloor(mediaPlaylist.segments, seekPositionUs, true,
|
chunkMediaSequence = Util.binarySearchFloor(mediaPlaylist.segments, seekPositionUs, true,
|
||||||
true) + mediaPlaylist.mediaSequence;
|
true) + mediaPlaylist.mediaSequence;
|
||||||
} else {
|
} else {
|
||||||
chunkMediaSequence = queue.get(queue.size() - 1).nextChunkIndex;
|
chunkMediaSequence = previousTsChunk.nextChunkIndex;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (chunkMediaSequence == -1) {
|
if (chunkMediaSequence == -1) {
|
||||||
out.chunk = null;
|
// We've reached the end of the stream.
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
int chunkIndex = chunkMediaSequence - mediaPlaylist.mediaSequence;
|
int chunkIndex = chunkMediaSequence - mediaPlaylist.mediaSequence;
|
||||||
// If the end of the playlist is reached.
|
|
||||||
if (chunkIndex >= mediaPlaylist.segments.size()) {
|
if (chunkIndex >= mediaPlaylist.segments.size()) {
|
||||||
if (mediaPlaylist.live && shouldRerequestMediaPlaylist()) {
|
if (mediaPlaylist.live && shouldRerequestMediaPlaylist()) {
|
||||||
out.chunk = newMediaPlaylistChunk();
|
return newMediaPlaylistChunk();
|
||||||
} else {
|
} else {
|
||||||
out.chunk = null;
|
return null;
|
||||||
}
|
}
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
HlsMediaPlaylist.Segment segment = mediaPlaylist.segments.get(chunkIndex);
|
HlsMediaPlaylist.Segment segment = mediaPlaylist.segments.get(chunkIndex);
|
||||||
@ -156,97 +182,204 @@ public class HlsChunkSource {
|
|||||||
if (!segment.encryptionKeyUri.equals(encryptionKeyUri)) {
|
if (!segment.encryptionKeyUri.equals(encryptionKeyUri)) {
|
||||||
// Encryption is specified and the key has changed.
|
// Encryption is specified and the key has changed.
|
||||||
Uri keyUri = Util.getMergedUri(mediaPlaylist.baseUri, segment.encryptionKeyUri);
|
Uri keyUri = Util.getMergedUri(mediaPlaylist.baseUri, segment.encryptionKeyUri);
|
||||||
out.chunk = newEncryptionKeyChunk(keyUri, segment.encryptionIV);
|
HlsChunk toReturn = newEncryptionKeyChunk(keyUri, segment.encryptionIV);
|
||||||
encryptionKeyUri = segment.encryptionKeyUri;
|
encryptionKeyUri = segment.encryptionKeyUri;
|
||||||
return;
|
return toReturn;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
encryptedDataSource = null;
|
encryptedDataSource = null;
|
||||||
encryptionKeyUri = null;
|
encryptionKeyUri = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
DataSpec dataSpec = new DataSpec(chunkUri, 0, C.LENGTH_UNBOUNDED, null);
|
|
||||||
|
|
||||||
long startTimeUs;
|
long startTimeUs;
|
||||||
|
boolean splicingIn = previousTsChunk != null && previousTsChunk.splicingOut;
|
||||||
int nextChunkMediaSequence = chunkMediaSequence + 1;
|
int nextChunkMediaSequence = chunkMediaSequence + 1;
|
||||||
if (mediaPlaylistWasLive) {
|
if (live) {
|
||||||
if (queue.isEmpty()) {
|
if (previousTsChunk == null) {
|
||||||
startTimeUs = 0;
|
startTimeUs = 0;
|
||||||
|
} else if (splicingIn) {
|
||||||
|
startTimeUs = previousTsChunk.startTimeUs;
|
||||||
} else {
|
} else {
|
||||||
startTimeUs = queue.get(queue.size() - 1).endTimeUs;
|
startTimeUs = previousTsChunk.endTimeUs;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Not live.
|
// Not live.
|
||||||
startTimeUs = segment.startTimeUs;
|
startTimeUs = segment.startTimeUs;
|
||||||
if (chunkIndex == mediaPlaylist.segments.size() - 1) {
|
|
||||||
nextChunkMediaSequence = -1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
if (!mediaPlaylist.live && chunkIndex == mediaPlaylist.segments.size() - 1) {
|
||||||
|
nextChunkMediaSequence = -1;
|
||||||
|
}
|
||||||
|
|
||||||
long endTimeUs = startTimeUs + (long) (segment.durationSecs * 1000000);
|
long endTimeUs = startTimeUs + (long) (segment.durationSecs * 1000000);
|
||||||
|
|
||||||
|
int currentVariantIndex = variantIndex;
|
||||||
|
boolean splicingOut = false;
|
||||||
|
if (splicingIn) {
|
||||||
|
// Do nothing.
|
||||||
|
} else if (enableAdaptive && nextChunkMediaSequence != -1) {
|
||||||
|
int idealVariantIndex = getVariantIndexForBandwdith(
|
||||||
|
(int) (bandwidthMeter.getBitrateEstimate() * BANDWIDTH_FRACTION));
|
||||||
|
long bufferedUs = startTimeUs - playbackPositionUs;
|
||||||
|
if ((idealVariantIndex > currentVariantIndex && bufferedUs < MAX_BUFFER_TO_SWITCH_DOWN_US)
|
||||||
|
|| (idealVariantIndex < currentVariantIndex && bufferedUs > MIN_BUFFER_TO_SWITCH_UP_US)) {
|
||||||
|
variantIndex = idealVariantIndex;
|
||||||
|
}
|
||||||
|
splicingOut = variantIndex != currentVariantIndex;
|
||||||
|
if (splicingOut) {
|
||||||
|
// If we're splicing out, we want to load the same chunk again next time, but for a
|
||||||
|
// different variant.
|
||||||
|
nextChunkMediaSequence = chunkMediaSequence;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure the datasource for loading the chunk.
|
||||||
DataSource dataSource;
|
DataSource dataSource;
|
||||||
if (encryptedDataSource != null) {
|
if (encryptedDataSource != null) {
|
||||||
dataSource = encryptedDataSource;
|
dataSource = encryptedDataSource;
|
||||||
} else {
|
} else {
|
||||||
dataSource = upstreamDataSource;
|
dataSource = upstreamDataSource;
|
||||||
}
|
}
|
||||||
out.chunk = new TsChunk(dataSource, dataSpec, 0, 0, startTimeUs, endTimeUs,
|
DataSpec dataSpec = new DataSpec(chunkUri, 0, C.LENGTH_UNBOUNDED, null);
|
||||||
nextChunkMediaSequence, segment.discontinuity, false);
|
|
||||||
|
// Configure the extractor that will read the chunk.
|
||||||
|
TsExtractor extractor;
|
||||||
|
if (previousTsChunk == null || splicingIn || segment.discontinuity) {
|
||||||
|
extractor = new TsExtractor(startTimeUs, samplePool);
|
||||||
|
} else {
|
||||||
|
extractor = previousTsChunk.extractor;
|
||||||
|
}
|
||||||
|
if (splicingOut) {
|
||||||
|
extractor.discardFromNextKeyframes();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new TsChunk(dataSource, dataSpec, extractor, enabledVariants[currentVariantIndex].index,
|
||||||
|
startTimeUs, endTimeUs, nextChunkMediaSequence, splicingOut);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getVariantIndexForBandwdith(int bandwidth) {
|
||||||
|
for (int i = 0; i < enabledVariants.length - 1; i++) {
|
||||||
|
if (enabledVariants[i].bandwidth <= bandwidth) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return enabledVariants.length - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean shouldRerequestMediaPlaylist() {
|
private boolean shouldRerequestMediaPlaylist() {
|
||||||
// Don't re-request media playlist more often than one-half of the target duration.
|
// Don't re-request media playlist more often than one-half of the target duration.
|
||||||
|
HlsMediaPlaylist mediaPlaylist = mediaPlaylists[variantIndex];
|
||||||
long timeSinceLastMediaPlaylistLoadMs =
|
long timeSinceLastMediaPlaylistLoadMs =
|
||||||
SystemClock.elapsedRealtime() - lastMediaPlaylistLoadTimeMs;
|
SystemClock.elapsedRealtime() - lastMediaPlaylistLoadTimesMs[variantIndex];
|
||||||
return timeSinceLastMediaPlaylistLoadMs >= (mediaPlaylist.targetDurationSecs * 1000) / 2;
|
return timeSinceLastMediaPlaylistLoadMs >= (mediaPlaylist.targetDurationSecs * 1000) / 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
private int getLiveStartChunkMediaSequence() {
|
private int getLiveStartChunkMediaSequence() {
|
||||||
// For live start playback from the third chunk from the end.
|
// For live start playback from the third chunk from the end.
|
||||||
|
HlsMediaPlaylist mediaPlaylist = mediaPlaylists[variantIndex];
|
||||||
int chunkIndex = mediaPlaylist.segments.size() > 3 ? mediaPlaylist.segments.size() - 3 : 0;
|
int chunkIndex = mediaPlaylist.segments.size() > 3 ? mediaPlaylist.segments.size() - 3 : 0;
|
||||||
return chunkIndex + mediaPlaylist.mediaSequence;
|
return chunkIndex + mediaPlaylist.mediaSequence;
|
||||||
}
|
}
|
||||||
|
|
||||||
private MediaPlaylistChunk newMediaPlaylistChunk() {
|
private MediaPlaylistChunk newMediaPlaylistChunk() {
|
||||||
Uri mediaPlaylistUri = Util.getMergedUri(masterPlaylist.baseUri,
|
Uri mediaPlaylistUri = Util.getMergedUri(baseUri, enabledVariants[variantIndex].url);
|
||||||
masterPlaylist.variants.get(0).url);
|
|
||||||
DataSpec dataSpec = new DataSpec(mediaPlaylistUri, 0, C.LENGTH_UNBOUNDED, null);
|
DataSpec dataSpec = new DataSpec(mediaPlaylistUri, 0, C.LENGTH_UNBOUNDED, null);
|
||||||
Uri mediaPlaylistBaseUri = Util.parseBaseUri(mediaPlaylistUri.toString());
|
Uri baseUri = Util.parseBaseUri(mediaPlaylistUri.toString());
|
||||||
return new MediaPlaylistChunk(upstreamDataSource, dataSpec, 0, mediaPlaylistBaseUri);
|
return new MediaPlaylistChunk(variantIndex, upstreamDataSource, dataSpec, baseUri);
|
||||||
}
|
}
|
||||||
|
|
||||||
private EncryptionKeyChunk newEncryptionKeyChunk(Uri keyUri, String iv) {
|
private EncryptionKeyChunk newEncryptionKeyChunk(Uri keyUri, String iv) {
|
||||||
DataSpec dataSpec = new DataSpec(keyUri, 0, C.LENGTH_UNBOUNDED, null);
|
DataSpec dataSpec = new DataSpec(keyUri, 0, C.LENGTH_UNBOUNDED, null);
|
||||||
return new EncryptionKeyChunk(upstreamDataSource, dataSpec, 0, iv);
|
return new EncryptionKeyChunk(upstreamDataSource, dataSpec, iv);
|
||||||
}
|
}
|
||||||
|
|
||||||
private class MediaPlaylistChunk extends HlsChunk {
|
private static Variant[] filterVariants(HlsMasterPlaylist masterPlaylist, int[] variantIndices) {
|
||||||
|
List<Variant> masterVariants = masterPlaylist.variants;
|
||||||
|
ArrayList<Variant> enabledVariants = new ArrayList<Variant>();
|
||||||
|
if (variantIndices != null) {
|
||||||
|
for (int i = 0; i < variantIndices.length; i++) {
|
||||||
|
enabledVariants.add(masterVariants.get(variantIndices[i]));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If variantIndices is null then all variants are initially considered.
|
||||||
|
enabledVariants.addAll(masterVariants);
|
||||||
|
}
|
||||||
|
|
||||||
private final Uri baseUri;
|
ArrayList<Variant> definiteVideoVariants = new ArrayList<Variant>();
|
||||||
|
ArrayList<Variant> definiteAudioOnlyVariants = new ArrayList<Variant>();
|
||||||
|
for (int i = 0; i < enabledVariants.size(); i++) {
|
||||||
|
Variant variant = enabledVariants.get(i);
|
||||||
|
if (variant.height > 0 || variantHasExplicitCodecWithPrefix(variant, "avc")) {
|
||||||
|
definiteVideoVariants.add(variant);
|
||||||
|
} else if (variantHasExplicitCodecWithPrefix(variant, "mp4a")) {
|
||||||
|
definiteAudioOnlyVariants.add(variant);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public MediaPlaylistChunk(DataSource dataSource, DataSpec dataSpec, int trigger, Uri baseUri) {
|
if (!definiteVideoVariants.isEmpty()) {
|
||||||
super(dataSource, dataSpec, trigger);
|
// We've identified some variants as definitely containing video. Assume variants within the
|
||||||
this.baseUri = baseUri;
|
// master playlist are marked consistently, and hence that we have the full set. Filter out
|
||||||
|
// any other variants, which are likely to be audio only.
|
||||||
|
enabledVariants = definiteVideoVariants;
|
||||||
|
} else if (definiteAudioOnlyVariants.size() < enabledVariants.size()) {
|
||||||
|
// We've identified some variants, but not all, as being audio only. Filter them out to leave
|
||||||
|
// the remaining variants, which are likely to contain video.
|
||||||
|
enabledVariants.removeAll(definiteAudioOnlyVariants);
|
||||||
|
} else {
|
||||||
|
// Leave the enabled variants unchanged. They're likely either all video or all audio.
|
||||||
|
}
|
||||||
|
|
||||||
|
Collections.sort(enabledVariants, new Variant.DecreasingBandwidthComparator());
|
||||||
|
|
||||||
|
Variant[] enabledVariantsArray = new Variant[enabledVariants.size()];
|
||||||
|
enabledVariants.toArray(enabledVariantsArray);
|
||||||
|
return enabledVariantsArray;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean variantHasExplicitCodecWithPrefix(Variant variant, String prefix) {
|
||||||
|
String[] codecs = variant.codecs;
|
||||||
|
if (codecs == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < codecs.length; i++) {
|
||||||
|
if (codecs[i].startsWith(prefix)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class MediaPlaylistChunk extends BitArrayChunk {
|
||||||
|
|
||||||
|
@SuppressWarnings("hiding")
|
||||||
|
private final int variantIndex;
|
||||||
|
private final Uri playlistBaseUri;
|
||||||
|
|
||||||
|
public MediaPlaylistChunk(int variantIndex, DataSource dataSource, DataSpec dataSpec,
|
||||||
|
Uri playlistBaseUri) {
|
||||||
|
super(dataSource, dataSpec, bitArray);
|
||||||
|
this.variantIndex = variantIndex;
|
||||||
|
this.playlistBaseUri = playlistBaseUri;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void consumeStream(NonBlockingInputStream stream) throws IOException {
|
protected void consume(BitArray data) throws IOException {
|
||||||
byte[] data = new byte[(int) stream.getAvailableByteCount()];
|
HlsMediaPlaylist mediaPlaylist = mediaPlaylistParser.parse(
|
||||||
stream.read(data, 0, data.length);
|
new ByteArrayInputStream(data.getData(), 0, data.bytesLeft()), null, null,
|
||||||
lastMediaPlaylistLoadTimeMs = SystemClock.elapsedRealtime();
|
playlistBaseUri);
|
||||||
mediaPlaylist = mediaPlaylistParser.parse(
|
mediaPlaylists[variantIndex] = mediaPlaylist;
|
||||||
new ByteArrayInputStream(data), null, null, baseUri);
|
lastMediaPlaylistLoadTimesMs[variantIndex] = SystemClock.elapsedRealtime();
|
||||||
mediaPlaylistWasLive |= mediaPlaylist.live;
|
live |= mediaPlaylist.live;
|
||||||
|
durationUs = mediaPlaylist.durationUs;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private class EncryptionKeyChunk extends HlsChunk {
|
private class EncryptionKeyChunk extends BitArrayChunk {
|
||||||
|
|
||||||
private final String iv;
|
private final String iv;
|
||||||
|
|
||||||
public EncryptionKeyChunk(DataSource dataSource, DataSpec dataSpec, int trigger, String iv) {
|
public EncryptionKeyChunk(DataSource dataSource, DataSpec dataSpec, String iv) {
|
||||||
super(dataSource, dataSpec, trigger);
|
super(dataSource, dataSpec, bitArray);
|
||||||
if (iv.toLowerCase(Locale.getDefault()).startsWith("0x")) {
|
if (iv.toLowerCase(Locale.getDefault()).startsWith("0x")) {
|
||||||
this.iv = iv.substring(2);
|
this.iv = iv.substring(2);
|
||||||
} else {
|
} else {
|
||||||
@ -255,9 +388,9 @@ public class HlsChunkSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void consumeStream(NonBlockingInputStream stream) throws IOException {
|
protected void consume(BitArray data) throws IOException {
|
||||||
byte[] keyData = new byte[(int) stream.getAvailableByteCount()];
|
byte[] secretKey = new byte[data.bytesLeft()];
|
||||||
stream.read(keyData, 0, keyData.length);
|
data.readBytes(secretKey, 0, secretKey.length);
|
||||||
|
|
||||||
int ivParsed = Integer.parseInt(iv, 16);
|
int ivParsed = Integer.parseInt(iv, 16);
|
||||||
String iv = String.format("%032X", ivParsed);
|
String iv = String.format("%032X", ivParsed);
|
||||||
@ -267,7 +400,7 @@ public class HlsChunkSource {
|
|||||||
System.arraycopy(ivData, 0, ivDataWithPadding, ivDataWithPadding.length - ivData.length,
|
System.arraycopy(ivData, 0, ivDataWithPadding, ivDataWithPadding.length - ivData.length,
|
||||||
ivData.length);
|
ivData.length);
|
||||||
|
|
||||||
encryptedDataSource = new Aes128DataSource(keyData, ivDataWithPadding, upstreamDataSource);
|
encryptedDataSource = new Aes128DataSource(secretKey, ivDataWithPadding, upstreamDataSource);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -24,25 +24,6 @@ import java.util.List;
|
|||||||
*/
|
*/
|
||||||
public final class HlsMasterPlaylist {
|
public final class HlsMasterPlaylist {
|
||||||
|
|
||||||
/**
|
|
||||||
* Variant stream reference.
|
|
||||||
*/
|
|
||||||
public static final class Variant {
|
|
||||||
public final int bandwidth;
|
|
||||||
public final String url;
|
|
||||||
public final String[] codecs;
|
|
||||||
public final int width;
|
|
||||||
public final int height;
|
|
||||||
|
|
||||||
public Variant(String url, int bandwidth, String[] codecs, int width, int height) {
|
|
||||||
this.bandwidth = bandwidth;
|
|
||||||
this.url = url;
|
|
||||||
this.codecs = codecs;
|
|
||||||
this.width = width;
|
|
||||||
this.height = height;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public final Uri baseUri;
|
public final Uri baseUri;
|
||||||
public final List<Variant> variants;
|
public final List<Variant> variants;
|
||||||
|
|
||||||
|
@ -15,7 +15,6 @@
|
|||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer.hls;
|
package com.google.android.exoplayer.hls;
|
||||||
|
|
||||||
import com.google.android.exoplayer.hls.HlsMasterPlaylist.Variant;
|
|
||||||
import com.google.android.exoplayer.util.ManifestParser;
|
import com.google.android.exoplayer.util.ManifestParser;
|
||||||
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
@ -61,6 +60,7 @@ public final class HlsMasterPlaylistParser implements ManifestParser<HlsMasterPl
|
|||||||
String[] codecs = null;
|
String[] codecs = null;
|
||||||
int width = -1;
|
int width = -1;
|
||||||
int height = -1;
|
int height = -1;
|
||||||
|
int variantIndex = 0;
|
||||||
|
|
||||||
String line;
|
String line;
|
||||||
while ((line = reader.readLine()) != null) {
|
while ((line = reader.readLine()) != null) {
|
||||||
@ -70,15 +70,14 @@ public final class HlsMasterPlaylistParser implements ManifestParser<HlsMasterPl
|
|||||||
}
|
}
|
||||||
if (line.startsWith(STREAM_INF_TAG)) {
|
if (line.startsWith(STREAM_INF_TAG)) {
|
||||||
bandwidth = HlsParserUtil.parseIntAttr(line, BANDWIDTH_ATTR_REGEX, BANDWIDTH_ATTR);
|
bandwidth = HlsParserUtil.parseIntAttr(line, BANDWIDTH_ATTR_REGEX, BANDWIDTH_ATTR);
|
||||||
String codecsString = HlsParserUtil.parseOptionalStringAttr(line, CODECS_ATTR_REGEX,
|
String codecsString = HlsParserUtil.parseOptionalStringAttr(line, CODECS_ATTR_REGEX);
|
||||||
CODECS_ATTR);
|
|
||||||
if (codecsString != null) {
|
if (codecsString != null) {
|
||||||
codecs = codecsString.split("(\\s*,\\s*)|(\\s*$)");
|
codecs = codecsString.split("(\\s*,\\s*)|(\\s*$)");
|
||||||
} else {
|
} else {
|
||||||
codecs = null;
|
codecs = null;
|
||||||
}
|
}
|
||||||
String resolutionString = HlsParserUtil.parseOptionalStringAttr(line, RESOLUTION_ATTR_REGEX,
|
String resolutionString = HlsParserUtil.parseOptionalStringAttr(line,
|
||||||
RESOLUTION_ATTR);
|
RESOLUTION_ATTR_REGEX);
|
||||||
if (resolutionString != null) {
|
if (resolutionString != null) {
|
||||||
String[] widthAndHeight = resolutionString.split("x");
|
String[] widthAndHeight = resolutionString.split("x");
|
||||||
width = Integer.parseInt(widthAndHeight[0]);
|
width = Integer.parseInt(widthAndHeight[0]);
|
||||||
@ -88,7 +87,7 @@ public final class HlsMasterPlaylistParser implements ManifestParser<HlsMasterPl
|
|||||||
height = -1;
|
height = -1;
|
||||||
}
|
}
|
||||||
} else if (!line.startsWith("#")) {
|
} else if (!line.startsWith("#")) {
|
||||||
variants.add(new Variant(line, bandwidth, codecs, width, height));
|
variants.add(new Variant(variantIndex++, line, bandwidth, codecs, width, height));
|
||||||
bandwidth = 0;
|
bandwidth = 0;
|
||||||
codecs = null;
|
codecs = null;
|
||||||
width = -1;
|
width = -1;
|
||||||
|
@ -114,8 +114,7 @@ public final class HlsMediaPlaylistParser implements ManifestParser<HlsMediaPlay
|
|||||||
} else {
|
} else {
|
||||||
segmentEncryptionKeyUri = HlsParserUtil.parseStringAttr(line, URI_ATTR_REGEX,
|
segmentEncryptionKeyUri = HlsParserUtil.parseStringAttr(line, URI_ATTR_REGEX,
|
||||||
URI_ATTR);
|
URI_ATTR);
|
||||||
segmentEncryptionIV = HlsParserUtil.parseOptionalStringAttr(line, IV_ATTR_REGEX,
|
segmentEncryptionIV = HlsParserUtil.parseOptionalStringAttr(line, IV_ATTR_REGEX);
|
||||||
IV_ATTR);
|
|
||||||
if (segmentEncryptionIV == null) {
|
if (segmentEncryptionIV == null) {
|
||||||
segmentEncryptionIV = Integer.toHexString(segmentMediaSequence);
|
segmentEncryptionIV = Integer.toHexString(segmentMediaSequence);
|
||||||
}
|
}
|
||||||
|
@ -36,7 +36,7 @@ import java.util.regex.Pattern;
|
|||||||
throw new ParserException(String.format("Couldn't match %s tag in %s", tag, line));
|
throw new ParserException(String.format("Couldn't match %s tag in %s", tag, line));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String parseOptionalStringAttr(String line, Pattern pattern, String tag) {
|
public static String parseOptionalStringAttr(String line, Pattern pattern) {
|
||||||
Matcher matcher = pattern.matcher(line);
|
Matcher matcher = pattern.matcher(line);
|
||||||
if (matcher.find() && matcher.groupCount() == 1) {
|
if (matcher.find() && matcher.groupCount() == 1) {
|
||||||
return matcher.group(1);
|
return matcher.group(1);
|
||||||
|
@ -15,47 +15,31 @@
|
|||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer.hls;
|
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.MediaFormat;
|
||||||
import com.google.android.exoplayer.MediaFormatHolder;
|
import com.google.android.exoplayer.MediaFormatHolder;
|
||||||
import com.google.android.exoplayer.SampleHolder;
|
import com.google.android.exoplayer.SampleHolder;
|
||||||
import com.google.android.exoplayer.SampleSource;
|
import com.google.android.exoplayer.SampleSource;
|
||||||
import com.google.android.exoplayer.TrackInfo;
|
import com.google.android.exoplayer.TrackInfo;
|
||||||
import com.google.android.exoplayer.TrackRenderer;
|
import com.google.android.exoplayer.TrackRenderer;
|
||||||
import com.google.android.exoplayer.parser.ts.TsExtractor;
|
|
||||||
import com.google.android.exoplayer.upstream.Loader;
|
import com.google.android.exoplayer.upstream.Loader;
|
||||||
import com.google.android.exoplayer.upstream.Loader.Loadable;
|
import com.google.android.exoplayer.upstream.Loader.Loadable;
|
||||||
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
|
|
||||||
import com.google.android.exoplayer.util.Assertions;
|
import com.google.android.exoplayer.util.Assertions;
|
||||||
|
|
||||||
import android.os.SystemClock;
|
import android.os.SystemClock;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.Iterator;
|
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A {@link SampleSource} for HLS streams.
|
* 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 {
|
public class HlsSampleSource implements SampleSource, Loader.Callback {
|
||||||
|
|
||||||
private static final long MAX_SAMPLE_INTERLEAVING_OFFSET_US = 5000000;
|
private static final long BUFFER_DURATION_US = 20000000;
|
||||||
private static final int NO_RESET_PENDING = -1;
|
private static final int NO_RESET_PENDING = -1;
|
||||||
|
|
||||||
private final TsExtractor.SamplePool samplePool;
|
|
||||||
private final LoadControl loadControl;
|
|
||||||
private final HlsChunkSource chunkSource;
|
private final HlsChunkSource chunkSource;
|
||||||
private final HlsChunkOperationHolder currentLoadableHolder;
|
|
||||||
private final LinkedList<TsExtractor> extractors;
|
private final LinkedList<TsExtractor> extractors;
|
||||||
private final LinkedList<TsChunk> mediaChunks;
|
|
||||||
private final List<TsChunk> readOnlyHlsChunks;
|
|
||||||
private final int bufferSizeContribution;
|
|
||||||
private final boolean frameAccurateSeeking;
|
private final boolean frameAccurateSeeking;
|
||||||
|
|
||||||
private int remainingReleaseCount;
|
private int remainingReleaseCount;
|
||||||
@ -70,7 +54,10 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
|
|||||||
private long downstreamPositionUs;
|
private long downstreamPositionUs;
|
||||||
private long lastSeekPositionUs;
|
private long lastSeekPositionUs;
|
||||||
private long pendingResetPositionUs;
|
private long pendingResetPositionUs;
|
||||||
private long lastPerformedBufferOperation;
|
|
||||||
|
private TsChunk previousTsLoadable;
|
||||||
|
private HlsChunk currentLoadable;
|
||||||
|
private boolean loadingFinished;
|
||||||
|
|
||||||
private Loader loader;
|
private Loader loader;
|
||||||
private IOException currentLoadableException;
|
private IOException currentLoadableException;
|
||||||
@ -78,18 +65,12 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
|
|||||||
private int currentLoadableExceptionCount;
|
private int currentLoadableExceptionCount;
|
||||||
private long currentLoadableExceptionTimestamp;
|
private long currentLoadableExceptionTimestamp;
|
||||||
|
|
||||||
public HlsSampleSource(HlsChunkSource chunkSource, LoadControl loadControl,
|
public HlsSampleSource(HlsChunkSource chunkSource,
|
||||||
int bufferSizeContribution, boolean frameAccurateSeeking, int downstreamRendererCount) {
|
boolean frameAccurateSeeking, int downstreamRendererCount) {
|
||||||
this.chunkSource = chunkSource;
|
this.chunkSource = chunkSource;
|
||||||
this.loadControl = loadControl;
|
|
||||||
this.bufferSizeContribution = bufferSizeContribution;
|
|
||||||
this.frameAccurateSeeking = frameAccurateSeeking;
|
this.frameAccurateSeeking = frameAccurateSeeking;
|
||||||
this.remainingReleaseCount = downstreamRendererCount;
|
this.remainingReleaseCount = downstreamRendererCount;
|
||||||
samplePool = new TsExtractor.SamplePool();
|
|
||||||
extractors = new LinkedList<TsExtractor>();
|
extractors = new LinkedList<TsExtractor>();
|
||||||
currentLoadableHolder = new HlsChunkOperationHolder();
|
|
||||||
mediaChunks = new LinkedList<TsChunk>();
|
|
||||||
readOnlyHlsChunks = Collections.unmodifiableList(mediaChunks);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -99,13 +80,12 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
|
|||||||
}
|
}
|
||||||
if (loader == null) {
|
if (loader == null) {
|
||||||
loader = new Loader("Loader:HLS");
|
loader = new Loader("Loader:HLS");
|
||||||
loadControl.register(this, bufferSizeContribution);
|
|
||||||
}
|
}
|
||||||
continueBufferingInternal();
|
continueBufferingInternal();
|
||||||
if (extractors.isEmpty()) {
|
if (extractors.isEmpty()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
TsExtractor extractor = extractors.get(0);
|
TsExtractor extractor = extractors.getFirst();
|
||||||
if (extractor.isPrepared()) {
|
if (extractor.isPrepared()) {
|
||||||
trackCount = extractor.getTrackCount();
|
trackCount = extractor.getTrackCount();
|
||||||
trackEnabledStates = new boolean[trackCount];
|
trackEnabledStates = new boolean[trackCount];
|
||||||
@ -156,8 +136,9 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
|
|||||||
if (loader.isLoading()) {
|
if (loader.isLoading()) {
|
||||||
loader.cancelLoading();
|
loader.cancelLoading();
|
||||||
} else {
|
} else {
|
||||||
clearHlsChunks();
|
discardExtractors();
|
||||||
clearCurrentLoadable();
|
clearCurrentLoadable();
|
||||||
|
previousTsLoadable = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -171,50 +152,15 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private boolean continueBufferingInternal() throws IOException {
|
private boolean continueBufferingInternal() throws IOException {
|
||||||
updateLoadControl();
|
maybeStartLoading();
|
||||||
if (isPendingReset()) {
|
if (isPendingReset() || extractors.isEmpty()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
boolean haveSamples = extractors.getFirst().hasSamples();
|
||||||
TsChunk mediaChunk = mediaChunks.getFirst();
|
if (!haveSamples && currentLoadableException != null) {
|
||||||
int currentVariant = mediaChunk.variantIndex;
|
|
||||||
|
|
||||||
TsExtractor extractor;
|
|
||||||
if (extractors.isEmpty()) {
|
|
||||||
extractor = new TsExtractor(mediaChunk.startTimeUs, samplePool);
|
|
||||||
extractors.addLast(extractor);
|
|
||||||
if (mediaChunk.discardFromFirstKeyframes) {
|
|
||||||
extractor.discardFromNextKeyframes();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
extractor = extractors.getLast();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mediaChunk.isReadFinished() && mediaChunks.size() > 1) {
|
|
||||||
discardDownstreamHlsChunk();
|
|
||||||
mediaChunk = mediaChunks.getFirst();
|
|
||||||
if (mediaChunk.discontinuity || mediaChunk.variantIndex != currentVariant) {
|
|
||||||
extractor = new TsExtractor(mediaChunk.startTimeUs, samplePool);
|
|
||||||
extractors.addLast(extractor);
|
|
||||||
}
|
|
||||||
if (mediaChunk.discardFromFirstKeyframes) {
|
|
||||||
extractor.discardFromNextKeyframes();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allow the extractor to consume from the current chunk.
|
|
||||||
NonBlockingInputStream inputStream = mediaChunk.getNonBlockingInputStream();
|
|
||||||
boolean haveSufficientSamples = extractor.consumeUntil(inputStream,
|
|
||||||
downstreamPositionUs + MAX_SAMPLE_INTERLEAVING_OFFSET_US);
|
|
||||||
if (!haveSufficientSamples) {
|
|
||||||
// If we can't read any more, then we always say we have sufficient samples.
|
|
||||||
haveSufficientSamples = mediaChunk.isLastChunk() && mediaChunk.isReadFinished();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!haveSufficientSamples && currentLoadableException != null) {
|
|
||||||
throw currentLoadableException;
|
throw currentLoadableException;
|
||||||
}
|
}
|
||||||
return haveSufficientSamples;
|
return haveSamples;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -228,11 +174,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
|
|||||||
return DISCONTINUITY_READ;
|
return DISCONTINUITY_READ;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (onlyReadDiscontinuity || isPendingReset()) {
|
if (onlyReadDiscontinuity || isPendingReset() || extractors.isEmpty()) {
|
||||||
return NOTHING_READ;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (extractors.isEmpty()) {
|
|
||||||
return NOTHING_READ;
|
return NOTHING_READ;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -266,12 +208,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
|
|||||||
return SAMPLE_READ;
|
return SAMPLE_READ;
|
||||||
}
|
}
|
||||||
|
|
||||||
TsChunk mediaChunk = mediaChunks.getFirst();
|
return loadingFinished ? END_OF_STREAM : NOTHING_READ;
|
||||||
if (mediaChunk.isLastChunk() && mediaChunk.isReadFinished()) {
|
|
||||||
return END_OF_STREAM;
|
|
||||||
}
|
|
||||||
|
|
||||||
return NOTHING_READ;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -283,32 +220,10 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
|
|||||||
if (pendingResetPositionUs == positionUs) {
|
if (pendingResetPositionUs == positionUs) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int i = 0; i < pendingDiscontinuities.length; i++) {
|
for (int i = 0; i < pendingDiscontinuities.length; i++) {
|
||||||
pendingDiscontinuities[i] = true;
|
pendingDiscontinuities[i] = true;
|
||||||
}
|
}
|
||||||
TsChunk mediaChunk = getHlsChunk(positionUs);
|
restartFrom(positionUs);
|
||||||
if (mediaChunk == null) {
|
|
||||||
restartFrom(positionUs);
|
|
||||||
} else {
|
|
||||||
discardExtractors();
|
|
||||||
discardDownstreamHlsChunks(mediaChunk);
|
|
||||||
mediaChunk.resetReadPosition();
|
|
||||||
updateLoadControl();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private TsChunk getHlsChunk(long positionUs) {
|
|
||||||
Iterator<TsChunk> mediaChunkIterator = mediaChunks.iterator();
|
|
||||||
while (mediaChunkIterator.hasNext()) {
|
|
||||||
TsChunk mediaChunk = mediaChunkIterator.next();
|
|
||||||
if (positionUs < mediaChunk.startTimeUs) {
|
|
||||||
return null;
|
|
||||||
} else if (mediaChunk.isLastChunk() || positionUs < mediaChunk.endTimeUs) {
|
|
||||||
return mediaChunk;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -317,22 +232,10 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
|
|||||||
Assertions.checkState(enabledTrackCount > 0);
|
Assertions.checkState(enabledTrackCount > 0);
|
||||||
if (isPendingReset()) {
|
if (isPendingReset()) {
|
||||||
return pendingResetPositionUs;
|
return pendingResetPositionUs;
|
||||||
}
|
} else if (loadingFinished) {
|
||||||
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;
|
return TrackRenderer.END_OF_TRACK_US;
|
||||||
} else {
|
} else {
|
||||||
return mediaChunk.endTimeUs;
|
return extractors.getLast().getLargestSampleTimestamp();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -340,7 +243,6 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
|
|||||||
public void release() {
|
public void release() {
|
||||||
Assertions.checkState(remainingReleaseCount > 0);
|
Assertions.checkState(remainingReleaseCount > 0);
|
||||||
if (--remainingReleaseCount == 0 && loader != null) {
|
if (--remainingReleaseCount == 0 && loader != null) {
|
||||||
loadControl.unregister(this);
|
|
||||||
loader.release();
|
loader.release();
|
||||||
loader = null;
|
loader = null;
|
||||||
}
|
}
|
||||||
@ -348,7 +250,6 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onLoadCompleted(Loadable loadable) {
|
public void onLoadCompleted(Loadable loadable) {
|
||||||
HlsChunk currentLoadable = currentLoadableHolder.chunk;
|
|
||||||
try {
|
try {
|
||||||
currentLoadable.consume();
|
currentLoadable.consume();
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
@ -357,28 +258,24 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
|
|||||||
currentLoadableExceptionTimestamp = SystemClock.elapsedRealtime();
|
currentLoadableExceptionTimestamp = SystemClock.elapsedRealtime();
|
||||||
currentLoadableExceptionFatal = true;
|
currentLoadableExceptionFatal = true;
|
||||||
} finally {
|
} finally {
|
||||||
if (!isTsChunk(currentLoadable)) {
|
if (isTsChunk(currentLoadable)) {
|
||||||
currentLoadable.release();
|
TsChunk tsChunk = (TsChunk) loadable;
|
||||||
|
loadingFinished = tsChunk.isLastChunk();
|
||||||
}
|
}
|
||||||
if (!currentLoadableExceptionFatal) {
|
if (!currentLoadableExceptionFatal) {
|
||||||
clearCurrentLoadable();
|
clearCurrentLoadable();
|
||||||
}
|
}
|
||||||
updateLoadControl();
|
maybeStartLoading();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onLoadCanceled(Loadable loadable) {
|
public void onLoadCanceled(Loadable loadable) {
|
||||||
HlsChunk currentLoadable = currentLoadableHolder.chunk;
|
|
||||||
if (!isTsChunk(currentLoadable)) {
|
|
||||||
currentLoadable.release();
|
|
||||||
}
|
|
||||||
clearCurrentLoadable();
|
clearCurrentLoadable();
|
||||||
if (enabledTrackCount > 0) {
|
if (enabledTrackCount > 0) {
|
||||||
restartFrom(pendingResetPositionUs);
|
restartFrom(pendingResetPositionUs);
|
||||||
} else {
|
} else {
|
||||||
clearHlsChunks();
|
previousTsLoadable = null;
|
||||||
loadControl.trimAllocator();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -387,142 +284,65 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
|
|||||||
currentLoadableException = e;
|
currentLoadableException = e;
|
||||||
currentLoadableExceptionCount++;
|
currentLoadableExceptionCount++;
|
||||||
currentLoadableExceptionTimestamp = SystemClock.elapsedRealtime();
|
currentLoadableExceptionTimestamp = SystemClock.elapsedRealtime();
|
||||||
updateLoadControl();
|
maybeStartLoading();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void restartFrom(long positionUs) {
|
private void restartFrom(long positionUs) {
|
||||||
pendingResetPositionUs = positionUs;
|
pendingResetPositionUs = positionUs;
|
||||||
|
previousTsLoadable = null;
|
||||||
|
loadingFinished = false;
|
||||||
|
discardExtractors();
|
||||||
if (loader.isLoading()) {
|
if (loader.isLoading()) {
|
||||||
loader.cancelLoading();
|
loader.cancelLoading();
|
||||||
} else {
|
} else {
|
||||||
clearHlsChunks();
|
|
||||||
clearCurrentLoadable();
|
|
||||||
updateLoadControl();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void clearHlsChunks() {
|
|
||||||
discardDownstreamHlsChunks(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void clearCurrentLoadable() {
|
|
||||||
currentLoadableHolder.chunk = null;
|
|
||||||
currentLoadableException = null;
|
|
||||||
currentLoadableExceptionCount = 0;
|
|
||||||
currentLoadableExceptionFatal = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateLoadControl() {
|
|
||||||
if (currentLoadableExceptionFatal) {
|
|
||||||
// We've failed, but we still need to update the control with our current state.
|
|
||||||
loadControl.update(this, downstreamPositionUs, -1, false, true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
long loadPositionUs;
|
|
||||||
if (isPendingReset()) {
|
|
||||||
loadPositionUs = pendingResetPositionUs;
|
|
||||||
} else {
|
|
||||||
TsChunk lastHlsChunk = mediaChunks.getLast();
|
|
||||||
loadPositionUs = lastHlsChunk.nextChunkIndex == -1 ? -1 : lastHlsChunk.endTimeUs;
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean isBackedOff = currentLoadableException != null;
|
|
||||||
boolean nextLoader = loadControl.update(this, downstreamPositionUs, loadPositionUs,
|
|
||||||
isBackedOff || loader.isLoading(), false);
|
|
||||||
|
|
||||||
long now = SystemClock.elapsedRealtime();
|
|
||||||
|
|
||||||
if (isBackedOff) {
|
|
||||||
long elapsedMillis = now - currentLoadableExceptionTimestamp;
|
|
||||||
if (elapsedMillis >= getRetryDelayMillis(currentLoadableExceptionCount)) {
|
|
||||||
resumeFromBackOff();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!loader.isLoading()) {
|
|
||||||
if (currentLoadableHolder.chunk == null || now - lastPerformedBufferOperation > 1000) {
|
|
||||||
lastPerformedBufferOperation = now;
|
|
||||||
currentLoadableHolder.queueSize = readOnlyHlsChunks.size();
|
|
||||||
chunkSource.getChunkOperation(readOnlyHlsChunks, pendingResetPositionUs,
|
|
||||||
downstreamPositionUs, currentLoadableHolder);
|
|
||||||
discardUpstreamHlsChunks(currentLoadableHolder.queueSize);
|
|
||||||
}
|
|
||||||
if (nextLoader) {
|
|
||||||
maybeStartLoading();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resumes loading.
|
|
||||||
* <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, pendingResetPositionUs, downstreamPositionUs,
|
|
||||||
currentLoadableHolder);
|
|
||||||
discardUpstreamHlsChunks(currentLoadableHolder.queueSize);
|
|
||||||
if (currentLoadableHolder.chunk == backedOffChunk) {
|
|
||||||
// HlsChunk was unchanged. Resume loading.
|
|
||||||
loader.startLoading(backedOffChunk, this);
|
|
||||||
} else {
|
|
||||||
backedOffChunk.release();
|
|
||||||
maybeStartLoading();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (backedOffChunk == mediaChunks.getFirst()) {
|
|
||||||
// We're not able to clear the first media chunk, so we have no choice but to continue
|
|
||||||
// loading it.
|
|
||||||
loader.startLoading(backedOffChunk, this);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The current loadable is the last media chunk. Remove it before we invoke the chunk source,
|
|
||||||
// and add it back again afterwards.
|
|
||||||
TsChunk removedChunk = mediaChunks.removeLast();
|
|
||||||
Assertions.checkState(backedOffChunk == removedChunk);
|
|
||||||
currentLoadableHolder.queueSize = readOnlyHlsChunks.size();
|
|
||||||
chunkSource.getChunkOperation(readOnlyHlsChunks, pendingResetPositionUs, downstreamPositionUs,
|
|
||||||
currentLoadableHolder);
|
|
||||||
mediaChunks.add(removedChunk);
|
|
||||||
|
|
||||||
if (currentLoadableHolder.chunk == backedOffChunk) {
|
|
||||||
// HlsChunk was unchanged. Resume loading.
|
|
||||||
loader.startLoading(backedOffChunk, this);
|
|
||||||
} else {
|
|
||||||
// This call will remove and release at least one chunk from the end of mediaChunks. Since
|
|
||||||
// the current loadable is the last media chunk, it is guaranteed to be removed.
|
|
||||||
discardUpstreamHlsChunks(currentLoadableHolder.queueSize);
|
|
||||||
clearCurrentLoadable();
|
clearCurrentLoadable();
|
||||||
maybeStartLoading();
|
maybeStartLoading();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void clearCurrentLoadable() {
|
||||||
|
currentLoadable = null;
|
||||||
|
currentLoadableException = null;
|
||||||
|
currentLoadableExceptionCount = 0;
|
||||||
|
currentLoadableExceptionFatal = false;
|
||||||
|
}
|
||||||
|
|
||||||
private void maybeStartLoading() {
|
private void maybeStartLoading() {
|
||||||
HlsChunk currentLoadable = currentLoadableHolder.chunk;
|
if (currentLoadableExceptionFatal || loadingFinished) {
|
||||||
if (currentLoadable == null) {
|
|
||||||
// Nothing to load.
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
currentLoadable.init(loadControl.getAllocator());
|
|
||||||
|
boolean isBackedOff = currentLoadableException != null;
|
||||||
|
if (isBackedOff) {
|
||||||
|
long elapsedMillis = SystemClock.elapsedRealtime() - currentLoadableExceptionTimestamp;
|
||||||
|
if (elapsedMillis >= getRetryDelayMillis(currentLoadableExceptionCount)) {
|
||||||
|
currentLoadableException = null;
|
||||||
|
loader.startLoading(currentLoadable, this);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean bufferFull = !extractors.isEmpty() && (extractors.getLast().getLargestSampleTimestamp()
|
||||||
|
- downstreamPositionUs) >= BUFFER_DURATION_US;
|
||||||
|
if (loader.isLoading() || bufferFull) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
HlsChunk nextLoadable = chunkSource.getChunkOperation(previousTsLoadable,
|
||||||
|
pendingResetPositionUs, downstreamPositionUs);
|
||||||
|
if (nextLoadable == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentLoadable = nextLoadable;
|
||||||
if (isTsChunk(currentLoadable)) {
|
if (isTsChunk(currentLoadable)) {
|
||||||
TsChunk mediaChunk = (TsChunk) currentLoadable;
|
previousTsLoadable = (TsChunk) currentLoadable;
|
||||||
mediaChunks.add(mediaChunk);
|
|
||||||
if (isPendingReset()) {
|
if (isPendingReset()) {
|
||||||
discardExtractors();
|
|
||||||
pendingResetPositionUs = NO_RESET_PENDING;
|
pendingResetPositionUs = NO_RESET_PENDING;
|
||||||
}
|
}
|
||||||
|
if (extractors.isEmpty() || extractors.getLast() != previousTsLoadable.extractor) {
|
||||||
|
extractors.addLast(previousTsLoadable.extractor);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
loader.startLoading(currentLoadable, this);
|
loader.startLoading(currentLoadable, this);
|
||||||
}
|
}
|
||||||
@ -534,39 +354,6 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
|
|||||||
extractors.clear();
|
extractors.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Discards downstream media chunks until {@code untilChunk} if found. {@code untilChunk} is not
|
|
||||||
* itself discarded. Null can be passed to discard all media chunks.
|
|
||||||
*
|
|
||||||
* @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) {
|
private boolean isTsChunk(HlsChunk chunk) {
|
||||||
return chunk instanceof TsChunk;
|
return chunk instanceof TsChunk;
|
||||||
}
|
}
|
||||||
|
@ -15,9 +15,12 @@
|
|||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer.hls;
|
package com.google.android.exoplayer.hls;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer.C;
|
||||||
import com.google.android.exoplayer.upstream.DataSource;
|
import com.google.android.exoplayer.upstream.DataSource;
|
||||||
import com.google.android.exoplayer.upstream.DataSpec;
|
import com.google.android.exoplayer.upstream.DataSpec;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A MPEG2TS chunk.
|
* A MPEG2TS chunk.
|
||||||
*/
|
*/
|
||||||
@ -40,40 +43,87 @@ public final class TsChunk extends HlsChunk {
|
|||||||
*/
|
*/
|
||||||
public final int nextChunkIndex;
|
public final int nextChunkIndex;
|
||||||
/**
|
/**
|
||||||
* The encoding discontinuity indicator.
|
* True if this is the final chunk being loaded for the current variant, as we splice to another
|
||||||
|
* one. False otherwise.
|
||||||
*/
|
*/
|
||||||
public final boolean discontinuity;
|
public final boolean splicingOut;
|
||||||
/**
|
/**
|
||||||
* For each track, whether samples from the first keyframe (inclusive) should be discarded.
|
* The extractor into which this chunk is being consumed.
|
||||||
*/
|
*/
|
||||||
public final boolean discardFromFirstKeyframes;
|
public final TsExtractor extractor;
|
||||||
|
|
||||||
|
private volatile int loadPosition;
|
||||||
|
private volatile boolean loadFinished;
|
||||||
|
private volatile boolean loadCanceled;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param dataSource A {@link DataSource} for loading the data.
|
* @param dataSource A {@link DataSource} for loading the data.
|
||||||
* @param dataSpec Defines the data to be loaded.
|
* @param dataSpec Defines the data to be loaded.
|
||||||
* @param trigger The reason for this chunk being selected.
|
|
||||||
* @param variantIndex The index of the variant in the master playlist.
|
* @param variantIndex The index of the variant in the master playlist.
|
||||||
* @param startTimeUs The start time of the media contained by the chunk, in microseconds.
|
* @param 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 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 nextChunkIndex The index of the next chunk, or -1 if this is the last chunk.
|
||||||
* @param discontinuity The encoding discontinuity indicator.
|
* @param splicingOut True if this is the final chunk being loaded for the current variant, as we
|
||||||
* @param discardFromFirstKeyframes For each contained media stream, whether samples from the
|
* splice to another one. False otherwise.
|
||||||
* first keyframe (inclusive) should be discarded.
|
|
||||||
*/
|
*/
|
||||||
public TsChunk(DataSource dataSource, DataSpec dataSpec, int trigger, int variantIndex,
|
public TsChunk(DataSource dataSource, DataSpec dataSpec, TsExtractor tsExtractor,
|
||||||
long startTimeUs, long endTimeUs, int nextChunkIndex, boolean discontinuity,
|
int variantIndex, long startTimeUs, long endTimeUs, int nextChunkIndex, boolean splicingOut) {
|
||||||
boolean discardFromFirstKeyframes) {
|
super(dataSource, dataSpec);
|
||||||
super(dataSource, dataSpec, trigger);
|
this.extractor = tsExtractor;
|
||||||
this.variantIndex = variantIndex;
|
this.variantIndex = variantIndex;
|
||||||
this.startTimeUs = startTimeUs;
|
this.startTimeUs = startTimeUs;
|
||||||
this.endTimeUs = endTimeUs;
|
this.endTimeUs = endTimeUs;
|
||||||
this.nextChunkIndex = nextChunkIndex;
|
this.nextChunkIndex = nextChunkIndex;
|
||||||
this.discontinuity = discontinuity;
|
this.splicingOut = splicingOut;
|
||||||
this.discardFromFirstKeyframes = discardFromFirstKeyframes;
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void consume() throws IOException {
|
||||||
|
// Do nothing.
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isLastChunk() {
|
public boolean isLastChunk() {
|
||||||
return nextChunkIndex == -1;
|
return nextChunkIndex == -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isLoadFinished() {
|
||||||
|
return loadFinished;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loadable implementation
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void cancelLoad() {
|
||||||
|
loadCanceled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isLoadCanceled() {
|
||||||
|
return loadCanceled;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void load() throws IOException, InterruptedException {
|
||||||
|
DataSpec loadDataSpec;
|
||||||
|
if (loadPosition == 0) {
|
||||||
|
loadDataSpec = dataSpec;
|
||||||
|
} else {
|
||||||
|
long remainingLength = dataSpec.length != C.LENGTH_UNBOUNDED
|
||||||
|
? dataSpec.length - loadPosition : C.LENGTH_UNBOUNDED;
|
||||||
|
loadDataSpec = new DataSpec(dataSpec.uri, dataSpec.position + loadPosition,
|
||||||
|
remainingLength, dataSpec.key);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
dataSource.open(loadDataSpec);
|
||||||
|
int bytesRead = 0;
|
||||||
|
while (bytesRead != -1 && !loadCanceled) {
|
||||||
|
bytesRead = extractor.read(dataSource);
|
||||||
|
}
|
||||||
|
loadFinished = !loadCanceled;
|
||||||
|
} finally {
|
||||||
|
dataSource.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -13,25 +13,27 @@
|
|||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer.parser.ts;
|
package com.google.android.exoplayer.hls;
|
||||||
|
|
||||||
import com.google.android.exoplayer.MediaFormat;
|
import com.google.android.exoplayer.MediaFormat;
|
||||||
import com.google.android.exoplayer.SampleHolder;
|
import com.google.android.exoplayer.SampleHolder;
|
||||||
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
|
import com.google.android.exoplayer.upstream.DataSource;
|
||||||
import com.google.android.exoplayer.util.Assertions;
|
import com.google.android.exoplayer.util.Assertions;
|
||||||
|
import com.google.android.exoplayer.util.BitArray;
|
||||||
import com.google.android.exoplayer.util.CodecSpecificDataUtil;
|
import com.google.android.exoplayer.util.CodecSpecificDataUtil;
|
||||||
import com.google.android.exoplayer.util.MimeTypes;
|
import com.google.android.exoplayer.util.MimeTypes;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.media.MediaCodec;
|
|
||||||
import android.media.MediaExtractor;
|
import android.media.MediaExtractor;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.util.Pair;
|
import android.util.Pair;
|
||||||
import android.util.SparseArray;
|
import android.util.SparseArray;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Queue;
|
import java.util.Queue;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -49,15 +51,15 @@ public final class TsExtractor {
|
|||||||
private static final int TS_STREAM_TYPE_H264 = 0x1B;
|
private static final int TS_STREAM_TYPE_H264 = 0x1B;
|
||||||
private static final int TS_STREAM_TYPE_ID3 = 0x15;
|
private static final int TS_STREAM_TYPE_ID3 = 0x15;
|
||||||
|
|
||||||
private final BitsArray tsPacketBuffer;
|
private final BitArray tsPacketBuffer;
|
||||||
private final SparseArray<PesPayloadReader> pesPayloadReaders; // Indexed by streamType
|
private final SparseArray<PesPayloadReader> pesPayloadReaders; // Indexed by streamType
|
||||||
private final SparseArray<TsPayloadReader> tsPayloadReaders; // Indexed by pid
|
private final SparseArray<TsPayloadReader> tsPayloadReaders; // Indexed by pid
|
||||||
private final SamplePool samplePool;
|
private final SamplePool samplePool;
|
||||||
|
/* package */ final long firstSampleTimestamp;
|
||||||
|
|
||||||
private boolean prepared;
|
private boolean prepared;
|
||||||
|
|
||||||
/* package */ boolean pendingFirstSampleTimestampAdjustment;
|
/* package */ boolean pendingFirstSampleTimestampAdjustment;
|
||||||
/* package */ long firstSampleTimestamp;
|
|
||||||
/* package */ long sampleTimestampOffsetUs;
|
/* package */ long sampleTimestampOffsetUs;
|
||||||
/* package */ long largestParsedTimestampUs;
|
/* package */ long largestParsedTimestampUs;
|
||||||
/* package */ boolean discardFromNextKeyframes;
|
/* package */ boolean discardFromNextKeyframes;
|
||||||
@ -66,7 +68,7 @@ public final class TsExtractor {
|
|||||||
this.firstSampleTimestamp = firstSampleTimestamp;
|
this.firstSampleTimestamp = firstSampleTimestamp;
|
||||||
this.samplePool = samplePool;
|
this.samplePool = samplePool;
|
||||||
pendingFirstSampleTimestampAdjustment = true;
|
pendingFirstSampleTimestampAdjustment = true;
|
||||||
tsPacketBuffer = new BitsArray();
|
tsPacketBuffer = new BitArray();
|
||||||
pesPayloadReaders = new SparseArray<PesPayloadReader>();
|
pesPayloadReaders = new SparseArray<PesPayloadReader>();
|
||||||
tsPayloadReaders = new SparseArray<TsPayloadReader>();
|
tsPayloadReaders = new SparseArray<TsPayloadReader>();
|
||||||
tsPayloadReaders.put(TS_PAT_PID, new PatReader());
|
tsPayloadReaders.put(TS_PAT_PID, new PatReader());
|
||||||
@ -117,31 +119,19 @@ public final class TsExtractor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* For each track, whether to discard samples from the next keyframe (inclusive).
|
* For each track, discards samples from the next keyframe (inclusive).
|
||||||
*/
|
*/
|
||||||
public void discardFromNextKeyframes() {
|
public void discardFromNextKeyframes() {
|
||||||
discardFromNextKeyframes = true;
|
discardFromNextKeyframes = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Consumes data from a {@link NonBlockingInputStream}.
|
* Gets the largest timestamp of any sample parsed by the extractor.
|
||||||
* <p>
|
|
||||||
* The read terminates if the end of the input stream is reached, if insufficient data is
|
|
||||||
* available to read a sample, or if the extractor has consumed up to the specified target
|
|
||||||
* timestamp.
|
|
||||||
*
|
*
|
||||||
* @param inputStream The input stream from which data should be read.
|
* @return The largest timestamp, or {@link Long#MIN_VALUE} if no samples have been parsed.
|
||||||
* @param targetTimestampUs A target timestamp to consume up to.
|
|
||||||
* @return True if the target timestamp was reached. False otherwise.
|
|
||||||
*/
|
*/
|
||||||
public boolean consumeUntil(NonBlockingInputStream inputStream, long targetTimestampUs) {
|
public long getLargestSampleTimestamp() {
|
||||||
while (largestParsedTimestampUs < targetTimestampUs && readTSPacket(inputStream) != -1) {
|
return largestParsedTimestampUs;
|
||||||
// Carry on.
|
|
||||||
}
|
|
||||||
if (!prepared) {
|
|
||||||
prepared = checkPrepared();
|
|
||||||
}
|
|
||||||
return largestParsedTimestampUs >= targetTimestampUs;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -204,23 +194,29 @@ public final class TsExtractor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read a single TS packet.
|
* Reads up to a single TS packet.
|
||||||
|
*
|
||||||
|
* @param dataSource The {@link DataSource} from which to read.
|
||||||
|
* @throws IOException If an error occurred reading from the source.
|
||||||
|
* @return The number of bytes read from the source.
|
||||||
*/
|
*/
|
||||||
private int readTSPacket(NonBlockingInputStream inputStream) {
|
public int read(DataSource dataSource) throws IOException {
|
||||||
// Read entire single TS packet.
|
int read = tsPacketBuffer.append(dataSource, TS_PACKET_SIZE - tsPacketBuffer.bytesLeft());
|
||||||
if (inputStream.getAvailableByteCount() < TS_PACKET_SIZE) {
|
if (read == -1) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
tsPacketBuffer.reset();
|
if (tsPacketBuffer.bytesLeft() != TS_PACKET_SIZE) {
|
||||||
tsPacketBuffer.append(inputStream, TS_PACKET_SIZE);
|
return read;
|
||||||
|
}
|
||||||
|
|
||||||
// Parse TS header.
|
// Parse TS header.
|
||||||
// Check sync byte.
|
// Check sync byte.
|
||||||
int syncByte = tsPacketBuffer.readUnsignedByte();
|
int syncByte = tsPacketBuffer.readUnsignedByte();
|
||||||
if (syncByte != TS_SYNC_BYTE) {
|
if (syncByte != TS_SYNC_BYTE) {
|
||||||
return 0;
|
return read;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip transportErrorIndicator.
|
// Skip transportErrorIndicator.
|
||||||
tsPacketBuffer.skipBits(1);
|
tsPacketBuffer.skipBits(1);
|
||||||
boolean payloadUnitStartIndicator = tsPacketBuffer.readBit();
|
boolean payloadUnitStartIndicator = tsPacketBuffer.readBit();
|
||||||
@ -243,12 +239,17 @@ public final class TsExtractor {
|
|||||||
// Read Payload.
|
// Read Payload.
|
||||||
if (payloadExists) {
|
if (payloadExists) {
|
||||||
TsPayloadReader payloadReader = tsPayloadReaders.get(pid);
|
TsPayloadReader payloadReader = tsPayloadReaders.get(pid);
|
||||||
if (payloadReader == null) {
|
if (payloadReader != null) {
|
||||||
return 0;
|
payloadReader.read(tsPacketBuffer, payloadUnitStartIndicator);
|
||||||
}
|
}
|
||||||
payloadReader.read(tsPacketBuffer, payloadUnitStartIndicator);
|
|
||||||
}
|
}
|
||||||
return 0;
|
|
||||||
|
if (!prepared) {
|
||||||
|
prepared = checkPrepared();
|
||||||
|
}
|
||||||
|
|
||||||
|
tsPacketBuffer.reset();
|
||||||
|
return read;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void convert(Sample in, SampleHolder out) {
|
private void convert(Sample in, SampleHolder out) {
|
||||||
@ -268,7 +269,7 @@ public final class TsExtractor {
|
|||||||
*/
|
*/
|
||||||
private abstract static class TsPayloadReader {
|
private abstract static class TsPayloadReader {
|
||||||
|
|
||||||
public abstract void read(BitsArray tsBuffer, boolean payloadUnitStartIndicator);
|
public abstract void read(BitArray tsBuffer, boolean payloadUnitStartIndicator);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -278,7 +279,7 @@ public final class TsExtractor {
|
|||||||
private class PatReader extends TsPayloadReader {
|
private class PatReader extends TsPayloadReader {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void read(BitsArray tsBuffer, boolean payloadUnitStartIndicator) {
|
public void read(BitArray tsBuffer, boolean payloadUnitStartIndicator) {
|
||||||
// Skip pointer.
|
// Skip pointer.
|
||||||
if (payloadUnitStartIndicator) {
|
if (payloadUnitStartIndicator) {
|
||||||
int pointerField = tsBuffer.readBits(8);
|
int pointerField = tsBuffer.readBits(8);
|
||||||
@ -311,7 +312,7 @@ public final class TsExtractor {
|
|||||||
private class PmtReader extends TsPayloadReader {
|
private class PmtReader extends TsPayloadReader {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void read(BitsArray tsBuffer, boolean payloadUnitStartIndicator) {
|
public void read(BitArray tsBuffer, boolean payloadUnitStartIndicator) {
|
||||||
// Skip pointer.
|
// Skip pointer.
|
||||||
if (payloadUnitStartIndicator) {
|
if (payloadUnitStartIndicator) {
|
||||||
int pointerField = tsBuffer.readBits(8);
|
int pointerField = tsBuffer.readBits(8);
|
||||||
@ -323,10 +324,10 @@ public final class TsExtractor {
|
|||||||
int sectionLength = tsBuffer.readBits(12);
|
int sectionLength = tsBuffer.readBits(12);
|
||||||
// Skip the rest of the PMT header.
|
// Skip the rest of the PMT header.
|
||||||
tsBuffer.skipBits(60); // 16+2+5+1+8+8+3+13+4
|
tsBuffer.skipBits(60); // 16+2+5+1+8+8+3+13+4
|
||||||
int programInfoLength = tsBuffer.readBits(12);
|
|
||||||
|
|
||||||
// Read descriptors.
|
int programInfoLength = tsBuffer.readBits(12);
|
||||||
readDescriptors(tsBuffer, programInfoLength);
|
// Skip the descriptors.
|
||||||
|
tsBuffer.skipBytes(programInfoLength);
|
||||||
|
|
||||||
int entriesSize = sectionLength - 9 /* size of the rest of the fields before descriptors */
|
int entriesSize = sectionLength - 9 /* size of the rest of the fields before descriptors */
|
||||||
- programInfoLength - 4 /* CRC size */;
|
- programInfoLength - 4 /* CRC size */;
|
||||||
@ -335,9 +336,10 @@ public final class TsExtractor {
|
|||||||
tsBuffer.skipBits(3);
|
tsBuffer.skipBits(3);
|
||||||
int elementaryPid = tsBuffer.readBits(13);
|
int elementaryPid = tsBuffer.readBits(13);
|
||||||
tsBuffer.skipBits(4);
|
tsBuffer.skipBits(4);
|
||||||
int esInfoLength = tsBuffer.readBits(12);
|
|
||||||
|
|
||||||
readDescriptors(tsBuffer, esInfoLength);
|
int esInfoLength = tsBuffer.readBits(12);
|
||||||
|
// Skip the descriptors.
|
||||||
|
tsBuffer.skipBytes(esInfoLength);
|
||||||
entriesSize -= esInfoLength + 5;
|
entriesSize -= esInfoLength + 5;
|
||||||
|
|
||||||
if (pesPayloadReaders.get(streamType) != null) {
|
if (pesPayloadReaders.get(streamType) != null) {
|
||||||
@ -366,19 +368,6 @@ public final class TsExtractor {
|
|||||||
// Skip CRC_32.
|
// Skip CRC_32.
|
||||||
}
|
}
|
||||||
|
|
||||||
private void readDescriptors(BitsArray tsBuffer, int descriptorsSize) {
|
|
||||||
while (descriptorsSize > 0) {
|
|
||||||
// Skip tag.
|
|
||||||
tsBuffer.skipBits(8);
|
|
||||||
int descriptorsLength = tsBuffer.readBits(8);
|
|
||||||
if (descriptorsLength > 0) {
|
|
||||||
// Skip entire descriptor data.
|
|
||||||
tsBuffer.skipBytes(descriptorsLength);
|
|
||||||
}
|
|
||||||
descriptorsSize -= descriptorsSize + 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -387,7 +376,7 @@ public final class TsExtractor {
|
|||||||
private class PesReader extends TsPayloadReader {
|
private class PesReader extends TsPayloadReader {
|
||||||
|
|
||||||
// Reusable buffer for incomplete PES data.
|
// Reusable buffer for incomplete PES data.
|
||||||
private final BitsArray pesBuffer;
|
private final BitArray pesBuffer;
|
||||||
// Parses PES payload and extracts individual samples.
|
// Parses PES payload and extracts individual samples.
|
||||||
private final PesPayloadReader pesPayloadReader;
|
private final PesPayloadReader pesPayloadReader;
|
||||||
|
|
||||||
@ -396,11 +385,11 @@ public final class TsExtractor {
|
|||||||
public PesReader(PesPayloadReader pesPayloadReader) {
|
public PesReader(PesPayloadReader pesPayloadReader) {
|
||||||
this.pesPayloadReader = pesPayloadReader;
|
this.pesPayloadReader = pesPayloadReader;
|
||||||
this.packetLength = -1;
|
this.packetLength = -1;
|
||||||
pesBuffer = new BitsArray();
|
pesBuffer = new BitArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void read(BitsArray tsBuffer, boolean payloadUnitStartIndicator) {
|
public void read(BitArray tsBuffer, boolean payloadUnitStartIndicator) {
|
||||||
if (payloadUnitStartIndicator && !pesBuffer.isEmpty()) {
|
if (payloadUnitStartIndicator && !pesBuffer.isEmpty()) {
|
||||||
// We've encountered the start of the next packet, but haven't yet read the body. Read it.
|
// We've encountered the start of the next packet, but haven't yet read the body. Read it.
|
||||||
// Note that this should only happen if the packet length was unspecified.
|
// Note that this should only happen if the packet length was unspecified.
|
||||||
@ -484,7 +473,7 @@ public final class TsExtractor {
|
|||||||
*/
|
*/
|
||||||
private abstract class PesPayloadReader {
|
private abstract class PesPayloadReader {
|
||||||
|
|
||||||
public final Queue<Sample> sampleQueue;
|
public final LinkedList<Sample> sampleQueue;
|
||||||
|
|
||||||
private MediaFormat mediaFormat;
|
private MediaFormat mediaFormat;
|
||||||
private boolean foundFirstKeyframe;
|
private boolean foundFirstKeyframe;
|
||||||
@ -506,7 +495,7 @@ public final class TsExtractor {
|
|||||||
this.mediaFormat = mediaFormat;
|
this.mediaFormat = mediaFormat;
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract void read(BitsArray pesBuffer, int pesPayloadSize, long pesTimeUs);
|
public abstract void read(BitArray pesBuffer, int pesPayloadSize, long pesTimeUs);
|
||||||
|
|
||||||
public void clear() {
|
public void clear() {
|
||||||
while (!sampleQueue.isEmpty()) {
|
while (!sampleQueue.isEmpty()) {
|
||||||
@ -521,7 +510,7 @@ public final class TsExtractor {
|
|||||||
* @param sampleSize The size of the sample data.
|
* @param sampleSize The size of the sample data.
|
||||||
* @param sampleTimeUs The sample time stamp.
|
* @param sampleTimeUs The sample time stamp.
|
||||||
*/
|
*/
|
||||||
protected void addSample(BitsArray buffer, int sampleSize, long sampleTimeUs, int flags) {
|
protected void addSample(BitArray buffer, int sampleSize, long sampleTimeUs, int flags) {
|
||||||
Sample sample = samplePool.get();
|
Sample sample = samplePool.get();
|
||||||
addToSample(sample, buffer, sampleSize);
|
addToSample(sample, buffer, sampleSize);
|
||||||
sample.flags = flags;
|
sample.flags = flags;
|
||||||
@ -531,7 +520,7 @@ public final class TsExtractor {
|
|||||||
|
|
||||||
@SuppressLint("InlinedApi")
|
@SuppressLint("InlinedApi")
|
||||||
protected void addSample(Sample sample) {
|
protected void addSample(Sample sample) {
|
||||||
boolean isKeyframe = (sample.flags & MediaCodec.BUFFER_FLAG_SYNC_FRAME) != 0;
|
boolean isKeyframe = (sample.flags & MediaExtractor.SAMPLE_FLAG_SYNC) != 0;
|
||||||
if (isKeyframe) {
|
if (isKeyframe) {
|
||||||
if (!foundFirstKeyframe) {
|
if (!foundFirstKeyframe) {
|
||||||
foundFirstKeyframe = true;
|
foundFirstKeyframe = true;
|
||||||
@ -549,7 +538,7 @@ public final class TsExtractor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void addToSample(Sample sample, BitsArray buffer, int size) {
|
protected void addToSample(Sample sample, BitArray buffer, int size) {
|
||||||
if (sample.data.length - sample.size < size) {
|
if (sample.data.length - sample.size < size) {
|
||||||
sample.expand(size - sample.data.length + sample.size);
|
sample.expand(size - sample.data.length + sample.size);
|
||||||
}
|
}
|
||||||
@ -572,22 +561,24 @@ public final class TsExtractor {
|
|||||||
*/
|
*/
|
||||||
private class H264Reader extends PesPayloadReader {
|
private class H264Reader extends PesPayloadReader {
|
||||||
|
|
||||||
// IDR picture.
|
|
||||||
private static final int NAL_UNIT_TYPE_IDR = 5;
|
private static final int NAL_UNIT_TYPE_IDR = 5;
|
||||||
// Access unit delimiter.
|
|
||||||
private static final int NAL_UNIT_TYPE_AUD = 9;
|
private static final int NAL_UNIT_TYPE_AUD = 9;
|
||||||
|
private static final int NAL_UNIT_TYPE_SPS = 7;
|
||||||
|
|
||||||
// Used to store uncompleted sample data.
|
// Used to store uncompleted sample data.
|
||||||
private Sample currentSample;
|
private Sample currentSample;
|
||||||
|
|
||||||
public H264Reader() {
|
@Override
|
||||||
// TODO: Parse the format from the stream.
|
public void clear() {
|
||||||
setMediaFormat(MediaFormat.createVideoFormat(MimeTypes.VIDEO_H264, MediaFormat.NO_VALUE,
|
super.clear();
|
||||||
1920, 1080, null));
|
if (currentSample != null) {
|
||||||
|
samplePool.recycle(currentSample);
|
||||||
|
currentSample = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void read(BitsArray pesBuffer, int pesPayloadSize, long pesTimeUs) {
|
public void read(BitArray pesBuffer, int pesPayloadSize, long pesTimeUs) {
|
||||||
// Read leftover frame data from previous PES packet.
|
// Read leftover frame data from previous PES packet.
|
||||||
pesPayloadSize -= readOneH264Frame(pesBuffer, true);
|
pesPayloadSize -= readOneH264Frame(pesBuffer, true);
|
||||||
|
|
||||||
@ -597,6 +588,9 @@ public final class TsExtractor {
|
|||||||
|
|
||||||
// Single PES packet should contain only one new H.264 frame.
|
// Single PES packet should contain only one new H.264 frame.
|
||||||
if (currentSample != null) {
|
if (currentSample != null) {
|
||||||
|
if (!hasMediaFormat() && (currentSample.flags & MediaExtractor.SAMPLE_FLAG_SYNC) != 0) {
|
||||||
|
parseMediaFormat(currentSample);
|
||||||
|
}
|
||||||
addSample(currentSample);
|
addSample(currentSample);
|
||||||
}
|
}
|
||||||
currentSample = samplePool.get();
|
currentSample = samplePool.get();
|
||||||
@ -609,32 +603,120 @@ public final class TsExtractor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("InlinedApi")
|
@SuppressLint("InlinedApi")
|
||||||
private int readOneH264Frame(BitsArray pesBuffer, boolean remainderOnly) {
|
private int readOneH264Frame(BitArray pesBuffer, boolean remainderOnly) {
|
||||||
int offset = remainderOnly ? 0 : 3;
|
int offset = remainderOnly ? 0 : 3;
|
||||||
int audStart = pesBuffer.findNextNalUnit(NAL_UNIT_TYPE_AUD, offset);
|
int audStart = pesBuffer.findNextNalUnit(NAL_UNIT_TYPE_AUD, offset);
|
||||||
int idrStart = pesBuffer.findNextNalUnit(NAL_UNIT_TYPE_IDR, offset);
|
if (currentSample != null) {
|
||||||
if (audStart > 0) {
|
int idrStart = pesBuffer.findNextNalUnit(NAL_UNIT_TYPE_IDR, offset);
|
||||||
if (currentSample != null) {
|
if (idrStart < audStart) {
|
||||||
addToSample(currentSample, pesBuffer, audStart);
|
currentSample.flags = MediaExtractor.SAMPLE_FLAG_SYNC;
|
||||||
if (idrStart < audStart) {
|
|
||||||
currentSample.flags = MediaExtractor.SAMPLE_FLAG_SYNC;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
pesBuffer.skipBytes(audStart);
|
|
||||||
}
|
}
|
||||||
return audStart;
|
addToSample(currentSample, pesBuffer, audStart);
|
||||||
|
} else {
|
||||||
|
pesBuffer.skipBytes(audStart);
|
||||||
}
|
}
|
||||||
return 0;
|
return audStart;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private void parseMediaFormat(Sample sample) {
|
||||||
public void clear() {
|
BitArray bitArray = new BitArray(sample.data, sample.size);
|
||||||
super.clear();
|
// Locate the SPS unit.
|
||||||
if (currentSample != null) {
|
int spsOffset = bitArray.findNextNalUnit(NAL_UNIT_TYPE_SPS, 0);
|
||||||
samplePool.recycle(currentSample);
|
if (spsOffset == bitArray.bytesLeft()) {
|
||||||
currentSample = null;
|
return;
|
||||||
}
|
}
|
||||||
|
int nextNalOffset = bitArray.findNextNalUnit(-1, spsOffset + 3);
|
||||||
|
|
||||||
|
// Unescape the SPS unit.
|
||||||
|
byte[] unescapedSps = unescapeStream(bitArray.getData(), spsOffset, nextNalOffset);
|
||||||
|
bitArray.reset(unescapedSps, unescapedSps.length);
|
||||||
|
|
||||||
|
// Parse the SPS unit
|
||||||
|
// Skip the NAL header.
|
||||||
|
bitArray.skipBytes(4);
|
||||||
|
// TODO: Handle different profiles properly.
|
||||||
|
bitArray.skipBytes(1);
|
||||||
|
// Skip 6 constraint bits, 2 reserved bits and level_idc.
|
||||||
|
bitArray.skipBytes(2);
|
||||||
|
// Skip seq_parameter_set_id.
|
||||||
|
bitArray.readExpGolombCodedInt();
|
||||||
|
// Skip log2_max_frame_num_minus4
|
||||||
|
bitArray.readExpGolombCodedInt();
|
||||||
|
long picOrderCntType = bitArray.readExpGolombCodedInt();
|
||||||
|
if (picOrderCntType == 0) {
|
||||||
|
// Skip log2_max_pic_order_cnt_lsb_minus4
|
||||||
|
bitArray.readExpGolombCodedInt();
|
||||||
|
} else if (picOrderCntType == 1) {
|
||||||
|
// Skip delta_pic_order_always_zero_flag
|
||||||
|
bitArray.skipBits(1);
|
||||||
|
// Skip offset_for_non_ref_pic (actually a signed value, but for skipping we can read it
|
||||||
|
// as though it were unsigned).
|
||||||
|
bitArray.readExpGolombCodedInt();
|
||||||
|
// Skip offset_for_top_to_bottom_field (actually a signed value, but for skipping we can
|
||||||
|
// read it as though it were unsigned).
|
||||||
|
bitArray.readExpGolombCodedInt();
|
||||||
|
long numRefFramesInPicOrderCntCycle = bitArray.readExpGolombCodedInt();
|
||||||
|
for (int i = 0; i < numRefFramesInPicOrderCntCycle; i++) {
|
||||||
|
// Skip offset_for_ref_frame[i]
|
||||||
|
bitArray.readExpGolombCodedInt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Skip max_num_ref_frames
|
||||||
|
bitArray.readExpGolombCodedInt();
|
||||||
|
// Skip gaps_in_frame_num_value_allowed_flag
|
||||||
|
bitArray.skipBits(1);
|
||||||
|
int picWidthInMbs = bitArray.readExpGolombCodedInt() + 1;
|
||||||
|
int picHeightInMapUnits = bitArray.readExpGolombCodedInt() + 1;
|
||||||
|
boolean frameMbsOnlyFlag = bitArray.readBit();
|
||||||
|
int frameHeightInMbs = (2 - (frameMbsOnlyFlag ? 1 : 0)) * picHeightInMapUnits;
|
||||||
|
|
||||||
|
// Set the format.
|
||||||
|
setMediaFormat(MediaFormat.createVideoFormat(MimeTypes.VIDEO_H264, MediaFormat.NO_VALUE,
|
||||||
|
picWidthInMbs * 16, frameHeightInMbs * 16, null));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replaces occurrences of [0, 0, 3] with [0, 0].
|
||||||
|
* <p>
|
||||||
|
* See ISO/IEC 14496-10:2005(E) page 36 for more information.
|
||||||
|
*/
|
||||||
|
private byte[] unescapeStream(byte[] data, int offset, int limit) {
|
||||||
|
int position = offset;
|
||||||
|
List<Integer> escapePositions = new ArrayList<Integer>();
|
||||||
|
while (position < limit) {
|
||||||
|
position = findNextUnescapeIndex(data, position, limit);
|
||||||
|
if (position < limit) {
|
||||||
|
escapePositions.add(position);
|
||||||
|
position += 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int escapeCount = escapePositions.size();
|
||||||
|
int escapedPosition = offset; // The position being read from.
|
||||||
|
int unescapedPosition = 0; // The position being written to.
|
||||||
|
byte[] unescapedData = new byte[limit - offset - escapeCount];
|
||||||
|
for (int i = 0; i < escapeCount; i++) {
|
||||||
|
int nextEscapePosition = escapePositions.get(i);
|
||||||
|
int copyLength = nextEscapePosition - escapedPosition;
|
||||||
|
System.arraycopy(data, escapedPosition, unescapedData, unescapedPosition, copyLength);
|
||||||
|
escapedPosition += copyLength + 3;
|
||||||
|
unescapedPosition += copyLength + 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
int remainingLength = unescapedData.length - unescapedPosition;
|
||||||
|
System.arraycopy(data, escapedPosition, unescapedData, unescapedPosition, remainingLength);
|
||||||
|
return unescapedData;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int findNextUnescapeIndex(byte[] bytes, int offset, int limit) {
|
||||||
|
for (int i = offset; i < limit - 2; i++) {
|
||||||
|
if (bytes[i] == 0x00 && bytes[i + 1] == 0x00 && bytes[i + 2] == 0x03) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return limit;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -642,15 +724,15 @@ public final class TsExtractor {
|
|||||||
*/
|
*/
|
||||||
private class AdtsReader extends PesPayloadReader {
|
private class AdtsReader extends PesPayloadReader {
|
||||||
|
|
||||||
private final BitsArray adtsBuffer;
|
private final BitArray adtsBuffer;
|
||||||
private long timeUs;
|
private long timeUs;
|
||||||
|
|
||||||
public AdtsReader() {
|
public AdtsReader() {
|
||||||
adtsBuffer = new BitsArray();
|
adtsBuffer = new BitArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void read(BitsArray pesBuffer, int pesPayloadSize, long pesTimeUs) {
|
public void read(BitArray pesBuffer, int pesPayloadSize, long pesTimeUs) {
|
||||||
boolean needToProcessLeftOvers = !adtsBuffer.isEmpty();
|
boolean needToProcessLeftOvers = !adtsBuffer.isEmpty();
|
||||||
adtsBuffer.append(pesBuffer, pesPayloadSize);
|
adtsBuffer.append(pesBuffer, pesPayloadSize);
|
||||||
// If there are leftovers from previous PES packet, process it with last calculated timeUs.
|
// If there are leftovers from previous PES packet, process it with last calculated timeUs.
|
||||||
@ -751,7 +833,7 @@ public final class TsExtractor {
|
|||||||
|
|
||||||
@SuppressLint("InlinedApi")
|
@SuppressLint("InlinedApi")
|
||||||
@Override
|
@Override
|
||||||
public void read(BitsArray pesBuffer, int pesPayloadSize, long pesTimeUs) {
|
public void read(BitArray pesBuffer, int pesPayloadSize, long pesTimeUs) {
|
||||||
addSample(pesBuffer, pesPayloadSize, pesTimeUs, MediaExtractor.SAMPLE_FLAG_SYNC);
|
addSample(pesBuffer, pesPayloadSize, pesTimeUs, MediaExtractor.SAMPLE_FLAG_SYNC);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -764,31 +846,33 @@ public final class TsExtractor {
|
|||||||
|
|
||||||
private static final int DEFAULT_BUFFER_SEGMENT_SIZE = 64 * 1024;
|
private static final int DEFAULT_BUFFER_SEGMENT_SIZE = 64 * 1024;
|
||||||
|
|
||||||
private final ArrayList<Sample> samples;
|
private Sample firstInPool;
|
||||||
|
|
||||||
public SamplePool() {
|
/* package */ synchronized Sample get() {
|
||||||
samples = new ArrayList<Sample>();
|
if (firstInPool == null) {
|
||||||
}
|
|
||||||
|
|
||||||
/* package */ Sample get() {
|
|
||||||
if (samples.isEmpty()) {
|
|
||||||
return new Sample(DEFAULT_BUFFER_SEGMENT_SIZE);
|
return new Sample(DEFAULT_BUFFER_SEGMENT_SIZE);
|
||||||
}
|
}
|
||||||
return samples.remove(samples.size() - 1);
|
Sample sample = firstInPool;
|
||||||
|
firstInPool = sample.nextInPool;
|
||||||
|
sample.nextInPool = null;
|
||||||
|
return sample;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* package */ void recycle(Sample sample) {
|
/* package */ synchronized void recycle(Sample sample) {
|
||||||
sample.reset();
|
sample.reset();
|
||||||
samples.add(sample);
|
sample.nextInPool = firstInPool;
|
||||||
|
firstInPool = sample;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Simplified version of SampleHolder for internal buffering.
|
* An internal variant of {@link SampleHolder} for internal pooling and buffering.
|
||||||
*/
|
*/
|
||||||
private static class Sample {
|
private static class Sample {
|
||||||
|
|
||||||
|
public Sample nextInPool;
|
||||||
|
|
||||||
public byte[] data;
|
public byte[] data;
|
||||||
public int flags;
|
public int flags;
|
||||||
public int size;
|
public int size;
|
@ -0,0 +1,56 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2014 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package com.google.android.exoplayer.hls;
|
||||||
|
|
||||||
|
import java.util.Comparator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Variant stream reference.
|
||||||
|
*/
|
||||||
|
public final class Variant {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sorts {@link Variant} objects in order of decreasing bandwidth.
|
||||||
|
* <p>
|
||||||
|
* When two {@link Variant}s have the same bandwidth, the one with the lowest index comes first.
|
||||||
|
*/
|
||||||
|
public static final class DecreasingBandwidthComparator implements Comparator<Variant> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int compare(Variant a, Variant b) {
|
||||||
|
int bandwidthDifference = b.bandwidth - a.bandwidth;
|
||||||
|
return bandwidthDifference != 0 ? bandwidthDifference : a.index - b.index;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public final int index;
|
||||||
|
public final int bandwidth;
|
||||||
|
public final String url;
|
||||||
|
public final String[] codecs;
|
||||||
|
public final int width;
|
||||||
|
public final int height;
|
||||||
|
|
||||||
|
public Variant(int index, String url, int bandwidth, String[] codecs, int width, int height) {
|
||||||
|
this.index = index;
|
||||||
|
this.bandwidth = bandwidth;
|
||||||
|
this.url = url;
|
||||||
|
this.codecs = codecs;
|
||||||
|
this.width = width;
|
||||||
|
this.height = height;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -16,7 +16,7 @@
|
|||||||
package com.google.android.exoplayer.metadata;
|
package com.google.android.exoplayer.metadata;
|
||||||
|
|
||||||
import com.google.android.exoplayer.ParserException;
|
import com.google.android.exoplayer.ParserException;
|
||||||
import com.google.android.exoplayer.parser.ts.BitsArray;
|
import com.google.android.exoplayer.util.BitArray;
|
||||||
import com.google.android.exoplayer.util.MimeTypes;
|
import com.google.android.exoplayer.util.MimeTypes;
|
||||||
|
|
||||||
import java.io.UnsupportedEncodingException;
|
import java.io.UnsupportedEncodingException;
|
||||||
@ -37,7 +37,7 @@ public class Id3Parser implements MetadataParser {
|
|||||||
@Override
|
@Override
|
||||||
public Map<String, Object> parse(byte[] data, int size)
|
public Map<String, Object> parse(byte[] data, int size)
|
||||||
throws UnsupportedEncodingException, ParserException {
|
throws UnsupportedEncodingException, ParserException {
|
||||||
BitsArray id3Buffer = new BitsArray(data, size);
|
BitArray id3Buffer = new BitArray(data, size);
|
||||||
int id3Size = parseId3Header(id3Buffer);
|
int id3Size = parseId3Header(id3Buffer);
|
||||||
|
|
||||||
Map<String, Object> metadata = new HashMap<String, Object>();
|
Map<String, Object> metadata = new HashMap<String, Object>();
|
||||||
@ -102,11 +102,11 @@ public class Id3Parser implements MetadataParser {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses ID3 header.
|
* Parses ID3 header.
|
||||||
* @param id3Buffer A {@link BitsArray} with raw ID3 data.
|
* @param id3Buffer A {@link BitArray} with raw ID3 data.
|
||||||
* @return The size of data that contains ID3 frames without header and footer.
|
* @return The size of data that contains ID3 frames without header and footer.
|
||||||
* @throws ParserException If ID3 file identifier != "ID3".
|
* @throws ParserException If ID3 file identifier != "ID3".
|
||||||
*/
|
*/
|
||||||
private static int parseId3Header(BitsArray id3Buffer) throws ParserException {
|
private static int parseId3Header(BitArray id3Buffer) throws ParserException {
|
||||||
int id1 = id3Buffer.readUnsignedByte();
|
int id1 = id3Buffer.readUnsignedByte();
|
||||||
int id2 = id3Buffer.readUnsignedByte();
|
int id2 = id3Buffer.readUnsignedByte();
|
||||||
int id3 = id3Buffer.readUnsignedByte();
|
int id3 = id3Buffer.readUnsignedByte();
|
||||||
|
@ -19,7 +19,7 @@ import java.io.IOException;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses {@link Metadata}s from binary data.
|
* Parses metadata objects from binary data.
|
||||||
*/
|
*/
|
||||||
public interface MetadataParser {
|
public interface MetadataParser {
|
||||||
|
|
||||||
|
@ -13,15 +13,16 @@
|
|||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer.parser.ts;
|
package com.google.android.exoplayer.util;
|
||||||
|
|
||||||
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
|
import com.google.android.exoplayer.upstream.DataSource;
|
||||||
import com.google.android.exoplayer.util.Assertions;
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wraps a byte array, providing methods that allow it to be read as a bitstream.
|
* Wraps a byte array, providing methods that allow it to be read as a bitstream.
|
||||||
*/
|
*/
|
||||||
public final class BitsArray {
|
public final class BitArray {
|
||||||
|
|
||||||
private byte[] data;
|
private byte[] data;
|
||||||
|
|
||||||
@ -33,16 +34,16 @@ public final class BitsArray {
|
|||||||
private int byteOffset;
|
private int byteOffset;
|
||||||
private int bitOffset;
|
private int bitOffset;
|
||||||
|
|
||||||
public BitsArray() {
|
public BitArray() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public BitsArray(byte[] data, int limit) {
|
public BitArray(byte[] data, int limit) {
|
||||||
this.data = data;
|
this.data = data;
|
||||||
this.limit = limit;
|
this.limit = limit;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resets the state.
|
* Clears all data, setting the offset and limit to zero.
|
||||||
*/
|
*/
|
||||||
public void reset() {
|
public void reset() {
|
||||||
byteOffset = 0;
|
byteOffset = 0;
|
||||||
@ -50,6 +51,28 @@ public final class BitsArray {
|
|||||||
limit = 0;
|
limit = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets to wrap the specified data, setting the offset to zero.
|
||||||
|
*
|
||||||
|
* @param data The data to wrap.
|
||||||
|
* @param limit The limit to set.
|
||||||
|
*/
|
||||||
|
public void reset(byte[] data, int limit) {
|
||||||
|
this.data = data;
|
||||||
|
this.limit = limit;
|
||||||
|
byteOffset = 0;
|
||||||
|
bitOffset = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the backing byte array.
|
||||||
|
*
|
||||||
|
* @return The backing byte array.
|
||||||
|
*/
|
||||||
|
public byte[] getData() {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the current byte offset.
|
* Gets the current byte offset.
|
||||||
*
|
*
|
||||||
@ -69,16 +92,16 @@ public final class BitsArray {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Appends data from a {@link NonBlockingInputStream}.
|
* Appends data from a {@link DataSource}.
|
||||||
*
|
*
|
||||||
* @param inputStream The {@link NonBlockingInputStream} whose data should be appended.
|
* @param dataSource The {@link DataSource} from which to read.
|
||||||
* @param length The maximum number of bytes to read and append.
|
* @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
|
* @return The number of bytes that were read and appended, or -1 if no more data is available.
|
||||||
* from the stream. -1 is returned if the end of the stream has been reached.
|
* @throws IOException If an error occurs reading from the source.
|
||||||
*/
|
*/
|
||||||
public int append(NonBlockingInputStream inputStream, int length) {
|
public int append(DataSource dataSource, int length) throws IOException {
|
||||||
expand(length);
|
expand(length);
|
||||||
int bytesRead = inputStream.read(data, limit, length);
|
int bytesRead = dataSource.read(data, limit, length);
|
||||||
if (bytesRead == -1) {
|
if (bytesRead == -1) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
@ -87,12 +110,12 @@ public final class BitsArray {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Appends data from another {@link BitsArray}.
|
* Appends data from another {@link BitArray}.
|
||||||
*
|
*
|
||||||
* @param bitsArray The {@link BitsArray} whose data should be appended.
|
* @param bitsArray The {@link BitArray} whose data should be appended.
|
||||||
* @param length The number of bytes to read and append.
|
* @param length The number of bytes to read and append.
|
||||||
*/
|
*/
|
||||||
public void append(BitsArray bitsArray, int length) {
|
public void append(BitArray bitsArray, int length) {
|
||||||
expand(length);
|
expand(length);
|
||||||
bitsArray.readBytes(data, limit, length);
|
bitsArray.readBytes(data, limit, length);
|
||||||
limit += length;
|
limit += length;
|
||||||
@ -256,6 +279,19 @@ public final class BitsArray {
|
|||||||
return limit == 0;
|
return limit == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads an Exp-Golomb-coded format integer.
|
||||||
|
*
|
||||||
|
* @return The value of the parsed Exp-Golomb-coded integer.
|
||||||
|
*/
|
||||||
|
public int readExpGolombCodedInt() {
|
||||||
|
int leadingZeros = 0;
|
||||||
|
while (!readBit()) {
|
||||||
|
leadingZeros++;
|
||||||
|
}
|
||||||
|
return (1 << leadingZeros) - 1 + (leadingZeros > 0 ? readBits(leadingZeros) : 0);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reads a Synchsafe integer.
|
* Reads a Synchsafe integer.
|
||||||
* Synchsafe integers are integers that keep the highest bit of every byte zeroed.
|
* Synchsafe integers are integers that keep the highest bit of every byte zeroed.
|
||||||
@ -293,7 +329,7 @@ public final class BitsArray {
|
|||||||
/**
|
/**
|
||||||
* Finds the next NAL unit.
|
* Finds the next NAL unit.
|
||||||
*
|
*
|
||||||
* @param nalUnitType The type of the NAL unit to search for.
|
* @param nalUnitType The type of the NAL unit to search for, or -1 for any NAL unit.
|
||||||
* @param offset The additional offset in the data to start the search from.
|
* @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
|
* @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.
|
* not found, then the offset to the end of the data is returned.
|
||||||
@ -302,7 +338,7 @@ public final class BitsArray {
|
|||||||
for (int i = byteOffset + offset; i < limit - 3; i++) {
|
for (int i = byteOffset + offset; i < limit - 3; i++) {
|
||||||
// Check for NAL unit start code prefix == 0x000001.
|
// Check for NAL unit start code prefix == 0x000001.
|
||||||
if ((data[i] == 0 && data[i + 1] == 0 && data[i + 2] == 1)
|
if ((data[i] == 0 && data[i + 1] == 0 && data[i + 2] == 1)
|
||||||
&& (nalUnitType == (data[i + 3] & 0x1F))) {
|
&& (nalUnitType == -1 || (nalUnitType == (data[i + 3] & 0x1F)))) {
|
||||||
return i - byteOffset;
|
return i - byteOffset;
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
x
Reference in New Issue
Block a user