Commit 7f44d8f9 by ojw28

Merge pull request #367 from google/dev

dev -> dev-webm-vp9-opus
parents 5dedf5d9 b73b9a05
Showing with 1886 additions and 660 deletions
......@@ -48,6 +48,7 @@ public class DemoUtil {
public static final int TYPE_SS = 1;
public static final int TYPE_OTHER = 2;
public static final int TYPE_HLS = 3;
public static final int TYPE_MP4 = 4;
private static final CookieManager defaultCookieManager;
......
......@@ -24,6 +24,7 @@ import com.google.android.exoplayer.demo.player.DefaultRendererBuilder;
import com.google.android.exoplayer.demo.player.DemoPlayer;
import com.google.android.exoplayer.demo.player.DemoPlayer.RendererBuilder;
import com.google.android.exoplayer.demo.player.HlsRendererBuilder;
import com.google.android.exoplayer.demo.player.Mp4RendererBuilder;
import com.google.android.exoplayer.demo.player.SmoothStreamingRendererBuilder;
import com.google.android.exoplayer.demo.player.UnsupportedDrmException;
import com.google.android.exoplayer.metadata.GeobMetadata;
......@@ -215,6 +216,8 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback,
new WidevineTestMediaDrmCallback(contentId), debugTextView, audioCapabilities);
case DemoUtil.TYPE_HLS:
return new HlsRendererBuilder(userAgent, contentUri.toString());
case DemoUtil.TYPE_MP4:
return new Mp4RendererBuilder(contentUri, debugTextView);
default:
return new DefaultRendererBuilder(this, contentUri, debugTextView);
}
......
......@@ -135,6 +135,12 @@ import java.util.Locale;
new Sample("Apple AAC 10s", "https://devimages.apple.com.edgekey.net/"
+ "streaming/examples/bipbop_4x3/gear0/fileSequence0.aac",
DemoUtil.TYPE_OTHER),
new Sample("Big Buck Bunny (MP4)",
"http://redirector.c.youtube.com/videoplayback?id=604ed5ce52eda7ee&itag=22&source=youtube"
+ "&sparams=ip,ipbits,expire&ip=0.0.0.0&ipbits=0&expire=19000000000&signature="
+ "2E853B992F6CAB9D28CA3BEBD84A6F26709A8A55.94344B0D8BA83A7417AAD24DACC8C71A9A878ECE"
+ "&key=ik0",
DemoUtil.TYPE_MP4),
};
private Samples() {}
......
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.demo.player;
import com.google.android.exoplayer.MediaCodecAudioTrackRenderer;
import com.google.android.exoplayer.MediaCodecVideoTrackRenderer;
import com.google.android.exoplayer.TrackRenderer;
import com.google.android.exoplayer.demo.player.DemoPlayer.RendererBuilder;
import com.google.android.exoplayer.demo.player.DemoPlayer.RendererBuilderCallback;
import com.google.android.exoplayer.source.DefaultSampleSource;
import com.google.android.exoplayer.source.Mp4SampleExtractor;
import com.google.android.exoplayer.upstream.DataSpec;
import com.google.android.exoplayer.upstream.UriDataSource;
import android.media.MediaCodec;
import android.net.Uri;
import android.widget.TextView;
/**
* A {@link RendererBuilder} for streams that can be read using {@link Mp4SampleExtractor}.
*/
public class Mp4RendererBuilder implements RendererBuilder {
private final Uri uri;
private final TextView debugTextView;
public Mp4RendererBuilder(Uri uri, TextView debugTextView) {
this.uri = uri;
this.debugTextView = debugTextView;
}
@Override
public void buildRenderers(DemoPlayer player, RendererBuilderCallback callback) {
// Build the video and audio renderers.
DefaultSampleSource sampleSource = new DefaultSampleSource(
new Mp4SampleExtractor(new UriDataSource("exoplayer", null), new DataSpec(uri)), 2);
MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(sampleSource,
null, true, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 5000, null, player.getMainHandler(),
player, 50);
MediaCodecAudioTrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer(sampleSource,
null, true, player.getMainHandler(), player);
// Build the debug renderer.
TrackRenderer debugRenderer = debugTextView != null
? new DebugTrackRenderer(debugTextView, videoRenderer)
: null;
// Invoke the callback.
TrackRenderer[] renderers = new TrackRenderer[DemoPlayer.RENDERER_COUNT];
renderers[DemoPlayer.TYPE_VIDEO] = videoRenderer;
renderers[DemoPlayer.TYPE_AUDIO] = audioRenderer;
renderers[DemoPlayer.TYPE_DEBUG] = debugRenderer;
callback.onRenderers(null, null, renderers);
}
}
......@@ -281,7 +281,7 @@ public final class Ac3PassthroughAudioTrackRenderer extends TrackRenderer {
protected void onDisabled() {
audioSessionId = AudioTrack.SESSION_ID_NOT_SET;
shouldReadInputBuffer = true;
audioTrack.reset();
audioTrack.release();
}
@Override
......
......@@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer;
import android.media.MediaCodec;
import android.media.MediaExtractor;
/**
......@@ -43,11 +44,23 @@ public final class C {
public static final String UTF8_NAME = "UTF-8";
/**
* Sample flag that indicates the sample is a synchronization sample.
* @see MediaExtractor#SAMPLE_FLAG_SYNC
*/
@SuppressWarnings("InlinedApi")
public static final int SAMPLE_FLAG_SYNC = MediaExtractor.SAMPLE_FLAG_SYNC;
/**
* @see MediaExtractor#SAMPLE_FLAG_ENCRYPTED
*/
@SuppressWarnings("InlinedApi")
public static final int SAMPLE_FLAG_ENCRYPTED = MediaExtractor.SAMPLE_FLAG_ENCRYPTED;
/**
* @see MediaCodec#CRYPTO_MODE_AES_CTR
*/
@SuppressWarnings("InlinedApi")
public static final int CRYPTO_MODE_AES_CTR = MediaCodec.CRYPTO_MODE_AES_CTR;
private C() {}
}
......@@ -202,7 +202,7 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer {
protected void onDisabled() {
audioSessionId = AudioTrack.SESSION_ID_NOT_SET;
try {
audioTrack.reset();
audioTrack.release();
} finally {
super.onDisabled();
}
......
......@@ -16,6 +16,7 @@
package com.google.android.exoplayer;
import com.google.android.exoplayer.MediaCodecUtil.DecoderQueryException;
import com.google.android.exoplayer.drm.DrmInitData;
import com.google.android.exoplayer.drm.DrmSessionManager;
import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.Util;
......@@ -25,7 +26,6 @@ import android.media.MediaCodec;
import android.media.MediaCodec.CodecException;
import android.media.MediaCodec.CryptoException;
import android.media.MediaCrypto;
import android.media.MediaExtractor;
import android.os.Handler;
import android.os.SystemClock;
......@@ -33,8 +33,6 @@ import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
/**
* An abstract {@link TrackRenderer} that uses {@link MediaCodec} to decode samples for rendering.
......@@ -164,7 +162,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
protected final Handler eventHandler;
private MediaFormat format;
private Map<UUID, byte[]> drmInitData;
private DrmInitData drmInitData;
private MediaCodec codec;
private boolean codecIsAdaptive;
private ByteBuffer[] inputBuffers;
......@@ -281,7 +279,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
throw new ExoPlaybackException("Media requires a DrmSessionManager");
}
if (!openedDrmSession) {
drmSessionManager.open(drmInitData, mimeType);
drmSessionManager.open(drmInitData);
openedDrmSession = true;
}
int drmSessionState = drmSessionManager.getState();
......@@ -584,7 +582,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
}
waitingForFirstSyncFrame = false;
}
boolean sampleEncrypted = (sampleHolder.flags & MediaExtractor.SAMPLE_FLAG_ENCRYPTED) != 0;
boolean sampleEncrypted = (sampleHolder.flags & C.SAMPLE_FLAG_ENCRYPTED) != 0;
waitingForKeys = shouldWaitForKeys(sampleEncrypted);
if (waitingForKeys) {
return false;
......
......@@ -40,6 +40,8 @@ public class MediaFormat {
public final String mimeType;
public final int maxInputSize;
public final long durationUs;
public final int width;
public final int height;
public final float pixelWidthHeightRatio;
......@@ -49,11 +51,11 @@ public class MediaFormat {
public final int bitrate;
public final List<byte[]> initializationData;
private int maxWidth;
private int maxHeight;
public final List<byte[]> initializationData;
// Lazy-initialized hashcode.
private int hashCode;
// Possibly-lazy-initialized framework media format.
......@@ -66,25 +68,38 @@ public class MediaFormat {
public static MediaFormat createVideoFormat(String mimeType, int maxInputSize, int width,
int height, List<byte[]> initializationData) {
return createVideoFormat(mimeType, maxInputSize, width, height, 1, initializationData);
return createVideoFormat(
mimeType, maxInputSize, C.UNKNOWN_TIME_US, width, height, initializationData);
}
public static MediaFormat createVideoFormat(String mimeType, int maxInputSize, int width,
int height, float pixelWidthHeightRatio, List<byte[]> initializationData) {
return new MediaFormat(mimeType, maxInputSize, width, height, pixelWidthHeightRatio, NO_VALUE,
NO_VALUE, NO_VALUE, initializationData);
public static MediaFormat createVideoFormat(String mimeType, int maxInputSize, long durationUs,
int width, int height, List<byte[]> initializationData) {
return createVideoFormat(
mimeType, maxInputSize, durationUs, width, height, 1, initializationData);
}
public static MediaFormat createVideoFormat(String mimeType, int maxInputSize, long durationUs,
int width, int height, float pixelWidthHeightRatio, List<byte[]> initializationData) {
return new MediaFormat(mimeType, maxInputSize, durationUs, width, height, pixelWidthHeightRatio,
NO_VALUE, NO_VALUE, NO_VALUE, initializationData);
}
public static MediaFormat createAudioFormat(String mimeType, int maxInputSize, int channelCount,
int sampleRate, List<byte[]> initializationData) {
return new MediaFormat(mimeType, maxInputSize, NO_VALUE, NO_VALUE, NO_VALUE, channelCount,
sampleRate, NO_VALUE, initializationData);
return createAudioFormat(
mimeType, maxInputSize, C.UNKNOWN_TIME_US, channelCount, sampleRate, initializationData);
}
public static MediaFormat createAudioFormat(String mimeType, int maxInputSize, int channelCount,
int sampleRate, int bitrate, List<byte[]> initializationData) {
return new MediaFormat(mimeType, maxInputSize, NO_VALUE, NO_VALUE, NO_VALUE, channelCount,
sampleRate, bitrate, initializationData);
public static MediaFormat createAudioFormat(String mimeType, int maxInputSize, long durationUs,
int channelCount, int sampleRate, List<byte[]> initializationData) {
return createAudioFormat(
mimeType, maxInputSize, durationUs, channelCount, sampleRate, NO_VALUE, initializationData);
}
public static MediaFormat createAudioFormat(String mimeType, int maxInputSize, long durationUs,
int channelCount, int sampleRate, int bitrate, List<byte[]> initializationData) {
return new MediaFormat(mimeType, maxInputSize, durationUs, NO_VALUE, NO_VALUE, NO_VALUE,
channelCount, sampleRate, bitrate, initializationData);
}
public static MediaFormat createId3Format() {
......@@ -100,8 +115,8 @@ public class MediaFormat {
}
public static MediaFormat createFormatForMimeType(String mimeType) {
return new MediaFormat(mimeType, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE,
NO_VALUE, null);
return new MediaFormat(mimeType, NO_VALUE, C.UNKNOWN_TIME_US, NO_VALUE, NO_VALUE, NO_VALUE,
NO_VALUE, NO_VALUE, NO_VALUE, null);
}
@TargetApi(16)
......@@ -123,15 +138,18 @@ public class MediaFormat {
initializationData.add(data);
buffer.flip();
}
durationUs = format.containsKey(android.media.MediaFormat.KEY_DURATION)
? format.getLong(android.media.MediaFormat.KEY_DURATION) : C.UNKNOWN_TIME_US;
maxWidth = NO_VALUE;
maxHeight = NO_VALUE;
}
private MediaFormat(String mimeType, int maxInputSize, int width, int height,
private MediaFormat(String mimeType, int maxInputSize, long durationUs, int width, int height,
float pixelWidthHeightRatio, int channelCount, int sampleRate, int bitrate,
List<byte[]> initializationData) {
this.mimeType = mimeType;
this.maxInputSize = maxInputSize;
this.durationUs = durationUs;
this.width = width;
this.height = height;
this.pixelWidthHeightRatio = pixelWidthHeightRatio;
......@@ -169,6 +187,7 @@ public class MediaFormat {
result = 31 * result + width;
result = 31 * result + height;
result = 31 * result + Float.floatToRawIntBits(pixelWidthHeightRatio);
result = 31 * result + (int) durationUs;
result = 31 * result + maxWidth;
result = 31 * result + maxHeight;
result = 31 * result + channelCount;
......@@ -225,7 +244,7 @@ public class MediaFormat {
public String toString() {
return "MediaFormat(" + mimeType + ", " + maxInputSize + ", " + width + ", " + height + ", "
+ pixelWidthHeightRatio + ", " + channelCount + ", " + sampleRate + ", " + bitrate + ", "
+ maxWidth + ", " + maxHeight + ")";
+ durationUs + ", " + maxWidth + ", " + maxHeight + ")";
}
/**
......@@ -246,6 +265,9 @@ public class MediaFormat {
for (int i = 0; i < initializationData.size(); i++) {
format.setByteBuffer("csd-" + i, ByteBuffer.wrap(initializationData.get(i)));
}
if (durationUs != C.UNKNOWN_TIME_US) {
format.setLong(android.media.MediaFormat.KEY_DURATION, durationUs);
}
maybeSetMaxDimensionsV16(format);
frameworkMediaFormat = format;
}
......
......@@ -15,8 +15,7 @@
*/
package com.google.android.exoplayer;
import java.util.Map;
import java.util.UUID;
import com.google.android.exoplayer.drm.DrmInitData;
/**
* Holds a {@link MediaFormat} and corresponding drm scheme initialization data.
......@@ -28,9 +27,8 @@ public final class MediaFormatHolder {
*/
public MediaFormat format;
/**
* Initialization data for each of the drm schemes supported by the media, keyed by scheme UUID.
* Null if the media is not encrypted.
* Initialization data for drm schemes supported by the media. Null if the media is not encrypted.
*/
public Map<UUID, byte[]> drmInitData;
public DrmInitData drmInitData;
}
......@@ -50,9 +50,8 @@ public final class SampleHolder {
public int size;
/**
* Flags that accompany the sample. A combination of
* {@link android.media.MediaExtractor#SAMPLE_FLAG_SYNC} and
* {@link android.media.MediaExtractor#SAMPLE_FLAG_ENCRYPTED}
* Flags that accompany the sample. A combination of {@link C#SAMPLE_FLAG_SYNC} and
* {@link C#SAMPLE_FLAG_ENCRYPTED}
*/
public int flags;
......
......@@ -44,6 +44,8 @@ import java.nio.ByteBuffer;
* <p>Call {@link #reconfigure} when the output format changes.
*
* <p>Call {@link #reset} to free resources. It is safe to re-{@link #initialize} the instance.
*
* <p>Call {@link #release} when the instance will no longer be used.
*/
@TargetApi(16)
public final class AudioTrack {
......@@ -91,6 +93,12 @@ public final class AudioTrack {
/** Returned by {@link #getCurrentPositionUs} when the position is not set. */
public static final long CURRENT_POSITION_NOT_SET = Long.MIN_VALUE;
/**
* Set to {@code true} to enable a workaround for an issue where an audio effect does not keep its
* session active across releasing/initializing a new audio track, on platform API version < 21.
*/
private static final boolean ENABLE_PRE_V21_AUDIO_SESSION_WORKAROUND = false;
/** A minimum length for the {@link android.media.AudioTrack} buffer, in microseconds. */
private static final long MIN_BUFFER_DURATION_US = 250000;
/** A maximum length for the {@link android.media.AudioTrack} buffer, in microseconds. */
......@@ -132,6 +140,9 @@ public final class AudioTrack {
private final ConditionVariable releasingConditionVariable;
private final long[] playheadOffsets;
/** Used to keep the audio session active on pre-V21 builds (see {@link #initialize()}). */
private android.media.AudioTrack keepSessionIdAudioTrack;
private android.media.AudioTrack audioTrack;
private AudioTrackUtil audioTrackUtil;
private int sampleRate;
......@@ -267,15 +278,37 @@ public final class AudioTrack {
audioTrack = new android.media.AudioTrack(AudioManager.STREAM_MUSIC, sampleRate,
channelConfig, encoding, bufferSize, android.media.AudioTrack.MODE_STREAM, sessionId);
}
checkAudioTrackInitialized();
sessionId = audioTrack.getAudioSessionId();
if (ENABLE_PRE_V21_AUDIO_SESSION_WORKAROUND) {
if (Util.SDK_INT < 21) {
// The workaround creates an audio track with a one byte buffer on the same session, and
// does not release it until this object is released, which keeps the session active.
if (keepSessionIdAudioTrack != null
&& sessionId != keepSessionIdAudioTrack.getAudioSessionId()) {
releaseKeepSessionIdAudioTrack();
}
if (keepSessionIdAudioTrack == null) {
int sampleRate = 4000; // Equal to private android.media.AudioTrack.MIN_SAMPLE_RATE.
int channelConfig = AudioFormat.CHANNEL_OUT_MONO;
int encoding = AudioFormat.ENCODING_PCM_8BIT;
int bufferSize = 1; // Use a one byte buffer, as it is not actually used for playback.
keepSessionIdAudioTrack = new android.media.AudioTrack(AudioManager.STREAM_MUSIC,
sampleRate, channelConfig, encoding, bufferSize, android.media.AudioTrack.MODE_STATIC,
sessionId);
}
}
}
if (Util.SDK_INT >= 19) {
audioTrackUtil = new AudioTrackUtilV19(audioTrack);
} else {
audioTrackUtil = new AudioTrackUtil(audioTrack);
}
setVolume(volume);
return audioTrack.getAudioSessionId();
return sessionId;
}
/**
......@@ -515,9 +548,9 @@ public final class AudioTrack {
}
/**
* Releases resources associated with this instance asynchronously. Calling {@link #initialize}
* will block until the audio track has been released, so it is safe to initialize immediately
* after resetting.
* Releases the underlying audio track asynchronously. Calling {@link #initialize} will block
* until the audio track has been released, so it is safe to initialize immediately after
* resetting. The audio session may remain active until the instance is {@link #release}d.
*/
public void reset() {
if (isInitialized()) {
......@@ -547,6 +580,29 @@ public final class AudioTrack {
}
}
/** Releases all resources associated with this instance. */
public void release() {
reset();
releaseKeepSessionIdAudioTrack();
}
/** Releases {@link #keepSessionIdAudioTrack} asynchronously, if it is non-{@code null}. */
private void releaseKeepSessionIdAudioTrack() {
if (keepSessionIdAudioTrack == null) {
return;
}
// AudioTrack.release can take some time, so we call it on a background thread.
final android.media.AudioTrack toRelease = keepSessionIdAudioTrack;
keepSessionIdAudioTrack = null;
new Thread() {
@Override
public void run() {
toRelease.release();
}
}.start();
}
/** Returns whether {@link #getCurrentPositionUs} can return the current playback position. */
private boolean hasCurrentPositionUs() {
return isInitialized() && startMediaTimeUs != START_NOT_SET;
......
......@@ -352,7 +352,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback {
if (mediaFormat != null && !mediaFormat.equals(downstreamMediaFormat, true)) {
chunkSource.getMaxVideoDimensions(mediaFormat);
formatHolder.format = mediaFormat;
formatHolder.drmInitData = mediaChunk.getPsshInfo();
formatHolder.drmInitData = mediaChunk.getDrmInitData();
downstreamMediaFormat = mediaFormat;
return FORMAT_READ;
}
......
......@@ -19,14 +19,12 @@ import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.chunk.parser.Extractor;
import com.google.android.exoplayer.drm.DrmInitData;
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DataSpec;
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
import com.google.android.exoplayer.util.Assertions;
import java.util.Map;
import java.util.UUID;
/**
* A {@link MediaChunk} extracted from a container.
*/
......@@ -38,7 +36,7 @@ public final class ContainerMediaChunk extends MediaChunk {
private boolean prepared;
private MediaFormat mediaFormat;
private Map<UUID, byte[]> psshInfo;
private DrmInitData drmInitData;
/**
* @deprecated Use the other constructor, passing null as {@code psshInfo}.
......@@ -60,8 +58,9 @@ public final class ContainerMediaChunk extends MediaChunk {
* @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 extractor The extractor that will be used to extract the samples.
* @param psshInfo Pssh data. May be null if pssh data is present within the stream, meaning it
* can be obtained directly from {@code extractor}, or if no pssh data is required.
* @param drmInitData DRM initialization data. May be null if DRM initialization data is present
* within the stream, meaning it can be obtained directly from {@code extractor}, or if no
* DRM initialization data is required.
* @param maybeSelfContained Set to true if this chunk might be self contained, meaning it might
* contain a moov atom defining the media format of the chunk. This parameter can always be
* safely set to true. Setting to false where the chunk is known to not be self contained may
......@@ -70,12 +69,12 @@ public final class ContainerMediaChunk extends MediaChunk {
*/
public ContainerMediaChunk(DataSource dataSource, DataSpec dataSpec, Format format,
int trigger, long startTimeUs, long endTimeUs, int nextChunkIndex, Extractor extractor,
Map<UUID, byte[]> psshInfo, boolean maybeSelfContained, long sampleOffsetUs) {
DrmInitData drmInitData, boolean maybeSelfContained, long sampleOffsetUs) {
super(dataSource, dataSpec, format, trigger, startTimeUs, endTimeUs, nextChunkIndex);
this.extractor = extractor;
this.maybeSelfContained = maybeSelfContained;
this.sampleOffsetUs = sampleOffsetUs;
this.psshInfo = psshInfo;
this.drmInitData = drmInitData;
}
@Override
......@@ -111,9 +110,9 @@ public final class ContainerMediaChunk extends MediaChunk {
}
if (prepared) {
mediaFormat = extractor.getFormat();
Map<UUID, byte[]> extractorPsshInfo = extractor.getPsshInfo();
if (extractorPsshInfo != null) {
psshInfo = extractorPsshInfo;
DrmInitData extractorDrmInitData = extractor.getDrmInitData();
if (extractorDrmInitData != null) {
drmInitData = extractorDrmInitData;
}
}
}
......@@ -145,8 +144,8 @@ public final class ContainerMediaChunk extends MediaChunk {
}
@Override
public Map<UUID, byte[]> getPsshInfo() {
return psshInfo;
public DrmInitData getDrmInitData() {
return drmInitData;
}
}
......@@ -18,12 +18,10 @@ package com.google.android.exoplayer.chunk;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.drm.DrmInitData;
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DataSpec;
import java.util.Map;
import java.util.UUID;
/**
* An abstract base class for {@link Chunk}s that contain media samples.
*/
......@@ -129,12 +127,12 @@ public abstract class MediaChunk extends Chunk {
public abstract MediaFormat getMediaFormat();
/**
* Returns the pssh information associated with the chunk.
* Returns the DRM initialization data associated with the chunk.
* <p>
* Should only be called after the chunk has been successfully prepared.
*
* @return The pssh information.
* @return The DRM initialization data.
*/
public abstract Map<UUID, byte[]> getPsshInfo();
public abstract DrmInitData getDrmInitData();
}
......@@ -17,14 +17,12 @@ package com.google.android.exoplayer.chunk;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.drm.DrmInitData;
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DataSpec;
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
import com.google.android.exoplayer.util.Assertions;
import java.util.Map;
import java.util.UUID;
/**
* A {@link MediaChunk} containing a single sample.
*/
......@@ -132,7 +130,7 @@ public class SingleSampleMediaChunk extends MediaChunk {
}
@Override
public Map<UUID, byte[]> getPsshInfo() {
public DrmInitData getDrmInitData() {
return null;
}
......
......@@ -15,15 +15,12 @@
*/
package com.google.android.exoplayer.chunk.parser;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.drm.DrmInitData;
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
import java.util.Map;
import java.util.UUID;
/**
* Facilitates extraction of media samples from a container format.
*/
......@@ -43,7 +40,7 @@ public interface Extractor {
public static final int RESULT_READ_SAMPLE = 4;
/**
* Initialization data was read. The parsed data can be read using {@link #getFormat()} and
* {@link #getPsshInfo}.
* {@link #getDrmInitData()}.
*/
public static final int RESULT_READ_INIT = 8;
/**
......@@ -80,17 +77,12 @@ public interface Extractor {
public MediaFormat getFormat();
/**
* Returns the duration of the stream in microseconds, or {@link C#UNKNOWN_TIME_US} if unknown.
*/
public long getDurationUs();
/**
* Returns the pssh information parsed from the stream.
* Returns DRM initialization data parsed from the stream.
*
* @return The pssh information. May be null if pssh data has yet to be parsed, or if the stream
* does not contain any pssh data.
* @return The DRM initialization data. May be null if the initialization data has yet to be
* parsed, or if the stream does not contain any DRM initialization data.
*/
public Map<UUID, byte[]> getPsshInfo();
public DrmInitData getDrmInitData();
/**
* Consumes data from a {@link NonBlockingInputStream}.
......
......@@ -21,6 +21,7 @@ import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.chunk.parser.Extractor;
import com.google.android.exoplayer.chunk.parser.SegmentIndex;
import com.google.android.exoplayer.drm.DrmInitData;
import com.google.android.exoplayer.mp4.Atom;
import com.google.android.exoplayer.mp4.Atom.ContainerAtom;
import com.google.android.exoplayer.mp4.Atom.LeafAtom;
......@@ -28,20 +29,15 @@ import com.google.android.exoplayer.mp4.CommonMp4AtomParsers;
import com.google.android.exoplayer.mp4.Mp4Util;
import com.google.android.exoplayer.mp4.Track;
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
import com.google.android.exoplayer.util.MimeTypes;
import com.google.android.exoplayer.util.ParsableByteArray;
import com.google.android.exoplayer.util.Util;
import android.annotation.SuppressLint;
import android.media.MediaCodec;
import android.media.MediaExtractor;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Stack;
import java.util.UUID;
......@@ -145,7 +141,7 @@ public final class FragmentedMp4Extractor implements Extractor {
private int lastSyncSampleIndex;
// Data parsed from moov and sidx atoms
private final HashMap<UUID, byte[]> psshData;
private DrmInitData.Mapped drmInitData;
private SegmentIndex segmentIndex;
private Track track;
private DefaultSampleValues extendsDefaults;
......@@ -165,7 +161,6 @@ public final class FragmentedMp4Extractor implements Extractor {
extendedTypeScratch = new byte[16];
containerAtoms = new Stack<ContainerAtom>();
fragmentRun = new TrackFragment();
psshData = new HashMap<UUID, byte[]>();
}
/**
......@@ -179,8 +174,8 @@ public final class FragmentedMp4Extractor implements Extractor {
}
@Override
public Map<UUID, byte[]> getPsshInfo() {
return psshData.isEmpty() ? null : psshData;
public DrmInitData getDrmInitData() {
return drmInitData;
}
@Override
......@@ -199,11 +194,6 @@ public final class FragmentedMp4Extractor implements Extractor {
}
@Override
public long getDurationUs() {
return track == null ? C.UNKNOWN_TIME_US : track.durationUs;
}
@Override
public int read(NonBlockingInputStream inputStream, SampleHolder out)
throws ParserException {
try {
......@@ -375,7 +365,10 @@ public final class FragmentedMp4Extractor implements Extractor {
int dataSize = psshAtom.readInt();
byte[] data = new byte[dataSize];
psshAtom.readBytes(data, 0, dataSize);
psshData.put(uuid, data);
if (drmInitData == null) {
drmInitData = new DrmInitData.Mapped(MimeTypes.VIDEO_MP4);
}
drmInitData.put(uuid, data);
}
}
ContainerAtom mvex = moov.getContainerAtomOfType(Atom.TYPE_mvex);
......@@ -798,12 +791,14 @@ public final class FragmentedMp4Extractor implements Extractor {
return RESULT_READ_SAMPLE;
}
@SuppressLint("InlinedApi")
private void readSampleEncryptionData(ParsableByteArray sampleEncryptionData, SampleHolder out) {
TrackEncryptionBox encryptionBox =
track.sampleDescriptionEncryptionBoxes[fragmentRun.sampleDescriptionIndex];
if (!encryptionBox.isEncrypted) {
return;
}
byte[] keyId = encryptionBox.keyId;
boolean isEncrypted = encryptionBox.isEncrypted;
int vectorSize = encryptionBox.initializationVectorSize;
boolean subsampleEncryption = fragmentRun.sampleHasSubsampleEncryptionTable[sampleIndex];
......@@ -831,11 +826,10 @@ public final class FragmentedMp4Extractor implements Extractor {
clearDataSizes[0] = 0;
encryptedDataSizes[0] = fragmentRun.sampleSizeTable[sampleIndex];
}
out.cryptoInfo.set(subsampleCount, clearDataSizes, encryptedDataSizes, keyId, vector,
isEncrypted ? MediaCodec.CRYPTO_MODE_AES_CTR : MediaCodec.CRYPTO_MODE_UNENCRYPTED);
if (isEncrypted) {
out.flags |= MediaExtractor.SAMPLE_FLAG_ENCRYPTED;
}
C.CRYPTO_MODE_AES_CTR);
out.flags |= C.SAMPLE_FLAG_ENCRYPTED;
}
}
......@@ -39,6 +39,7 @@ import com.google.android.exoplayer.dash.mpd.MediaPresentationDescription;
import com.google.android.exoplayer.dash.mpd.Period;
import com.google.android.exoplayer.dash.mpd.RangedUri;
import com.google.android.exoplayer.dash.mpd.Representation;
import com.google.android.exoplayer.drm.DrmInitData;
import com.google.android.exoplayer.text.webvtt.WebvttParser;
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DataSpec;
......@@ -54,8 +55,6 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
/**
* An {@link ChunkSource} for DASH streams.
......@@ -96,7 +95,7 @@ public class DashChunkSource implements ChunkSource {
private final ManifestFetcher<MediaPresentationDescription> manifestFetcher;
private final int adaptationSetIndex;
private final int[] representationIndices;
private final Map<UUID, byte[]> psshInfo;
private final DrmInitData drmInitData;
private MediaPresentationDescription currentManifest;
private boolean finishedCurrentManifest;
......@@ -190,7 +189,7 @@ public class DashChunkSource implements ChunkSource {
this.evaluation = new Evaluation();
this.headerBuilder = new StringBuilder();
psshInfo = getPsshInfo(currentManifest, adaptationSetIndex);
drmInitData = getDrmInitData(currentManifest, adaptationSetIndex);
Representation[] representations = getFilteredRepresentations(currentManifest,
adaptationSetIndex, representationIndices);
long periodDurationUs = (representations[0].periodDurationMs == TrackRenderer.UNKNOWN_TIME_US)
......@@ -407,7 +406,7 @@ public class DashChunkSource implements ChunkSource {
// Do nothing.
}
private boolean mimeTypeIsWebm(String mimeType) {
private static boolean mimeTypeIsWebm(String mimeType) {
return mimeType.startsWith(MimeTypes.VIDEO_WEBM) || mimeType.startsWith(MimeTypes.AUDIO_WEBM);
}
......@@ -475,8 +474,8 @@ public class DashChunkSource implements ChunkSource {
startTimeUs, endTimeUs, nextAbsoluteSegmentNum, null, representationHolder.vttHeader);
} else {
return new ContainerMediaChunk(dataSource, dataSpec, representation.format, trigger,
startTimeUs, endTimeUs, nextAbsoluteSegmentNum, representationHolder.extractor, psshInfo,
false, presentationTimeOffsetUs);
startTimeUs, endTimeUs, nextAbsoluteSegmentNum, representationHolder.extractor,
drmInitData, false, presentationTimeOffsetUs);
}
}
......@@ -529,19 +528,24 @@ public class DashChunkSource implements ChunkSource {
}
}
private static Map<UUID, byte[]> getPsshInfo(MediaPresentationDescription manifest,
private static DrmInitData getDrmInitData(MediaPresentationDescription manifest,
int adaptationSetIndex) {
AdaptationSet adaptationSet = manifest.periods.get(0).adaptationSets.get(adaptationSetIndex);
String drmInitMimeType = mimeTypeIsWebm(adaptationSet.representations.get(0).format.mimeType)
? MimeTypes.VIDEO_WEBM : MimeTypes.VIDEO_MP4;
if (adaptationSet.contentProtections.isEmpty()) {
return null;
} else {
Map<UUID, byte[]> psshInfo = new HashMap<UUID, byte[]>();
DrmInitData.Mapped drmInitData = null;
for (ContentProtection contentProtection : adaptationSet.contentProtections) {
if (contentProtection.uuid != null && contentProtection.data != null) {
psshInfo.put(contentProtection.uuid, contentProtection.data);
if (drmInitData == null) {
drmInitData = new DrmInitData.Mapped(drmInitMimeType);
}
drmInitData.put(contentProtection.uuid, contentProtection.data);
}
}
return psshInfo.isEmpty() ? null : psshInfo;
return drmInitData;
}
}
......
/*
* 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.drm;
import android.media.MediaDrm;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* Encapsulates initialization data required by a {@link MediaDrm} instance.
*/
public abstract class DrmInitData {
/**
* The container mime type.
*/
public final String mimeType;
public DrmInitData(String mimeType) {
this.mimeType = mimeType;
}
/**
* Retrieves initialization data for a given DRM scheme, specified by its UUID.
*
* @param schemeUuid The DRM scheme's UUID.
* @return The initialization data for the scheme, or null if the scheme is not supported.
*/
public abstract byte[] get(UUID schemeUuid);
/**
* A {@link DrmInitData} implementation that maps UUID onto scheme specific data.
*/
public static final class Mapped extends DrmInitData {
private final Map<UUID, byte[]> schemeData;
public Mapped(String mimeType) {
super(mimeType);
schemeData = new HashMap<UUID, byte[]>();
}
@Override
public byte[] get(UUID schemeUuid) {
return schemeData.get(schemeUuid);
}
/**
* Inserts scheme specific initialization data.
*
* @param schemeUuid The scheme UUID.
* @param data The corresponding initialization data.
*/
public void put(UUID schemeUuid, byte[] data) {
schemeData.put(schemeUuid, data);
}
/**
* Inserts scheme specific initialization data.
*
* @param data A mapping from scheme UUID to initialization data.
*/
public void putAll(Map<UUID, byte[]> data) {
schemeData.putAll(data);
}
}
/**
* A {@link DrmInitData} implementation that returns the same initialization data for all schemes.
*/
public static final class Universal extends DrmInitData {
private byte[] data;
public Universal(String mimeType, byte[] data) {
super(mimeType);
this.data = data;
}
@Override
public byte[] get(UUID schemeUuid) {
return data;
}
}
}
......@@ -18,9 +18,6 @@ package com.google.android.exoplayer.drm;
import android.annotation.TargetApi;
import android.media.MediaCrypto;
import java.util.Map;
import java.util.UUID;
/**
* Manages a DRM session.
*/
......@@ -36,7 +33,7 @@ public interface DrmSessionManager {
*/
public static final int STATE_CLOSED = 1;
/**
* The session is being opened (i.e. {@link #open(Map, String)} has been called, but the session
* The session is being opened (i.e. {@link #open(DrmInitData)} has been called, but the session
* is not yet open).
*/
public static final int STATE_OPENING = 2;
......@@ -52,11 +49,9 @@ public interface DrmSessionManager {
/**
* Opens the session, possibly asynchronously.
*
* @param drmInitData Initialization data for the drm schemes supported by the media, keyed by
* scheme UUID.
* @param mimeType The mimeType of the media.
* @param drmInitData DRM initialization data.
*/
void open(Map<UUID, byte[]> drmInitData, String mimeType);
void open(DrmInitData drmInitData);
/**
* Closes the session.
......
......@@ -31,7 +31,6 @@ import android.os.Looper;
import android.os.Message;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
......@@ -168,7 +167,7 @@ public class StreamingDrmSessionManager implements DrmSessionManager {
}
@Override
public void open(Map<UUID, byte[]> psshData, String mimeType) {
public void open(DrmInitData drmInitData) {
if (++openCount != 1) {
return;
}
......@@ -178,8 +177,8 @@ public class StreamingDrmSessionManager implements DrmSessionManager {
postRequestHandler = new PostRequestHandler(requestHandlerThread.getLooper());
}
if (this.schemePsshData == null) {
this.mimeType = mimeType;
schemePsshData = psshData.get(uuid);
mimeType = drmInitData.mimeType;
schemePsshData = drmInitData.get(uuid);
if (schemePsshData == null) {
onError(new IllegalStateException("Media does not support uuid: " + uuid));
return;
......
......@@ -19,6 +19,7 @@ import com.google.android.exoplayer.C;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.hls.parser.AdtsExtractor;
import com.google.android.exoplayer.hls.parser.HlsExtractor;
import com.google.android.exoplayer.hls.parser.HlsExtractorWrapper;
import com.google.android.exoplayer.hls.parser.TsExtractor;
import com.google.android.exoplayer.upstream.Aes128DataSource;
import com.google.android.exoplayer.upstream.BandwidthMeter;
......@@ -305,7 +306,7 @@ public class HlsChunkSource {
Uri chunkUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, segment.url);
// Check if encryption is specified.
if (HlsMediaPlaylist.ENCRYPTION_METHOD_AES_128.equals(segment.encryptionMethod)) {
if (segment.isEncrypted) {
Uri keyUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, segment.encryptionKeyUri);
if (!keyUri.equals(encryptionKeyUri)) {
// Encryption is specified and the key has changed.
......@@ -341,16 +342,17 @@ public class HlsChunkSource {
boolean isLastChunk = !mediaPlaylist.live && chunkIndex == mediaPlaylist.segments.size() - 1;
// Configure the extractor that will read the chunk.
HlsExtractor extractor;
HlsExtractorWrapper extractorWrapper;
if (previousTsChunk == null || segment.discontinuity || switchingVariant || liveDiscontinuity) {
extractor = chunkUri.getLastPathSegment().endsWith(AAC_FILE_EXTENSION)
? new AdtsExtractor(switchingVariantSpliced, startTimeUs, bufferPool)
: new TsExtractor(switchingVariantSpliced, startTimeUs, bufferPool);
HlsExtractor extractor = chunkUri.getLastPathSegment().endsWith(AAC_FILE_EXTENSION)
? new AdtsExtractor(startTimeUs)
: new TsExtractor(startTimeUs);
extractorWrapper = new HlsExtractorWrapper(bufferPool, extractor, switchingVariantSpliced);
} else {
extractor = previousTsChunk.extractor;
extractorWrapper = previousTsChunk.extractor;
}
return new TsChunk(dataSource, dataSpec, extractor, enabledVariants[variantIndex].index,
return new TsChunk(dataSource, dataSpec, extractorWrapper, enabledVariants[variantIndex].index,
startTimeUs, endTimeUs, chunkMediaSequence, isLastChunk);
}
......@@ -387,16 +389,24 @@ public class HlsChunkSource {
private int getNextVariantIndex(TsChunk previousTsChunk, long playbackPositionUs) {
clearStaleBlacklistedPlaylists();
if (previousTsChunk == null) {
// Don't consider switching if we don't have a previous chunk.
return variantIndex;
}
long bitrateEstimate = bandwidthMeter.getBitrateEstimate();
if (bitrateEstimate == BandwidthMeter.NO_ESTIMATE) {
// Don't consider switching if we don't have a bandwidth estimate.
return variantIndex;
}
int idealVariantIndex = getVariantIndexForBandwdith(
(int) (bandwidthMeter.getBitrateEstimate() * BANDWIDTH_FRACTION));
(int) (bitrateEstimate * BANDWIDTH_FRACTION));
if (idealVariantIndex == variantIndex) {
// We're already using the ideal variant.
return variantIndex;
}
// We're not using the ideal variant for the available bandwidth, but only switch if the
// conditions are appropriate.
long bufferedPositionUs = previousTsChunk == null ? playbackPositionUs
: adaptiveMode == ADAPTIVE_MODE_SPLICE ? previousTsChunk.startTimeUs
long bufferedPositionUs = adaptiveMode == ADAPTIVE_MODE_SPLICE ? previousTsChunk.startTimeUs
: previousTsChunk.endTimeUs;
long bufferedUs = bufferedPositionUs - playbackPositionUs;
if (mediaPlaylistBlacklistTimesMs[variantIndex] != 0
......
......@@ -23,10 +23,12 @@ import java.util.List;
public final class HlsMasterPlaylist extends HlsPlaylist {
public final List<Variant> variants;
public final List<Subtitle> subtitles;
public HlsMasterPlaylist(String baseUri, List<Variant> variants) {
public HlsMasterPlaylist(String baseUri, List<Variant> variants, List<Subtitle> subtitles) {
super(baseUri, HlsPlaylist.TYPE_MASTER);
this.variants = variants;
this.subtitles = subtitles;
}
}
......@@ -28,24 +28,25 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
* Media segment reference.
*/
public static final class Segment implements Comparable<Long> {
public final boolean discontinuity;
public final double durationSecs;
public final String url;
public final long startTimeUs;
public final String encryptionMethod;
public final boolean isEncrypted;
public final String encryptionKeyUri;
public final String encryptionIV;
public final int byterangeOffset;
public final int byterangeLength;
public Segment(String uri, double durationSecs, boolean discontinuity, long startTimeUs,
String encryptionMethod, String encryptionKeyUri, String encryptionIV,
int byterangeOffset, int byterangeLength) {
boolean isEncrypted, String encryptionKeyUri, String encryptionIV, int byterangeOffset,
int byterangeLength) {
this.url = uri;
this.durationSecs = durationSecs;
this.discontinuity = discontinuity;
this.startTimeUs = startTimeUs;
this.encryptionMethod = encryptionMethod;
this.isEncrypted = isEncrypted;
this.encryptionKeyUri = encryptionKeyUri;
this.encryptionIV = encryptionIV;
this.byterangeOffset = byterangeOffset;
......
......@@ -25,6 +25,8 @@ import java.util.regex.Pattern;
*/
/* package */ class HlsParserUtil {
private static final String BOOLEAN_YES = "YES";
private HlsParserUtil() {}
public static String parseStringAttr(String line, Pattern pattern, String tag)
......@@ -36,14 +38,6 @@ import java.util.regex.Pattern;
throw new ParserException(String.format("Couldn't match %s tag in %s", tag, line));
}
public static String parseOptionalStringAttr(String line, Pattern pattern) {
Matcher matcher = pattern.matcher(line);
if (matcher.find() && matcher.groupCount() == 1) {
return matcher.group(1);
}
return null;
}
public static int parseIntAttr(String line, Pattern pattern, String tag)
throws ParserException {
return Integer.parseInt(parseStringAttr(line, pattern, tag));
......@@ -54,4 +48,20 @@ import java.util.regex.Pattern;
return Double.parseDouble(parseStringAttr(line, pattern, tag));
}
public static String parseOptionalStringAttr(String line, Pattern pattern) {
Matcher matcher = pattern.matcher(line);
if (matcher.find() && matcher.groupCount() == 1) {
return matcher.group(1);
}
return null;
}
public static boolean parseOptionalBoolAttr(String line, Pattern pattern) {
Matcher matcher = pattern.matcher(line);
if (matcher.find() && matcher.groupCount() == 1) {
return BOOLEAN_YES.equals(matcher.group(1));
}
return false;
}
}
......@@ -21,7 +21,7 @@ import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.SampleSource;
import com.google.android.exoplayer.TrackInfo;
import com.google.android.exoplayer.TrackRenderer;
import com.google.android.exoplayer.hls.parser.HlsExtractor;
import com.google.android.exoplayer.hls.parser.HlsExtractorWrapper;
import com.google.android.exoplayer.upstream.Loader;
import com.google.android.exoplayer.upstream.Loader.Loadable;
import com.google.android.exoplayer.util.Assertions;
......@@ -44,7 +44,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
private static final int NO_RESET_PENDING = -1;
private final HlsChunkSource chunkSource;
private final LinkedList<HlsExtractor> extractors;
private final LinkedList<HlsExtractorWrapper> extractors;
private final boolean frameAccurateSeeking;
private final int minLoadableRetryCount;
......@@ -83,7 +83,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
this.frameAccurateSeeking = frameAccurateSeeking;
this.remainingReleaseCount = downstreamRendererCount;
this.minLoadableRetryCount = minLoadableRetryCount;
extractors = new LinkedList<HlsExtractor>();
extractors = new LinkedList<HlsExtractorWrapper>();
}
@Override
......@@ -96,7 +96,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
}
continueBufferingInternal();
if (!extractors.isEmpty()) {
HlsExtractor extractor = extractors.getFirst();
HlsExtractorWrapper extractor = extractors.getFirst();
if (extractor.isPrepared()) {
trackCount = extractor.getTrackCount();
trackEnabledStates = new boolean[trackCount];
......@@ -195,7 +195,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
return NOTHING_READ;
}
HlsExtractor extractor = getCurrentExtractor();
HlsExtractorWrapper extractor = getCurrentExtractor();
if (extractors.size() > 1) {
// If there's more than one extractor, attempt to configure a seamless splice from the
// current one to the next one.
......@@ -328,8 +328,8 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
*
* @return The current extractor from which samples should be read. Guaranteed to be non-null.
*/
private HlsExtractor getCurrentExtractor() {
HlsExtractor extractor = extractors.getFirst();
private HlsExtractorWrapper getCurrentExtractor() {
HlsExtractorWrapper extractor = extractors.getFirst();
while (extractors.size() > 1 && !haveSamplesForEnabledTracks(extractor)) {
// We're finished reading from the extractor for all tracks, and so can discard it.
extractors.removeFirst().release();
......@@ -338,7 +338,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
return extractor;
}
private void discardSamplesForDisabledTracks(HlsExtractor extractor, long timeUs) {
private void discardSamplesForDisabledTracks(HlsExtractorWrapper extractor, long timeUs) {
if (!extractor.isPrepared()) {
return;
}
......@@ -349,7 +349,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
}
}
private boolean haveSamplesForEnabledTracks(HlsExtractor extractor) {
private boolean haveSamplesForEnabledTracks(HlsExtractorWrapper extractor) {
if (!extractor.isPrepared()) {
return false;
}
......
/*
* 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;
/**
* Subtitle media tag.
*/
public final class Subtitle {
public final String name;
public final String uri;
public final String language;
public final boolean isDefault;
public final boolean autoSelect;
public Subtitle(String name, String uri, String language, boolean isDefault, boolean autoSelect) {
this.name = name;
this.uri = uri;
this.language = language;
this.autoSelect = autoSelect;
this.isDefault = isDefault;
}
}
......@@ -15,7 +15,9 @@
*/
package com.google.android.exoplayer.hls;
import com.google.android.exoplayer.hls.parser.HlsExtractor;
import com.google.android.exoplayer.hls.parser.DataSourceExtractorInput;
import com.google.android.exoplayer.hls.parser.HlsExtractor.ExtractorInput;
import com.google.android.exoplayer.hls.parser.HlsExtractorWrapper;
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DataSpec;
......@@ -26,8 +28,6 @@ import java.io.IOException;
*/
public final class TsChunk extends HlsChunk {
private static final byte[] SCRATCH_SPACE = new byte[4096];
/**
* The index of the variant in the master playlist.
*/
......@@ -51,7 +51,7 @@ public final class TsChunk extends HlsChunk {
/**
* The extractor into which this chunk is being consumed.
*/
public final HlsExtractor extractor;
public final HlsExtractorWrapper extractor;
private int loadPosition;
private volatile boolean loadFinished;
......@@ -67,7 +67,7 @@ public final class TsChunk extends HlsChunk {
* @param chunkIndex The index of the chunk.
* @param isLastChunk True if this is the last chunk in the media. False otherwise.
*/
public TsChunk(DataSource dataSource, DataSpec dataSpec, HlsExtractor extractor,
public TsChunk(DataSource dataSource, DataSpec dataSpec, HlsExtractorWrapper extractor,
int variantIndex, long startTimeUs, long endTimeUs, int chunkIndex, boolean isLastChunk) {
super(dataSource, dataSpec);
this.extractor = extractor;
......@@ -102,30 +102,23 @@ public final class TsChunk extends HlsChunk {
@Override
public void load() throws IOException, InterruptedException {
ExtractorInput input = new DataSourceExtractorInput(dataSource, 0);
try {
dataSource.open(dataSpec);
int bytesRead = 0;
int bytesSkipped = 0;
// If we previously fed part of this chunk to the extractor, skip it this time.
// TODO: Ideally we'd construct a dataSpec that only loads the remainder of the data here,
// rather than loading the whole chunk again and then skipping data we previously loaded. To
// do this is straightforward for non-encrypted content, but more complicated for content
// encrypted with AES, for which we'll need to modify the way that decryption is performed.
while (bytesRead != -1 && !loadCanceled && bytesSkipped < loadPosition) {
int skipLength = Math.min(loadPosition - bytesSkipped, SCRATCH_SPACE.length);
bytesRead = dataSource.read(SCRATCH_SPACE, 0, skipLength);
if (bytesRead != -1) {
bytesSkipped += bytesRead;
}
}
// Feed the remaining data into the extractor.
while (bytesRead != -1 && !loadCanceled) {
bytesRead = extractor.read(dataSource);
if (bytesRead != -1) {
loadPosition += bytesRead;
input.skipFully(loadPosition);
try {
while (!input.isEnded() && !loadCanceled) {
extractor.read(input);
}
} finally {
loadPosition = (int) input.getPosition();
loadFinished = !loadCanceled;
}
loadFinished = !loadCanceled;
} finally {
dataSource.close();
}
......
......@@ -15,11 +15,6 @@
*/
package com.google.android.exoplayer.hls.parser;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.upstream.BufferPool;
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.ParsableByteArray;
import java.io.IOException;
......@@ -28,82 +23,37 @@ import java.io.IOException;
* Facilitates the extraction of AAC samples from elementary audio files formatted as AAC with ADTS
* headers.
*/
public class AdtsExtractor extends HlsExtractor {
public class AdtsExtractor implements HlsExtractor {
private static final int MAX_PACKET_SIZE = 200;
private final long firstSampleTimestamp;
private final ParsableByteArray packetBuffer;
private final AdtsReader adtsReader;
// Accessed only by the loading thread.
private AdtsReader adtsReader;
private boolean firstPacket;
// Accessed by both the loading and consuming threads.
private volatile boolean prepared;
public AdtsExtractor(boolean shouldSpliceIn, long firstSampleTimestamp, BufferPool bufferPool) {
super(shouldSpliceIn);
public AdtsExtractor(long firstSampleTimestamp) {
this.firstSampleTimestamp = firstSampleTimestamp;
packetBuffer = new ParsableByteArray(MAX_PACKET_SIZE);
adtsReader = new AdtsReader(bufferPool);
firstPacket = true;
}
@Override
public int getTrackCount() {
Assertions.checkState(prepared);
return 1;
public void init(TrackOutputBuilder output) {
adtsReader = new AdtsReader(output.buildOutput(0));
output.allOutputsBuilt();
}
@Override
public MediaFormat getFormat(int track) {
Assertions.checkState(prepared);
return adtsReader.getMediaFormat();
}
@Override
public boolean isPrepared() {
return prepared;
}
@Override
public void release() {
adtsReader.release();
}
@Override
public long getLargestSampleTimestamp() {
return adtsReader.getLargestParsedTimestampUs();
}
@Override
public boolean getSample(int track, SampleHolder holder) {
Assertions.checkState(prepared);
Assertions.checkState(track == 0);
return adtsReader.getSample(holder);
}
@Override
public void discardUntil(int track, long timeUs) {
Assertions.checkState(prepared);
Assertions.checkState(track == 0);
adtsReader.discardUntil(timeUs);
}
@Override
public boolean hasSamples(int track) {
Assertions.checkState(prepared);
Assertions.checkState(track == 0);
return !adtsReader.isEmpty();
}
@Override
public int read(DataSource dataSource) throws IOException {
int bytesRead = dataSource.read(packetBuffer.data, 0, MAX_PACKET_SIZE);
public void read(ExtractorInput input) throws IOException, InterruptedException {
int bytesRead = input.read(packetBuffer.data, 0, MAX_PACKET_SIZE);
if (bytesRead == -1) {
return -1;
return;
}
// Feed whatever data we have to the reader, regardless of whether the read finished or not.
packetBuffer.setPosition(0);
packetBuffer.setLimit(bytesRead);
......@@ -111,16 +61,6 @@ public class AdtsExtractor extends HlsExtractor {
// unnecessary to copy the data through packetBuffer.
adtsReader.consume(packetBuffer, firstSampleTimestamp, firstPacket);
firstPacket = false;
if (!prepared) {
prepared = adtsReader.hasMediaFormat();
}
return bytesRead;
}
@Override
protected SampleQueue getSampleQueue(int track) {
Assertions.checkState(track == 0);
return adtsReader;
}
}
......@@ -17,7 +17,7 @@ package com.google.android.exoplayer.hls.parser;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.upstream.BufferPool;
import com.google.android.exoplayer.hls.parser.HlsExtractor.TrackOutput;
import com.google.android.exoplayer.util.CodecSpecificDataUtil;
import com.google.android.exoplayer.util.MimeTypes;
import com.google.android.exoplayer.util.ParsableBitArray;
......@@ -55,8 +55,8 @@ import java.util.Collections;
// Used when reading the samples.
private long timeUs;
public AdtsReader(BufferPool bufferPool) {
super(bufferPool);
public AdtsReader(TrackOutput output) {
super(output);
adtsScratch = new ParsableBitArray(new byte[HEADER_SIZE + CRC_SIZE]);
state = STATE_FINDING_SYNC;
}
......@@ -78,17 +78,17 @@ import java.util.Collections;
int targetLength = hasCrc ? HEADER_SIZE + CRC_SIZE : HEADER_SIZE;
if (continueRead(data, adtsScratch.getData(), targetLength)) {
parseHeader();
startSample(timeUs);
output.startSample(timeUs, 0);
bytesRead = 0;
state = STATE_READING_SAMPLE;
}
break;
case STATE_READING_SAMPLE:
int bytesToRead = Math.min(data.bytesLeft(), sampleSize - bytesRead);
appendData(data, bytesToRead);
output.appendData(data, bytesToRead);
bytesRead += bytesToRead;
if (bytesRead == sampleSize) {
commitSample(true);
output.commitSample(C.SAMPLE_FLAG_SYNC, 0, null);
timeUs += frameDurationUs;
bytesRead = 0;
state = STATE_FINDING_SYNC;
......@@ -152,7 +152,7 @@ import java.util.Collections;
private void parseHeader() {
adtsScratch.setPosition(0);
if (!hasMediaFormat()) {
if (!output.hasFormat()) {
int audioObjectType = adtsScratch.readBits(2) + 1;
int sampleRateIndex = adtsScratch.readBits(4);
adtsScratch.skipBits(1);
......@@ -167,7 +167,7 @@ import java.util.Collections;
MediaFormat.NO_VALUE, audioParams.second, audioParams.first,
Collections.singletonList(audioSpecificConfig));
frameDurationUs = (C.MICROS_PER_SECOND * 1024L) / mediaFormat.sampleRate;
setMediaFormat(mediaFormat);
output.setFormat(mediaFormat);
} else {
adtsScratch.skipBits(10);
}
......
/*
* 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.parser;
import com.google.android.exoplayer.hls.parser.HlsExtractor.ExtractorInput;
import com.google.android.exoplayer.upstream.DataSource;
import java.io.IOException;
/**
* An {@link ExtractorInput} that wraps a {@link DataSource}.
*/
public final class DataSourceExtractorInput implements ExtractorInput {
private static final byte[] SCRATCH_SPACE = new byte[4096];
private final DataSource dataSource;
private long position;
private boolean isEnded;
/**
* @param dataSource The wrapped {@link DataSource}.
* @param position The initial position in the stream.
*/
public DataSourceExtractorInput(DataSource dataSource, long position) {
this.dataSource = dataSource;
this.position = position;
}
@Override
public int read(byte[] target, int offset, int length) throws IOException, InterruptedException {
if (Thread.interrupted()) {
throw new InterruptedException();
}
int bytesRead = dataSource.read(target, offset, length);
if (bytesRead == -1) {
isEnded = true;
return -1;
}
position += bytesRead;
return bytesRead;
}
@Override
public boolean readFully(byte[] target, int offset, int length)
throws IOException, InterruptedException {
int remaining = length;
while (remaining > 0) {
if (Thread.interrupted()) {
throw new InterruptedException();
}
int bytesRead = dataSource.read(target, offset, remaining);
if (bytesRead == -1) {
isEnded = true;
return false;
}
offset += bytesRead;
remaining -= bytesRead;
}
position += length;
return true;
}
@Override
public boolean skipFully(int length) throws IOException, InterruptedException {
int remaining = length;
while (remaining > 0) {
if (Thread.interrupted()) {
throw new InterruptedException();
}
int bytesRead = dataSource.read(SCRATCH_SPACE, 0, Math.min(SCRATCH_SPACE.length, remaining));
if (bytesRead == -1) {
isEnded = true;
return false;
}
remaining -= bytesRead;
}
position += length;
return true;
}
@Override
public long getPosition() {
return position;
}
@Override
public boolean isEnded() {
return isEnded;
}
}
......@@ -15,16 +15,21 @@
*/
package com.google.android.exoplayer.hls.parser;
import com.google.android.exoplayer.upstream.BufferPool;
import com.google.android.exoplayer.hls.parser.HlsExtractor.TrackOutput;
import com.google.android.exoplayer.util.ParsableByteArray;
/**
* Extracts individual samples from an elementary media stream, preserving original order.
*/
/* package */ abstract class ElementaryStreamReader extends SampleQueue {
/* package */ abstract class ElementaryStreamReader {
protected ElementaryStreamReader(BufferPool bufferPool) {
super(bufferPool);
protected final TrackOutput output;
/**
* @param output A {@link TrackOutput} to which samples should be written.
*/
protected ElementaryStreamReader(TrackOutput output) {
this.output = output;
}
/**
......
......@@ -15,9 +15,10 @@
*/
package com.google.android.exoplayer.hls.parser;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.hls.parser.HlsExtractor.TrackOutput;
import com.google.android.exoplayer.mp4.Mp4Util;
import com.google.android.exoplayer.upstream.BufferPool;
import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.MimeTypes;
import com.google.android.exoplayer.util.ParsableBitArray;
......@@ -43,18 +44,20 @@ import java.util.List;
private final NalUnitTargetBuffer sps;
private final NalUnitTargetBuffer pps;
private final NalUnitTargetBuffer sei;
private final ParsableByteArray seiWrapper;
private int scratchEscapeCount;
private int[] scratchEscapePositions;
private boolean isKeyframe;
public H264Reader(BufferPool bufferPool, SeiReader seiReader) {
super(bufferPool);
public H264Reader(TrackOutput output, SeiReader seiReader) {
super(output);
this.seiReader = seiReader;
prefixFlags = new boolean[3];
sps = new NalUnitTargetBuffer(NAL_UNIT_TYPE_SPS, 128);
pps = new NalUnitTargetBuffer(NAL_UNIT_TYPE_PPS, 128);
sei = new NalUnitTargetBuffer(NAL_UNIT_TYPE_SEI, 128);
seiWrapper = new ParsableByteArray();
scratchEscapePositions = new int[10];
}
......@@ -66,7 +69,7 @@ import java.util.List;
byte[] dataArray = data.data;
// Append the data to the buffer.
appendData(data, data.bytesLeft());
output.appendData(data, data.bytesLeft());
// Scan the appended data, processing NAL units as they are encountered
while (offset < limit) {
......@@ -84,13 +87,13 @@ import java.util.List;
int nalUnitType = Mp4Util.getNalUnitType(dataArray, nextNalUnitOffset);
int nalUnitOffsetInData = nextNalUnitOffset - limit;
if (nalUnitType == NAL_UNIT_TYPE_AUD) {
if (writingSample()) {
if (isKeyframe && !hasMediaFormat() && sps.isCompleted() && pps.isCompleted()) {
if (output.isWritingSample()) {
if (isKeyframe && !output.hasFormat() && sps.isCompleted() && pps.isCompleted()) {
parseMediaFormat(sps, pps);
}
commitSample(isKeyframe, nalUnitOffsetInData);
output.commitSample(isKeyframe ? C.SAMPLE_FLAG_SYNC : 0, nalUnitOffsetInData, null);
}
startSample(pesTimeUs, nalUnitOffsetInData);
output.startSample(pesTimeUs, nalUnitOffsetInData);
isKeyframe = false;
} else if (nalUnitType == NAL_UNIT_TYPE_IDR) {
isKeyframe = true;
......@@ -117,7 +120,7 @@ import java.util.List;
}
private void feedNalUnitTargetBuffersStart(int nalUnitType) {
if (!hasMediaFormat()) {
if (!output.hasFormat()) {
sps.startNalUnit(nalUnitType);
pps.startNalUnit(nalUnitType);
}
......@@ -125,7 +128,7 @@ import java.util.List;
}
private void feedNalUnitTargetBuffersData(byte[] dataArray, int offset, int limit) {
if (!hasMediaFormat()) {
if (!output.hasFormat()) {
sps.appendToNalUnit(dataArray, offset, limit);
pps.appendToNalUnit(dataArray, offset, limit);
}
......@@ -137,7 +140,8 @@ import java.util.List;
pps.endNalUnit(discardPadding);
if (sei.endNalUnit(discardPadding)) {
int unescapedLength = unescapeStream(sei.nalData, sei.nalLength);
seiReader.read(sei.nalData, 0, unescapedLength, pesTimeUs);
seiWrapper.reset(sei.nalData, unescapedLength);
seiReader.consume(seiWrapper, pesTimeUs, true);
}
}
......@@ -229,7 +233,7 @@ import java.util.List;
}
// Set the format.
setMediaFormat(MediaFormat.createVideoFormat(MimeTypes.VIDEO_H264, MediaFormat.NO_VALUE,
output.setFormat(MediaFormat.createVideoFormat(MimeTypes.VIDEO_H264, MediaFormat.NO_VALUE,
frameWidth, frameHeight, initializationData));
}
......
......@@ -16,136 +16,138 @@
package com.google.android.exoplayer.hls.parser;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.util.ParsableByteArray;
import java.io.IOException;
/**
* Facilitates extraction of media samples for HLS playbacks.
*/
// TODO: Consider consolidating more common logic in this base class.
public abstract class HlsExtractor {
public interface HlsExtractor {
private final boolean shouldSpliceIn;
// Accessed only by the consuming thread.
private boolean spliceConfigured;
/**
* An object from which source data can be read.
*/
public interface ExtractorInput {
/**
* Reads up to {@code length} bytes from the input.
* <p>
* This method blocks until at least one byte of data can be read, the end of the input is
* detected, or an exception is thrown.
*
* @param target A target array into which data should be written.
* @param offset The offset into the target array at which to write.
* @param length The maximum number of bytes to read from the input.
* @return The number of bytes read, or -1 if the input has ended.
* @throws IOException If an error occurs reading from the input.
* @throws InterruptedException If the thread has been interrupted.
*/
int read(byte[] target, int offset, int length) throws IOException, InterruptedException;
/**
* Like {@link #read(byte[], int, int)}, but guaranteed to read request {@code length} in full
* unless the end of the input is detected, or an exception is thrown.
*
* TODO: Firm up behavior of this method if (a) zero bytes are read before EOS, (b) the read
* is partially satisfied before EOS.
*
* @param target A target array into which data should be written.
* @param offset The offset into the target array at which to write.
* @param length The number of bytes to read from the input.
* @return True if the read was successful. False if the end of the input was reached.
* @throws IOException If an error occurs reading from the input.
* @throws InterruptedException If the thread has been interrupted.
*/
boolean readFully(byte[] target, int offset, int length)
throws IOException, InterruptedException;
/**
* Like {@link #readFully(byte[], int, int)}, except the data is skipped instead of read.
*
* TODO: Firm up behavior of this method if (a) zero bytes are skipped before EOS, (b) the skip
* is partially satisfied before EOS.
*
* @param length The number of bytes to skip from the input.
* @return True if the read was successful. False if the end of the input was reached.
* @throws IOException If an error occurs reading from the input.
* @throws InterruptedException If the thread is interrupted.
*/
boolean skipFully(int length) throws IOException, InterruptedException;
/**
* The current position in the stream.
*
* @return The position in the stream.
*/
long getPosition();
/**
* Whether or not the input has ended.
*
* @return True if the input has ended. False otherwise.
*/
boolean isEnded();
public HlsExtractor(boolean shouldSpliceIn) {
this.shouldSpliceIn = shouldSpliceIn;
}
/**
* Attempts to configure a splice from this extractor to the next.
* <p>
* The splice is performed such that for each track the samples read from the next extractor
* start with a keyframe, and continue from where the samples read from this extractor finish.
* A successful splice may discard samples from either or both extractors.
* <p>
* Splice configuration may fail if the next extractor is not yet in a state that allows the
* splice to be performed. Calling this method is a noop if the splice has already been
* configured. Hence this method should be called repeatedly during the window within which a
* splice can be performed.
*
* @param nextExtractor The extractor being spliced to.
* An object to which extracted data should be output.
*/
public final void configureSpliceTo(HlsExtractor nextExtractor) {
if (spliceConfigured || !nextExtractor.shouldSpliceIn || !nextExtractor.isPrepared()) {
// The splice is already configured, or the next extractor doesn't want to be spliced in, or
// the next extractor isn't ready to be spliced in.
return;
}
boolean spliceConfigured = true;
int trackCount = getTrackCount();
for (int i = 0; i < trackCount; i++) {
spliceConfigured &= getSampleQueue(i).configureSpliceTo(nextExtractor.getSampleQueue(i));
}
this.spliceConfigured = spliceConfigured;
return;
public interface TrackOutputBuilder {
/**
* Invoked to build a {@link TrackOutput} to which data should be output for a given track.
*
* @param trackId A stable track id.
* @return The corresponding {@link TrackOutput}.
*/
TrackOutput buildOutput(int trackId);
/**
* Invoked when all {@link TrackOutput}s have been built, meaning {@link #buildOutput(int)}
* will not be invoked again.
*/
void allOutputsBuilt();
}
/**
* Gets the number of available tracks.
* <p>
* This method should only be called after the extractor has been prepared.
*
* @return The number of available tracks.
* An object to which extracted data belonging to a given track should be output.
*/
public abstract int getTrackCount();
public interface TrackOutput {
/**
* Gets the format of the specified track.
* <p>
* This method must only be called after the extractor has been prepared.
*
* @param track The track index.
* @return The corresponding format.
*/
public abstract MediaFormat getFormat(int track);
boolean hasFormat();
/**
* Whether the extractor is prepared.
*
* @return True if the extractor is prepared. False otherwise.
*/
public abstract boolean isPrepared();
void setFormat(MediaFormat format);
/**
* Releases the extractor, recycling any pending or incomplete samples to the sample pool.
* <p>
* This method should not be called whilst {@link #read(DataSource)} is also being invoked.
*/
public abstract void release();
boolean isWritingSample();
/**
* Gets the largest timestamp of any sample parsed by the extractor.
*
* @return The largest timestamp, or {@link Long#MIN_VALUE} if no samples have been parsed.
*/
public abstract long getLargestSampleTimestamp();
int appendData(DataSource dataSource, int length) throws IOException;
/**
* Gets the next sample for the specified track.
*
* @param track The track from which to read.
* @param holder A {@link SampleHolder} into which the sample should be read.
* @return True if a sample was read. False otherwise.
*/
public abstract boolean getSample(int track, SampleHolder holder);
void appendData(ParsableByteArray data, int length);
/**
* Discards samples for the specified track up to the specified time.
*
* @param track The track from which samples should be discarded.
* @param timeUs The time up to which samples should be discarded, in microseconds.
*/
public abstract void discardUntil(int track, long timeUs);
void startSample(long timeUs, int offset);
/**
* Whether samples are available for reading from {@link #getSample(int, SampleHolder)} for the
* specified track.
*
* @return True if samples are available for reading from {@link #getSample(int, SampleHolder)}
* for the specified track. False otherwise.
*/
public abstract boolean hasSamples(int track);
void commitSample(int flags, int offset, byte[] encryptionKey);
}
/**
* Reads up to a single TS packet.
* Initializes the extractor.
*
* @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.
* @param output A {@link TrackOutputBuilder} to which extracted data should be output.
*/
public abstract int read(DataSource dataSource) throws IOException;
void init(TrackOutputBuilder output);
/**
* Gets the {@link SampleQueue} for the specified track.
* Reads from the provided {@link ExtractorInput}.
*
* @param track The track index.
* @return The corresponding sample queue.
* @param input The {@link ExtractorInput} from which to read.
* @throws IOException If an error occurred reading from the source.
* @throws InterruptedException If the thread was interrupted.
*/
protected abstract SampleQueue getSampleQueue(int track);
void read(ExtractorInput input) throws IOException, InterruptedException;
}
/*
* 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.parser;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.hls.parser.HlsExtractor.ExtractorInput;
import com.google.android.exoplayer.hls.parser.HlsExtractor.TrackOutput;
import com.google.android.exoplayer.upstream.BufferPool;
import com.google.android.exoplayer.util.Assertions;
import android.util.SparseArray;
import java.io.IOException;
/**
* Wraps a {@link HlsExtractor}, adding functionality to enable reading of the extracted samples.
*/
public final class HlsExtractorWrapper implements HlsExtractor.TrackOutputBuilder {
private final BufferPool bufferPool;
private final HlsExtractor extractor;
private final SparseArray<SampleQueue> sampleQueues;
private final boolean shouldSpliceIn;
private volatile boolean outputsBuilt;
// Accessed only by the consuming thread.
private boolean prepared;
private boolean spliceConfigured;
public HlsExtractorWrapper(BufferPool bufferPool, HlsExtractor extractor,
boolean shouldSpliceIn) {
this.bufferPool = bufferPool;
this.extractor = extractor;
this.shouldSpliceIn = shouldSpliceIn;
sampleQueues = new SparseArray<SampleQueue>();
extractor.init(this);
}
/**
* Attempts to configure a splice from this extractor to the next.
* <p>
* The splice is performed such that for each track the samples read from the next extractor
* start with a keyframe, and continue from where the samples read from this extractor finish.
* A successful splice may discard samples from either or both extractors.
* <p>
* Splice configuration may fail if the next extractor is not yet in a state that allows the
* splice to be performed. Calling this method is a noop if the splice has already been
* configured. Hence this method should be called repeatedly during the window within which a
* splice can be performed.
*
* @param nextExtractor The extractor being spliced to.
*/
public final void configureSpliceTo(HlsExtractorWrapper nextExtractor) {
if (spliceConfigured || !nextExtractor.shouldSpliceIn || !nextExtractor.isPrepared()) {
// The splice is already configured, or the next extractor doesn't want to be spliced in, or
// the next extractor isn't ready to be spliced in.
return;
}
boolean spliceConfigured = true;
int trackCount = getTrackCount();
for (int i = 0; i < trackCount; i++) {
SampleQueue currentSampleQueue = sampleQueues.valueAt(i);
SampleQueue nextSampleQueue = nextExtractor.sampleQueues.valueAt(i);
spliceConfigured &= currentSampleQueue.configureSpliceTo(nextSampleQueue);
}
this.spliceConfigured = spliceConfigured;
return;
}
/**
* Gets the number of available tracks.
* <p>
* This method should only be called after the extractor has been prepared.
*
* @return The number of available tracks.
*/
public int getTrackCount() {
return sampleQueues.size();
}
/**
* Gets the format of the specified track.
* <p>
* This method must only be called after the extractor has been prepared.
*
* @param track The track index.
* @return The corresponding format.
*/
public MediaFormat getFormat(int track) {
return sampleQueues.valueAt(track).getFormat();
}
/**
* Whether the extractor is prepared.
*
* @return True if the extractor is prepared. False otherwise.
*/
public boolean isPrepared() {
if (!prepared && outputsBuilt) {
for (int i = 0; i < sampleQueues.size(); i++) {
if (!sampleQueues.valueAt(i).hasFormat()) {
return false;
}
}
prepared = true;
}
return prepared;
}
/**
* Releases the extractor, recycling any pending or incomplete samples to the sample pool.
* <p>
* This method should not be called whilst {@link #read(ExtractorInput)} is also being invoked.
*/
public void release() {
for (int i = 0; i < sampleQueues.size(); i++) {
sampleQueues.valueAt(i).release();
}
}
/**
* Gets the largest timestamp of any sample parsed by the extractor.
*
* @return The largest timestamp, or {@link Long#MIN_VALUE} if no samples have been parsed.
*/
public long getLargestSampleTimestamp() {
long largestParsedTimestampUs = Long.MIN_VALUE;
for (int i = 0; i < sampleQueues.size(); i++) {
largestParsedTimestampUs = Math.max(largestParsedTimestampUs,
sampleQueues.valueAt(i).getLargestParsedTimestampUs());
}
return largestParsedTimestampUs;
}
/**
* Gets the next sample for the specified track.
*
* @param track The track from which to read.
* @param holder A {@link SampleHolder} into which the sample should be read.
* @return True if a sample was read. False otherwise.
*/
public boolean getSample(int track, SampleHolder holder) {
Assertions.checkState(isPrepared());
return sampleQueues.valueAt(track).getSample(holder);
}
/**
* Discards samples for the specified track up to the specified time.
*
* @param track The track from which samples should be discarded.
* @param timeUs The time up to which samples should be discarded, in microseconds.
*/
public void discardUntil(int track, long timeUs) {
Assertions.checkState(isPrepared());
sampleQueues.valueAt(track).discardUntil(timeUs);
}
/**
* Whether samples are available for reading from {@link #getSample(int, SampleHolder)} for the
* specified track.
*
* @return True if samples are available for reading from {@link #getSample(int, SampleHolder)}
* for the specified track. False otherwise.
*/
public boolean hasSamples(int track) {
Assertions.checkState(isPrepared());
return !sampleQueues.valueAt(track).isEmpty();
}
/**
* Reads from the provided {@link ExtractorInput}.
*
* @param input The {@link ExtractorInput} from which to read.
* @throws IOException If an error occurred reading from the source.
* @throws InterruptedException If the thread was interrupted.
*/
public void read(ExtractorInput input) throws IOException, InterruptedException {
extractor.read(input);
}
// ExtractorOutput implementation.
@Override
public TrackOutput buildOutput(int id) {
SampleQueue sampleQueue = new SampleQueue(bufferPool);
sampleQueues.put(id, sampleQueue);
return sampleQueue;
}
@Override
public void allOutputsBuilt() {
this.outputsBuilt = true;
}
}
......@@ -15,8 +15,9 @@
*/
package com.google.android.exoplayer.hls.parser;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.upstream.BufferPool;
import com.google.android.exoplayer.hls.parser.HlsExtractor.TrackOutput;
import com.google.android.exoplayer.util.ParsableByteArray;
/**
......@@ -24,24 +25,24 @@ import com.google.android.exoplayer.util.ParsableByteArray;
*/
/* package */ class Id3Reader extends ElementaryStreamReader {
public Id3Reader(BufferPool bufferPool) {
super(bufferPool);
setMediaFormat(MediaFormat.createId3Format());
public Id3Reader(TrackOutput output) {
super(output);
output.setFormat(MediaFormat.createId3Format());
}
@Override
public void consume(ParsableByteArray data, long pesTimeUs, boolean startOfPacket) {
if (startOfPacket) {
startSample(pesTimeUs);
output.startSample(pesTimeUs, 0);
}
if (writingSample()) {
appendData(data, data.bytesLeft());
if (output.isWritingSample()) {
output.appendData(data, data.bytesLeft());
}
}
@Override
public void packetFinished() {
commitSample(true);
output.commitSample(C.SAMPLE_FLAG_SYNC, 0, null);
}
}
......@@ -18,15 +18,19 @@ package com.google.android.exoplayer.hls.parser;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.hls.parser.HlsExtractor.TrackOutput;
import com.google.android.exoplayer.upstream.BufferPool;
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.util.ParsableByteArray;
import java.io.IOException;
/**
* Wraps a {@link RollingSampleBuffer}, adding higher level functionality such as enforcing that
* the first sample returned from the queue is a keyframe, allowing splicing to another queue, and
* so on.
*/
/* package */ abstract class SampleQueue {
public final class SampleQueue implements TrackOutput {
private final RollingSampleBuffer rollingBuffer;
private final SampleHolder sampleInfoHolder;
......@@ -40,10 +44,10 @@ import com.google.android.exoplayer.util.ParsableByteArray;
private boolean writingSample;
// Accessed by both the loading and consuming threads.
private volatile MediaFormat mediaFormat;
private volatile long largestParsedTimestampUs;
private volatile MediaFormat format;
protected SampleQueue(BufferPool bufferPool) {
public SampleQueue(BufferPool bufferPool) {
rollingBuffer = new RollingSampleBuffer(bufferPool);
sampleInfoHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_DISABLED);
needKeyframe = true;
......@@ -58,16 +62,12 @@ import com.google.android.exoplayer.util.ParsableByteArray;
// Called by the consuming thread.
public long getLargestParsedTimestampUs() {
return largestParsedTimestampUs;
}
public boolean hasMediaFormat() {
return mediaFormat != null;
public MediaFormat getFormat() {
return format;
}
public MediaFormat getMediaFormat() {
return mediaFormat;
public long getLargestParsedTimestampUs() {
return largestParsedTimestampUs;
}
public boolean isEmpty() {
......@@ -166,37 +166,44 @@ import com.google.android.exoplayer.util.ParsableByteArray;
return true;
}
// Called by the loading thread.
// TrackOutput implementation. Called by the loading thread.
protected boolean writingSample() {
return writingSample;
@Override
public boolean hasFormat() {
return format != null;
}
protected void setMediaFormat(MediaFormat mediaFormat) {
this.mediaFormat = mediaFormat;
@Override
public void setFormat(MediaFormat format) {
this.format = format;
}
protected void startSample(long sampleTimeUs) {
startSample(sampleTimeUs, 0);
@Override
public int appendData(DataSource dataSource, int length) throws IOException {
return rollingBuffer.appendData(dataSource, length);
}
protected void startSample(long sampleTimeUs, int offset) {
@Override
public void appendData(ParsableByteArray buffer, int length) {
rollingBuffer.appendData(buffer, length);
}
@Override
public void startSample(long sampleTimeUs, int offset) {
writingSample = true;
largestParsedTimestampUs = Math.max(largestParsedTimestampUs, sampleTimeUs);
rollingBuffer.startSample(sampleTimeUs, offset);
}
protected void appendData(ParsableByteArray buffer, int length) {
rollingBuffer.appendData(buffer, length);
}
protected void commitSample(boolean isKeyframe) {
commitSample(isKeyframe, 0);
@Override
public void commitSample(int flags, int offset, byte[] encryptionKey) {
rollingBuffer.commitSample(flags, offset, encryptionKey);
writingSample = false;
}
protected void commitSample(boolean isKeyframe, int offset) {
rollingBuffer.commitSample(isKeyframe, offset);
writingSample = false;
@Override
public boolean isWritingSample() {
return writingSample;
}
}
......@@ -15,9 +15,10 @@
*/
package com.google.android.exoplayer.hls.parser;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.hls.parser.HlsExtractor.TrackOutput;
import com.google.android.exoplayer.text.eia608.Eia608Parser;
import com.google.android.exoplayer.upstream.BufferPool;
import com.google.android.exoplayer.util.ParsableByteArray;
/**
......@@ -26,20 +27,17 @@ import com.google.android.exoplayer.util.ParsableByteArray;
* TODO: Technically, we shouldn't allow a sample to be read from the queue until we're sure that
* a sample with an earlier timestamp won't be added to it.
*/
/* package */ class SeiReader extends SampleQueue {
/* package */ class SeiReader extends ElementaryStreamReader {
private final ParsableByteArray seiBuffer;
public SeiReader(BufferPool bufferPool) {
super(bufferPool);
setMediaFormat(MediaFormat.createEia608Format());
seiBuffer = new ParsableByteArray();
public SeiReader(TrackOutput output) {
super(output);
output.setFormat(MediaFormat.createEia608Format());
}
public void read(byte[] data, int position, int limit, long pesTimeUs) {
seiBuffer.reset(data, limit);
@Override
public void consume(ParsableByteArray seiBuffer, long pesTimeUs, boolean startOfPacket) {
// Skip the NAL prefix and type.
seiBuffer.setPosition(position + 4);
seiBuffer.skip(4);
int b;
while (seiBuffer.bytesLeft() > 1 /* last byte will be rbsp_trailing_bits */) {
......@@ -57,13 +55,18 @@ import com.google.android.exoplayer.util.ParsableByteArray;
} while (b == 0xFF);
// Process the payload. We only support EIA-608 payloads currently.
if (Eia608Parser.isSeiMessageEia608(payloadType, payloadSize, seiBuffer)) {
startSample(pesTimeUs);
appendData(seiBuffer, payloadSize);
commitSample(true);
output.startSample(pesTimeUs, 0);
output.appendData(seiBuffer, payloadSize);
output.commitSample(C.SAMPLE_FLAG_SYNC, 0, null);
} else {
seiBuffer.skip(payloadSize);
}
}
}
@Override
public void packetFinished() {
// Do nothing.
}
}
......@@ -16,11 +16,6 @@
package com.google.android.exoplayer.hls.parser;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.upstream.BufferPool;
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.ParsableBitArray;
import com.google.android.exoplayer.util.ParsableByteArray;
......@@ -32,7 +27,7 @@ import java.io.IOException;
/**
* Facilitates the extraction of data from the MPEG-2 TS container format.
*/
public final class TsExtractor extends HlsExtractor {
public final class TsExtractor implements HlsExtractor {
private static final String TAG = "TsExtractor";
......@@ -48,119 +43,41 @@ public final class TsExtractor extends HlsExtractor {
private static final long MAX_PTS = 0x1FFFFFFFFL;
private final ParsableByteArray tsPacketBuffer;
private final SparseArray<SampleQueue> sampleQueues; // Indexed by streamType
private final SparseArray<ElementaryStreamReader> streamReaders; // Indexed by streamType
private final SparseArray<TsPayloadReader> tsPayloadReaders; // Indexed by pid
private final BufferPool bufferPool;
private final long firstSampleTimestamp;
private final ParsableBitArray tsScratch;
// Accessed only by the loading thread.
private int tsPacketBytesRead;
private TrackOutputBuilder output;
private long timestampOffsetUs;
private long lastPts;
// Accessed by both the loading and consuming threads.
private volatile boolean prepared;
public TsExtractor(boolean shouldSpliceIn, long firstSampleTimestamp, BufferPool bufferPool) {
super(shouldSpliceIn);
public TsExtractor(long firstSampleTimestamp) {
this.firstSampleTimestamp = firstSampleTimestamp;
this.bufferPool = bufferPool;
tsScratch = new ParsableBitArray(new byte[3]);
tsPacketBuffer = new ParsableByteArray(TS_PACKET_SIZE);
sampleQueues = new SparseArray<SampleQueue>();
streamReaders = new SparseArray<ElementaryStreamReader>();
tsPayloadReaders = new SparseArray<TsPayloadReader>();
tsPayloadReaders.put(TS_PAT_PID, new PatReader());
lastPts = Long.MIN_VALUE;
}
@Override
public int getTrackCount() {
Assertions.checkState(prepared);
return sampleQueues.size();
}
@Override
public MediaFormat getFormat(int track) {
Assertions.checkState(prepared);
return sampleQueues.valueAt(track).getMediaFormat();
}
@Override
public boolean isPrepared() {
return prepared;
}
@Override
public void release() {
for (int i = 0; i < sampleQueues.size(); i++) {
sampleQueues.valueAt(i).release();
}
}
@Override
public long getLargestSampleTimestamp() {
long largestParsedTimestampUs = Long.MIN_VALUE;
for (int i = 0; i < sampleQueues.size(); i++) {
largestParsedTimestampUs = Math.max(largestParsedTimestampUs,
sampleQueues.valueAt(i).getLargestParsedTimestampUs());
}
return largestParsedTimestampUs;
}
@Override
public boolean getSample(int track, SampleHolder holder) {
Assertions.checkState(prepared);
return sampleQueues.valueAt(track).getSample(holder);
}
@Override
public void discardUntil(int track, long timeUs) {
Assertions.checkState(prepared);
sampleQueues.valueAt(track).discardUntil(timeUs);
}
@Override
public boolean hasSamples(int track) {
Assertions.checkState(prepared);
return !sampleQueues.valueAt(track).isEmpty();
}
private boolean checkPrepared() {
int pesPayloadReaderCount = sampleQueues.size();
if (pesPayloadReaderCount == 0) {
return false;
}
for (int i = 0; i < pesPayloadReaderCount; i++) {
if (!sampleQueues.valueAt(i).hasMediaFormat()) {
return false;
}
}
return true;
public void init(TrackOutputBuilder output) {
this.output = output;
}
@Override
public int read(DataSource dataSource) throws IOException {
int bytesRead = dataSource.read(tsPacketBuffer.data, tsPacketBytesRead,
TS_PACKET_SIZE - tsPacketBytesRead);
if (bytesRead == -1) {
return -1;
public void read(ExtractorInput input) throws IOException, InterruptedException {
if (!input.readFully(tsPacketBuffer.data, 0, TS_PACKET_SIZE)) {
return;
}
tsPacketBytesRead += bytesRead;
if (tsPacketBytesRead < TS_PACKET_SIZE) {
// We haven't read the whole packet yet.
return bytesRead;
}
// Reset before reading the packet.
tsPacketBytesRead = 0;
tsPacketBuffer.setPosition(0);
tsPacketBuffer.setLimit(TS_PACKET_SIZE);
int syncByte = tsPacketBuffer.readUnsignedByte();
if (syncByte != TS_SYNC_BYTE) {
return bytesRead;
return;
}
tsPacketBuffer.readBytes(tsScratch, 3);
......@@ -183,20 +100,9 @@ public final class TsExtractor extends HlsExtractor {
if (payloadExists) {
TsPayloadReader payloadReader = tsPayloadReaders.get(pid);
if (payloadReader != null) {
payloadReader.consume(tsPacketBuffer, payloadUnitStartIndicator);
payloadReader.consume(tsPacketBuffer, payloadUnitStartIndicator, output);
}
}
if (!prepared) {
prepared = checkPrepared();
}
return bytesRead;
}
@Override
protected SampleQueue getSampleQueue(int track) {
return sampleQueues.valueAt(track);
}
/**
......@@ -231,7 +137,8 @@ public final class TsExtractor extends HlsExtractor {
*/
private abstract static class TsPayloadReader {
public abstract void consume(ParsableByteArray data, boolean payloadUnitStartIndicator);
public abstract void consume(ParsableByteArray data, boolean payloadUnitStartIndicator,
TrackOutputBuilder output);
}
......@@ -247,7 +154,8 @@ public final class TsExtractor extends HlsExtractor {
}
@Override
public void consume(ParsableByteArray data, boolean payloadUnitStartIndicator) {
public void consume(ParsableByteArray data, boolean payloadUnitStartIndicator,
TrackOutputBuilder output) {
// Skip pointer.
if (payloadUnitStartIndicator) {
int pointerField = data.readUnsignedByte();
......@@ -286,7 +194,8 @@ public final class TsExtractor extends HlsExtractor {
}
@Override
public void consume(ParsableByteArray data, boolean payloadUnitStartIndicator) {
public void consume(ParsableByteArray data, boolean payloadUnitStartIndicator,
TrackOutputBuilder output) {
// Skip pointer.
if (payloadUnitStartIndicator) {
int pointerField = data.readUnsignedByte();
......@@ -323,32 +232,33 @@ public final class TsExtractor extends HlsExtractor {
data.skip(esInfoLength);
entriesSize -= esInfoLength + 5;
if (sampleQueues.get(streamType) != null) {
if (streamReaders.get(streamType) != null) {
continue;
}
ElementaryStreamReader pesPayloadReader = null;
switch (streamType) {
case TS_STREAM_TYPE_AAC:
pesPayloadReader = new AdtsReader(bufferPool);
pesPayloadReader = new AdtsReader(output.buildOutput(TS_STREAM_TYPE_AAC));
break;
case TS_STREAM_TYPE_H264:
SeiReader seiReader = new SeiReader(bufferPool);
sampleQueues.put(TS_STREAM_TYPE_EIA608, seiReader);
pesPayloadReader = new H264Reader(bufferPool, seiReader);
SeiReader seiReader = new SeiReader(output.buildOutput(TS_STREAM_TYPE_EIA608));
streamReaders.put(TS_STREAM_TYPE_EIA608, seiReader);
pesPayloadReader = new H264Reader(output.buildOutput(TS_STREAM_TYPE_H264),
seiReader);
break;
case TS_STREAM_TYPE_ID3:
pesPayloadReader = new Id3Reader(bufferPool);
pesPayloadReader = new Id3Reader(output.buildOutput(TS_STREAM_TYPE_ID3));
break;
}
if (pesPayloadReader != null) {
sampleQueues.put(streamType, pesPayloadReader);
streamReaders.put(streamType, pesPayloadReader);
tsPayloadReaders.put(elementaryPid, new PesReader(pesPayloadReader));
}
}
// Skip CRC_32.
output.allOutputsBuilt();
}
}
......@@ -387,7 +297,8 @@ public final class TsExtractor extends HlsExtractor {
}
@Override
public void consume(ParsableByteArray data, boolean payloadUnitStartIndicator) {
public void consume(ParsableByteArray data, boolean payloadUnitStartIndicator,
TrackOutputBuilder output) {
if (payloadUnitStartIndicator) {
switch (state) {
case STATE_FINDING_HEADER:
......
......@@ -67,7 +67,7 @@ public final class CommonMp4AtomParsers {
long mediaTimescale = parseMdhd(mdia.getLeafAtomOfType(Atom.TYPE_mdhd).data);
Pair<MediaFormat, TrackEncryptionBox[]> sampleDescriptions =
parseStsd(stbl.getLeafAtomOfType(Atom.TYPE_stsd).data);
parseStsd(stbl.getLeafAtomOfType(Atom.TYPE_stsd).data, durationUs);
return new Track(id, trackType, mediaTimescale, durationUs, sampleDescriptions.first,
sampleDescriptions.second);
}
......@@ -321,7 +321,8 @@ public final class CommonMp4AtomParsers {
return mdhd.readUnsignedInt();
}
private static Pair<MediaFormat, TrackEncryptionBox[]> parseStsd(ParsableByteArray stsd) {
private static Pair<MediaFormat, TrackEncryptionBox[]> parseStsd(
ParsableByteArray stsd, long durationUs) {
stsd.setPosition(Mp4Util.FULL_ATOM_HEADER_SIZE);
int numberOfEntries = stsd.readInt();
MediaFormat mediaFormat = null;
......@@ -334,19 +335,19 @@ public final class CommonMp4AtomParsers {
if (childAtomType == Atom.TYPE_avc1 || childAtomType == Atom.TYPE_avc3
|| childAtomType == Atom.TYPE_encv) {
Pair<MediaFormat, TrackEncryptionBox> avc =
parseAvcFromParent(stsd, childStartPosition, childAtomSize);
parseAvcFromParent(stsd, childStartPosition, childAtomSize, durationUs);
mediaFormat = avc.first;
trackEncryptionBoxes[i] = avc.second;
} else if (childAtomType == Atom.TYPE_mp4a || childAtomType == Atom.TYPE_enca
|| childAtomType == Atom.TYPE_ac_3) {
Pair<MediaFormat, TrackEncryptionBox> audioSampleEntry =
parseAudioSampleEntry(stsd, childAtomType, childStartPosition, childAtomSize);
Pair<MediaFormat, TrackEncryptionBox> audioSampleEntry = parseAudioSampleEntry(stsd,
childAtomType, childStartPosition, childAtomSize, durationUs);
mediaFormat = audioSampleEntry.first;
trackEncryptionBoxes[i] = audioSampleEntry.second;
} else if (childAtomType == Atom.TYPE_TTML) {
mediaFormat = MediaFormat.createTtmlFormat();
} else if (childAtomType == Atom.TYPE_mp4v) {
mediaFormat = parseMp4vFromParent(stsd, childStartPosition, childAtomSize);
mediaFormat = parseMp4vFromParent(stsd, childStartPosition, childAtomSize, durationUs);
}
stsd.setPosition(childStartPosition + childAtomSize);
}
......@@ -355,7 +356,7 @@ public final class CommonMp4AtomParsers {
/** Returns the media format for an avc1 box. */
private static Pair<MediaFormat, TrackEncryptionBox> parseAvcFromParent(ParsableByteArray parent,
int position, int size) {
int position, int size, long durationUs) {
parent.setPosition(position + Mp4Util.ATOM_HEADER_SIZE);
parent.skip(24);
......@@ -388,7 +389,7 @@ public final class CommonMp4AtomParsers {
}
MediaFormat format = MediaFormat.createVideoFormat(MimeTypes.VIDEO_H264, MediaFormat.NO_VALUE,
width, height, pixelWidthHeightRatio, initializationData);
durationUs, width, height, pixelWidthHeightRatio, initializationData);
return Pair.create(format, trackEncryptionBox);
}
......@@ -468,8 +469,8 @@ public final class CommonMp4AtomParsers {
}
/** Returns the media format for an mp4v box. */
private static MediaFormat parseMp4vFromParent(ParsableByteArray parent,
int position, int size) {
private static MediaFormat parseMp4vFromParent(ParsableByteArray parent, int position, int size,
long durationUs) {
parent.setPosition(position + Mp4Util.ATOM_HEADER_SIZE);
parent.skip(24);
......@@ -492,11 +493,11 @@ public final class CommonMp4AtomParsers {
}
return MediaFormat.createVideoFormat(
MimeTypes.VIDEO_MP4V, MediaFormat.NO_VALUE, width, height, initializationData);
MimeTypes.VIDEO_MP4V, MediaFormat.NO_VALUE, durationUs, width, height, initializationData);
}
private static Pair<MediaFormat, TrackEncryptionBox> parseAudioSampleEntry(
ParsableByteArray parent, int atomType, int position, int size) {
ParsableByteArray parent, int atomType, int position, int size, long durationUs) {
parent.setPosition(position + Mp4Util.ATOM_HEADER_SIZE);
parent.skip(16);
int channelCount = parent.readUnsignedShort();
......@@ -555,7 +556,7 @@ public final class CommonMp4AtomParsers {
}
MediaFormat format = MediaFormat.createAudioFormat(
mimeType, sampleSize, channelCount, sampleRate, bitrate,
mimeType, sampleSize, durationUs, channelCount, sampleRate, bitrate,
initializationData == null ? null : Collections.singletonList(initializationData));
return Pair.create(format, trackEncryptionBox);
}
......
......@@ -30,6 +30,7 @@ import com.google.android.exoplayer.chunk.MediaChunk;
import com.google.android.exoplayer.chunk.parser.Extractor;
import com.google.android.exoplayer.chunk.parser.mp4.FragmentedMp4Extractor;
import com.google.android.exoplayer.chunk.parser.mp4.TrackEncryptionBox;
import com.google.android.exoplayer.drm.DrmInitData;
import com.google.android.exoplayer.mp4.Track;
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.ProtectionElement;
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.StreamElement;
......@@ -38,6 +39,7 @@ import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DataSpec;
import com.google.android.exoplayer.util.CodecSpecificDataUtil;
import com.google.android.exoplayer.util.ManifestFetcher;
import com.google.android.exoplayer.util.MimeTypes;
import android.net.Uri;
import android.os.SystemClock;
......@@ -48,8 +50,6 @@ import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.UUID;
/**
* An {@link ChunkSource} for SmoothStreaming.
......@@ -71,7 +71,7 @@ public class SmoothStreamingChunkSource implements ChunkSource {
private final int maxHeight;
private final SparseArray<FragmentedMp4Extractor> extractors;
private final Map<UUID, byte[]> psshInfo;
private final DrmInitData drmInitData;
private final SmoothStreamingFormat[] formats;
private SmoothStreamingManifest currentManifest;
......@@ -143,9 +143,11 @@ public class SmoothStreamingChunkSource implements ChunkSource {
byte[] keyId = getKeyId(protectionElement.data);
trackEncryptionBoxes = new TrackEncryptionBox[1];
trackEncryptionBoxes[0] = new TrackEncryptionBox(true, INITIALIZATION_VECTOR_SIZE, keyId);
psshInfo = Collections.singletonMap(protectionElement.uuid, protectionElement.data);
DrmInitData.Mapped drmInitData = new DrmInitData.Mapped(MimeTypes.VIDEO_MP4);
drmInitData.put(protectionElement.uuid, protectionElement.data);
this.drmInitData = drmInitData;
} else {
psshInfo = null;
drmInitData = null;
}
int trackCount = trackIndices != null ? trackIndices.length : streamElement.tracks.length;
......@@ -299,7 +301,7 @@ public class SmoothStreamingChunkSource implements ChunkSource {
Uri uri = streamElement.buildRequestUri(selectedFormat.trackIndex, chunkIndex);
Chunk mediaChunk = newMediaChunk(selectedFormat, uri, null,
extractors.get(Integer.parseInt(selectedFormat.id)), psshInfo, dataSource,
extractors.get(Integer.parseInt(selectedFormat.id)), drmInitData, dataSource,
currentAbsoluteChunkIndex, isLastChunk, chunkStartTimeUs, nextChunkStartTimeUs, 0);
out.chunk = mediaChunk;
}
......@@ -365,7 +367,7 @@ public class SmoothStreamingChunkSource implements ChunkSource {
}
private static MediaChunk newMediaChunk(Format formatInfo, Uri uri, String cacheKey,
Extractor extractor, Map<UUID, byte[]> psshInfo, DataSource dataSource, int chunkIndex,
Extractor extractor, DrmInitData drmInitData, DataSource dataSource, int chunkIndex,
boolean isLast, long chunkStartTimeUs, long nextChunkStartTimeUs, int trigger) {
int nextChunkIndex = isLast ? -1 : chunkIndex + 1;
long nextStartTimeUs = isLast ? -1 : nextChunkStartTimeUs;
......@@ -374,7 +376,7 @@ public class SmoothStreamingChunkSource implements ChunkSource {
// In SmoothStreaming each chunk contains sample timestamps relative to the start of the chunk.
// To convert them the absolute timestamps, we need to set sampleOffsetUs to -chunkStartTimeUs.
return new ContainerMediaChunk(dataSource, dataSpec, formatInfo, trigger, chunkStartTimeUs,
nextStartTimeUs, nextChunkIndex, extractor, psshInfo, false, -chunkStartTimeUs);
nextStartTimeUs, nextChunkIndex, extractor, drmInitData, false, -chunkStartTimeUs);
}
private static byte[] getKeyId(byte[] initData) {
......
......@@ -16,6 +16,7 @@
package com.google.android.exoplayer.source;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.MediaFormatHolder;
import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.SampleSource;
......@@ -62,9 +63,14 @@ public final class DefaultSampleSource implements SampleSource {
if (sampleExtractor.prepare()) {
prepared = true;
trackInfos = sampleExtractor.getTrackInfos();
trackStates = new int[trackInfos.length];
pendingDiscontinuities = new boolean[trackInfos.length];
int trackCount = sampleExtractor.getTrackCount();
trackStates = new int[trackCount];
pendingDiscontinuities = new boolean[trackCount];
trackInfos = new TrackInfo[trackCount];
for (int track = 0; track < trackCount; track++) {
MediaFormat mediaFormat = sampleExtractor.getMediaFormat(track);
trackInfos[track] = new TrackInfo(mediaFormat.mimeType, mediaFormat.durationUs);
}
}
return prepared;
......@@ -119,7 +125,8 @@ public final class DefaultSampleSource implements SampleSource {
return NOTHING_READ;
}
if (trackStates[track] != TRACK_STATE_FORMAT_SENT) {
sampleExtractor.getTrackMediaFormat(track, formatHolder);
formatHolder.format = sampleExtractor.getMediaFormat(track);
formatHolder.drmInitData = sampleExtractor.getDrmInitData(track);
trackStates[track] = TRACK_STATE_FORMAT_SENT;
return FORMAT_READ;
}
......
......@@ -17,12 +17,12 @@ package com.google.android.exoplayer.source;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.MediaFormatHolder;
import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.SampleSource;
import com.google.android.exoplayer.TrackInfo;
import com.google.android.exoplayer.TrackRenderer;
import com.google.android.exoplayer.drm.DrmInitData;
import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.MimeTypes;
import com.google.android.exoplayer.util.Util;
import android.annotation.TargetApi;
......@@ -53,8 +53,6 @@ public final class FrameworkSampleExtractor implements SampleExtractor {
private final MediaExtractor mediaExtractor;
private TrackInfo[] trackInfos;
/**
* Instantiates a new sample extractor reading from the specified {@code uri}.
*
......@@ -106,25 +104,10 @@ public final class FrameworkSampleExtractor implements SampleExtractor {
mediaExtractor.setDataSource(fileDescriptor, fileDescriptorOffset, fileDescriptorLength);
}
int trackCount = mediaExtractor.getTrackCount();
trackInfos = new TrackInfo[trackCount];
for (int i = 0; i < trackCount; i++) {
android.media.MediaFormat format = mediaExtractor.getTrackFormat(i);
long durationUs = format.containsKey(android.media.MediaFormat.KEY_DURATION)
? format.getLong(android.media.MediaFormat.KEY_DURATION) : C.UNKNOWN_TIME_US;
String mime = format.getString(android.media.MediaFormat.KEY_MIME);
trackInfos[i] = new TrackInfo(mime, durationUs);
}
return true;
}
@Override
public TrackInfo[] getTrackInfos() {
return trackInfos;
}
@Override
public void selectTrack(int index) {
mediaExtractor.selectTrack(index);
}
......@@ -151,10 +134,18 @@ public final class FrameworkSampleExtractor implements SampleExtractor {
}
@Override
public void getTrackMediaFormat(int track, MediaFormatHolder mediaFormatHolder) {
mediaFormatHolder.format =
MediaFormat.createFromFrameworkMediaFormatV16(mediaExtractor.getTrackFormat(track));
mediaFormatHolder.drmInitData = Util.SDK_INT >= 18 ? getPsshInfoV18() : null;
public int getTrackCount() {
return mediaExtractor.getTrackCount();
}
@Override
public MediaFormat getMediaFormat(int track) {
return MediaFormat.createFromFrameworkMediaFormatV16(mediaExtractor.getTrackFormat(track));
}
@Override
public DrmInitData getDrmInitData(int track) {
return Util.SDK_INT >= 18 ? getDrmInitDataV18() : null;
}
@Override
......@@ -173,7 +164,7 @@ public final class FrameworkSampleExtractor implements SampleExtractor {
}
sampleHolder.timeUs = mediaExtractor.getSampleTime();
sampleHolder.flags = mediaExtractor.getSampleFlags();
if ((sampleHolder.flags & MediaExtractor.SAMPLE_FLAG_ENCRYPTED) != 0) {
if ((sampleHolder.flags & C.SAMPLE_FLAG_ENCRYPTED) != 0) {
sampleHolder.cryptoInfo.setFromExtractorV16(mediaExtractor);
}
......@@ -188,9 +179,15 @@ public final class FrameworkSampleExtractor implements SampleExtractor {
}
@TargetApi(18)
private Map<UUID, byte[]> getPsshInfoV18() {
private DrmInitData getDrmInitDataV18() {
// MediaExtractor only supports psshInfo for MP4, so it's ok to hard code the mimeType here.
Map<UUID, byte[]> psshInfo = mediaExtractor.getPsshInfo();
return (psshInfo == null || psshInfo.isEmpty()) ? null : psshInfo;
if (psshInfo == null || psshInfo.isEmpty()) {
return null;
}
DrmInitData.Mapped drmInitData = new DrmInitData.Mapped(MimeTypes.VIDEO_MP4);
drmInitData.putAll(psshInfo);
return drmInitData;
}
}
......@@ -16,11 +16,10 @@
package com.google.android.exoplayer.source;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.MediaFormatHolder;
import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.SampleSource;
import com.google.android.exoplayer.TrackInfo;
import com.google.android.exoplayer.TrackRenderer;
import com.google.android.exoplayer.drm.DrmInitData;
import java.io.IOException;
......@@ -28,7 +27,7 @@ import java.io.IOException;
* Extractor for reading track metadata and samples stored in tracks.
*
* <p>Call {@link #prepare} until it returns {@code true}, then access track metadata via
* {@link #getTrackInfos} and {@link #getTrackMediaFormat}.
* {@link #getMediaFormat}.
*
* <p>Pass indices of tracks to read from to {@link #selectTrack}. A track can later be deselected
* by calling {@link #deselectTrack}. It is safe to select/deselect tracks after reading sample
......@@ -46,9 +45,6 @@ public interface SampleExtractor {
*/
boolean prepare() throws IOException;
/** Returns track information about all tracks that can be selected. */
TrackInfo[] getTrackInfos();
/** Selects the track at {@code index} for reading sample data. */
void selectTrack(int index);
......@@ -75,8 +71,14 @@ public interface SampleExtractor {
*/
void seekTo(long positionUs);
/** Stores the {@link MediaFormat} of {@code track}. */
void getTrackMediaFormat(int track, MediaFormatHolder mediaFormatHolder);
/** Returns the number of tracks, if {@link #prepare} has returned {@code true}. */
int getTrackCount();
/** Returns the {@link MediaFormat} of {@code track}. */
MediaFormat getMediaFormat(int track);
/** Returns the DRM initialization data for {@code track}. */
DrmInitData getDrmInitData(int track);
/**
* Reads the next sample in the track at index {@code track} into {@code sampleHolder}, returning
......
......@@ -57,6 +57,8 @@ package com.google.android.exoplayer.text.eia608;
public static final byte CARRIAGE_RETURN = 0x2D;
public static final byte ERASE_NON_DISPLAYED_MEMORY = 0x2E;
public static final byte BACKSPACE = 0x21;
public static final byte MID_ROW_CHAN_1 = 0x11;
public static final byte MID_ROW_CHAN_2 = 0x19;
......
......@@ -82,6 +82,26 @@ public class Eia608Parser {
0xFB // 3F: 251 'û' "Latin small letter U with circumflex"
};
// Extended Spanish/Miscellaneous and French char set.
private static final int[] SPECIAL_ES_FR_CHARACTER_SET = new int[] {
// Spanish and misc.
0xC1, 0xC9, 0xD3, 0xDA, 0xDC, 0xFC, 0x2018, 0xA1,
0x2A, 0x27, 0x2014, 0xA9, 0x2120, 0x2022, 0x201C, 0x201D,
// French.
0xC0, 0xC2, 0xC7, 0xC8, 0xCA, 0xCB, 0xEB, 0xCE,
0xCF, 0xEF, 0xD4, 0xD9, 0xF9, 0xDB, 0xAB, 0xBB
};
//Extended Portuguese and German/Danish char set.
private static final int[] SPECIAL_PT_DE_CHARACTER_SET = new int[] {
// Portuguese.
0xC3, 0xE3, 0xCD, 0xCC, 0xEC, 0xD2, 0xF2, 0xD5,
0xF5, 0x7B, 0x7D, 0x5C, 0x5E, 0x5F, 0x7C, 0x7E,
// German/Danish.
0xC4, 0xE4, 0xD6, 0xF6, 0xDF, 0xA5, 0xA4, 0x2502,
0xC5, 0xE5, 0xD8, 0xF8, 0x250C, 0x2510, 0x2514, 0x2518
};
private final ParsableBitArray seiBuffer;
private final StringBuilder stringBuilder;
private final ArrayList<ClosedCaption> captions;
......@@ -134,31 +154,45 @@ public class Eia608Parser {
}
// Special North American character set.
if ((ccData1 == 0x11) && ((ccData2 & 0x70) == 0x30)) {
// ccData2 - P|0|1|1|X|X|X|X
if ((ccData1 == 0x11 || ccData1 == 0x19)
&& ((ccData2 & 0x70) == 0x30)) {
stringBuilder.append(getSpecialChar(ccData2));
continue;
}
// Extended Spanish/Miscellaneous and French character set.
// ccData2 - P|0|1|X|X|X|X|X
if ((ccData1 == 0x12 || ccData1 == 0x1A)
&& ((ccData2 & 0x60) == 0x20)) {
backspace(); // Remove standard equivalent of the special extended char.
stringBuilder.append(getExtendedEsFrChar(ccData2));
continue;
}
// Extended Portuguese and German/Danish character set.
// ccData2 - P|0|1|X|X|X|X|X
if ((ccData1 == 0x13 || ccData1 == 0x1B)
&& ((ccData2 & 0x60) == 0x20)) {
backspace(); // Remove standard equivalent of the special extended char.
stringBuilder.append(getExtendedPtDeChar(ccData2));
continue;
}
// Control character.
if (ccData1 < 0x20) {
if (stringBuilder.length() > 0) {
captions.add(new ClosedCaptionText(stringBuilder.toString()));
stringBuilder.setLength(0);
}
captions.add(new ClosedCaptionCtrl(ccData1, ccData2));
addCtrl(ccData1, ccData2);
continue;
}
// Basic North American character set.
stringBuilder.append(getChar(ccData1));
if (ccData2 != 0) {
if (ccData2 >= 0x20) {
stringBuilder.append(getChar(ccData2));
}
}
if (stringBuilder.length() > 0) {
captions.add(new ClosedCaptionText(stringBuilder.toString()));
}
addBufferedText();
if (captions.isEmpty()) {
return null;
......@@ -179,6 +213,32 @@ public class Eia608Parser {
return (char) SPECIAL_CHARACTER_SET[index];
}
private static char getExtendedEsFrChar(byte ccData) {
int index = ccData & 0x1F;
return (char) SPECIAL_ES_FR_CHARACTER_SET[index];
}
private static char getExtendedPtDeChar(byte ccData) {
int index = ccData & 0x1F;
return (char) SPECIAL_PT_DE_CHARACTER_SET[index];
}
private void addBufferedText() {
if (stringBuilder.length() > 0) {
captions.add(new ClosedCaptionText(stringBuilder.toString()));
stringBuilder.setLength(0);
}
}
private void addCtrl(byte ccData1, byte ccData2) {
addBufferedText();
captions.add(new ClosedCaptionCtrl(ccData1, ccData2));
}
private void backspace() {
addCtrl((byte) 0x14, ClosedCaptionCtrl.BACKSPACE);
}
/**
* Inspects an sei message to determine whether it contains EIA-608.
* <p>
......
......@@ -317,6 +317,11 @@ public class Eia608TrackRenderer extends TrackRenderer implements Callback {
case ClosedCaptionCtrl.CARRIAGE_RETURN:
maybeAppendNewline();
return;
case ClosedCaptionCtrl.BACKSPACE:
if (captionStringBuilder.length() > 0) {
captionStringBuilder.setLength(captionStringBuilder.length() - 1);
}
return;
}
}
......
/*
* 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.upstream;
import com.google.android.exoplayer.util.Assertions;
import java.nio.ByteBuffer;
/**
* Input stream with non-blocking reading/skipping that also stores read/skipped data in a buffer.
* Call {@link #mark} to discard any buffered data before the current reading position. Call
* {@link #returnToMark} to move the current reading position back to the marked position, which is
* initially the start of the input stream.
*/
public final class BufferedNonBlockingInputStream implements NonBlockingInputStream {
private final NonBlockingInputStream inputStream;
private final byte[] bufferedBytes;
private long inputStreamPosition;
private int readPosition;
private int writePosition;
/**
* Wraps the specified {@code nonBlockingInputStream} for buffered reading using a buffer of size
* {@code bufferSize} bytes.
*/
public BufferedNonBlockingInputStream(
NonBlockingInputStream nonBlockingInputStream, int bufferSize) {
inputStream = Assertions.checkNotNull(nonBlockingInputStream);
bufferedBytes = new byte[bufferSize];
}
@Override
public int skip(int length) {
return consumeStream(null, null, 0, length);
}
@Override
public int read(byte[] buffer, int offset, int length) {
return consumeStream(null, buffer, offset, length);
}
@Override
public int read(ByteBuffer buffer, int length) {
return consumeStream(buffer, null, 0, length);
}
@Override
public long getAvailableByteCount() {
// The amount that can be read from the input stream is limited by how much can be buffered.
return (writePosition - readPosition)
+ Math.min(inputStream.getAvailableByteCount(), bufferedBytes.length - writePosition);
}
@Override
public boolean isEndOfStream() {
return writePosition == readPosition && inputStream.isEndOfStream();
}
@Override
public void close() {
inputStream.close();
inputStreamPosition = -1;
}
/** Returns the current position in the stream. */
public long getReadPosition() {
return inputStreamPosition - (writePosition - readPosition);
}
/**
* Moves the mark to be at the current position. Any data before the current position is
* discarded. After calling this method, calling {@link #returnToMark} will move the reading
* position back to the mark position.
*/
public void mark() {
System.arraycopy(bufferedBytes, readPosition, bufferedBytes, 0, writePosition - readPosition);
writePosition -= readPosition;
readPosition = 0;
}
/** Moves the current position back to the mark position. */
public void returnToMark() {
readPosition = 0;
}
/**
* Reads or skips data from the input stream. If {@code byteBuffer} is non-{@code null}, reads
* {@code length} bytes into {@code byteBuffer} (other arguments are ignored). If
* {@code byteArray} is non-{@code null}, reads {@code length} bytes into {@code byteArray} at
* {@code offset} (other arguments are ignored). Otherwise, skips {@code length} bytes.
*
* @param byteBuffer {@link ByteBuffer} to read into, or {@code null} to read into
* {@code byteArray} or skip.
* @param byteArray Byte array to read into, or {@code null} to read into {@code byteBuffer} or
* skip.
* @param offset Offset in {@code byteArray} to write to, if it is non-{@code null}.
* @param length Number of bytes to read or skip.
* @return The number of bytes consumed, or -1 if nothing was consumed and the end of stream was
* reached.
*/
private int consumeStream(ByteBuffer byteBuffer, byte[] byteArray, int offset, int length) {
// If necessary, reduce length so that we do not need to write past the end of the array.
int pendingBytes = writePosition - readPosition;
length = Math.min(length, bufferedBytes.length - writePosition + pendingBytes);
// If reading past the end of buffered data, request more and populate the buffer.
int streamBytesRead = 0;
if (length - pendingBytes > 0) {
streamBytesRead = inputStream.read(bufferedBytes, writePosition, length - pendingBytes);
if (streamBytesRead > 0) {
inputStreamPosition += streamBytesRead;
writePosition += streamBytesRead;
pendingBytes += streamBytesRead;
}
}
// Signal the end of the stream if nothing more will be read.
if (streamBytesRead == -1 && pendingBytes == 0) {
return -1;
}
// Fill the buffer using buffered data if reading, or just skip otherwise.
length = Math.min(pendingBytes, length);
if (byteBuffer != null) {
byteBuffer.put(bufferedBytes, readPosition, length);
} else if (byteArray != null) {
System.arraycopy(bufferedBytes, readPosition, byteArray, offset, length);
}
readPosition += length;
return length;
}
}
......@@ -55,13 +55,15 @@ public interface DataSource {
/**
* Reads up to {@code length} bytes of data and stores them into {@code buffer}, starting at
* index {@code offset}. This method blocks until at least one byte of data can be read, the end
* of the opened range is detected, or an exception is thrown.
* index {@code offset}.
* <p>
* This method blocks until at least one byte of data can be read, the end of the opened range is
* detected, or an exception is thrown.
*
* @param buffer The buffer into which the read data should be stored.
* @param offset The start offset into {@code buffer} at which data should be written.
* @param readLength The maximum number of bytes to read.
* @return The actual number of bytes read, or -1 if the end of the opened range is reached.
* @return The number of bytes read, or -1 if the end of the opened range is reached.
* @throws IOException If an error occurs reading from the source.
*/
public int read(byte[] buffer, int offset, int readLength) throws IOException;
......
......@@ -47,6 +47,10 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream
private final Allocator allocator;
private final ReadHead readHead;
/** Whether {@link #allocation}'s capacity is fixed. If true, the allocation is not resized. */
private final boolean isAllocationFixedSize;
private final int allocationSize;
private Allocation allocation;
private volatile boolean loadCanceled;
......@@ -58,6 +62,9 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream
private int writeFragmentRemainingLength;
/**
* Constructs an instance whose allocation grows to contain all of the data specified by the
* {@code dataSpec}.
*
* @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
......@@ -72,12 +79,48 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream
this.allocator = allocator;
resolvedLength = C.LENGTH_UNBOUNDED;
readHead = new ReadHead();
isAllocationFixedSize = false;
allocationSize = 0;
}
/**
* Constructs an instance whose allocation is of a fixed size, which may be smaller than the data
* specified by the {@code dataSpec}.
* <p>
* The allocation size determines how far ahead loading can proceed relative to the current
* reading position.
*
* @param dataSource The source form which the data should be loaded.
* @param dataSpec Defines the data to be loaded.
* @param allocator Used to obtain an {@link Allocation} for holding the data.
* @param allocationSize The minimum size for a fixed-size allocation that will hold the data
* loaded from {@code dataSource}.
*/
public DataSourceStream(
DataSource dataSource, DataSpec dataSpec, Allocator allocator, int allocationSize) {
Assertions.checkState(dataSpec.length <= Integer.MAX_VALUE);
this.dataSource = dataSource;
this.dataSpec = dataSpec;
this.allocator = allocator;
this.allocationSize = allocationSize;
resolvedLength = C.LENGTH_UNBOUNDED;
readHead = new ReadHead();
isAllocationFixedSize = true;
}
/**
* Resets the read position to the start of the data.
*
* @throws UnsupportedOperationException Thrown if the allocation size is fixed.
*/
public void resetReadPosition() {
if (isAllocationFixedSize) {
throw new UnsupportedOperationException(
"The read position cannot be reset when using a fixed allocation");
}
readHead.reset();
}
......@@ -176,7 +219,12 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream
byte[][] buffers = allocation.getBuffers();
while (bytesRead < bytesToRead) {
if (readHead.fragmentRemaining == 0) {
readHead.fragmentIndex++;
if (readHead.fragmentIndex == buffers.length - 1) {
Assertions.checkState(isAllocationFixedSize);
readHead.fragmentIndex = 0;
} else {
readHead.fragmentIndex++;
}
readHead.fragmentOffset = allocation.getFragmentOffset(readHead.fragmentIndex);
readHead.fragmentRemaining = allocation.getFragmentLength(readHead.fragmentIndex);
}
......@@ -194,6 +242,13 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream
readHead.fragmentRemaining -= bufferReadLength;
}
if (isAllocationFixedSize) {
synchronized (readHead) {
// Notify load() of the updated position so it can resume.
readHead.notify();
}
}
return bytesRead;
}
......@@ -210,6 +265,7 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream
}
@Override
@SuppressWarnings("NonAtomicVolatileUpdate")
public void load() throws IOException, InterruptedException {
if (loadCanceled || isLoadFinished()) {
// The load was canceled, or is already complete.
......@@ -221,7 +277,7 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream
if (loadPosition == 0 && resolvedLength == C.LENGTH_UNBOUNDED) {
loadDataSpec = dataSpec;
long resolvedLength = dataSource.open(loadDataSpec);
if (resolvedLength > Integer.MAX_VALUE) {
if (!isAllocationFixedSize && resolvedLength > Integer.MAX_VALUE) {
throw new DataSourceStreamLoadException(
new UnexpectedLengthException(dataSpec.length, resolvedLength));
}
......@@ -235,9 +291,13 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream
}
if (allocation == null) {
int initialAllocationSize = resolvedLength != C.LENGTH_UNBOUNDED
? (int) resolvedLength : CHUNKED_ALLOCATION_INCREMENT;
allocation = allocator.allocate(initialAllocationSize);
if (isAllocationFixedSize) {
allocation = allocator.allocate(allocationSize);
} else {
int initialAllocationSize = resolvedLength != C.LENGTH_UNBOUNDED
? (int) resolvedLength : CHUNKED_ALLOCATION_INCREMENT;
allocation = allocator.allocate(initialAllocationSize);
}
}
int allocationCapacity = allocation.capacity();
......@@ -253,18 +313,25 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream
if (Thread.interrupted()) {
throw new InterruptedException();
}
read = dataSource.read(buffers[writeFragmentIndex], writeFragmentOffset,
writeFragmentRemainingLength);
int bytesToWrite = getBytesToWrite();
read = dataSource.read(buffers[writeFragmentIndex], writeFragmentOffset, bytesToWrite);
if (read > 0) {
loadPosition += read;
writeFragmentOffset += read;
writeFragmentRemainingLength -= read;
if (writeFragmentRemainingLength == 0 && maybeMoreToLoad()) {
writeFragmentIndex++;
if (loadPosition == allocationCapacity) {
allocation.ensureCapacity(allocationCapacity + CHUNKED_ALLOCATION_INCREMENT);
allocationCapacity = allocation.capacity();
buffers = allocation.getBuffers();
if (writeFragmentIndex == buffers.length) {
if (isAllocationFixedSize) {
// Wrap back to the first fragment.
writeFragmentIndex = 0;
} else {
// Grow the allocation.
allocation.ensureCapacity(allocationCapacity + CHUNKED_ALLOCATION_INCREMENT);
allocationCapacity = allocation.capacity();
buffers = allocation.getBuffers();
}
}
writeFragmentOffset = allocation.getFragmentOffset(writeFragmentIndex);
writeFragmentRemainingLength = allocation.getFragmentLength(writeFragmentIndex);
......@@ -281,6 +348,25 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream
}
}
/**
* Returns the number of bytes that can be written to the current fragment, blocking until the
* reader has consumed data if the allocation has a fixed size and is full.
*/
private int getBytesToWrite() throws InterruptedException {
if (!isAllocationFixedSize) {
return writeFragmentRemainingLength;
}
synchronized (readHead) {
while (loadPosition == readHead.position + allocation.capacity()) {
readHead.wait();
}
}
return Math.min(writeFragmentRemainingLength,
allocation.capacity() - (int) (loadPosition - readHead.position));
}
private boolean maybeMoreToLoad() {
return resolvedLength == C.LENGTH_UNBOUNDED || loadPosition < resolvedLength;
}
......
......@@ -18,6 +18,7 @@ package com.google.android.exoplayer.upstream;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.Predicate;
import com.google.android.exoplayer.util.Util;
import android.text.TextUtils;
import android.util.Log;
......@@ -226,6 +227,7 @@ public class DefaultHttpDataSource implements HttpDataSource {
public void close() throws HttpDataSourceException {
try {
if (inputStream != null) {
Util.maybeTerminateInputStream(connection, bytesRemaining());
try {
inputStream.close();
} catch (IOException e) {
......
......@@ -128,6 +128,21 @@ public final class Loader {
}
/**
* Invokes {@link #startLoading(Looper, Loadable, Callback)}, using the {@link Looper}
* associated with the calling thread. Loading is delayed by {@code delayMs}.
*
* @param loadable The {@link Loadable} to load.
* @param callback A callback to invoke when the load ends.
* @param delayMs Number of milliseconds to wait before calling {@link Loadable#load()}.
* @throws IllegalStateException If the calling thread does not have an associated {@link Looper}.
*/
public void startLoading(Loadable loadable, Callback callback, int delayMs) {
Looper myLooper = Looper.myLooper();
Assertions.checkState(myLooper != null);
startLoading(myLooper, loadable, callback, delayMs);
}
/**
* Start loading a {@link Loadable}.
* <p>
* A {@link Loader} instance can only load one {@link Loadable} at a time, and so this method
......@@ -138,9 +153,24 @@ public final class Loader {
* @param callback A callback to invoke when the load ends.
*/
public void startLoading(Looper looper, Loadable loadable, Callback callback) {
startLoading(looper, loadable, callback, 0);
}
/**
* Start loading a {@link Loadable} after {@code delayMs} has elapsed.
* <p>
* A {@link Loader} instance can only load one {@link Loadable} at a time, and so this method
* must not be called when another load is in progress.
*
* @param looper The looper of the thread on which the callback should be invoked.
* @param loadable The {@link Loadable} to load.
* @param callback A callback to invoke when the load ends.
* @param delayMs Number of milliseconds to wait before calling {@link Loadable#load()}.
*/
public void startLoading(Looper looper, Loadable loadable, Callback callback, int delayMs) {
Assertions.checkState(!loading);
loading = true;
currentTask = new LoadTask(looper, loadable, callback);
currentTask = new LoadTask(looper, loadable, callback, delayMs);
downloadExecutorService.submit(currentTask);
}
......@@ -182,13 +212,15 @@ public final class Loader {
private final Loadable loadable;
private final Loader.Callback callback;
private final int delayMs;
private volatile Thread executorThread;
public LoadTask(Looper looper, Loadable loadable, Loader.Callback callback) {
public LoadTask(Looper looper, Loadable loadable, Loader.Callback callback, int delayMs) {
super(looper);
this.loadable = loadable;
this.callback = callback;
this.delayMs = delayMs;
}
public void quit() {
......@@ -202,6 +234,9 @@ public final class Loader {
public void run() {
try {
executorThread = Thread.currentThread();
if (delayMs > 0) {
Thread.sleep(delayMs);
}
if (!loadable.isLoadCanceled()) {
loadable.load();
}
......
......@@ -15,12 +15,16 @@
*/
package com.google.android.exoplayer.util;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.upstream.DataSource;
import android.text.TextUtils;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.net.HttpURLConnection;
import java.net.URL;
import java.text.ParseException;
import java.util.Arrays;
......@@ -57,6 +61,8 @@ public final class Util {
Pattern.compile("^(-)?P(([0-9]*)Y)?(([0-9]*)M)?(([0-9]*)D)?"
+ "(T(([0-9]*)H)?(([0-9]*)M)?(([0-9.]*)S)?)?$");
private static final long MAX_BYTES_TO_DRAIN = 2048;
private Util() {}
/**
......@@ -396,4 +402,48 @@ public final class Util {
return intArray;
}
/**
* On platform API levels 19 and 20, okhttp's implementation of {@link InputStream#close} can
* block for a long time if the stream has a lot of data remaining. Call this method before
* closing the input stream to make a best effort to cause the input stream to encounter an
* unexpected end of input, working around this issue. On other platform API levels, the method
* does nothing.
*
* @param connection The connection whose {@link InputStream} should be terminated.
* @param bytesRemaining The number of bytes remaining to be read from the input stream if its
* length is known. {@link C#LENGTH_UNBOUNDED} otherwise.
*/
public static void maybeTerminateInputStream(HttpURLConnection connection, long bytesRemaining) {
if (SDK_INT != 19 && SDK_INT != 20) {
return;
}
try {
InputStream inputStream = connection.getInputStream();
if (bytesRemaining == C.LENGTH_UNBOUNDED) {
// If the input stream has already ended, do nothing. The socket may be re-used.
if (inputStream.read() == -1) {
return;
}
} else if (bytesRemaining <= MAX_BYTES_TO_DRAIN) {
// There isn't much data left. Prefer to allow it to drain, which may allow the socket to be
// re-used.
return;
}
String className = inputStream.getClass().getName();
if (className.equals("com.android.okhttp.internal.http.HttpTransport$ChunkedInputStream")
|| className.equals(
"com.android.okhttp.internal.http.HttpTransport$FixedLengthInputStream")) {
Class<?> superclass = inputStream.getClass().getSuperclass();
Method unexpectedEndOfInput = superclass.getDeclaredMethod("unexpectedEndOfInput");
unexpectedEndOfInput.setAccessible(true);
unexpectedEndOfInput.invoke(inputStream);
}
} catch (IOException e) {
// The connection didn't ever have an input stream, or it was closed already.
} catch (Exception e) {
// Something went wrong. The device probably isn't using okhttp.
}
}
}
package com.google.android.exoplayer;
import android.annotation.SuppressLint;
import android.media.MediaCodec;
import android.media.MediaExtractor;
import junit.framework.TestCase;
/**
* Unit test for {@link C}.
*/
public class CTest extends TestCase {
@SuppressLint("InlinedApi")
public static final void testContants() {
// Sanity check that constant values match those defined by the platform.
assertEquals(MediaExtractor.SAMPLE_FLAG_SYNC, C.SAMPLE_FLAG_SYNC);
assertEquals(MediaExtractor.SAMPLE_FLAG_ENCRYPTED, C.SAMPLE_FLAG_ENCRYPTED);
assertEquals(MediaCodec.CRYPTO_MODE_AES_CTR, C.CRYPTO_MODE_AES_CTR);
}
}
......@@ -42,9 +42,9 @@ public class MediaFormatTest extends TestCase {
initData.add(initData2);
testConversionToFrameworkFormatV16(
MediaFormat.createVideoFormat("video/xyz", 102400, 1280, 720, 1.5f, initData));
MediaFormat.createVideoFormat("video/xyz", 102400, 1000L, 1280, 720, 1.5f, initData));
testConversionToFrameworkFormatV16(
MediaFormat.createAudioFormat("audio/xyz", 102400, 5, 44100, initData));
MediaFormat.createAudioFormat("audio/xyz", 102400, 1000L, 5, 44100, initData));
}
@TargetApi(16)
......
......@@ -81,7 +81,7 @@ public class HlsMediaPlaylistParserTest extends TestCase {
assertEquals(false, segments.get(0).discontinuity);
assertEquals(7.975, segments.get(0).durationSecs);
assertEquals(null, segments.get(0).encryptionMethod);
assertEquals(false, segments.get(0).isEncrypted);
assertEquals(null, segments.get(0).encryptionKeyUri);
assertEquals(null, segments.get(0).encryptionIV);
assertEquals(51370, segments.get(0).byterangeLength);
......@@ -90,7 +90,7 @@ public class HlsMediaPlaylistParserTest extends TestCase {
assertEquals(false, segments.get(1).discontinuity);
assertEquals(7.975, segments.get(1).durationSecs);
assertEquals("AES-128", segments.get(1).encryptionMethod);
assertEquals(true, segments.get(1).isEncrypted);
assertEquals("https://priv.example.com/key.php?r=2680", segments.get(1).encryptionKeyUri);
assertEquals("0x1566B", segments.get(1).encryptionIV);
assertEquals(51501, segments.get(1).byterangeLength);
......@@ -99,7 +99,7 @@ public class HlsMediaPlaylistParserTest extends TestCase {
assertEquals(false, segments.get(2).discontinuity);
assertEquals(7.941, segments.get(2).durationSecs);
assertEquals(HlsMediaPlaylist.ENCRYPTION_METHOD_NONE, segments.get(2).encryptionMethod);
assertEquals(false, segments.get(2).isEncrypted);
assertEquals(null, segments.get(2).encryptionKeyUri);
assertEquals(null, segments.get(2).encryptionIV);
assertEquals(51501, segments.get(2).byterangeLength);
......@@ -108,7 +108,7 @@ public class HlsMediaPlaylistParserTest extends TestCase {
assertEquals(true, segments.get(3).discontinuity);
assertEquals(7.975, segments.get(3).durationSecs);
assertEquals("AES-128", segments.get(3).encryptionMethod);
assertEquals(true, segments.get(3).isEncrypted);
assertEquals("https://priv.example.com/key.php?r=2682", segments.get(3).encryptionKeyUri);
// 0xA7A == 2682.
assertNotNull(segments.get(3).encryptionIV);
......@@ -119,11 +119,11 @@ public class HlsMediaPlaylistParserTest extends TestCase {
assertEquals(false, segments.get(4).discontinuity);
assertEquals(7.975, segments.get(4).durationSecs);
assertEquals("AES-128", segments.get(4).encryptionMethod);
assertEquals(true, segments.get(4).isEncrypted);
assertEquals("https://priv.example.com/key.php?r=2682", segments.get(4).encryptionKeyUri);
// 0xA7A == 2682.
// 0xA7B == 2683.
assertNotNull(segments.get(4).encryptionIV);
assertEquals("A7A", segments.get(4).encryptionIV.toUpperCase(Locale.getDefault()));
assertEquals("A7B", segments.get(4).encryptionIV.toUpperCase(Locale.getDefault()));
assertEquals(C.LENGTH_UNBOUNDED, segments.get(4).byterangeLength);
assertEquals(0, segments.get(4).byterangeOffset);
assertEquals("https://priv.example.com/fileSequence2683.ts", segments.get(4).url);
......
/*
* 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.source;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.MediaFormatHolder;
import com.google.android.exoplayer.SampleSource;
import com.google.android.exoplayer.util.MimeTypes;
import junit.framework.TestCase;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
/** Tests for {@link DefaultSampleSource}. */
public final class DefaultSampleSourceTest extends TestCase {
private static final int RENDERER_COUNT = 2;
private static final MediaFormat FAKE_MEDIA_FORMAT =
MediaFormat.createFormatForMimeType(MimeTypes.AUDIO_AAC);
private DefaultSampleSource defaultSampleSource;
@Mock SampleExtractor mockSampleExtractor;
@Override
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
when(mockSampleExtractor.prepare()).thenReturn(true);
when(mockSampleExtractor.getTrackCount()).thenReturn(2);
when(mockSampleExtractor.getMediaFormat(anyInt())).thenReturn(FAKE_MEDIA_FORMAT);
defaultSampleSource = new DefaultSampleSource(mockSampleExtractor, RENDERER_COUNT);
}
public void testSourceReleasedWhenRenderersReleased() throws Exception {
// Given a prepared sample source
defaultSampleSource.prepare();
// When releasing it once, it is not released.
defaultSampleSource.release();
verify(mockSampleExtractor, never()).release();
// When releasing RENDERER_COUNT times, it is released.
defaultSampleSource.release();
verify(mockSampleExtractor).release();
}
public void testEnablingTracksAtStartDoesNotSeek() throws Exception {
// Given a prepared sample source
defaultSampleSource.prepare();
// When the first track is enabled at t=0, the sample extractor does not seek.
defaultSampleSource.enable(0, 0L);
verify(mockSampleExtractor, never()).seekTo(0);
// When the second track is enabled at t=0, the sample extractor does not seek.
defaultSampleSource.enable(1, 0L);
verify(mockSampleExtractor, never()).seekTo(0);
}
public void testEnablingTracksInMiddleDoesSeek() throws Exception {
// Given a prepared sample source
defaultSampleSource.prepare();
// When the first track is enabled at t!=0, the sample extractor does seek.
defaultSampleSource.enable(0, 1000L);
verify(mockSampleExtractor, times(1)).seekTo(1000L);
// When the second track is enabled at t!=0, the sample extractor does seek.
defaultSampleSource.enable(1, 1000L);
verify(mockSampleExtractor, times(2)).seekTo(1000L);
}
public void testEnablingTrackSelectsTrack() throws Exception {
// Given a prepared sample source
defaultSampleSource.prepare();
// When the first track is enabled, it selects the first track.
defaultSampleSource.enable(0, 0L);
verify(mockSampleExtractor).selectTrack(0);
}
public void testReadDataInitiallyReadsFormat() throws Exception {
// Given a prepared sample source with the first track selected
defaultSampleSource.prepare();
defaultSampleSource.enable(0, 0L);
// A format is read.
MediaFormatHolder mediaFormatHolder = new MediaFormatHolder();
assertEquals(SampleSource.FORMAT_READ,
defaultSampleSource.readData(0, 0, mediaFormatHolder, null, false));
}
public void testSeekAndReadDataReadsDiscontinuity() throws Exception {
// Given a prepared sample source with the first track selected
defaultSampleSource.prepare();
defaultSampleSource.enable(0, 1L);
// A discontinuity is read.
assertEquals(
SampleSource.DISCONTINUITY_READ, defaultSampleSource.readData(0, 0, null, null, false));
}
}
/*
* 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.upstream;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import com.google.android.exoplayer.SampleSource;
import junit.framework.TestCase;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import java.util.Arrays;
/**
* Tests for {@link BufferedNonBlockingInputStream}.
*/
public class BufferedNonBlockingInputStreamTest extends TestCase {
private static final int BUFFER_SIZE_BYTES = 16;
@Mock private NonBlockingInputStream mockInputStream;
private BufferedNonBlockingInputStream bufferedInputStream;
@Override
public void setUp() {
MockitoAnnotations.initMocks(this);
bufferedInputStream = new BufferedNonBlockingInputStream(mockInputStream, BUFFER_SIZE_BYTES);
}
public void testSkipClipsCountToBufferSizeWhenMarkSet() {
// When marking and skipping more than the buffer size
bufferedInputStream.mark();
bufferedInputStream.skip(BUFFER_SIZE_BYTES + 1);
// Then BUFFER_SIZE_BYTES are read.
verify(mockInputStream).read((byte[]) any(), eq(0), eq(BUFFER_SIZE_BYTES));
}
public void testSkipResetSkipUsesBufferedData() {
// Given a buffered input stream that has already read BUFFER_SIZE_BYTES
stubInputStreamForReadingBytes();
bufferedInputStream.mark();
bufferedInputStream.skip(BUFFER_SIZE_BYTES);
verify(mockInputStream).read((byte[]) any(), eq(0), eq(BUFFER_SIZE_BYTES));
// When resetting and reading the same amount, no extra data are read.
bufferedInputStream.returnToMark();
bufferedInputStream.skip(BUFFER_SIZE_BYTES);
verify(mockInputStream).read((byte[]) any(), eq(0), eq(BUFFER_SIZE_BYTES));
}
public void testReturnsEndOfStreamAfterBufferedData() {
// Given a buffered input stream that has read 1 byte (to end-of-stream) and has been reset
stubInputStreamForReadingBytes();
bufferedInputStream.mark();
bufferedInputStream.skip(1);
stubInputStreamForReadingEndOfStream();
bufferedInputStream.returnToMark();
// When skipping, first 1 byte is returned, then end-of-stream.
assertEquals(1, bufferedInputStream.skip(1));
assertEquals(SampleSource.END_OF_STREAM, bufferedInputStream.skip(1));
}
public void testReadAtOffset() {
// Given a mock input stream that provide non-zero data
stubInputStreamForReadingBytes();
// When reading a byte at offset 1
byte[] bytes = new byte[2];
bufferedInputStream.mark();
bufferedInputStream.read(bytes, 1, 1);
// Then only the second byte is set.
assertTrue(Arrays.equals(new byte[] {(byte) 0, (byte) 0xFF}, bytes));
}
public void testSkipAfterMark() {
// Given a mock input stream that provides non-zero data, with three bytes read
stubInputStreamForReadingBytes();
bufferedInputStream.skip(1);
bufferedInputStream.mark();
bufferedInputStream.skip(2);
bufferedInputStream.returnToMark();
// Then it is possible to skip one byte after the mark and read two bytes.
assertEquals(1, bufferedInputStream.skip(1));
assertEquals(2, bufferedInputStream.read(new byte[2], 0, 2));
verify(mockInputStream).read((byte[]) any(), eq(0), eq(1));
verify(mockInputStream).read((byte[]) any(), eq(0), eq(2));
verify(mockInputStream).read((byte[]) any(), eq(2), eq(1));
}
/** Stubs the input stream to read 0xFF for all requests. */
private void stubInputStreamForReadingBytes() {
when(mockInputStream.read((byte[]) any(), anyInt(), anyInt())).thenAnswer(
new Answer<Integer>() {
@Override
public Integer answer(InvocationOnMock invocation) throws Throwable {
byte[] bytes = (byte[]) invocation.getArguments()[0];
int offset = (int) invocation.getArguments()[1];
int length = (int) invocation.getArguments()[2];
for (int i = 0; i < length; i++) {
bytes[i + offset] = (byte) 0xFF;
}
return length;
}
});
when(mockInputStream.skip(anyInt())).thenAnswer(new Answer<Integer>() {
@Override
public Integer answer(InvocationOnMock invocation) throws Throwable {
return (int) invocation.getArguments()[0];
}
});
}
/** Stubs the input stream to read end-of-stream for all requests. */
private void stubInputStreamForReadingEndOfStream() {
when(mockInputStream.read((byte[]) any(), anyInt(), anyInt()))
.thenReturn(SampleSource.END_OF_STREAM);
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment