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 { ...@@ -48,6 +48,7 @@ public class DemoUtil {
public static final int TYPE_SS = 1; public static final int TYPE_SS = 1;
public static final int TYPE_OTHER = 2; public static final int TYPE_OTHER = 2;
public static final int TYPE_HLS = 3; public static final int TYPE_HLS = 3;
public static final int TYPE_MP4 = 4;
private static final CookieManager defaultCookieManager; private static final CookieManager defaultCookieManager;
......
...@@ -24,6 +24,7 @@ import com.google.android.exoplayer.demo.player.DefaultRendererBuilder; ...@@ -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;
import com.google.android.exoplayer.demo.player.DemoPlayer.RendererBuilder; import com.google.android.exoplayer.demo.player.DemoPlayer.RendererBuilder;
import com.google.android.exoplayer.demo.player.HlsRendererBuilder; 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.SmoothStreamingRendererBuilder;
import com.google.android.exoplayer.demo.player.UnsupportedDrmException; import com.google.android.exoplayer.demo.player.UnsupportedDrmException;
import com.google.android.exoplayer.metadata.GeobMetadata; import com.google.android.exoplayer.metadata.GeobMetadata;
...@@ -215,6 +216,8 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback, ...@@ -215,6 +216,8 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback,
new WidevineTestMediaDrmCallback(contentId), debugTextView, audioCapabilities); new WidevineTestMediaDrmCallback(contentId), debugTextView, audioCapabilities);
case DemoUtil.TYPE_HLS: case DemoUtil.TYPE_HLS:
return new HlsRendererBuilder(userAgent, contentUri.toString()); return new HlsRendererBuilder(userAgent, contentUri.toString());
case DemoUtil.TYPE_MP4:
return new Mp4RendererBuilder(contentUri, debugTextView);
default: default:
return new DefaultRendererBuilder(this, contentUri, debugTextView); return new DefaultRendererBuilder(this, contentUri, debugTextView);
} }
......
...@@ -135,6 +135,12 @@ import java.util.Locale; ...@@ -135,6 +135,12 @@ import java.util.Locale;
new Sample("Apple AAC 10s", "https://devimages.apple.com.edgekey.net/" new Sample("Apple AAC 10s", "https://devimages.apple.com.edgekey.net/"
+ "streaming/examples/bipbop_4x3/gear0/fileSequence0.aac", + "streaming/examples/bipbop_4x3/gear0/fileSequence0.aac",
DemoUtil.TYPE_OTHER), 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() {} 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 { ...@@ -281,7 +281,7 @@ public final class Ac3PassthroughAudioTrackRenderer extends TrackRenderer {
protected void onDisabled() { protected void onDisabled() {
audioSessionId = AudioTrack.SESSION_ID_NOT_SET; audioSessionId = AudioTrack.SESSION_ID_NOT_SET;
shouldReadInputBuffer = true; shouldReadInputBuffer = true;
audioTrack.reset(); audioTrack.release();
} }
@Override @Override
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer; package com.google.android.exoplayer;
import android.media.MediaCodec;
import android.media.MediaExtractor; import android.media.MediaExtractor;
/** /**
...@@ -43,11 +44,23 @@ public final class C { ...@@ -43,11 +44,23 @@ public final class C {
public static final String UTF8_NAME = "UTF-8"; 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") @SuppressWarnings("InlinedApi")
public static final int SAMPLE_FLAG_SYNC = MediaExtractor.SAMPLE_FLAG_SYNC; 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() {} private C() {}
} }
...@@ -202,7 +202,7 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer { ...@@ -202,7 +202,7 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer {
protected void onDisabled() { protected void onDisabled() {
audioSessionId = AudioTrack.SESSION_ID_NOT_SET; audioSessionId = AudioTrack.SESSION_ID_NOT_SET;
try { try {
audioTrack.reset(); audioTrack.release();
} finally { } finally {
super.onDisabled(); super.onDisabled();
} }
......
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
package com.google.android.exoplayer; package com.google.android.exoplayer;
import com.google.android.exoplayer.MediaCodecUtil.DecoderQueryException; 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.drm.DrmSessionManager;
import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.Util; import com.google.android.exoplayer.util.Util;
...@@ -25,7 +26,6 @@ import android.media.MediaCodec; ...@@ -25,7 +26,6 @@ import android.media.MediaCodec;
import android.media.MediaCodec.CodecException; import android.media.MediaCodec.CodecException;
import android.media.MediaCodec.CryptoException; import android.media.MediaCodec.CryptoException;
import android.media.MediaCrypto; import android.media.MediaCrypto;
import android.media.MediaExtractor;
import android.os.Handler; import android.os.Handler;
import android.os.SystemClock; import android.os.SystemClock;
...@@ -33,8 +33,6 @@ import java.io.IOException; ...@@ -33,8 +33,6 @@ import java.io.IOException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; 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. * An abstract {@link TrackRenderer} that uses {@link MediaCodec} to decode samples for rendering.
...@@ -164,7 +162,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { ...@@ -164,7 +162,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
protected final Handler eventHandler; protected final Handler eventHandler;
private MediaFormat format; private MediaFormat format;
private Map<UUID, byte[]> drmInitData; private DrmInitData drmInitData;
private MediaCodec codec; private MediaCodec codec;
private boolean codecIsAdaptive; private boolean codecIsAdaptive;
private ByteBuffer[] inputBuffers; private ByteBuffer[] inputBuffers;
...@@ -281,7 +279,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { ...@@ -281,7 +279,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
throw new ExoPlaybackException("Media requires a DrmSessionManager"); throw new ExoPlaybackException("Media requires a DrmSessionManager");
} }
if (!openedDrmSession) { if (!openedDrmSession) {
drmSessionManager.open(drmInitData, mimeType); drmSessionManager.open(drmInitData);
openedDrmSession = true; openedDrmSession = true;
} }
int drmSessionState = drmSessionManager.getState(); int drmSessionState = drmSessionManager.getState();
...@@ -584,7 +582,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { ...@@ -584,7 +582,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
} }
waitingForFirstSyncFrame = false; waitingForFirstSyncFrame = false;
} }
boolean sampleEncrypted = (sampleHolder.flags & MediaExtractor.SAMPLE_FLAG_ENCRYPTED) != 0; boolean sampleEncrypted = (sampleHolder.flags & C.SAMPLE_FLAG_ENCRYPTED) != 0;
waitingForKeys = shouldWaitForKeys(sampleEncrypted); waitingForKeys = shouldWaitForKeys(sampleEncrypted);
if (waitingForKeys) { if (waitingForKeys) {
return false; return false;
......
...@@ -40,6 +40,8 @@ public class MediaFormat { ...@@ -40,6 +40,8 @@ public class MediaFormat {
public final String mimeType; public final String mimeType;
public final int maxInputSize; public final int maxInputSize;
public final long durationUs;
public final int width; public final int width;
public final int height; public final int height;
public final float pixelWidthHeightRatio; public final float pixelWidthHeightRatio;
...@@ -49,11 +51,11 @@ public class MediaFormat { ...@@ -49,11 +51,11 @@ public class MediaFormat {
public final int bitrate; public final int bitrate;
public final List<byte[]> initializationData;
private int maxWidth; private int maxWidth;
private int maxHeight; private int maxHeight;
public final List<byte[]> initializationData;
// Lazy-initialized hashcode. // Lazy-initialized hashcode.
private int hashCode; private int hashCode;
// Possibly-lazy-initialized framework media format. // Possibly-lazy-initialized framework media format.
...@@ -66,25 +68,38 @@ public class MediaFormat { ...@@ -66,25 +68,38 @@ public class MediaFormat {
public static MediaFormat createVideoFormat(String mimeType, int maxInputSize, int width, public static MediaFormat createVideoFormat(String mimeType, int maxInputSize, int width,
int height, List<byte[]> initializationData) { 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, public static MediaFormat createVideoFormat(String mimeType, int maxInputSize, long durationUs,
int height, float pixelWidthHeightRatio, List<byte[]> initializationData) { int width, int height, List<byte[]> initializationData) {
return new MediaFormat(mimeType, maxInputSize, width, height, pixelWidthHeightRatio, NO_VALUE, return createVideoFormat(
NO_VALUE, NO_VALUE, initializationData); 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, public static MediaFormat createAudioFormat(String mimeType, int maxInputSize, int channelCount,
int sampleRate, List<byte[]> initializationData) { int sampleRate, List<byte[]> initializationData) {
return new MediaFormat(mimeType, maxInputSize, NO_VALUE, NO_VALUE, NO_VALUE, channelCount, return createAudioFormat(
sampleRate, NO_VALUE, initializationData); mimeType, maxInputSize, C.UNKNOWN_TIME_US, channelCount, sampleRate, initializationData);
} }
public static MediaFormat createAudioFormat(String mimeType, int maxInputSize, int channelCount, public static MediaFormat createAudioFormat(String mimeType, int maxInputSize, long durationUs,
int sampleRate, int bitrate, List<byte[]> initializationData) { int channelCount, int sampleRate, List<byte[]> initializationData) {
return new MediaFormat(mimeType, maxInputSize, NO_VALUE, NO_VALUE, NO_VALUE, channelCount, return createAudioFormat(
sampleRate, bitrate, initializationData); 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() { public static MediaFormat createId3Format() {
...@@ -100,8 +115,8 @@ public class MediaFormat { ...@@ -100,8 +115,8 @@ public class MediaFormat {
} }
public static MediaFormat createFormatForMimeType(String mimeType) { public static MediaFormat createFormatForMimeType(String mimeType) {
return new MediaFormat(mimeType, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, return new MediaFormat(mimeType, NO_VALUE, C.UNKNOWN_TIME_US, NO_VALUE, NO_VALUE, NO_VALUE,
NO_VALUE, null); NO_VALUE, NO_VALUE, NO_VALUE, null);
} }
@TargetApi(16) @TargetApi(16)
...@@ -123,15 +138,18 @@ public class MediaFormat { ...@@ -123,15 +138,18 @@ public class MediaFormat {
initializationData.add(data); initializationData.add(data);
buffer.flip(); buffer.flip();
} }
durationUs = format.containsKey(android.media.MediaFormat.KEY_DURATION)
? format.getLong(android.media.MediaFormat.KEY_DURATION) : C.UNKNOWN_TIME_US;
maxWidth = NO_VALUE; maxWidth = NO_VALUE;
maxHeight = 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, float pixelWidthHeightRatio, int channelCount, int sampleRate, int bitrate,
List<byte[]> initializationData) { List<byte[]> initializationData) {
this.mimeType = mimeType; this.mimeType = mimeType;
this.maxInputSize = maxInputSize; this.maxInputSize = maxInputSize;
this.durationUs = durationUs;
this.width = width; this.width = width;
this.height = height; this.height = height;
this.pixelWidthHeightRatio = pixelWidthHeightRatio; this.pixelWidthHeightRatio = pixelWidthHeightRatio;
...@@ -169,6 +187,7 @@ public class MediaFormat { ...@@ -169,6 +187,7 @@ public class MediaFormat {
result = 31 * result + width; result = 31 * result + width;
result = 31 * result + height; result = 31 * result + height;
result = 31 * result + Float.floatToRawIntBits(pixelWidthHeightRatio); result = 31 * result + Float.floatToRawIntBits(pixelWidthHeightRatio);
result = 31 * result + (int) durationUs;
result = 31 * result + maxWidth; result = 31 * result + maxWidth;
result = 31 * result + maxHeight; result = 31 * result + maxHeight;
result = 31 * result + channelCount; result = 31 * result + channelCount;
...@@ -225,7 +244,7 @@ public class MediaFormat { ...@@ -225,7 +244,7 @@ public class MediaFormat {
public String toString() { public String toString() {
return "MediaFormat(" + mimeType + ", " + maxInputSize + ", " + width + ", " + height + ", " return "MediaFormat(" + mimeType + ", " + maxInputSize + ", " + width + ", " + height + ", "
+ pixelWidthHeightRatio + ", " + channelCount + ", " + sampleRate + ", " + bitrate + ", " + pixelWidthHeightRatio + ", " + channelCount + ", " + sampleRate + ", " + bitrate + ", "
+ maxWidth + ", " + maxHeight + ")"; + durationUs + ", " + maxWidth + ", " + maxHeight + ")";
} }
/** /**
...@@ -246,6 +265,9 @@ public class MediaFormat { ...@@ -246,6 +265,9 @@ public class MediaFormat {
for (int i = 0; i < initializationData.size(); i++) { for (int i = 0; i < initializationData.size(); i++) {
format.setByteBuffer("csd-" + i, ByteBuffer.wrap(initializationData.get(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); maybeSetMaxDimensionsV16(format);
frameworkMediaFormat = format; frameworkMediaFormat = format;
} }
......
...@@ -15,8 +15,7 @@ ...@@ -15,8 +15,7 @@
*/ */
package com.google.android.exoplayer; package com.google.android.exoplayer;
import java.util.Map; import com.google.android.exoplayer.drm.DrmInitData;
import java.util.UUID;
/** /**
* Holds a {@link MediaFormat} and corresponding drm scheme initialization data. * Holds a {@link MediaFormat} and corresponding drm scheme initialization data.
...@@ -28,9 +27,8 @@ public final class MediaFormatHolder { ...@@ -28,9 +27,8 @@ public final class MediaFormatHolder {
*/ */
public MediaFormat format; public MediaFormat format;
/** /**
* Initialization data for each of the drm schemes supported by the media, keyed by scheme UUID. * Initialization data for drm schemes supported by the media. Null if the media is not encrypted.
* Null if the media is not encrypted.
*/ */
public Map<UUID, byte[]> drmInitData; public DrmInitData drmInitData;
} }
...@@ -50,9 +50,8 @@ public final class SampleHolder { ...@@ -50,9 +50,8 @@ public final class SampleHolder {
public int size; public int size;
/** /**
* Flags that accompany the sample. A combination of * Flags that accompany the sample. A combination of {@link C#SAMPLE_FLAG_SYNC} and
* {@link android.media.MediaExtractor#SAMPLE_FLAG_SYNC} and * {@link C#SAMPLE_FLAG_ENCRYPTED}
* {@link android.media.MediaExtractor#SAMPLE_FLAG_ENCRYPTED}
*/ */
public int flags; public int flags;
......
...@@ -44,6 +44,8 @@ import java.nio.ByteBuffer; ...@@ -44,6 +44,8 @@ import java.nio.ByteBuffer;
* <p>Call {@link #reconfigure} when the output format changes. * <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 #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) @TargetApi(16)
public final class AudioTrack { public final class AudioTrack {
...@@ -91,6 +93,12 @@ public final class AudioTrack { ...@@ -91,6 +93,12 @@ public final class AudioTrack {
/** Returned by {@link #getCurrentPositionUs} when the position is not set. */ /** Returned by {@link #getCurrentPositionUs} when the position is not set. */
public static final long CURRENT_POSITION_NOT_SET = Long.MIN_VALUE; 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. */ /** A minimum length for the {@link android.media.AudioTrack} buffer, in microseconds. */
private static final long MIN_BUFFER_DURATION_US = 250000; private static final long MIN_BUFFER_DURATION_US = 250000;
/** A maximum length for the {@link android.media.AudioTrack} buffer, in microseconds. */ /** A maximum length for the {@link android.media.AudioTrack} buffer, in microseconds. */
...@@ -132,6 +140,9 @@ public final class AudioTrack { ...@@ -132,6 +140,9 @@ public final class AudioTrack {
private final ConditionVariable releasingConditionVariable; private final ConditionVariable releasingConditionVariable;
private final long[] playheadOffsets; 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 android.media.AudioTrack audioTrack;
private AudioTrackUtil audioTrackUtil; private AudioTrackUtil audioTrackUtil;
private int sampleRate; private int sampleRate;
...@@ -267,15 +278,37 @@ public final class AudioTrack { ...@@ -267,15 +278,37 @@ public final class AudioTrack {
audioTrack = new android.media.AudioTrack(AudioManager.STREAM_MUSIC, sampleRate, audioTrack = new android.media.AudioTrack(AudioManager.STREAM_MUSIC, sampleRate,
channelConfig, encoding, bufferSize, android.media.AudioTrack.MODE_STREAM, sessionId); channelConfig, encoding, bufferSize, android.media.AudioTrack.MODE_STREAM, sessionId);
} }
checkAudioTrackInitialized(); 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) { if (Util.SDK_INT >= 19) {
audioTrackUtil = new AudioTrackUtilV19(audioTrack); audioTrackUtil = new AudioTrackUtilV19(audioTrack);
} else { } else {
audioTrackUtil = new AudioTrackUtil(audioTrack); audioTrackUtil = new AudioTrackUtil(audioTrack);
} }
setVolume(volume); setVolume(volume);
return audioTrack.getAudioSessionId();
return sessionId;
} }
/** /**
...@@ -515,9 +548,9 @@ public final class AudioTrack { ...@@ -515,9 +548,9 @@ public final class AudioTrack {
} }
/** /**
* Releases resources associated with this instance asynchronously. Calling {@link #initialize} * Releases the underlying audio track asynchronously. Calling {@link #initialize} will block
* will block until the audio track has been released, so it is safe to initialize immediately * until the audio track has been released, so it is safe to initialize immediately after
* after resetting. * resetting. The audio session may remain active until the instance is {@link #release}d.
*/ */
public void reset() { public void reset() {
if (isInitialized()) { if (isInitialized()) {
...@@ -547,6 +580,29 @@ public final class AudioTrack { ...@@ -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. */ /** Returns whether {@link #getCurrentPositionUs} can return the current playback position. */
private boolean hasCurrentPositionUs() { private boolean hasCurrentPositionUs() {
return isInitialized() && startMediaTimeUs != START_NOT_SET; return isInitialized() && startMediaTimeUs != START_NOT_SET;
......
...@@ -352,7 +352,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback { ...@@ -352,7 +352,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback {
if (mediaFormat != null && !mediaFormat.equals(downstreamMediaFormat, true)) { if (mediaFormat != null && !mediaFormat.equals(downstreamMediaFormat, true)) {
chunkSource.getMaxVideoDimensions(mediaFormat); chunkSource.getMaxVideoDimensions(mediaFormat);
formatHolder.format = mediaFormat; formatHolder.format = mediaFormat;
formatHolder.drmInitData = mediaChunk.getPsshInfo(); formatHolder.drmInitData = mediaChunk.getDrmInitData();
downstreamMediaFormat = mediaFormat; downstreamMediaFormat = mediaFormat;
return FORMAT_READ; return FORMAT_READ;
} }
......
...@@ -19,14 +19,12 @@ import com.google.android.exoplayer.MediaFormat; ...@@ -19,14 +19,12 @@ import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.ParserException; import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.SampleHolder; import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.chunk.parser.Extractor; 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.DataSource;
import com.google.android.exoplayer.upstream.DataSpec; import com.google.android.exoplayer.upstream.DataSpec;
import com.google.android.exoplayer.upstream.NonBlockingInputStream; import com.google.android.exoplayer.upstream.NonBlockingInputStream;
import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.Assertions;
import java.util.Map;
import java.util.UUID;
/** /**
* A {@link MediaChunk} extracted from a container. * A {@link MediaChunk} extracted from a container.
*/ */
...@@ -38,7 +36,7 @@ public final class ContainerMediaChunk extends MediaChunk { ...@@ -38,7 +36,7 @@ public final class ContainerMediaChunk extends MediaChunk {
private boolean prepared; private boolean prepared;
private MediaFormat mediaFormat; private MediaFormat mediaFormat;
private Map<UUID, byte[]> psshInfo; private DrmInitData drmInitData;
/** /**
* @deprecated Use the other constructor, passing null as {@code psshInfo}. * @deprecated Use the other constructor, passing null as {@code psshInfo}.
...@@ -60,8 +58,9 @@ public final class ContainerMediaChunk extends MediaChunk { ...@@ -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 endTimeUs The end time of the media contained by the chunk, in microseconds.
* @param nextChunkIndex The index of the next chunk, or -1 if this is the last chunk. * @param nextChunkIndex The index of the next chunk, or -1 if this is the last chunk.
* @param extractor The extractor that will be used to extract the samples. * @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 * @param drmInitData DRM initialization data. May be null if DRM initialization data is present
* can be obtained directly from {@code extractor}, or if no pssh data is required. * 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 * @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 * 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 * 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 { ...@@ -70,12 +69,12 @@ public final class ContainerMediaChunk extends MediaChunk {
*/ */
public ContainerMediaChunk(DataSource dataSource, DataSpec dataSpec, Format format, public ContainerMediaChunk(DataSource dataSource, DataSpec dataSpec, Format format,
int trigger, long startTimeUs, long endTimeUs, int nextChunkIndex, Extractor extractor, 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); super(dataSource, dataSpec, format, trigger, startTimeUs, endTimeUs, nextChunkIndex);
this.extractor = extractor; this.extractor = extractor;
this.maybeSelfContained = maybeSelfContained; this.maybeSelfContained = maybeSelfContained;
this.sampleOffsetUs = sampleOffsetUs; this.sampleOffsetUs = sampleOffsetUs;
this.psshInfo = psshInfo; this.drmInitData = drmInitData;
} }
@Override @Override
...@@ -111,9 +110,9 @@ public final class ContainerMediaChunk extends MediaChunk { ...@@ -111,9 +110,9 @@ public final class ContainerMediaChunk extends MediaChunk {
} }
if (prepared) { if (prepared) {
mediaFormat = extractor.getFormat(); mediaFormat = extractor.getFormat();
Map<UUID, byte[]> extractorPsshInfo = extractor.getPsshInfo(); DrmInitData extractorDrmInitData = extractor.getDrmInitData();
if (extractorPsshInfo != null) { if (extractorDrmInitData != null) {
psshInfo = extractorPsshInfo; drmInitData = extractorDrmInitData;
} }
} }
} }
...@@ -145,8 +144,8 @@ public final class ContainerMediaChunk extends MediaChunk { ...@@ -145,8 +144,8 @@ public final class ContainerMediaChunk extends MediaChunk {
} }
@Override @Override
public Map<UUID, byte[]> getPsshInfo() { public DrmInitData getDrmInitData() {
return psshInfo; return drmInitData;
} }
} }
...@@ -18,12 +18,10 @@ package com.google.android.exoplayer.chunk; ...@@ -18,12 +18,10 @@ package com.google.android.exoplayer.chunk;
import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.ParserException; import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.SampleHolder; 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.DataSource;
import com.google.android.exoplayer.upstream.DataSpec; 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. * An abstract base class for {@link Chunk}s that contain media samples.
*/ */
...@@ -129,12 +127,12 @@ public abstract class MediaChunk extends Chunk { ...@@ -129,12 +127,12 @@ public abstract class MediaChunk extends Chunk {
public abstract MediaFormat getMediaFormat(); public abstract MediaFormat getMediaFormat();
/** /**
* Returns the pssh information associated with the chunk. * Returns the DRM initialization data associated with the chunk.
* <p> * <p>
* Should only be called after the chunk has been successfully prepared. * 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; ...@@ -17,14 +17,12 @@ package com.google.android.exoplayer.chunk;
import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.SampleHolder; import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.drm.DrmInitData;
import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DataSpec; import com.google.android.exoplayer.upstream.DataSpec;
import com.google.android.exoplayer.upstream.NonBlockingInputStream; import com.google.android.exoplayer.upstream.NonBlockingInputStream;
import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.Assertions;
import java.util.Map;
import java.util.UUID;
/** /**
* A {@link MediaChunk} containing a single sample. * A {@link MediaChunk} containing a single sample.
*/ */
...@@ -132,7 +130,7 @@ public class SingleSampleMediaChunk extends MediaChunk { ...@@ -132,7 +130,7 @@ public class SingleSampleMediaChunk extends MediaChunk {
} }
@Override @Override
public Map<UUID, byte[]> getPsshInfo() { public DrmInitData getDrmInitData() {
return null; return null;
} }
......
...@@ -15,15 +15,12 @@ ...@@ -15,15 +15,12 @@
*/ */
package com.google.android.exoplayer.chunk.parser; package com.google.android.exoplayer.chunk.parser;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.ParserException; import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.SampleHolder; import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.drm.DrmInitData;
import com.google.android.exoplayer.upstream.NonBlockingInputStream; import com.google.android.exoplayer.upstream.NonBlockingInputStream;
import java.util.Map;
import java.util.UUID;
/** /**
* Facilitates extraction of media samples from a container format. * Facilitates extraction of media samples from a container format.
*/ */
...@@ -43,7 +40,7 @@ public interface Extractor { ...@@ -43,7 +40,7 @@ public interface Extractor {
public static final int RESULT_READ_SAMPLE = 4; public static final int RESULT_READ_SAMPLE = 4;
/** /**
* Initialization data was read. The parsed data can be read using {@link #getFormat()} and * 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; public static final int RESULT_READ_INIT = 8;
/** /**
...@@ -80,17 +77,12 @@ public interface Extractor { ...@@ -80,17 +77,12 @@ public interface Extractor {
public MediaFormat getFormat(); public MediaFormat getFormat();
/** /**
* Returns the duration of the stream in microseconds, or {@link C#UNKNOWN_TIME_US} if unknown. * Returns DRM initialization data parsed from the stream.
*/
public long getDurationUs();
/**
* Returns the pssh information parsed from the stream.
* *
* @return The pssh information. May be null if pssh data has yet to be parsed, or if the stream * @return The DRM initialization data. May be null if the initialization data has yet to be
* does not contain any pssh data. * 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}. * Consumes data from a {@link NonBlockingInputStream}.
......
...@@ -21,6 +21,7 @@ import com.google.android.exoplayer.ParserException; ...@@ -21,6 +21,7 @@ import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.SampleHolder; import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.chunk.parser.Extractor; import com.google.android.exoplayer.chunk.parser.Extractor;
import com.google.android.exoplayer.chunk.parser.SegmentIndex; 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;
import com.google.android.exoplayer.mp4.Atom.ContainerAtom; import com.google.android.exoplayer.mp4.Atom.ContainerAtom;
import com.google.android.exoplayer.mp4.Atom.LeafAtom; import com.google.android.exoplayer.mp4.Atom.LeafAtom;
...@@ -28,20 +29,15 @@ import com.google.android.exoplayer.mp4.CommonMp4AtomParsers; ...@@ -28,20 +29,15 @@ import com.google.android.exoplayer.mp4.CommonMp4AtomParsers;
import com.google.android.exoplayer.mp4.Mp4Util; import com.google.android.exoplayer.mp4.Mp4Util;
import com.google.android.exoplayer.mp4.Track; import com.google.android.exoplayer.mp4.Track;
import com.google.android.exoplayer.upstream.NonBlockingInputStream; 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.ParsableByteArray;
import com.google.android.exoplayer.util.Util; 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.nio.ByteBuffer;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.Stack; import java.util.Stack;
import java.util.UUID; import java.util.UUID;
...@@ -145,7 +141,7 @@ public final class FragmentedMp4Extractor implements Extractor { ...@@ -145,7 +141,7 @@ public final class FragmentedMp4Extractor implements Extractor {
private int lastSyncSampleIndex; private int lastSyncSampleIndex;
// Data parsed from moov and sidx atoms // Data parsed from moov and sidx atoms
private final HashMap<UUID, byte[]> psshData; private DrmInitData.Mapped drmInitData;
private SegmentIndex segmentIndex; private SegmentIndex segmentIndex;
private Track track; private Track track;
private DefaultSampleValues extendsDefaults; private DefaultSampleValues extendsDefaults;
...@@ -165,7 +161,6 @@ public final class FragmentedMp4Extractor implements Extractor { ...@@ -165,7 +161,6 @@ public final class FragmentedMp4Extractor implements Extractor {
extendedTypeScratch = new byte[16]; extendedTypeScratch = new byte[16];
containerAtoms = new Stack<ContainerAtom>(); containerAtoms = new Stack<ContainerAtom>();
fragmentRun = new TrackFragment(); fragmentRun = new TrackFragment();
psshData = new HashMap<UUID, byte[]>();
} }
/** /**
...@@ -179,8 +174,8 @@ public final class FragmentedMp4Extractor implements Extractor { ...@@ -179,8 +174,8 @@ public final class FragmentedMp4Extractor implements Extractor {
} }
@Override @Override
public Map<UUID, byte[]> getPsshInfo() { public DrmInitData getDrmInitData() {
return psshData.isEmpty() ? null : psshData; return drmInitData;
} }
@Override @Override
...@@ -199,11 +194,6 @@ public final class FragmentedMp4Extractor implements Extractor { ...@@ -199,11 +194,6 @@ public final class FragmentedMp4Extractor implements Extractor {
} }
@Override @Override
public long getDurationUs() {
return track == null ? C.UNKNOWN_TIME_US : track.durationUs;
}
@Override
public int read(NonBlockingInputStream inputStream, SampleHolder out) public int read(NonBlockingInputStream inputStream, SampleHolder out)
throws ParserException { throws ParserException {
try { try {
...@@ -375,7 +365,10 @@ public final class FragmentedMp4Extractor implements Extractor { ...@@ -375,7 +365,10 @@ public final class FragmentedMp4Extractor implements Extractor {
int dataSize = psshAtom.readInt(); int dataSize = psshAtom.readInt();
byte[] data = new byte[dataSize]; byte[] data = new byte[dataSize];
psshAtom.readBytes(data, 0, 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); ContainerAtom mvex = moov.getContainerAtomOfType(Atom.TYPE_mvex);
...@@ -798,12 +791,14 @@ public final class FragmentedMp4Extractor implements Extractor { ...@@ -798,12 +791,14 @@ public final class FragmentedMp4Extractor implements Extractor {
return RESULT_READ_SAMPLE; return RESULT_READ_SAMPLE;
} }
@SuppressLint("InlinedApi")
private void readSampleEncryptionData(ParsableByteArray sampleEncryptionData, SampleHolder out) { private void readSampleEncryptionData(ParsableByteArray sampleEncryptionData, SampleHolder out) {
TrackEncryptionBox encryptionBox = TrackEncryptionBox encryptionBox =
track.sampleDescriptionEncryptionBoxes[fragmentRun.sampleDescriptionIndex]; track.sampleDescriptionEncryptionBoxes[fragmentRun.sampleDescriptionIndex];
if (!encryptionBox.isEncrypted) {
return;
}
byte[] keyId = encryptionBox.keyId; byte[] keyId = encryptionBox.keyId;
boolean isEncrypted = encryptionBox.isEncrypted;
int vectorSize = encryptionBox.initializationVectorSize; int vectorSize = encryptionBox.initializationVectorSize;
boolean subsampleEncryption = fragmentRun.sampleHasSubsampleEncryptionTable[sampleIndex]; boolean subsampleEncryption = fragmentRun.sampleHasSubsampleEncryptionTable[sampleIndex];
...@@ -831,11 +826,10 @@ public final class FragmentedMp4Extractor implements Extractor { ...@@ -831,11 +826,10 @@ public final class FragmentedMp4Extractor implements Extractor {
clearDataSizes[0] = 0; clearDataSizes[0] = 0;
encryptedDataSizes[0] = fragmentRun.sampleSizeTable[sampleIndex]; encryptedDataSizes[0] = fragmentRun.sampleSizeTable[sampleIndex];
} }
out.cryptoInfo.set(subsampleCount, clearDataSizes, encryptedDataSizes, keyId, vector, out.cryptoInfo.set(subsampleCount, clearDataSizes, encryptedDataSizes, keyId, vector,
isEncrypted ? MediaCodec.CRYPTO_MODE_AES_CTR : MediaCodec.CRYPTO_MODE_UNENCRYPTED); C.CRYPTO_MODE_AES_CTR);
if (isEncrypted) { out.flags |= C.SAMPLE_FLAG_ENCRYPTED;
out.flags |= MediaExtractor.SAMPLE_FLAG_ENCRYPTED;
}
} }
} }
...@@ -39,6 +39,7 @@ import com.google.android.exoplayer.dash.mpd.MediaPresentationDescription; ...@@ -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.Period;
import com.google.android.exoplayer.dash.mpd.RangedUri; import com.google.android.exoplayer.dash.mpd.RangedUri;
import com.google.android.exoplayer.dash.mpd.Representation; 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.text.webvtt.WebvttParser;
import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DataSpec; import com.google.android.exoplayer.upstream.DataSpec;
...@@ -54,8 +55,6 @@ import java.util.Arrays; ...@@ -54,8 +55,6 @@ import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.UUID;
/** /**
* An {@link ChunkSource} for DASH streams. * An {@link ChunkSource} for DASH streams.
...@@ -96,7 +95,7 @@ public class DashChunkSource implements ChunkSource { ...@@ -96,7 +95,7 @@ public class DashChunkSource implements ChunkSource {
private final ManifestFetcher<MediaPresentationDescription> manifestFetcher; private final ManifestFetcher<MediaPresentationDescription> manifestFetcher;
private final int adaptationSetIndex; private final int adaptationSetIndex;
private final int[] representationIndices; private final int[] representationIndices;
private final Map<UUID, byte[]> psshInfo; private final DrmInitData drmInitData;
private MediaPresentationDescription currentManifest; private MediaPresentationDescription currentManifest;
private boolean finishedCurrentManifest; private boolean finishedCurrentManifest;
...@@ -190,7 +189,7 @@ public class DashChunkSource implements ChunkSource { ...@@ -190,7 +189,7 @@ public class DashChunkSource implements ChunkSource {
this.evaluation = new Evaluation(); this.evaluation = new Evaluation();
this.headerBuilder = new StringBuilder(); this.headerBuilder = new StringBuilder();
psshInfo = getPsshInfo(currentManifest, adaptationSetIndex); drmInitData = getDrmInitData(currentManifest, adaptationSetIndex);
Representation[] representations = getFilteredRepresentations(currentManifest, Representation[] representations = getFilteredRepresentations(currentManifest,
adaptationSetIndex, representationIndices); adaptationSetIndex, representationIndices);
long periodDurationUs = (representations[0].periodDurationMs == TrackRenderer.UNKNOWN_TIME_US) long periodDurationUs = (representations[0].periodDurationMs == TrackRenderer.UNKNOWN_TIME_US)
...@@ -407,7 +406,7 @@ public class DashChunkSource implements ChunkSource { ...@@ -407,7 +406,7 @@ public class DashChunkSource implements ChunkSource {
// Do nothing. // Do nothing.
} }
private boolean mimeTypeIsWebm(String mimeType) { private static boolean mimeTypeIsWebm(String mimeType) {
return mimeType.startsWith(MimeTypes.VIDEO_WEBM) || mimeType.startsWith(MimeTypes.AUDIO_WEBM); return mimeType.startsWith(MimeTypes.VIDEO_WEBM) || mimeType.startsWith(MimeTypes.AUDIO_WEBM);
} }
...@@ -475,8 +474,8 @@ public class DashChunkSource implements ChunkSource { ...@@ -475,8 +474,8 @@ public class DashChunkSource implements ChunkSource {
startTimeUs, endTimeUs, nextAbsoluteSegmentNum, null, representationHolder.vttHeader); startTimeUs, endTimeUs, nextAbsoluteSegmentNum, null, representationHolder.vttHeader);
} else { } else {
return new ContainerMediaChunk(dataSource, dataSpec, representation.format, trigger, return new ContainerMediaChunk(dataSource, dataSpec, representation.format, trigger,
startTimeUs, endTimeUs, nextAbsoluteSegmentNum, representationHolder.extractor, psshInfo, startTimeUs, endTimeUs, nextAbsoluteSegmentNum, representationHolder.extractor,
false, presentationTimeOffsetUs); drmInitData, false, presentationTimeOffsetUs);
} }
} }
...@@ -529,19 +528,24 @@ public class DashChunkSource implements ChunkSource { ...@@ -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) { int adaptationSetIndex) {
AdaptationSet adaptationSet = manifest.periods.get(0).adaptationSets.get(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()) { if (adaptationSet.contentProtections.isEmpty()) {
return null; return null;
} else { } else {
Map<UUID, byte[]> psshInfo = new HashMap<UUID, byte[]>(); DrmInitData.Mapped drmInitData = null;
for (ContentProtection contentProtection : adaptationSet.contentProtections) { for (ContentProtection contentProtection : adaptationSet.contentProtections) {
if (contentProtection.uuid != null && contentProtection.data != null) { 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; ...@@ -18,9 +18,6 @@ package com.google.android.exoplayer.drm;
import android.annotation.TargetApi; import android.annotation.TargetApi;
import android.media.MediaCrypto; import android.media.MediaCrypto;
import java.util.Map;
import java.util.UUID;
/** /**
* Manages a DRM session. * Manages a DRM session.
*/ */
...@@ -36,7 +33,7 @@ public interface DrmSessionManager { ...@@ -36,7 +33,7 @@ public interface DrmSessionManager {
*/ */
public static final int STATE_CLOSED = 1; 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). * is not yet open).
*/ */
public static final int STATE_OPENING = 2; public static final int STATE_OPENING = 2;
...@@ -52,11 +49,9 @@ public interface DrmSessionManager { ...@@ -52,11 +49,9 @@ public interface DrmSessionManager {
/** /**
* Opens the session, possibly asynchronously. * Opens the session, possibly asynchronously.
* *
* @param drmInitData Initialization data for the drm schemes supported by the media, keyed by * @param drmInitData DRM initialization data.
* scheme UUID.
* @param mimeType The mimeType of the media.
*/ */
void open(Map<UUID, byte[]> drmInitData, String mimeType); void open(DrmInitData drmInitData);
/** /**
* Closes the session. * Closes the session.
......
...@@ -31,7 +31,6 @@ import android.os.Looper; ...@@ -31,7 +31,6 @@ import android.os.Looper;
import android.os.Message; import android.os.Message;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map;
import java.util.UUID; import java.util.UUID;
/** /**
...@@ -168,7 +167,7 @@ public class StreamingDrmSessionManager implements DrmSessionManager { ...@@ -168,7 +167,7 @@ public class StreamingDrmSessionManager implements DrmSessionManager {
} }
@Override @Override
public void open(Map<UUID, byte[]> psshData, String mimeType) { public void open(DrmInitData drmInitData) {
if (++openCount != 1) { if (++openCount != 1) {
return; return;
} }
...@@ -178,8 +177,8 @@ public class StreamingDrmSessionManager implements DrmSessionManager { ...@@ -178,8 +177,8 @@ public class StreamingDrmSessionManager implements DrmSessionManager {
postRequestHandler = new PostRequestHandler(requestHandlerThread.getLooper()); postRequestHandler = new PostRequestHandler(requestHandlerThread.getLooper());
} }
if (this.schemePsshData == null) { if (this.schemePsshData == null) {
this.mimeType = mimeType; mimeType = drmInitData.mimeType;
schemePsshData = psshData.get(uuid); schemePsshData = drmInitData.get(uuid);
if (schemePsshData == null) { if (schemePsshData == null) {
onError(new IllegalStateException("Media does not support uuid: " + uuid)); onError(new IllegalStateException("Media does not support uuid: " + uuid));
return; return;
......
...@@ -19,6 +19,7 @@ import com.google.android.exoplayer.C; ...@@ -19,6 +19,7 @@ import com.google.android.exoplayer.C;
import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.hls.parser.AdtsExtractor; import com.google.android.exoplayer.hls.parser.AdtsExtractor;
import com.google.android.exoplayer.hls.parser.HlsExtractor; 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.hls.parser.TsExtractor;
import com.google.android.exoplayer.upstream.Aes128DataSource; import com.google.android.exoplayer.upstream.Aes128DataSource;
import com.google.android.exoplayer.upstream.BandwidthMeter; import com.google.android.exoplayer.upstream.BandwidthMeter;
...@@ -305,7 +306,7 @@ public class HlsChunkSource { ...@@ -305,7 +306,7 @@ public class HlsChunkSource {
Uri chunkUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, segment.url); Uri chunkUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, segment.url);
// Check if encryption is specified. // 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); Uri keyUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, segment.encryptionKeyUri);
if (!keyUri.equals(encryptionKeyUri)) { if (!keyUri.equals(encryptionKeyUri)) {
// Encryption is specified and the key has changed. // Encryption is specified and the key has changed.
...@@ -341,16 +342,17 @@ public class HlsChunkSource { ...@@ -341,16 +342,17 @@ public class HlsChunkSource {
boolean isLastChunk = !mediaPlaylist.live && chunkIndex == mediaPlaylist.segments.size() - 1; boolean isLastChunk = !mediaPlaylist.live && chunkIndex == mediaPlaylist.segments.size() - 1;
// Configure the extractor that will read the chunk. // Configure the extractor that will read the chunk.
HlsExtractor extractor; HlsExtractorWrapper extractorWrapper;
if (previousTsChunk == null || segment.discontinuity || switchingVariant || liveDiscontinuity) { if (previousTsChunk == null || segment.discontinuity || switchingVariant || liveDiscontinuity) {
extractor = chunkUri.getLastPathSegment().endsWith(AAC_FILE_EXTENSION) HlsExtractor extractor = chunkUri.getLastPathSegment().endsWith(AAC_FILE_EXTENSION)
? new AdtsExtractor(switchingVariantSpliced, startTimeUs, bufferPool) ? new AdtsExtractor(startTimeUs)
: new TsExtractor(switchingVariantSpliced, startTimeUs, bufferPool); : new TsExtractor(startTimeUs);
extractorWrapper = new HlsExtractorWrapper(bufferPool, extractor, switchingVariantSpliced);
} else { } 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); startTimeUs, endTimeUs, chunkMediaSequence, isLastChunk);
} }
...@@ -387,16 +389,24 @@ public class HlsChunkSource { ...@@ -387,16 +389,24 @@ public class HlsChunkSource {
private int getNextVariantIndex(TsChunk previousTsChunk, long playbackPositionUs) { private int getNextVariantIndex(TsChunk previousTsChunk, long playbackPositionUs) {
clearStaleBlacklistedPlaylists(); 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 idealVariantIndex = getVariantIndexForBandwdith(
(int) (bandwidthMeter.getBitrateEstimate() * BANDWIDTH_FRACTION)); (int) (bitrateEstimate * BANDWIDTH_FRACTION));
if (idealVariantIndex == variantIndex) { if (idealVariantIndex == variantIndex) {
// We're already using the ideal variant. // We're already using the ideal variant.
return variantIndex; return variantIndex;
} }
// We're not using the ideal variant for the available bandwidth, but only switch if the // We're not using the ideal variant for the available bandwidth, but only switch if the
// conditions are appropriate. // conditions are appropriate.
long bufferedPositionUs = previousTsChunk == null ? playbackPositionUs long bufferedPositionUs = adaptiveMode == ADAPTIVE_MODE_SPLICE ? previousTsChunk.startTimeUs
: adaptiveMode == ADAPTIVE_MODE_SPLICE ? previousTsChunk.startTimeUs
: previousTsChunk.endTimeUs; : previousTsChunk.endTimeUs;
long bufferedUs = bufferedPositionUs - playbackPositionUs; long bufferedUs = bufferedPositionUs - playbackPositionUs;
if (mediaPlaylistBlacklistTimesMs[variantIndex] != 0 if (mediaPlaylistBlacklistTimesMs[variantIndex] != 0
......
...@@ -23,10 +23,12 @@ import java.util.List; ...@@ -23,10 +23,12 @@ import java.util.List;
public final class HlsMasterPlaylist extends HlsPlaylist { public final class HlsMasterPlaylist extends HlsPlaylist {
public final List<Variant> variants; 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); super(baseUri, HlsPlaylist.TYPE_MASTER);
this.variants = variants; this.variants = variants;
this.subtitles = subtitles;
} }
} }
...@@ -28,24 +28,25 @@ public final class HlsMediaPlaylist extends HlsPlaylist { ...@@ -28,24 +28,25 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
* Media segment reference. * Media segment reference.
*/ */
public static final class Segment implements Comparable<Long> { public static final class Segment implements Comparable<Long> {
public final boolean discontinuity; public final boolean discontinuity;
public final double durationSecs; public final double durationSecs;
public final String url; public final String url;
public final long startTimeUs; public final long startTimeUs;
public final String encryptionMethod; public final boolean isEncrypted;
public final String encryptionKeyUri; public final String encryptionKeyUri;
public final String encryptionIV; public final String encryptionIV;
public final int byterangeOffset; public final int byterangeOffset;
public final int byterangeLength; public final int byterangeLength;
public Segment(String uri, double durationSecs, boolean discontinuity, long startTimeUs, public Segment(String uri, double durationSecs, boolean discontinuity, long startTimeUs,
String encryptionMethod, String encryptionKeyUri, String encryptionIV, boolean isEncrypted, String encryptionKeyUri, String encryptionIV, int byterangeOffset,
int byterangeOffset, int byterangeLength) { int byterangeLength) {
this.url = uri; this.url = uri;
this.durationSecs = durationSecs; this.durationSecs = durationSecs;
this.discontinuity = discontinuity; this.discontinuity = discontinuity;
this.startTimeUs = startTimeUs; this.startTimeUs = startTimeUs;
this.encryptionMethod = encryptionMethod; this.isEncrypted = isEncrypted;
this.encryptionKeyUri = encryptionKeyUri; this.encryptionKeyUri = encryptionKeyUri;
this.encryptionIV = encryptionIV; this.encryptionIV = encryptionIV;
this.byterangeOffset = byterangeOffset; this.byterangeOffset = byterangeOffset;
......
...@@ -25,6 +25,8 @@ import java.util.regex.Pattern; ...@@ -25,6 +25,8 @@ import java.util.regex.Pattern;
*/ */
/* package */ class HlsParserUtil { /* package */ class HlsParserUtil {
private static final String BOOLEAN_YES = "YES";
private HlsParserUtil() {} private HlsParserUtil() {}
public static String parseStringAttr(String line, Pattern pattern, String tag) public static String parseStringAttr(String line, Pattern pattern, String tag)
...@@ -36,14 +38,6 @@ import java.util.regex.Pattern; ...@@ -36,14 +38,6 @@ import java.util.regex.Pattern;
throw new ParserException(String.format("Couldn't match %s tag in %s", tag, line)); throw new ParserException(String.format("Couldn't match %s tag in %s", tag, line));
} }
public static String parseOptionalStringAttr(String line, Pattern pattern) {
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) public static int parseIntAttr(String line, Pattern pattern, String tag)
throws ParserException { throws ParserException {
return Integer.parseInt(parseStringAttr(line, pattern, tag)); return Integer.parseInt(parseStringAttr(line, pattern, tag));
...@@ -54,4 +48,20 @@ import java.util.regex.Pattern; ...@@ -54,4 +48,20 @@ import java.util.regex.Pattern;
return Double.parseDouble(parseStringAttr(line, pattern, tag)); 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; ...@@ -21,7 +21,7 @@ import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.SampleSource; import com.google.android.exoplayer.SampleSource;
import com.google.android.exoplayer.TrackInfo; import com.google.android.exoplayer.TrackInfo;
import com.google.android.exoplayer.TrackRenderer; import com.google.android.exoplayer.TrackRenderer;
import com.google.android.exoplayer.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;
import com.google.android.exoplayer.upstream.Loader.Loadable; import com.google.android.exoplayer.upstream.Loader.Loadable;
import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.Assertions;
...@@ -44,7 +44,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { ...@@ -44,7 +44,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
private static final int NO_RESET_PENDING = -1; private static final int NO_RESET_PENDING = -1;
private final HlsChunkSource chunkSource; private final HlsChunkSource chunkSource;
private final LinkedList<HlsExtractor> extractors; private final LinkedList<HlsExtractorWrapper> extractors;
private final boolean frameAccurateSeeking; private final boolean frameAccurateSeeking;
private final int minLoadableRetryCount; private final int minLoadableRetryCount;
...@@ -83,7 +83,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { ...@@ -83,7 +83,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
this.frameAccurateSeeking = frameAccurateSeeking; this.frameAccurateSeeking = frameAccurateSeeking;
this.remainingReleaseCount = downstreamRendererCount; this.remainingReleaseCount = downstreamRendererCount;
this.minLoadableRetryCount = minLoadableRetryCount; this.minLoadableRetryCount = minLoadableRetryCount;
extractors = new LinkedList<HlsExtractor>(); extractors = new LinkedList<HlsExtractorWrapper>();
} }
@Override @Override
...@@ -96,7 +96,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { ...@@ -96,7 +96,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
} }
continueBufferingInternal(); continueBufferingInternal();
if (!extractors.isEmpty()) { if (!extractors.isEmpty()) {
HlsExtractor extractor = extractors.getFirst(); HlsExtractorWrapper extractor = extractors.getFirst();
if (extractor.isPrepared()) { if (extractor.isPrepared()) {
trackCount = extractor.getTrackCount(); trackCount = extractor.getTrackCount();
trackEnabledStates = new boolean[trackCount]; trackEnabledStates = new boolean[trackCount];
...@@ -195,7 +195,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { ...@@ -195,7 +195,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
return NOTHING_READ; return NOTHING_READ;
} }
HlsExtractor extractor = getCurrentExtractor(); HlsExtractorWrapper extractor = getCurrentExtractor();
if (extractors.size() > 1) { if (extractors.size() > 1) {
// If there's more than one extractor, attempt to configure a seamless splice from the // If there's more than one extractor, attempt to configure a seamless splice from the
// current one to the next one. // current one to the next one.
...@@ -328,8 +328,8 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { ...@@ -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. * @return The current extractor from which samples should be read. Guaranteed to be non-null.
*/ */
private HlsExtractor getCurrentExtractor() { private HlsExtractorWrapper getCurrentExtractor() {
HlsExtractor extractor = extractors.getFirst(); HlsExtractorWrapper extractor = extractors.getFirst();
while (extractors.size() > 1 && !haveSamplesForEnabledTracks(extractor)) { while (extractors.size() > 1 && !haveSamplesForEnabledTracks(extractor)) {
// We're finished reading from the extractor for all tracks, and so can discard it. // We're finished reading from the extractor for all tracks, and so can discard it.
extractors.removeFirst().release(); extractors.removeFirst().release();
...@@ -338,7 +338,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { ...@@ -338,7 +338,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
return extractor; return extractor;
} }
private void discardSamplesForDisabledTracks(HlsExtractor extractor, long timeUs) { private void discardSamplesForDisabledTracks(HlsExtractorWrapper extractor, long timeUs) {
if (!extractor.isPrepared()) { if (!extractor.isPrepared()) {
return; return;
} }
...@@ -349,7 +349,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { ...@@ -349,7 +349,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
} }
} }
private boolean haveSamplesForEnabledTracks(HlsExtractor extractor) { private boolean haveSamplesForEnabledTracks(HlsExtractorWrapper extractor) {
if (!extractor.isPrepared()) { if (!extractor.isPrepared()) {
return false; 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 @@ ...@@ -15,7 +15,9 @@
*/ */
package com.google.android.exoplayer.hls; 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.DataSource;
import com.google.android.exoplayer.upstream.DataSpec; import com.google.android.exoplayer.upstream.DataSpec;
...@@ -26,8 +28,6 @@ import java.io.IOException; ...@@ -26,8 +28,6 @@ import java.io.IOException;
*/ */
public final class TsChunk extends HlsChunk { public final class TsChunk extends HlsChunk {
private static final byte[] SCRATCH_SPACE = new byte[4096];
/** /**
* The index of the variant in the master playlist. * The index of the variant in the master playlist.
*/ */
...@@ -51,7 +51,7 @@ public final class TsChunk extends HlsChunk { ...@@ -51,7 +51,7 @@ public final class TsChunk extends HlsChunk {
/** /**
* The extractor into which this chunk is being consumed. * The extractor into which this chunk is being consumed.
*/ */
public final HlsExtractor extractor; public final HlsExtractorWrapper extractor;
private int loadPosition; private int loadPosition;
private volatile boolean loadFinished; private volatile boolean loadFinished;
...@@ -67,7 +67,7 @@ public final class TsChunk extends HlsChunk { ...@@ -67,7 +67,7 @@ public final class TsChunk extends HlsChunk {
* @param chunkIndex The index of the chunk. * @param chunkIndex The index of the chunk.
* @param isLastChunk True if this is the last chunk in the media. False otherwise. * @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) { int variantIndex, long startTimeUs, long endTimeUs, int chunkIndex, boolean isLastChunk) {
super(dataSource, dataSpec); super(dataSource, dataSpec);
this.extractor = extractor; this.extractor = extractor;
...@@ -102,30 +102,23 @@ public final class TsChunk extends HlsChunk { ...@@ -102,30 +102,23 @@ public final class TsChunk extends HlsChunk {
@Override @Override
public void load() throws IOException, InterruptedException { public void load() throws IOException, InterruptedException {
ExtractorInput input = new DataSourceExtractorInput(dataSource, 0);
try { try {
dataSource.open(dataSpec); dataSource.open(dataSpec);
int bytesRead = 0;
int bytesSkipped = 0;
// If we previously fed part of this chunk to the extractor, skip it this time. // 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, // 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 // 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 // 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. // encrypted with AES, for which we'll need to modify the way that decryption is performed.
while (bytesRead != -1 && !loadCanceled && bytesSkipped < loadPosition) { input.skipFully(loadPosition);
int skipLength = Math.min(loadPosition - bytesSkipped, SCRATCH_SPACE.length); try {
bytesRead = dataSource.read(SCRATCH_SPACE, 0, skipLength); while (!input.isEnded() && !loadCanceled) {
if (bytesRead != -1) { extractor.read(input);
bytesSkipped += bytesRead;
}
}
// Feed the remaining data into the extractor.
while (bytesRead != -1 && !loadCanceled) {
bytesRead = extractor.read(dataSource);
if (bytesRead != -1) {
loadPosition += bytesRead;
} }
} finally {
loadPosition = (int) input.getPosition();
loadFinished = !loadCanceled;
} }
loadFinished = !loadCanceled;
} finally { } finally {
dataSource.close(); dataSource.close();
} }
......
...@@ -15,11 +15,6 @@ ...@@ -15,11 +15,6 @@
*/ */
package com.google.android.exoplayer.hls.parser; 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 com.google.android.exoplayer.util.ParsableByteArray;
import java.io.IOException; import java.io.IOException;
...@@ -28,82 +23,37 @@ 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 * Facilitates the extraction of AAC samples from elementary audio files formatted as AAC with ADTS
* headers. * headers.
*/ */
public class AdtsExtractor extends HlsExtractor { public class AdtsExtractor implements HlsExtractor {
private static final int MAX_PACKET_SIZE = 200; private static final int MAX_PACKET_SIZE = 200;
private final long firstSampleTimestamp; private final long firstSampleTimestamp;
private final ParsableByteArray packetBuffer; private final ParsableByteArray packetBuffer;
private final AdtsReader adtsReader;
// Accessed only by the loading thread. // Accessed only by the loading thread.
private AdtsReader adtsReader;
private boolean firstPacket; private boolean firstPacket;
// Accessed by both the loading and consuming threads.
private volatile boolean prepared;
public AdtsExtractor(boolean shouldSpliceIn, long firstSampleTimestamp, BufferPool bufferPool) { public AdtsExtractor(long firstSampleTimestamp) {
super(shouldSpliceIn);
this.firstSampleTimestamp = firstSampleTimestamp; this.firstSampleTimestamp = firstSampleTimestamp;
packetBuffer = new ParsableByteArray(MAX_PACKET_SIZE); packetBuffer = new ParsableByteArray(MAX_PACKET_SIZE);
adtsReader = new AdtsReader(bufferPool);
firstPacket = true; firstPacket = true;
} }
@Override @Override
public int getTrackCount() { public void init(TrackOutputBuilder output) {
Assertions.checkState(prepared); adtsReader = new AdtsReader(output.buildOutput(0));
return 1; output.allOutputsBuilt();
} }
@Override @Override
public MediaFormat getFormat(int track) { public void read(ExtractorInput input) throws IOException, InterruptedException {
Assertions.checkState(prepared); int bytesRead = input.read(packetBuffer.data, 0, MAX_PACKET_SIZE);
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);
if (bytesRead == -1) { 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.setPosition(0);
packetBuffer.setLimit(bytesRead); packetBuffer.setLimit(bytesRead);
...@@ -111,16 +61,6 @@ public class AdtsExtractor extends HlsExtractor { ...@@ -111,16 +61,6 @@ public class AdtsExtractor extends HlsExtractor {
// unnecessary to copy the data through packetBuffer. // unnecessary to copy the data through packetBuffer.
adtsReader.consume(packetBuffer, firstSampleTimestamp, firstPacket); adtsReader.consume(packetBuffer, firstSampleTimestamp, firstPacket);
firstPacket = false; 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; ...@@ -17,7 +17,7 @@ package com.google.android.exoplayer.hls.parser;
import com.google.android.exoplayer.C; import com.google.android.exoplayer.C;
import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.upstream.BufferPool; import com.google.android.exoplayer.hls.parser.HlsExtractor.TrackOutput;
import com.google.android.exoplayer.util.CodecSpecificDataUtil; import com.google.android.exoplayer.util.CodecSpecificDataUtil;
import com.google.android.exoplayer.util.MimeTypes; import com.google.android.exoplayer.util.MimeTypes;
import com.google.android.exoplayer.util.ParsableBitArray; import com.google.android.exoplayer.util.ParsableBitArray;
...@@ -55,8 +55,8 @@ import java.util.Collections; ...@@ -55,8 +55,8 @@ import java.util.Collections;
// Used when reading the samples. // Used when reading the samples.
private long timeUs; private long timeUs;
public AdtsReader(BufferPool bufferPool) { public AdtsReader(TrackOutput output) {
super(bufferPool); super(output);
adtsScratch = new ParsableBitArray(new byte[HEADER_SIZE + CRC_SIZE]); adtsScratch = new ParsableBitArray(new byte[HEADER_SIZE + CRC_SIZE]);
state = STATE_FINDING_SYNC; state = STATE_FINDING_SYNC;
} }
...@@ -78,17 +78,17 @@ import java.util.Collections; ...@@ -78,17 +78,17 @@ import java.util.Collections;
int targetLength = hasCrc ? HEADER_SIZE + CRC_SIZE : HEADER_SIZE; int targetLength = hasCrc ? HEADER_SIZE + CRC_SIZE : HEADER_SIZE;
if (continueRead(data, adtsScratch.getData(), targetLength)) { if (continueRead(data, adtsScratch.getData(), targetLength)) {
parseHeader(); parseHeader();
startSample(timeUs); output.startSample(timeUs, 0);
bytesRead = 0; bytesRead = 0;
state = STATE_READING_SAMPLE; state = STATE_READING_SAMPLE;
} }
break; break;
case STATE_READING_SAMPLE: case STATE_READING_SAMPLE:
int bytesToRead = Math.min(data.bytesLeft(), sampleSize - bytesRead); int bytesToRead = Math.min(data.bytesLeft(), sampleSize - bytesRead);
appendData(data, bytesToRead); output.appendData(data, bytesToRead);
bytesRead += bytesToRead; bytesRead += bytesToRead;
if (bytesRead == sampleSize) { if (bytesRead == sampleSize) {
commitSample(true); output.commitSample(C.SAMPLE_FLAG_SYNC, 0, null);
timeUs += frameDurationUs; timeUs += frameDurationUs;
bytesRead = 0; bytesRead = 0;
state = STATE_FINDING_SYNC; state = STATE_FINDING_SYNC;
...@@ -152,7 +152,7 @@ import java.util.Collections; ...@@ -152,7 +152,7 @@ import java.util.Collections;
private void parseHeader() { private void parseHeader() {
adtsScratch.setPosition(0); adtsScratch.setPosition(0);
if (!hasMediaFormat()) { if (!output.hasFormat()) {
int audioObjectType = adtsScratch.readBits(2) + 1; int audioObjectType = adtsScratch.readBits(2) + 1;
int sampleRateIndex = adtsScratch.readBits(4); int sampleRateIndex = adtsScratch.readBits(4);
adtsScratch.skipBits(1); adtsScratch.skipBits(1);
...@@ -167,7 +167,7 @@ import java.util.Collections; ...@@ -167,7 +167,7 @@ import java.util.Collections;
MediaFormat.NO_VALUE, audioParams.second, audioParams.first, MediaFormat.NO_VALUE, audioParams.second, audioParams.first,
Collections.singletonList(audioSpecificConfig)); Collections.singletonList(audioSpecificConfig));
frameDurationUs = (C.MICROS_PER_SECOND * 1024L) / mediaFormat.sampleRate; frameDurationUs = (C.MICROS_PER_SECOND * 1024L) / mediaFormat.sampleRate;
setMediaFormat(mediaFormat); output.setFormat(mediaFormat);
} else { } else {
adtsScratch.skipBits(10); 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 @@ ...@@ -15,16 +15,21 @@
*/ */
package com.google.android.exoplayer.hls.parser; 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; import com.google.android.exoplayer.util.ParsableByteArray;
/** /**
* Extracts individual samples from an elementary media stream, preserving original order. * 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) { protected final TrackOutput output;
super(bufferPool);
/**
* @param output A {@link TrackOutput} to which samples should be written.
*/
protected ElementaryStreamReader(TrackOutput output) {
this.output = output;
} }
/** /**
......
...@@ -15,9 +15,10 @@ ...@@ -15,9 +15,10 @@
*/ */
package com.google.android.exoplayer.hls.parser; package com.google.android.exoplayer.hls.parser;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.MediaFormat; 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.mp4.Mp4Util;
import com.google.android.exoplayer.upstream.BufferPool;
import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.MimeTypes; import com.google.android.exoplayer.util.MimeTypes;
import com.google.android.exoplayer.util.ParsableBitArray; import com.google.android.exoplayer.util.ParsableBitArray;
...@@ -43,18 +44,20 @@ import java.util.List; ...@@ -43,18 +44,20 @@ import java.util.List;
private final NalUnitTargetBuffer sps; private final NalUnitTargetBuffer sps;
private final NalUnitTargetBuffer pps; private final NalUnitTargetBuffer pps;
private final NalUnitTargetBuffer sei; private final NalUnitTargetBuffer sei;
private final ParsableByteArray seiWrapper;
private int scratchEscapeCount; private int scratchEscapeCount;
private int[] scratchEscapePositions; private int[] scratchEscapePositions;
private boolean isKeyframe; private boolean isKeyframe;
public H264Reader(BufferPool bufferPool, SeiReader seiReader) { public H264Reader(TrackOutput output, SeiReader seiReader) {
super(bufferPool); super(output);
this.seiReader = seiReader; this.seiReader = seiReader;
prefixFlags = new boolean[3]; prefixFlags = new boolean[3];
sps = new NalUnitTargetBuffer(NAL_UNIT_TYPE_SPS, 128); sps = new NalUnitTargetBuffer(NAL_UNIT_TYPE_SPS, 128);
pps = new NalUnitTargetBuffer(NAL_UNIT_TYPE_PPS, 128); pps = new NalUnitTargetBuffer(NAL_UNIT_TYPE_PPS, 128);
sei = new NalUnitTargetBuffer(NAL_UNIT_TYPE_SEI, 128); sei = new NalUnitTargetBuffer(NAL_UNIT_TYPE_SEI, 128);
seiWrapper = new ParsableByteArray();
scratchEscapePositions = new int[10]; scratchEscapePositions = new int[10];
} }
...@@ -66,7 +69,7 @@ import java.util.List; ...@@ -66,7 +69,7 @@ import java.util.List;
byte[] dataArray = data.data; byte[] dataArray = data.data;
// Append the data to the buffer. // 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 // Scan the appended data, processing NAL units as they are encountered
while (offset < limit) { while (offset < limit) {
...@@ -84,13 +87,13 @@ import java.util.List; ...@@ -84,13 +87,13 @@ import java.util.List;
int nalUnitType = Mp4Util.getNalUnitType(dataArray, nextNalUnitOffset); int nalUnitType = Mp4Util.getNalUnitType(dataArray, nextNalUnitOffset);
int nalUnitOffsetInData = nextNalUnitOffset - limit; int nalUnitOffsetInData = nextNalUnitOffset - limit;
if (nalUnitType == NAL_UNIT_TYPE_AUD) { if (nalUnitType == NAL_UNIT_TYPE_AUD) {
if (writingSample()) { if (output.isWritingSample()) {
if (isKeyframe && !hasMediaFormat() && sps.isCompleted() && pps.isCompleted()) { if (isKeyframe && !output.hasFormat() && sps.isCompleted() && pps.isCompleted()) {
parseMediaFormat(sps, pps); 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; isKeyframe = false;
} else if (nalUnitType == NAL_UNIT_TYPE_IDR) { } else if (nalUnitType == NAL_UNIT_TYPE_IDR) {
isKeyframe = true; isKeyframe = true;
...@@ -117,7 +120,7 @@ import java.util.List; ...@@ -117,7 +120,7 @@ import java.util.List;
} }
private void feedNalUnitTargetBuffersStart(int nalUnitType) { private void feedNalUnitTargetBuffersStart(int nalUnitType) {
if (!hasMediaFormat()) { if (!output.hasFormat()) {
sps.startNalUnit(nalUnitType); sps.startNalUnit(nalUnitType);
pps.startNalUnit(nalUnitType); pps.startNalUnit(nalUnitType);
} }
...@@ -125,7 +128,7 @@ import java.util.List; ...@@ -125,7 +128,7 @@ import java.util.List;
} }
private void feedNalUnitTargetBuffersData(byte[] dataArray, int offset, int limit) { private void feedNalUnitTargetBuffersData(byte[] dataArray, int offset, int limit) {
if (!hasMediaFormat()) { if (!output.hasFormat()) {
sps.appendToNalUnit(dataArray, offset, limit); sps.appendToNalUnit(dataArray, offset, limit);
pps.appendToNalUnit(dataArray, offset, limit); pps.appendToNalUnit(dataArray, offset, limit);
} }
...@@ -137,7 +140,8 @@ import java.util.List; ...@@ -137,7 +140,8 @@ import java.util.List;
pps.endNalUnit(discardPadding); pps.endNalUnit(discardPadding);
if (sei.endNalUnit(discardPadding)) { if (sei.endNalUnit(discardPadding)) {
int unescapedLength = unescapeStream(sei.nalData, sei.nalLength); 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; ...@@ -229,7 +233,7 @@ import java.util.List;
} }
// Set the format. // 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)); frameWidth, frameHeight, initializationData));
} }
......
...@@ -16,136 +16,138 @@ ...@@ -16,136 +16,138 @@
package com.google.android.exoplayer.hls.parser; package com.google.android.exoplayer.hls.parser;
import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.util.ParsableByteArray;
import java.io.IOException; import java.io.IOException;
/** /**
* Facilitates extraction of media samples for HLS playbacks. * Facilitates extraction of media samples for HLS playbacks.
*/ */
// TODO: Consider consolidating more common logic in this base class. public interface HlsExtractor {
public abstract class HlsExtractor {
private final boolean shouldSpliceIn; /**
* An object from which source data can be read.
// Accessed only by the consuming thread. */
private boolean spliceConfigured; 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. * An object to which extracted data should be output.
* <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(HlsExtractor nextExtractor) { public interface TrackOutputBuilder {
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. * Invoked to build a {@link TrackOutput} to which data should be output for a given track.
return; *
} * @param trackId A stable track id.
boolean spliceConfigured = true; * @return The corresponding {@link TrackOutput}.
int trackCount = getTrackCount(); */
for (int i = 0; i < trackCount; i++) { TrackOutput buildOutput(int trackId);
spliceConfigured &= getSampleQueue(i).configureSpliceTo(nextExtractor.getSampleQueue(i));
} /**
this.spliceConfigured = spliceConfigured; * Invoked when all {@link TrackOutput}s have been built, meaning {@link #buildOutput(int)}
return; * will not be invoked again.
*/
void allOutputsBuilt();
} }
/** /**
* Gets the number of available tracks. * An object to which extracted data belonging to a given track should be output.
* <p>
* This method should only be called after the extractor has been prepared.
*
* @return The number of available tracks.
*/ */
public abstract int getTrackCount(); public interface TrackOutput {
/** boolean hasFormat();
* 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);
/** void setFormat(MediaFormat format);
* Whether the extractor is prepared.
*
* @return True if the extractor is prepared. False otherwise.
*/
public abstract boolean isPrepared();
/** boolean isWritingSample();
* 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();
/** int appendData(DataSource dataSource, int length) throws IOException;
* 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();
/** void appendData(ParsableByteArray data, int length);
* 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 startSample(long timeUs, int offset);
* 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 commitSample(int flags, int offset, byte[] encryptionKey);
* 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);
/** /**
* Reads up to a single TS packet. * Initializes the extractor.
* *
* @param dataSource The {@link DataSource} from which to read. * @param output A {@link TrackOutputBuilder} to which extracted data should be output.
* @throws IOException If an error occurred reading from the source.
* @return The number of bytes read from the source.
*/ */
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. * @param input The {@link ExtractorInput} from which to read.
* @return The corresponding sample queue. * @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 @@ ...@@ -15,8 +15,9 @@
*/ */
package com.google.android.exoplayer.hls.parser; package com.google.android.exoplayer.hls.parser;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.MediaFormat; 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; import com.google.android.exoplayer.util.ParsableByteArray;
/** /**
...@@ -24,24 +25,24 @@ import com.google.android.exoplayer.util.ParsableByteArray; ...@@ -24,24 +25,24 @@ import com.google.android.exoplayer.util.ParsableByteArray;
*/ */
/* package */ class Id3Reader extends ElementaryStreamReader { /* package */ class Id3Reader extends ElementaryStreamReader {
public Id3Reader(BufferPool bufferPool) { public Id3Reader(TrackOutput output) {
super(bufferPool); super(output);
setMediaFormat(MediaFormat.createId3Format()); output.setFormat(MediaFormat.createId3Format());
} }
@Override @Override
public void consume(ParsableByteArray data, long pesTimeUs, boolean startOfPacket) { public void consume(ParsableByteArray data, long pesTimeUs, boolean startOfPacket) {
if (startOfPacket) { if (startOfPacket) {
startSample(pesTimeUs); output.startSample(pesTimeUs, 0);
} }
if (writingSample()) { if (output.isWritingSample()) {
appendData(data, data.bytesLeft()); output.appendData(data, data.bytesLeft());
} }
} }
@Override @Override
public void packetFinished() { public void packetFinished() {
commitSample(true); output.commitSample(C.SAMPLE_FLAG_SYNC, 0, null);
} }
} }
...@@ -18,15 +18,19 @@ package com.google.android.exoplayer.hls.parser; ...@@ -18,15 +18,19 @@ package com.google.android.exoplayer.hls.parser;
import com.google.android.exoplayer.C; import com.google.android.exoplayer.C;
import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.SampleHolder; 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.BufferPool;
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.util.ParsableByteArray; import com.google.android.exoplayer.util.ParsableByteArray;
import java.io.IOException;
/** /**
* Wraps a {@link RollingSampleBuffer}, adding higher level functionality such as enforcing that * 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 * the first sample returned from the queue is a keyframe, allowing splicing to another queue, and
* so on. * so on.
*/ */
/* package */ abstract class SampleQueue { public final class SampleQueue implements TrackOutput {
private final RollingSampleBuffer rollingBuffer; private final RollingSampleBuffer rollingBuffer;
private final SampleHolder sampleInfoHolder; private final SampleHolder sampleInfoHolder;
...@@ -40,10 +44,10 @@ import com.google.android.exoplayer.util.ParsableByteArray; ...@@ -40,10 +44,10 @@ import com.google.android.exoplayer.util.ParsableByteArray;
private boolean writingSample; private boolean writingSample;
// Accessed by both the loading and consuming threads. // Accessed by both the loading and consuming threads.
private volatile MediaFormat mediaFormat;
private volatile long largestParsedTimestampUs; private volatile long largestParsedTimestampUs;
private volatile MediaFormat format;
protected SampleQueue(BufferPool bufferPool) { public SampleQueue(BufferPool bufferPool) {
rollingBuffer = new RollingSampleBuffer(bufferPool); rollingBuffer = new RollingSampleBuffer(bufferPool);
sampleInfoHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_DISABLED); sampleInfoHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_DISABLED);
needKeyframe = true; needKeyframe = true;
...@@ -58,16 +62,12 @@ import com.google.android.exoplayer.util.ParsableByteArray; ...@@ -58,16 +62,12 @@ import com.google.android.exoplayer.util.ParsableByteArray;
// Called by the consuming thread. // Called by the consuming thread.
public long getLargestParsedTimestampUs() { public MediaFormat getFormat() {
return largestParsedTimestampUs; return format;
}
public boolean hasMediaFormat() {
return mediaFormat != null;
} }
public MediaFormat getMediaFormat() { public long getLargestParsedTimestampUs() {
return mediaFormat; return largestParsedTimestampUs;
} }
public boolean isEmpty() { public boolean isEmpty() {
...@@ -166,37 +166,44 @@ import com.google.android.exoplayer.util.ParsableByteArray; ...@@ -166,37 +166,44 @@ import com.google.android.exoplayer.util.ParsableByteArray;
return true; return true;
} }
// Called by the loading thread. // TrackOutput implementation. Called by the loading thread.
protected boolean writingSample() { @Override
return writingSample; public boolean hasFormat() {
return format != null;
} }
protected void setMediaFormat(MediaFormat mediaFormat) { @Override
this.mediaFormat = mediaFormat; public void setFormat(MediaFormat format) {
this.format = format;
} }
protected void startSample(long sampleTimeUs) { @Override
startSample(sampleTimeUs, 0); 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; writingSample = true;
largestParsedTimestampUs = Math.max(largestParsedTimestampUs, sampleTimeUs); largestParsedTimestampUs = Math.max(largestParsedTimestampUs, sampleTimeUs);
rollingBuffer.startSample(sampleTimeUs, offset); rollingBuffer.startSample(sampleTimeUs, offset);
} }
protected void appendData(ParsableByteArray buffer, int length) { @Override
rollingBuffer.appendData(buffer, length); public void commitSample(int flags, int offset, byte[] encryptionKey) {
} rollingBuffer.commitSample(flags, offset, encryptionKey);
writingSample = false;
protected void commitSample(boolean isKeyframe) {
commitSample(isKeyframe, 0);
} }
protected void commitSample(boolean isKeyframe, int offset) { @Override
rollingBuffer.commitSample(isKeyframe, offset); public boolean isWritingSample() {
writingSample = false; return writingSample;
} }
} }
...@@ -15,9 +15,10 @@ ...@@ -15,9 +15,10 @@
*/ */
package com.google.android.exoplayer.hls.parser; package com.google.android.exoplayer.hls.parser;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.MediaFormat; 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.text.eia608.Eia608Parser;
import com.google.android.exoplayer.upstream.BufferPool;
import com.google.android.exoplayer.util.ParsableByteArray; import com.google.android.exoplayer.util.ParsableByteArray;
/** /**
...@@ -26,20 +27,17 @@ 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 * 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. * 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(TrackOutput output) {
super(output);
public SeiReader(BufferPool bufferPool) { output.setFormat(MediaFormat.createEia608Format());
super(bufferPool);
setMediaFormat(MediaFormat.createEia608Format());
seiBuffer = new ParsableByteArray();
} }
public void read(byte[] data, int position, int limit, long pesTimeUs) { @Override
seiBuffer.reset(data, limit); public void consume(ParsableByteArray seiBuffer, long pesTimeUs, boolean startOfPacket) {
// Skip the NAL prefix and type. // Skip the NAL prefix and type.
seiBuffer.setPosition(position + 4); seiBuffer.skip(4);
int b; int b;
while (seiBuffer.bytesLeft() > 1 /* last byte will be rbsp_trailing_bits */) { while (seiBuffer.bytesLeft() > 1 /* last byte will be rbsp_trailing_bits */) {
...@@ -57,13 +55,18 @@ import com.google.android.exoplayer.util.ParsableByteArray; ...@@ -57,13 +55,18 @@ import com.google.android.exoplayer.util.ParsableByteArray;
} while (b == 0xFF); } while (b == 0xFF);
// Process the payload. We only support EIA-608 payloads currently. // Process the payload. We only support EIA-608 payloads currently.
if (Eia608Parser.isSeiMessageEia608(payloadType, payloadSize, seiBuffer)) { if (Eia608Parser.isSeiMessageEia608(payloadType, payloadSize, seiBuffer)) {
startSample(pesTimeUs); output.startSample(pesTimeUs, 0);
appendData(seiBuffer, payloadSize); output.appendData(seiBuffer, payloadSize);
commitSample(true); output.commitSample(C.SAMPLE_FLAG_SYNC, 0, null);
} else { } else {
seiBuffer.skip(payloadSize); seiBuffer.skip(payloadSize);
} }
} }
} }
@Override
public void packetFinished() {
// Do nothing.
}
} }
...@@ -16,11 +16,6 @@ ...@@ -16,11 +16,6 @@
package com.google.android.exoplayer.hls.parser; package com.google.android.exoplayer.hls.parser;
import com.google.android.exoplayer.C; 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.ParsableBitArray;
import com.google.android.exoplayer.util.ParsableByteArray; import com.google.android.exoplayer.util.ParsableByteArray;
...@@ -32,7 +27,7 @@ import java.io.IOException; ...@@ -32,7 +27,7 @@ import java.io.IOException;
/** /**
* Facilitates the extraction of data from the MPEG-2 TS container format. * 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"; private static final String TAG = "TsExtractor";
...@@ -48,119 +43,41 @@ public final class TsExtractor extends HlsExtractor { ...@@ -48,119 +43,41 @@ public final class TsExtractor extends HlsExtractor {
private static final long MAX_PTS = 0x1FFFFFFFFL; private static final long MAX_PTS = 0x1FFFFFFFFL;
private final ParsableByteArray tsPacketBuffer; 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 SparseArray<TsPayloadReader> tsPayloadReaders; // Indexed by pid
private final BufferPool bufferPool;
private final long firstSampleTimestamp; private final long firstSampleTimestamp;
private final ParsableBitArray tsScratch; private final ParsableBitArray tsScratch;
// Accessed only by the loading thread. // Accessed only by the loading thread.
private int tsPacketBytesRead; private TrackOutputBuilder output;
private long timestampOffsetUs; private long timestampOffsetUs;
private long lastPts; private long lastPts;
// Accessed by both the loading and consuming threads. public TsExtractor(long firstSampleTimestamp) {
private volatile boolean prepared;
public TsExtractor(boolean shouldSpliceIn, long firstSampleTimestamp, BufferPool bufferPool) {
super(shouldSpliceIn);
this.firstSampleTimestamp = firstSampleTimestamp; this.firstSampleTimestamp = firstSampleTimestamp;
this.bufferPool = bufferPool;
tsScratch = new ParsableBitArray(new byte[3]); tsScratch = new ParsableBitArray(new byte[3]);
tsPacketBuffer = new ParsableByteArray(TS_PACKET_SIZE); tsPacketBuffer = new ParsableByteArray(TS_PACKET_SIZE);
sampleQueues = new SparseArray<SampleQueue>(); streamReaders = new SparseArray<ElementaryStreamReader>();
tsPayloadReaders = new SparseArray<TsPayloadReader>(); tsPayloadReaders = new SparseArray<TsPayloadReader>();
tsPayloadReaders.put(TS_PAT_PID, new PatReader()); tsPayloadReaders.put(TS_PAT_PID, new PatReader());
lastPts = Long.MIN_VALUE; lastPts = Long.MIN_VALUE;
} }
@Override @Override
public int getTrackCount() { public void init(TrackOutputBuilder output) {
Assertions.checkState(prepared); this.output = output;
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;
} }
@Override @Override
public int read(DataSource dataSource) throws IOException { public void read(ExtractorInput input) throws IOException, InterruptedException {
int bytesRead = dataSource.read(tsPacketBuffer.data, tsPacketBytesRead, if (!input.readFully(tsPacketBuffer.data, 0, TS_PACKET_SIZE)) {
TS_PACKET_SIZE - tsPacketBytesRead); return;
if (bytesRead == -1) {
return -1;
} }
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.setPosition(0);
tsPacketBuffer.setLimit(TS_PACKET_SIZE);
int syncByte = tsPacketBuffer.readUnsignedByte(); int syncByte = tsPacketBuffer.readUnsignedByte();
if (syncByte != TS_SYNC_BYTE) { if (syncByte != TS_SYNC_BYTE) {
return bytesRead; return;
} }
tsPacketBuffer.readBytes(tsScratch, 3); tsPacketBuffer.readBytes(tsScratch, 3);
...@@ -183,20 +100,9 @@ public final class TsExtractor extends HlsExtractor { ...@@ -183,20 +100,9 @@ public final class TsExtractor extends HlsExtractor {
if (payloadExists) { if (payloadExists) {
TsPayloadReader payloadReader = tsPayloadReaders.get(pid); TsPayloadReader payloadReader = tsPayloadReaders.get(pid);
if (payloadReader != null) { 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 { ...@@ -231,7 +137,8 @@ public final class TsExtractor extends HlsExtractor {
*/ */
private abstract static class TsPayloadReader { 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 { ...@@ -247,7 +154,8 @@ public final class TsExtractor extends HlsExtractor {
} }
@Override @Override
public void consume(ParsableByteArray data, boolean payloadUnitStartIndicator) { public void consume(ParsableByteArray data, boolean payloadUnitStartIndicator,
TrackOutputBuilder output) {
// Skip pointer. // Skip pointer.
if (payloadUnitStartIndicator) { if (payloadUnitStartIndicator) {
int pointerField = data.readUnsignedByte(); int pointerField = data.readUnsignedByte();
...@@ -286,7 +194,8 @@ public final class TsExtractor extends HlsExtractor { ...@@ -286,7 +194,8 @@ public final class TsExtractor extends HlsExtractor {
} }
@Override @Override
public void consume(ParsableByteArray data, boolean payloadUnitStartIndicator) { public void consume(ParsableByteArray data, boolean payloadUnitStartIndicator,
TrackOutputBuilder output) {
// Skip pointer. // Skip pointer.
if (payloadUnitStartIndicator) { if (payloadUnitStartIndicator) {
int pointerField = data.readUnsignedByte(); int pointerField = data.readUnsignedByte();
...@@ -323,32 +232,33 @@ public final class TsExtractor extends HlsExtractor { ...@@ -323,32 +232,33 @@ public final class TsExtractor extends HlsExtractor {
data.skip(esInfoLength); data.skip(esInfoLength);
entriesSize -= esInfoLength + 5; entriesSize -= esInfoLength + 5;
if (sampleQueues.get(streamType) != null) { if (streamReaders.get(streamType) != null) {
continue; continue;
} }
ElementaryStreamReader pesPayloadReader = null; ElementaryStreamReader pesPayloadReader = null;
switch (streamType) { switch (streamType) {
case TS_STREAM_TYPE_AAC: case TS_STREAM_TYPE_AAC:
pesPayloadReader = new AdtsReader(bufferPool); pesPayloadReader = new AdtsReader(output.buildOutput(TS_STREAM_TYPE_AAC));
break; break;
case TS_STREAM_TYPE_H264: case TS_STREAM_TYPE_H264:
SeiReader seiReader = new SeiReader(bufferPool); SeiReader seiReader = new SeiReader(output.buildOutput(TS_STREAM_TYPE_EIA608));
sampleQueues.put(TS_STREAM_TYPE_EIA608, seiReader); streamReaders.put(TS_STREAM_TYPE_EIA608, seiReader);
pesPayloadReader = new H264Reader(bufferPool, seiReader); pesPayloadReader = new H264Reader(output.buildOutput(TS_STREAM_TYPE_H264),
seiReader);
break; break;
case TS_STREAM_TYPE_ID3: case TS_STREAM_TYPE_ID3:
pesPayloadReader = new Id3Reader(bufferPool); pesPayloadReader = new Id3Reader(output.buildOutput(TS_STREAM_TYPE_ID3));
break; break;
} }
if (pesPayloadReader != null) { if (pesPayloadReader != null) {
sampleQueues.put(streamType, pesPayloadReader); streamReaders.put(streamType, pesPayloadReader);
tsPayloadReaders.put(elementaryPid, new PesReader(pesPayloadReader)); tsPayloadReaders.put(elementaryPid, new PesReader(pesPayloadReader));
} }
} }
// Skip CRC_32. output.allOutputsBuilt();
} }
} }
...@@ -387,7 +297,8 @@ public final class TsExtractor extends HlsExtractor { ...@@ -387,7 +297,8 @@ public final class TsExtractor extends HlsExtractor {
} }
@Override @Override
public void consume(ParsableByteArray data, boolean payloadUnitStartIndicator) { public void consume(ParsableByteArray data, boolean payloadUnitStartIndicator,
TrackOutputBuilder output) {
if (payloadUnitStartIndicator) { if (payloadUnitStartIndicator) {
switch (state) { switch (state) {
case STATE_FINDING_HEADER: case STATE_FINDING_HEADER:
......
...@@ -67,7 +67,7 @@ public final class CommonMp4AtomParsers { ...@@ -67,7 +67,7 @@ public final class CommonMp4AtomParsers {
long mediaTimescale = parseMdhd(mdia.getLeafAtomOfType(Atom.TYPE_mdhd).data); long mediaTimescale = parseMdhd(mdia.getLeafAtomOfType(Atom.TYPE_mdhd).data);
Pair<MediaFormat, TrackEncryptionBox[]> sampleDescriptions = 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, return new Track(id, trackType, mediaTimescale, durationUs, sampleDescriptions.first,
sampleDescriptions.second); sampleDescriptions.second);
} }
...@@ -321,7 +321,8 @@ public final class CommonMp4AtomParsers { ...@@ -321,7 +321,8 @@ public final class CommonMp4AtomParsers {
return mdhd.readUnsignedInt(); 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); stsd.setPosition(Mp4Util.FULL_ATOM_HEADER_SIZE);
int numberOfEntries = stsd.readInt(); int numberOfEntries = stsd.readInt();
MediaFormat mediaFormat = null; MediaFormat mediaFormat = null;
...@@ -334,19 +335,19 @@ public final class CommonMp4AtomParsers { ...@@ -334,19 +335,19 @@ public final class CommonMp4AtomParsers {
if (childAtomType == Atom.TYPE_avc1 || childAtomType == Atom.TYPE_avc3 if (childAtomType == Atom.TYPE_avc1 || childAtomType == Atom.TYPE_avc3
|| childAtomType == Atom.TYPE_encv) { || childAtomType == Atom.TYPE_encv) {
Pair<MediaFormat, TrackEncryptionBox> avc = Pair<MediaFormat, TrackEncryptionBox> avc =
parseAvcFromParent(stsd, childStartPosition, childAtomSize); parseAvcFromParent(stsd, childStartPosition, childAtomSize, durationUs);
mediaFormat = avc.first; mediaFormat = avc.first;
trackEncryptionBoxes[i] = avc.second; trackEncryptionBoxes[i] = avc.second;
} else if (childAtomType == Atom.TYPE_mp4a || childAtomType == Atom.TYPE_enca } else if (childAtomType == Atom.TYPE_mp4a || childAtomType == Atom.TYPE_enca
|| childAtomType == Atom.TYPE_ac_3) { || childAtomType == Atom.TYPE_ac_3) {
Pair<MediaFormat, TrackEncryptionBox> audioSampleEntry = Pair<MediaFormat, TrackEncryptionBox> audioSampleEntry = parseAudioSampleEntry(stsd,
parseAudioSampleEntry(stsd, childAtomType, childStartPosition, childAtomSize); childAtomType, childStartPosition, childAtomSize, durationUs);
mediaFormat = audioSampleEntry.first; mediaFormat = audioSampleEntry.first;
trackEncryptionBoxes[i] = audioSampleEntry.second; trackEncryptionBoxes[i] = audioSampleEntry.second;
} else if (childAtomType == Atom.TYPE_TTML) { } else if (childAtomType == Atom.TYPE_TTML) {
mediaFormat = MediaFormat.createTtmlFormat(); mediaFormat = MediaFormat.createTtmlFormat();
} else if (childAtomType == Atom.TYPE_mp4v) { } else if (childAtomType == Atom.TYPE_mp4v) {
mediaFormat = parseMp4vFromParent(stsd, childStartPosition, childAtomSize); mediaFormat = parseMp4vFromParent(stsd, childStartPosition, childAtomSize, durationUs);
} }
stsd.setPosition(childStartPosition + childAtomSize); stsd.setPosition(childStartPosition + childAtomSize);
} }
...@@ -355,7 +356,7 @@ public final class CommonMp4AtomParsers { ...@@ -355,7 +356,7 @@ public final class CommonMp4AtomParsers {
/** Returns the media format for an avc1 box. */ /** Returns the media format for an avc1 box. */
private static Pair<MediaFormat, TrackEncryptionBox> parseAvcFromParent(ParsableByteArray parent, 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.setPosition(position + Mp4Util.ATOM_HEADER_SIZE);
parent.skip(24); parent.skip(24);
...@@ -388,7 +389,7 @@ public final class CommonMp4AtomParsers { ...@@ -388,7 +389,7 @@ public final class CommonMp4AtomParsers {
} }
MediaFormat format = MediaFormat.createVideoFormat(MimeTypes.VIDEO_H264, MediaFormat.NO_VALUE, MediaFormat format = MediaFormat.createVideoFormat(MimeTypes.VIDEO_H264, MediaFormat.NO_VALUE,
width, height, pixelWidthHeightRatio, initializationData); durationUs, width, height, pixelWidthHeightRatio, initializationData);
return Pair.create(format, trackEncryptionBox); return Pair.create(format, trackEncryptionBox);
} }
...@@ -468,8 +469,8 @@ public final class CommonMp4AtomParsers { ...@@ -468,8 +469,8 @@ public final class CommonMp4AtomParsers {
} }
/** Returns the media format for an mp4v box. */ /** Returns the media format for an mp4v box. */
private static MediaFormat parseMp4vFromParent(ParsableByteArray parent, private static MediaFormat parseMp4vFromParent(ParsableByteArray parent, int position, int size,
int position, int size) { long durationUs) {
parent.setPosition(position + Mp4Util.ATOM_HEADER_SIZE); parent.setPosition(position + Mp4Util.ATOM_HEADER_SIZE);
parent.skip(24); parent.skip(24);
...@@ -492,11 +493,11 @@ public final class CommonMp4AtomParsers { ...@@ -492,11 +493,11 @@ public final class CommonMp4AtomParsers {
} }
return MediaFormat.createVideoFormat( 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( 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.setPosition(position + Mp4Util.ATOM_HEADER_SIZE);
parent.skip(16); parent.skip(16);
int channelCount = parent.readUnsignedShort(); int channelCount = parent.readUnsignedShort();
...@@ -555,7 +556,7 @@ public final class CommonMp4AtomParsers { ...@@ -555,7 +556,7 @@ public final class CommonMp4AtomParsers {
} }
MediaFormat format = MediaFormat.createAudioFormat( MediaFormat format = MediaFormat.createAudioFormat(
mimeType, sampleSize, channelCount, sampleRate, bitrate, mimeType, sampleSize, durationUs, channelCount, sampleRate, bitrate,
initializationData == null ? null : Collections.singletonList(initializationData)); initializationData == null ? null : Collections.singletonList(initializationData));
return Pair.create(format, trackEncryptionBox); return Pair.create(format, trackEncryptionBox);
} }
......
...@@ -30,6 +30,7 @@ import com.google.android.exoplayer.chunk.MediaChunk; ...@@ -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.Extractor;
import com.google.android.exoplayer.chunk.parser.mp4.FragmentedMp4Extractor; import com.google.android.exoplayer.chunk.parser.mp4.FragmentedMp4Extractor;
import com.google.android.exoplayer.chunk.parser.mp4.TrackEncryptionBox; 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.mp4.Track;
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.ProtectionElement; import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.ProtectionElement;
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.StreamElement; import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.StreamElement;
...@@ -38,6 +39,7 @@ import com.google.android.exoplayer.upstream.DataSource; ...@@ -38,6 +39,7 @@ import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DataSpec; import com.google.android.exoplayer.upstream.DataSpec;
import com.google.android.exoplayer.util.CodecSpecificDataUtil; import com.google.android.exoplayer.util.CodecSpecificDataUtil;
import com.google.android.exoplayer.util.ManifestFetcher; import com.google.android.exoplayer.util.ManifestFetcher;
import com.google.android.exoplayer.util.MimeTypes;
import android.net.Uri; import android.net.Uri;
import android.os.SystemClock; import android.os.SystemClock;
...@@ -48,8 +50,6 @@ import java.io.IOException; ...@@ -48,8 +50,6 @@ import java.io.IOException;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.UUID;
/** /**
* An {@link ChunkSource} for SmoothStreaming. * An {@link ChunkSource} for SmoothStreaming.
...@@ -71,7 +71,7 @@ public class SmoothStreamingChunkSource implements ChunkSource { ...@@ -71,7 +71,7 @@ public class SmoothStreamingChunkSource implements ChunkSource {
private final int maxHeight; private final int maxHeight;
private final SparseArray<FragmentedMp4Extractor> extractors; private final SparseArray<FragmentedMp4Extractor> extractors;
private final Map<UUID, byte[]> psshInfo; private final DrmInitData drmInitData;
private final SmoothStreamingFormat[] formats; private final SmoothStreamingFormat[] formats;
private SmoothStreamingManifest currentManifest; private SmoothStreamingManifest currentManifest;
...@@ -143,9 +143,11 @@ public class SmoothStreamingChunkSource implements ChunkSource { ...@@ -143,9 +143,11 @@ public class SmoothStreamingChunkSource implements ChunkSource {
byte[] keyId = getKeyId(protectionElement.data); byte[] keyId = getKeyId(protectionElement.data);
trackEncryptionBoxes = new TrackEncryptionBox[1]; trackEncryptionBoxes = new TrackEncryptionBox[1];
trackEncryptionBoxes[0] = new TrackEncryptionBox(true, INITIALIZATION_VECTOR_SIZE, keyId); 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 { } else {
psshInfo = null; drmInitData = null;
} }
int trackCount = trackIndices != null ? trackIndices.length : streamElement.tracks.length; int trackCount = trackIndices != null ? trackIndices.length : streamElement.tracks.length;
...@@ -299,7 +301,7 @@ public class SmoothStreamingChunkSource implements ChunkSource { ...@@ -299,7 +301,7 @@ public class SmoothStreamingChunkSource implements ChunkSource {
Uri uri = streamElement.buildRequestUri(selectedFormat.trackIndex, chunkIndex); Uri uri = streamElement.buildRequestUri(selectedFormat.trackIndex, chunkIndex);
Chunk mediaChunk = newMediaChunk(selectedFormat, uri, null, 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); currentAbsoluteChunkIndex, isLastChunk, chunkStartTimeUs, nextChunkStartTimeUs, 0);
out.chunk = mediaChunk; out.chunk = mediaChunk;
} }
...@@ -365,7 +367,7 @@ public class SmoothStreamingChunkSource implements ChunkSource { ...@@ -365,7 +367,7 @@ public class SmoothStreamingChunkSource implements ChunkSource {
} }
private static MediaChunk newMediaChunk(Format formatInfo, Uri uri, String cacheKey, 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) { boolean isLast, long chunkStartTimeUs, long nextChunkStartTimeUs, int trigger) {
int nextChunkIndex = isLast ? -1 : chunkIndex + 1; int nextChunkIndex = isLast ? -1 : chunkIndex + 1;
long nextStartTimeUs = isLast ? -1 : nextChunkStartTimeUs; long nextStartTimeUs = isLast ? -1 : nextChunkStartTimeUs;
...@@ -374,7 +376,7 @@ public class SmoothStreamingChunkSource implements ChunkSource { ...@@ -374,7 +376,7 @@ public class SmoothStreamingChunkSource implements ChunkSource {
// In SmoothStreaming each chunk contains sample timestamps relative to the start of the chunk. // 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. // To convert them the absolute timestamps, we need to set sampleOffsetUs to -chunkStartTimeUs.
return new ContainerMediaChunk(dataSource, dataSpec, formatInfo, trigger, 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) { private static byte[] getKeyId(byte[] initData) {
......
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
package com.google.android.exoplayer.source; package com.google.android.exoplayer.source;
import com.google.android.exoplayer.C; import com.google.android.exoplayer.C;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.MediaFormatHolder; import com.google.android.exoplayer.MediaFormatHolder;
import com.google.android.exoplayer.SampleHolder; import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.SampleSource; import com.google.android.exoplayer.SampleSource;
...@@ -62,9 +63,14 @@ public final class DefaultSampleSource implements SampleSource { ...@@ -62,9 +63,14 @@ public final class DefaultSampleSource implements SampleSource {
if (sampleExtractor.prepare()) { if (sampleExtractor.prepare()) {
prepared = true; prepared = true;
trackInfos = sampleExtractor.getTrackInfos(); int trackCount = sampleExtractor.getTrackCount();
trackStates = new int[trackInfos.length]; trackStates = new int[trackCount];
pendingDiscontinuities = new boolean[trackInfos.length]; 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; return prepared;
...@@ -119,7 +125,8 @@ public final class DefaultSampleSource implements SampleSource { ...@@ -119,7 +125,8 @@ public final class DefaultSampleSource implements SampleSource {
return NOTHING_READ; return NOTHING_READ;
} }
if (trackStates[track] != TRACK_STATE_FORMAT_SENT) { 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; trackStates[track] = TRACK_STATE_FORMAT_SENT;
return FORMAT_READ; return FORMAT_READ;
} }
......
...@@ -17,12 +17,12 @@ package com.google.android.exoplayer.source; ...@@ -17,12 +17,12 @@ package com.google.android.exoplayer.source;
import com.google.android.exoplayer.C; import com.google.android.exoplayer.C;
import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.MediaFormatHolder;
import com.google.android.exoplayer.SampleHolder; import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.SampleSource; import com.google.android.exoplayer.SampleSource;
import com.google.android.exoplayer.TrackInfo;
import com.google.android.exoplayer.TrackRenderer; 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.Assertions;
import com.google.android.exoplayer.util.MimeTypes;
import com.google.android.exoplayer.util.Util; import com.google.android.exoplayer.util.Util;
import android.annotation.TargetApi; import android.annotation.TargetApi;
...@@ -53,8 +53,6 @@ public final class FrameworkSampleExtractor implements SampleExtractor { ...@@ -53,8 +53,6 @@ public final class FrameworkSampleExtractor implements SampleExtractor {
private final MediaExtractor mediaExtractor; private final MediaExtractor mediaExtractor;
private TrackInfo[] trackInfos;
/** /**
* Instantiates a new sample extractor reading from the specified {@code uri}. * Instantiates a new sample extractor reading from the specified {@code uri}.
* *
...@@ -106,25 +104,10 @@ public final class FrameworkSampleExtractor implements SampleExtractor { ...@@ -106,25 +104,10 @@ public final class FrameworkSampleExtractor implements SampleExtractor {
mediaExtractor.setDataSource(fileDescriptor, fileDescriptorOffset, fileDescriptorLength); 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; return true;
} }
@Override @Override
public TrackInfo[] getTrackInfos() {
return trackInfos;
}
@Override
public void selectTrack(int index) { public void selectTrack(int index) {
mediaExtractor.selectTrack(index); mediaExtractor.selectTrack(index);
} }
...@@ -151,10 +134,18 @@ public final class FrameworkSampleExtractor implements SampleExtractor { ...@@ -151,10 +134,18 @@ public final class FrameworkSampleExtractor implements SampleExtractor {
} }
@Override @Override
public void getTrackMediaFormat(int track, MediaFormatHolder mediaFormatHolder) { public int getTrackCount() {
mediaFormatHolder.format = return mediaExtractor.getTrackCount();
MediaFormat.createFromFrameworkMediaFormatV16(mediaExtractor.getTrackFormat(track)); }
mediaFormatHolder.drmInitData = Util.SDK_INT >= 18 ? getPsshInfoV18() : null;
@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 @Override
...@@ -173,7 +164,7 @@ public final class FrameworkSampleExtractor implements SampleExtractor { ...@@ -173,7 +164,7 @@ public final class FrameworkSampleExtractor implements SampleExtractor {
} }
sampleHolder.timeUs = mediaExtractor.getSampleTime(); sampleHolder.timeUs = mediaExtractor.getSampleTime();
sampleHolder.flags = mediaExtractor.getSampleFlags(); sampleHolder.flags = mediaExtractor.getSampleFlags();
if ((sampleHolder.flags & MediaExtractor.SAMPLE_FLAG_ENCRYPTED) != 0) { if ((sampleHolder.flags & C.SAMPLE_FLAG_ENCRYPTED) != 0) {
sampleHolder.cryptoInfo.setFromExtractorV16(mediaExtractor); sampleHolder.cryptoInfo.setFromExtractorV16(mediaExtractor);
} }
...@@ -188,9 +179,15 @@ public final class FrameworkSampleExtractor implements SampleExtractor { ...@@ -188,9 +179,15 @@ public final class FrameworkSampleExtractor implements SampleExtractor {
} }
@TargetApi(18) @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(); 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 @@ ...@@ -16,11 +16,10 @@
package com.google.android.exoplayer.source; package com.google.android.exoplayer.source;
import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.MediaFormatHolder;
import com.google.android.exoplayer.SampleHolder; import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.SampleSource; import com.google.android.exoplayer.SampleSource;
import com.google.android.exoplayer.TrackInfo;
import com.google.android.exoplayer.TrackRenderer; import com.google.android.exoplayer.TrackRenderer;
import com.google.android.exoplayer.drm.DrmInitData;
import java.io.IOException; import java.io.IOException;
...@@ -28,7 +27,7 @@ import java.io.IOException; ...@@ -28,7 +27,7 @@ import java.io.IOException;
* Extractor for reading track metadata and samples stored in tracks. * Extractor for reading track metadata and samples stored in tracks.
* *
* <p>Call {@link #prepare} until it returns {@code true}, then access track metadata via * <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 * <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 * by calling {@link #deselectTrack}. It is safe to select/deselect tracks after reading sample
...@@ -46,9 +45,6 @@ public interface SampleExtractor { ...@@ -46,9 +45,6 @@ public interface SampleExtractor {
*/ */
boolean prepare() throws IOException; 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. */ /** Selects the track at {@code index} for reading sample data. */
void selectTrack(int index); void selectTrack(int index);
...@@ -75,8 +71,14 @@ public interface SampleExtractor { ...@@ -75,8 +71,14 @@ public interface SampleExtractor {
*/ */
void seekTo(long positionUs); void seekTo(long positionUs);
/** Stores the {@link MediaFormat} of {@code track}. */ /** Returns the number of tracks, if {@link #prepare} has returned {@code true}. */
void getTrackMediaFormat(int track, MediaFormatHolder mediaFormatHolder); 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 * 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; ...@@ -57,6 +57,8 @@ package com.google.android.exoplayer.text.eia608;
public static final byte CARRIAGE_RETURN = 0x2D; public static final byte CARRIAGE_RETURN = 0x2D;
public static final byte ERASE_NON_DISPLAYED_MEMORY = 0x2E; 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_1 = 0x11;
public static final byte MID_ROW_CHAN_2 = 0x19; public static final byte MID_ROW_CHAN_2 = 0x19;
......
...@@ -82,6 +82,26 @@ public class Eia608Parser { ...@@ -82,6 +82,26 @@ public class Eia608Parser {
0xFB // 3F: 251 'û' "Latin small letter U with circumflex" 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 ParsableBitArray seiBuffer;
private final StringBuilder stringBuilder; private final StringBuilder stringBuilder;
private final ArrayList<ClosedCaption> captions; private final ArrayList<ClosedCaption> captions;
...@@ -134,31 +154,45 @@ public class Eia608Parser { ...@@ -134,31 +154,45 @@ public class Eia608Parser {
} }
// Special North American character set. // 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)); stringBuilder.append(getSpecialChar(ccData2));
continue; 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. // Control character.
if (ccData1 < 0x20) { if (ccData1 < 0x20) {
if (stringBuilder.length() > 0) { addCtrl(ccData1, ccData2);
captions.add(new ClosedCaptionText(stringBuilder.toString()));
stringBuilder.setLength(0);
}
captions.add(new ClosedCaptionCtrl(ccData1, ccData2));
continue; continue;
} }
// Basic North American character set. // Basic North American character set.
stringBuilder.append(getChar(ccData1)); stringBuilder.append(getChar(ccData1));
if (ccData2 != 0) { if (ccData2 >= 0x20) {
stringBuilder.append(getChar(ccData2)); stringBuilder.append(getChar(ccData2));
} }
} }
if (stringBuilder.length() > 0) { addBufferedText();
captions.add(new ClosedCaptionText(stringBuilder.toString()));
}
if (captions.isEmpty()) { if (captions.isEmpty()) {
return null; return null;
...@@ -179,6 +213,32 @@ public class Eia608Parser { ...@@ -179,6 +213,32 @@ public class Eia608Parser {
return (char) SPECIAL_CHARACTER_SET[index]; 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. * Inspects an sei message to determine whether it contains EIA-608.
* <p> * <p>
......
...@@ -317,6 +317,11 @@ public class Eia608TrackRenderer extends TrackRenderer implements Callback { ...@@ -317,6 +317,11 @@ public class Eia608TrackRenderer extends TrackRenderer implements Callback {
case ClosedCaptionCtrl.CARRIAGE_RETURN: case ClosedCaptionCtrl.CARRIAGE_RETURN:
maybeAppendNewline(); maybeAppendNewline();
return; 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 { ...@@ -55,13 +55,15 @@ public interface DataSource {
/** /**
* Reads up to {@code length} bytes of data and stores them into {@code buffer}, starting at * 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 * index {@code offset}.
* of the opened range is detected, or an exception is thrown. * <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 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 offset The start offset into {@code buffer} at which data should be written.
* @param readLength The maximum number of bytes to read. * @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. * @throws IOException If an error occurs reading from the source.
*/ */
public int read(byte[] buffer, int offset, int readLength) throws IOException; public int read(byte[] buffer, int offset, int readLength) throws IOException;
......
...@@ -47,6 +47,10 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream ...@@ -47,6 +47,10 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream
private final Allocator allocator; private final Allocator allocator;
private final ReadHead readHead; 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 Allocation allocation;
private volatile boolean loadCanceled; private volatile boolean loadCanceled;
...@@ -58,6 +62,9 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream ...@@ -58,6 +62,9 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream
private int writeFragmentRemainingLength; 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 dataSource The source from which the data should be loaded.
* @param dataSpec Defines the data to be loaded. {@code dataSpec.length} must not exceed * @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 * {@link Integer#MAX_VALUE}. If {@code dataSpec.length == C.LENGTH_UNBOUNDED} then
...@@ -72,12 +79,48 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream ...@@ -72,12 +79,48 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream
this.allocator = allocator; this.allocator = allocator;
resolvedLength = C.LENGTH_UNBOUNDED; resolvedLength = C.LENGTH_UNBOUNDED;
readHead = new ReadHead(); 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. * Resets the read position to the start of the data.
*
* @throws UnsupportedOperationException Thrown if the allocation size is fixed.
*/ */
public void resetReadPosition() { public void resetReadPosition() {
if (isAllocationFixedSize) {
throw new UnsupportedOperationException(
"The read position cannot be reset when using a fixed allocation");
}
readHead.reset(); readHead.reset();
} }
...@@ -176,7 +219,12 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream ...@@ -176,7 +219,12 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream
byte[][] buffers = allocation.getBuffers(); byte[][] buffers = allocation.getBuffers();
while (bytesRead < bytesToRead) { while (bytesRead < bytesToRead) {
if (readHead.fragmentRemaining == 0) { 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.fragmentOffset = allocation.getFragmentOffset(readHead.fragmentIndex);
readHead.fragmentRemaining = allocation.getFragmentLength(readHead.fragmentIndex); readHead.fragmentRemaining = allocation.getFragmentLength(readHead.fragmentIndex);
} }
...@@ -194,6 +242,13 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream ...@@ -194,6 +242,13 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream
readHead.fragmentRemaining -= bufferReadLength; readHead.fragmentRemaining -= bufferReadLength;
} }
if (isAllocationFixedSize) {
synchronized (readHead) {
// Notify load() of the updated position so it can resume.
readHead.notify();
}
}
return bytesRead; return bytesRead;
} }
...@@ -210,6 +265,7 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream ...@@ -210,6 +265,7 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream
} }
@Override @Override
@SuppressWarnings("NonAtomicVolatileUpdate")
public void load() throws IOException, InterruptedException { public void load() throws IOException, InterruptedException {
if (loadCanceled || isLoadFinished()) { if (loadCanceled || isLoadFinished()) {
// The load was canceled, or is already complete. // The load was canceled, or is already complete.
...@@ -221,7 +277,7 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream ...@@ -221,7 +277,7 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream
if (loadPosition == 0 && resolvedLength == C.LENGTH_UNBOUNDED) { if (loadPosition == 0 && resolvedLength == C.LENGTH_UNBOUNDED) {
loadDataSpec = dataSpec; loadDataSpec = dataSpec;
long resolvedLength = dataSource.open(loadDataSpec); long resolvedLength = dataSource.open(loadDataSpec);
if (resolvedLength > Integer.MAX_VALUE) { if (!isAllocationFixedSize && resolvedLength > Integer.MAX_VALUE) {
throw new DataSourceStreamLoadException( throw new DataSourceStreamLoadException(
new UnexpectedLengthException(dataSpec.length, resolvedLength)); new UnexpectedLengthException(dataSpec.length, resolvedLength));
} }
...@@ -235,9 +291,13 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream ...@@ -235,9 +291,13 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream
} }
if (allocation == null) { if (allocation == null) {
int initialAllocationSize = resolvedLength != C.LENGTH_UNBOUNDED if (isAllocationFixedSize) {
? (int) resolvedLength : CHUNKED_ALLOCATION_INCREMENT; allocation = allocator.allocate(allocationSize);
allocation = allocator.allocate(initialAllocationSize); } else {
int initialAllocationSize = resolvedLength != C.LENGTH_UNBOUNDED
? (int) resolvedLength : CHUNKED_ALLOCATION_INCREMENT;
allocation = allocator.allocate(initialAllocationSize);
}
} }
int allocationCapacity = allocation.capacity(); int allocationCapacity = allocation.capacity();
...@@ -253,18 +313,25 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream ...@@ -253,18 +313,25 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream
if (Thread.interrupted()) { if (Thread.interrupted()) {
throw new InterruptedException(); throw new InterruptedException();
} }
read = dataSource.read(buffers[writeFragmentIndex], writeFragmentOffset,
writeFragmentRemainingLength); int bytesToWrite = getBytesToWrite();
read = dataSource.read(buffers[writeFragmentIndex], writeFragmentOffset, bytesToWrite);
if (read > 0) { if (read > 0) {
loadPosition += read; loadPosition += read;
writeFragmentOffset += read; writeFragmentOffset += read;
writeFragmentRemainingLength -= read; writeFragmentRemainingLength -= read;
if (writeFragmentRemainingLength == 0 && maybeMoreToLoad()) { if (writeFragmentRemainingLength == 0 && maybeMoreToLoad()) {
writeFragmentIndex++; writeFragmentIndex++;
if (loadPosition == allocationCapacity) { if (writeFragmentIndex == buffers.length) {
allocation.ensureCapacity(allocationCapacity + CHUNKED_ALLOCATION_INCREMENT); if (isAllocationFixedSize) {
allocationCapacity = allocation.capacity(); // Wrap back to the first fragment.
buffers = allocation.getBuffers(); writeFragmentIndex = 0;
} else {
// Grow the allocation.
allocation.ensureCapacity(allocationCapacity + CHUNKED_ALLOCATION_INCREMENT);
allocationCapacity = allocation.capacity();
buffers = allocation.getBuffers();
}
} }
writeFragmentOffset = allocation.getFragmentOffset(writeFragmentIndex); writeFragmentOffset = allocation.getFragmentOffset(writeFragmentIndex);
writeFragmentRemainingLength = allocation.getFragmentLength(writeFragmentIndex); writeFragmentRemainingLength = allocation.getFragmentLength(writeFragmentIndex);
...@@ -281,6 +348,25 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream ...@@ -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() { private boolean maybeMoreToLoad() {
return resolvedLength == C.LENGTH_UNBOUNDED || loadPosition < resolvedLength; return resolvedLength == C.LENGTH_UNBOUNDED || loadPosition < resolvedLength;
} }
......
...@@ -18,6 +18,7 @@ package com.google.android.exoplayer.upstream; ...@@ -18,6 +18,7 @@ package com.google.android.exoplayer.upstream;
import com.google.android.exoplayer.C; import com.google.android.exoplayer.C;
import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.Predicate; import com.google.android.exoplayer.util.Predicate;
import com.google.android.exoplayer.util.Util;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Log; import android.util.Log;
...@@ -226,6 +227,7 @@ public class DefaultHttpDataSource implements HttpDataSource { ...@@ -226,6 +227,7 @@ public class DefaultHttpDataSource implements HttpDataSource {
public void close() throws HttpDataSourceException { public void close() throws HttpDataSourceException {
try { try {
if (inputStream != null) { if (inputStream != null) {
Util.maybeTerminateInputStream(connection, bytesRemaining());
try { try {
inputStream.close(); inputStream.close();
} catch (IOException e) { } catch (IOException e) {
......
...@@ -128,6 +128,21 @@ public final class Loader { ...@@ -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}. * Start loading a {@link Loadable}.
* <p> * <p>
* A {@link Loader} instance can only load one {@link Loadable} at a time, and so this method * 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 { ...@@ -138,9 +153,24 @@ public final class Loader {
* @param callback A callback to invoke when the load ends. * @param callback A callback to invoke when the load ends.
*/ */
public void startLoading(Looper looper, Loadable loadable, Callback callback) { 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); Assertions.checkState(!loading);
loading = true; loading = true;
currentTask = new LoadTask(looper, loadable, callback); currentTask = new LoadTask(looper, loadable, callback, delayMs);
downloadExecutorService.submit(currentTask); downloadExecutorService.submit(currentTask);
} }
...@@ -182,13 +212,15 @@ public final class Loader { ...@@ -182,13 +212,15 @@ public final class Loader {
private final Loadable loadable; private final Loadable loadable;
private final Loader.Callback callback; private final Loader.Callback callback;
private final int delayMs;
private volatile Thread executorThread; 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); super(looper);
this.loadable = loadable; this.loadable = loadable;
this.callback = callback; this.callback = callback;
this.delayMs = delayMs;
} }
public void quit() { public void quit() {
...@@ -202,6 +234,9 @@ public final class Loader { ...@@ -202,6 +234,9 @@ public final class Loader {
public void run() { public void run() {
try { try {
executorThread = Thread.currentThread(); executorThread = Thread.currentThread();
if (delayMs > 0) {
Thread.sleep(delayMs);
}
if (!loadable.isLoadCanceled()) { if (!loadable.isLoadCanceled()) {
loadable.load(); loadable.load();
} }
......
...@@ -15,12 +15,16 @@ ...@@ -15,12 +15,16 @@
*/ */
package com.google.android.exoplayer.util; package com.google.android.exoplayer.util;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DataSource;
import android.text.TextUtils; import android.text.TextUtils;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.net.HttpURLConnection;
import java.net.URL; import java.net.URL;
import java.text.ParseException; import java.text.ParseException;
import java.util.Arrays; import java.util.Arrays;
...@@ -57,6 +61,8 @@ public final class Util { ...@@ -57,6 +61,8 @@ public final class Util {
Pattern.compile("^(-)?P(([0-9]*)Y)?(([0-9]*)M)?(([0-9]*)D)?" Pattern.compile("^(-)?P(([0-9]*)Y)?(([0-9]*)M)?(([0-9]*)D)?"
+ "(T(([0-9]*)H)?(([0-9]*)M)?(([0-9.]*)S)?)?$"); + "(T(([0-9]*)H)?(([0-9]*)M)?(([0-9.]*)S)?)?$");
private static final long MAX_BYTES_TO_DRAIN = 2048;
private Util() {} private Util() {}
/** /**
...@@ -396,4 +402,48 @@ public final class Util { ...@@ -396,4 +402,48 @@ public final class Util {
return intArray; 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 { ...@@ -42,9 +42,9 @@ public class MediaFormatTest extends TestCase {
initData.add(initData2); initData.add(initData2);
testConversionToFrameworkFormatV16( testConversionToFrameworkFormatV16(
MediaFormat.createVideoFormat("video/xyz", 102400, 1280, 720, 1.5f, initData)); MediaFormat.createVideoFormat("video/xyz", 102400, 1000L, 1280, 720, 1.5f, initData));
testConversionToFrameworkFormatV16( testConversionToFrameworkFormatV16(
MediaFormat.createAudioFormat("audio/xyz", 102400, 5, 44100, initData)); MediaFormat.createAudioFormat("audio/xyz", 102400, 1000L, 5, 44100, initData));
} }
@TargetApi(16) @TargetApi(16)
......
...@@ -81,7 +81,7 @@ public class HlsMediaPlaylistParserTest extends TestCase { ...@@ -81,7 +81,7 @@ public class HlsMediaPlaylistParserTest extends TestCase {
assertEquals(false, segments.get(0).discontinuity); assertEquals(false, segments.get(0).discontinuity);
assertEquals(7.975, segments.get(0).durationSecs); 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).encryptionKeyUri);
assertEquals(null, segments.get(0).encryptionIV); assertEquals(null, segments.get(0).encryptionIV);
assertEquals(51370, segments.get(0).byterangeLength); assertEquals(51370, segments.get(0).byterangeLength);
...@@ -90,7 +90,7 @@ public class HlsMediaPlaylistParserTest extends TestCase { ...@@ -90,7 +90,7 @@ public class HlsMediaPlaylistParserTest extends TestCase {
assertEquals(false, segments.get(1).discontinuity); assertEquals(false, segments.get(1).discontinuity);
assertEquals(7.975, segments.get(1).durationSecs); 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("https://priv.example.com/key.php?r=2680", segments.get(1).encryptionKeyUri);
assertEquals("0x1566B", segments.get(1).encryptionIV); assertEquals("0x1566B", segments.get(1).encryptionIV);
assertEquals(51501, segments.get(1).byterangeLength); assertEquals(51501, segments.get(1).byterangeLength);
...@@ -99,7 +99,7 @@ public class HlsMediaPlaylistParserTest extends TestCase { ...@@ -99,7 +99,7 @@ public class HlsMediaPlaylistParserTest extends TestCase {
assertEquals(false, segments.get(2).discontinuity); assertEquals(false, segments.get(2).discontinuity);
assertEquals(7.941, segments.get(2).durationSecs); 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).encryptionKeyUri);
assertEquals(null, segments.get(2).encryptionIV); assertEquals(null, segments.get(2).encryptionIV);
assertEquals(51501, segments.get(2).byterangeLength); assertEquals(51501, segments.get(2).byterangeLength);
...@@ -108,7 +108,7 @@ public class HlsMediaPlaylistParserTest extends TestCase { ...@@ -108,7 +108,7 @@ public class HlsMediaPlaylistParserTest extends TestCase {
assertEquals(true, segments.get(3).discontinuity); assertEquals(true, segments.get(3).discontinuity);
assertEquals(7.975, segments.get(3).durationSecs); 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); assertEquals("https://priv.example.com/key.php?r=2682", segments.get(3).encryptionKeyUri);
// 0xA7A == 2682. // 0xA7A == 2682.
assertNotNull(segments.get(3).encryptionIV); assertNotNull(segments.get(3).encryptionIV);
...@@ -119,11 +119,11 @@ public class HlsMediaPlaylistParserTest extends TestCase { ...@@ -119,11 +119,11 @@ public class HlsMediaPlaylistParserTest extends TestCase {
assertEquals(false, segments.get(4).discontinuity); assertEquals(false, segments.get(4).discontinuity);
assertEquals(7.975, segments.get(4).durationSecs); 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); assertEquals("https://priv.example.com/key.php?r=2682", segments.get(4).encryptionKeyUri);
// 0xA7A == 2682. // 0xA7B == 2683.
assertNotNull(segments.get(4).encryptionIV); 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(C.LENGTH_UNBOUNDED, segments.get(4).byterangeLength);
assertEquals(0, segments.get(4).byterangeOffset); assertEquals(0, segments.get(4).byterangeOffset);
assertEquals("https://priv.example.com/fileSequence2683.ts", segments.get(4).url); 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