Commit da7ae2a9 by Sergio Moreno Mozota

Merge remote-tracking branch 'upstream/dev' into dev

parents 9208c655 6c5af232
Showing with 8652 additions and 1493 deletions
...@@ -17,9 +17,11 @@ ...@@ -17,9 +17,11 @@
buildscript { buildscript {
repositories { repositories {
mavenCentral() mavenCentral()
jcenter()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:1.0.0' classpath 'com.android.tools.build:gradle:1.0.0'
classpath 'com.novoda:bintray-release:0.2.7'
} }
} }
......
...@@ -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;
......
...@@ -163,6 +163,12 @@ public class EventLogger implements DemoPlayer.Listener, DemoPlayer.InfoListener ...@@ -163,6 +163,12 @@ public class EventLogger implements DemoPlayer.Listener, DemoPlayer.InfoListener
printInternalError("cryptoError", e); printInternalError("cryptoError", e);
} }
@Override
public void onDecoderInitialized(String decoderName, long elapsedRealtimeMs,
long initializationDurationMs) {
Log.d(TAG, "decoderInitialized [" + getSessionTimeString() + "]");
}
private void printInternalError(String type, Exception e) { private void printInternalError(String type, Exception e) {
Log.e(TAG, "internalError [" + getSessionTimeString() + ", " + type + "]", e); Log.e(TAG, "internalError [" + getSessionTimeString() + ", " + type + "]", e);
} }
......
...@@ -24,8 +24,11 @@ import com.google.android.exoplayer.demo.player.DefaultRendererBuilder; ...@@ -24,8 +24,11 @@ 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.PrivMetadata;
import com.google.android.exoplayer.metadata.TxxxMetadata; import com.google.android.exoplayer.metadata.TxxxMetadata;
import com.google.android.exoplayer.text.CaptionStyleCompat; import com.google.android.exoplayer.text.CaptionStyleCompat;
import com.google.android.exoplayer.text.SubtitleView; import com.google.android.exoplayer.text.SubtitleView;
...@@ -213,6 +216,8 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback, ...@@ -213,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);
} }
...@@ -446,11 +451,22 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback, ...@@ -446,11 +451,22 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback,
@Override @Override
public void onId3Metadata(Map<String, Object> metadata) { public void onId3Metadata(Map<String, Object> metadata) {
for (int i = 0; i < metadata.size(); i++) { for (Map.Entry<String, Object> entry : metadata.entrySet()) {
if (metadata.containsKey(TxxxMetadata.TYPE)) { if (TxxxMetadata.TYPE.equals(entry.getKey())) {
TxxxMetadata txxxMetadata = (TxxxMetadata) metadata.get(TxxxMetadata.TYPE); TxxxMetadata txxxMetadata = (TxxxMetadata) entry.getValue();
Log.i(TAG, String.format("ID3 TimedMetadata: description=%s, value=%s", Log.i(TAG, String.format("ID3 TimedMetadata %s: description=%s, value=%s",
txxxMetadata.description, txxxMetadata.value)); TxxxMetadata.TYPE, txxxMetadata.description, txxxMetadata.value));
} else if (PrivMetadata.TYPE.equals(entry.getKey())) {
PrivMetadata privMetadata = (PrivMetadata) entry.getValue();
Log.i(TAG, String.format("ID3 TimedMetadata %s: owner=%s",
PrivMetadata.TYPE, privMetadata.owner));
} else if (GeobMetadata.TYPE.equals(entry.getKey())) {
GeobMetadata geobMetadata = (GeobMetadata) entry.getValue();
Log.i(TAG, String.format("ID3 TimedMetadata %s: mimeType=%s, filename=%s, description=%s",
GeobMetadata.TYPE, geobMetadata.mimeType, geobMetadata.filename,
geobMetadata.description));
} else {
Log.i(TAG, String.format("ID3 TimedMetadata %s", entry.getKey()));
} }
} }
} }
......
...@@ -103,7 +103,7 @@ import java.util.Locale; ...@@ -103,7 +103,7 @@ import java.util.Locale;
+ "as=fmp4_audio_cenc,fmp4_sd_hd_cenc&sparams=ip,ipbits,expire,as&ip=0.0.0.0&ipbits=0" + "as=fmp4_audio_cenc,fmp4_sd_hd_cenc&sparams=ip,ipbits,expire,as&ip=0.0.0.0&ipbits=0"
+ "&expire=19000000000&signature=61611F115EEEC7BADE5536827343FFFE2D83D14F." + "&expire=19000000000&signature=61611F115EEEC7BADE5536827343FFFE2D83D14F."
+ "2FDF4BFA502FB5865C5C86401314BDDEA4799BD0&key=ik0", DemoUtil.TYPE_DASH), + "2FDF4BFA502FB5865C5C86401314BDDEA4799BD0&key=ik0", DemoUtil.TYPE_DASH),
new Sample("WV: 30s license duration", "f9a34cab7b05881a", new Sample("WV: 30s license duration (fails at ~30s)", "f9a34cab7b05881a",
"http://www.youtube.com/api/manifest/dash/id/f9a34cab7b05881a/source/youtube?" "http://www.youtube.com/api/manifest/dash/id/f9a34cab7b05881a/source/youtube?"
+ "as=fmp4_audio_cenc,fmp4_sd_hd_cenc&sparams=ip,ipbits,expire,as&ip=0.0.0.0&ipbits=0" + "as=fmp4_audio_cenc,fmp4_sd_hd_cenc&sparams=ip,ipbits,expire,as&ip=0.0.0.0&ipbits=0"
+ "&expire=19000000000&signature=88DC53943385CED8CF9F37ADD9E9843E3BF621E6." + "&expire=19000000000&signature=88DC53943385CED8CF9F37ADD9E9843E3BF621E6."
...@@ -123,6 +123,8 @@ import java.util.Locale; ...@@ -123,6 +123,8 @@ import java.util.Locale;
new Sample("Apple AAC media playlist", new Sample("Apple AAC media playlist",
"https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/gear0/" "https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/gear0/"
+ "prog_index.m3u8", DemoUtil.TYPE_HLS), + "prog_index.m3u8", DemoUtil.TYPE_HLS),
new Sample("Apple ID3 metadata", "http://devimages.apple.com/samplecode/adDemo/ad.m3u8",
DemoUtil.TYPE_HLS),
}; };
public static final Sample[] MISC = new Sample[] { public static final Sample[] MISC = new Sample[] {
...@@ -133,6 +135,12 @@ import java.util.Locale; ...@@ -133,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() {}
......
...@@ -129,6 +129,8 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi ...@@ -129,6 +129,8 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
void onLoadStarted(int sourceId, String formatId, int trigger, boolean isInitialization, void onLoadStarted(int sourceId, String formatId, int trigger, boolean isInitialization,
int mediaStartTimeMs, int mediaEndTimeMs, long length); int mediaStartTimeMs, int mediaEndTimeMs, long length);
void onLoadCompleted(int sourceId, long bytesLoaded); void onLoadCompleted(int sourceId, long bytesLoaded);
void onDecoderInitialized(String decoderName, long elapsedRealtimeMs,
long initializationDurationMs);
} }
/** /**
...@@ -478,6 +480,16 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi ...@@ -478,6 +480,16 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
} }
@Override @Override
public void onDecoderInitialized(
String decoderName,
long elapsedRealtimeMs,
long initializationDurationMs) {
if (infoListener != null) {
infoListener.onDecoderInitialized(decoderName, elapsedRealtimeMs, initializationDurationMs);
}
}
@Override
public void onUpstreamError(int sourceId, IOException e) { public void onUpstreamError(int sourceId, IOException e) {
if (internalErrorListener != null) { if (internalErrorListener != null) {
internalErrorListener.onUpstreamError(sourceId, e); internalErrorListener.onUpstreamError(sourceId, e);
......
/*
* 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);
}
}
...@@ -12,6 +12,7 @@ ...@@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
apply plugin: 'com.android.library' apply plugin: 'com.android.library'
apply plugin: 'bintray-release'
android { android {
compileSdkVersion 21 compileSdkVersion 21
...@@ -47,3 +48,13 @@ android.libraryVariants.all { variant -> ...@@ -47,3 +48,13 @@ android.libraryVariants.all { variant ->
task.from variant.javaCompile.destinationDir task.from variant.javaCompile.destinationDir
artifacts.add('archives', task); artifacts.add('archives', task);
} }
publish {
repoName = 'exoplayer'
userOrg = 'google'
groupId = 'com.google.android.exoplayer'
artifactId = 'exoplayer'
version = 'r1.2.3'
description = 'The ExoPlayer library.'
website = 'https://github.com/google/ExoPlayer'
}
...@@ -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,33 @@ public final class C { ...@@ -43,11 +44,33 @@ 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 MediaCodec#CRYPTO_MODE_AES_CTR
*/
@SuppressWarnings("InlinedApi")
public static final int CRYPTO_MODE_AES_CTR = MediaCodec.CRYPTO_MODE_AES_CTR;
/**
* @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;
/**
* Indicates that a sample should be decoded but not rendered.
*/
public static final int SAMPLE_FLAG_DECODE_ONLY = 0x8000000;
/**
* A return value for methods where the end of an input was encountered.
*/
public static final int RESULT_END_OF_INPUT = -1;
private C() {} private C() {}
} }
...@@ -141,14 +141,6 @@ public interface ExoPlayer { ...@@ -141,14 +141,6 @@ public interface ExoPlayer {
return new ExoPlayerImpl(rendererCount, DEFAULT_MIN_BUFFER_MS, DEFAULT_MIN_REBUFFER_MS); return new ExoPlayerImpl(rendererCount, DEFAULT_MIN_BUFFER_MS, DEFAULT_MIN_REBUFFER_MS);
} }
/**
* @deprecated Please use {@link #newInstance(int, int, int)}.
*/
@Deprecated
public static ExoPlayer newInstance(int rendererCount, int minRebufferMs) {
return new ExoPlayerImpl(rendererCount, DEFAULT_MIN_BUFFER_MS, minRebufferMs);
}
} }
/** /**
...@@ -160,7 +152,8 @@ public interface ExoPlayer { ...@@ -160,7 +152,8 @@ public interface ExoPlayer {
* {@link ExoPlayer#getPlaybackState()} changes. * {@link ExoPlayer#getPlaybackState()} changes.
* *
* @param playWhenReady Whether playback will proceed when ready. * @param playWhenReady Whether playback will proceed when ready.
* @param playbackState One of the {@code STATE} constants defined in this class. * @param playbackState One of the {@code STATE} constants defined in the {@link ExoPlayer}
* interface.
*/ */
void onPlayerStateChanged(boolean playWhenReady, int playbackState); void onPlayerStateChanged(boolean playWhenReady, int playbackState);
/** /**
...@@ -256,7 +249,7 @@ public interface ExoPlayer { ...@@ -256,7 +249,7 @@ public interface ExoPlayer {
/** /**
* Returns the current state of the player. * Returns the current state of the player.
* *
* @return One of the {@code STATE} constants defined in this class. * @return One of the {@code STATE} constants defined in this interface.
*/ */
public int getPlaybackState(); public int getPlaybackState();
......
...@@ -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.
...@@ -61,6 +59,17 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { ...@@ -61,6 +59,17 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
*/ */
void onCryptoError(CryptoException e); void onCryptoError(CryptoException e);
/**
* Invoked when a decoder is successfully created.
*
* @param decoderName The decoder that was configured and created.
* @param elapsedRealtimeMs {@code elapsedRealtime} timestamp of when the initialization
* finished.
* @param initializationDurationMs Amount of time taken to initialize the decoder.
*/
void onDecoderInitialized(String decoderName, long elapsedRealtimeMs,
long initializationDurationMs);
} }
/** /**
...@@ -151,6 +160,23 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { ...@@ -151,6 +160,23 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
*/ */
private static final int RECONFIGURATION_STATE_QUEUE_PENDING = 2; private static final int RECONFIGURATION_STATE_QUEUE_PENDING = 2;
/**
* The codec does not need to be re-initialized.
*/
private static final int REINITIALIZATION_STATE_NONE = 0;
/**
* The input format has changed in a way that requires the codec to be re-initialized, but we
* haven't yet signaled an end of stream to the existing codec. We need to do so in order to
* ensure that it outputs any remaining buffers before we release it.
*/
private static final int REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM = 1;
/**
* The input format has changed in a way that requires the codec to be re-initialized, and we've
* signaled an end of stream to the existing codec. We're waiting for the codec to output an end
* of stream signal to indicate that it has output any remaining buffers before we release it.
*/
private static final int REINITIALIZATION_STATE_WAIT_END_OF_STREAM = 2;
public final CodecCounters codecCounters; public final CodecCounters codecCounters;
private final DrmSessionManager drmSessionManager; private final DrmSessionManager drmSessionManager;
...@@ -164,7 +190,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { ...@@ -164,7 +190,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;
...@@ -175,6 +201,8 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { ...@@ -175,6 +201,8 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
private boolean openedDrmSession; private boolean openedDrmSession;
private boolean codecReconfigured; private boolean codecReconfigured;
private int codecReconfigurationState; private int codecReconfigurationState;
private int codecReinitializationState;
private boolean codecHasQueuedBuffers;
private int trackIndex; private int trackIndex;
private int sourceState; private int sourceState;
...@@ -210,6 +238,8 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { ...@@ -210,6 +238,8 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
formatHolder = new MediaFormatHolder(); formatHolder = new MediaFormatHolder();
decodeOnlyPresentationTimestamps = new ArrayList<Long>(); decodeOnlyPresentationTimestamps = new ArrayList<Long>();
outputBufferInfo = new MediaCodec.BufferInfo(); outputBufferInfo = new MediaCodec.BufferInfo();
codecReconfigurationState = RECONFIGURATION_STATE_NONE;
codecReinitializationState = REINITIALIZATION_STATE_NONE;
} }
@Override @Override
...@@ -281,7 +311,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { ...@@ -281,7 +311,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();
...@@ -313,9 +343,13 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { ...@@ -313,9 +343,13 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
String decoderName = decoderInfo.name; String decoderName = decoderInfo.name;
codecIsAdaptive = decoderInfo.adaptive; codecIsAdaptive = decoderInfo.adaptive;
try { try {
long codecInitializingTimestamp = SystemClock.elapsedRealtime();
codec = MediaCodec.createByCodecName(decoderName); codec = MediaCodec.createByCodecName(decoderName);
configureCodec(codec, format.getFrameworkMediaFormatV16(), mediaCrypto); configureCodec(codec, format.getFrameworkMediaFormatV16(), mediaCrypto);
codec.start(); codec.start();
long codecInitializedTimestamp = SystemClock.elapsedRealtime();
notifyDecoderInitialized(decoderName, codecInitializedTimestamp,
codecInitializedTimestamp - codecInitializingTimestamp);
inputBuffers = codec.getInputBuffers(); inputBuffers = codec.getInputBuffers();
outputBuffers = codec.getOutputBuffers(); outputBuffers = codec.getOutputBuffers();
} catch (Exception e) { } catch (Exception e) {
...@@ -370,12 +404,15 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { ...@@ -370,12 +404,15 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
codecHotswapTimeMs = -1; codecHotswapTimeMs = -1;
inputIndex = -1; inputIndex = -1;
outputIndex = -1; outputIndex = -1;
waitingForKeys = false;
decodeOnlyPresentationTimestamps.clear(); decodeOnlyPresentationTimestamps.clear();
inputBuffers = null; inputBuffers = null;
outputBuffers = null; outputBuffers = null;
codecReconfigured = false; codecReconfigured = false;
codecHasQueuedBuffers = false;
codecIsAdaptive = false; codecIsAdaptive = false;
codecReconfigurationState = RECONFIGURATION_STATE_NONE; codecReconfigurationState = RECONFIGURATION_STATE_NONE;
codecReinitializationState = REINITIALIZATION_STATE_NONE;
codecCounters.codecReleaseCount++; codecCounters.codecReleaseCount++;
try { try {
codec.stop(); codec.stop();
...@@ -418,7 +455,6 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { ...@@ -418,7 +455,6 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
sourceState = SOURCE_STATE_NOT_READY; sourceState = SOURCE_STATE_NOT_READY;
inputStreamEnded = false; inputStreamEnded = false;
outputStreamEnded = false; outputStreamEnded = false;
waitingForKeys = false;
} }
@Override @Override
...@@ -478,11 +514,13 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { ...@@ -478,11 +514,13 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
inputIndex = -1; inputIndex = -1;
outputIndex = -1; outputIndex = -1;
waitingForFirstSyncFrame = true; waitingForFirstSyncFrame = true;
waitingForKeys = false;
decodeOnlyPresentationTimestamps.clear(); decodeOnlyPresentationTimestamps.clear();
// Workaround for framework bugs. // Workaround for framework bugs.
// See [Internal: b/8347958], [Internal: b/8578467], [Internal: b/8543366]. // See [Internal: b/8347958], [Internal: b/8578467], [Internal: b/8543366].
if (Util.SDK_INT >= 18) { if (Util.SDK_INT >= 18 && codecReinitializationState == REINITIALIZATION_STATE_NONE) {
codec.flush(); codec.flush();
codecHasQueuedBuffers = false;
} else { } else {
releaseCodec(); releaseCodec();
maybeInitCodec(); maybeInitCodec();
...@@ -502,9 +540,13 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { ...@@ -502,9 +540,13 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
* @throws ExoPlaybackException If an error occurs feeding the input buffer. * @throws ExoPlaybackException If an error occurs feeding the input buffer.
*/ */
private boolean feedInputBuffer(boolean firstFeed) throws IOException, ExoPlaybackException { private boolean feedInputBuffer(boolean firstFeed) throws IOException, ExoPlaybackException {
if (inputStreamEnded) { if (inputStreamEnded
|| codecReinitializationState == REINITIALIZATION_STATE_WAIT_END_OF_STREAM) {
// The input stream has ended, or we need to re-initialize the codec but are still waiting
// for the existing codec to output any final output buffers.
return false; return false;
} }
if (inputIndex < 0) { if (inputIndex < 0) {
inputIndex = codec.dequeueInputBuffer(0); inputIndex = codec.dequeueInputBuffer(0);
if (inputIndex < 0) { if (inputIndex < 0) {
...@@ -514,6 +556,15 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { ...@@ -514,6 +556,15 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
sampleHolder.data.clear(); sampleHolder.data.clear();
} }
if (codecReinitializationState == REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM) {
// We need to re-initialize the codec. Send an end of stream signal to the existing codec so
// that it outputs any remaining buffers before we release it.
codec.queueInputBuffer(inputIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
inputIndex = -1;
codecReinitializationState = REINITIALIZATION_STATE_WAIT_END_OF_STREAM;
return false;
}
int result; int result;
if (waitingForKeys) { if (waitingForKeys) {
// We've already read an encrypted sample into sampleHolder, and are waiting for keys. // We've already read an encrypted sample into sampleHolder, and are waiting for keys.
...@@ -572,7 +623,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { ...@@ -572,7 +623,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
if (waitingForFirstSyncFrame) { if (waitingForFirstSyncFrame) {
// TODO: Find out if it's possible to supply samples prior to the first sync // TODO: Find out if it's possible to supply samples prior to the first sync
// frame for HE-AAC. // frame for HE-AAC.
if ((sampleHolder.flags & C.SAMPLE_FLAG_SYNC) == 0) { if (!sampleHolder.isSyncFrame()) {
sampleHolder.data.clear(); sampleHolder.data.clear();
if (codecReconfigurationState == RECONFIGURATION_STATE_QUEUE_PENDING) { if (codecReconfigurationState == RECONFIGURATION_STATE_QUEUE_PENDING) {
// The buffer we just cleared contained reconfiguration data. We need to re-write this // The buffer we just cleared contained reconfiguration data. We need to re-write this
...@@ -583,7 +634,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { ...@@ -583,7 +634,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
} }
waitingForFirstSyncFrame = false; waitingForFirstSyncFrame = false;
} }
boolean sampleEncrypted = (sampleHolder.flags & MediaExtractor.SAMPLE_FLAG_ENCRYPTED) != 0; boolean sampleEncrypted = sampleHolder.isEncrypted();
waitingForKeys = shouldWaitForKeys(sampleEncrypted); waitingForKeys = shouldWaitForKeys(sampleEncrypted);
if (waitingForKeys) { if (waitingForKeys) {
return false; return false;
...@@ -592,7 +643,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { ...@@ -592,7 +643,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
int bufferSize = sampleHolder.data.position(); int bufferSize = sampleHolder.data.position();
int adaptiveReconfigurationBytes = bufferSize - sampleHolder.size; int adaptiveReconfigurationBytes = bufferSize - sampleHolder.size;
long presentationTimeUs = sampleHolder.timeUs; long presentationTimeUs = sampleHolder.timeUs;
if (sampleHolder.decodeOnly) { if (sampleHolder.isDecodeOnly()) {
decodeOnlyPresentationTimestamps.add(presentationTimeUs); decodeOnlyPresentationTimestamps.add(presentationTimeUs);
} }
if (sampleEncrypted) { if (sampleEncrypted) {
...@@ -603,6 +654,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { ...@@ -603,6 +654,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
codec.queueInputBuffer(inputIndex, 0 , bufferSize, presentationTimeUs, 0); codec.queueInputBuffer(inputIndex, 0 , bufferSize, presentationTimeUs, 0);
} }
inputIndex = -1; inputIndex = -1;
codecHasQueuedBuffers = true;
codecReconfigurationState = RECONFIGURATION_STATE_NONE; codecReconfigurationState = RECONFIGURATION_STATE_NONE;
} catch (CryptoException e) { } catch (CryptoException e) {
notifyCryptoError(e); notifyCryptoError(e);
...@@ -656,8 +708,14 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { ...@@ -656,8 +708,14 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
codecReconfigured = true; codecReconfigured = true;
codecReconfigurationState = RECONFIGURATION_STATE_WRITE_PENDING; codecReconfigurationState = RECONFIGURATION_STATE_WRITE_PENDING;
} else { } else {
releaseCodec(); if (codecHasQueuedBuffers) {
maybeInitCodec(); // Signal end of stream and wait for any final output buffers before re-initialization.
codecReinitializationState = REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM;
} else {
// There aren't any final output buffers, so perform re-initialization immediately.
releaseCodec();
maybeInitCodec();
}
} }
} }
...@@ -745,7 +803,13 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { ...@@ -745,7 +803,13 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
} }
if ((outputBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) { if ((outputBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
outputStreamEnded = true; if (codecReinitializationState == REINITIALIZATION_STATE_WAIT_END_OF_STREAM) {
// We're waiting to re-initialize the codec, and have now received all final output buffers.
releaseCodec();
maybeInitCodec();
} else {
outputStreamEnded = true;
}
return false; return false;
} }
...@@ -797,6 +861,19 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { ...@@ -797,6 +861,19 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
} }
} }
private void notifyDecoderInitialized(final String decoderName,
final long initializedTimestamp, final long initializationDuration) {
if (eventHandler != null && eventListener != null) {
eventHandler.post(new Runnable() {
@Override
public void run() {
eventListener.onDecoderInitialized(decoderName, initializedTimestamp,
initializationDuration);
}
});
}
}
private int getDecodeOnlyIndex(long presentationTimeUs) { private int getDecodeOnlyIndex(long presentationTimeUs) {
final int size = decodeOnlyPresentationTimestamps.size(); final int size = decodeOnlyPresentationTimestamps.size();
for (int i = 0; i < size; i++) { for (int i = 0; i < size; i++) {
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer; package com.google.android.exoplayer;
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.Util; import com.google.android.exoplayer.util.Util;
...@@ -180,6 +181,33 @@ public class MediaCodecUtil { ...@@ -180,6 +181,33 @@ public class MediaCodecUtil {
} }
/** /**
* Tests whether the device advertises it can decode video of a given type at a specified
* width, height, and frame rate.
* <p>
* Must not be called if the device SDK version is less than 21.
*
* @param mimeType The mime type.
* @param secure Whether the decoder is required to support secure decryption. Always pass false
* unless secure decryption really is required.
* @param width Width in pixels.
* @param height Height in pixels.
* @param frameRate Frame rate in frames per second.
* @return Whether the decoder advertises support of the given size and frame rate.
*/
@TargetApi(21)
public static boolean isSizeAndRateSupportedV21(String mimeType, boolean secure,
int width, int height, double frameRate) throws DecoderQueryException {
Assertions.checkState(Util.SDK_INT >= 21);
Pair<String, CodecCapabilities> info = getMediaCodecInfo(mimeType, secure);
if (info == null) {
return false;
}
MediaCodecInfo.VideoCapabilities videoCapabilities = info.second.getVideoCapabilities();
return videoCapabilities != null
&& videoCapabilities.areSizeAndRateSupported(width, height, frameRate);
}
/**
* @param profile An AVC profile constant from {@link CodecProfileLevel}. * @param profile An AVC profile constant from {@link CodecProfileLevel}.
* @param level An AVC profile level from {@link CodecProfileLevel}. * @param level An AVC profile level from {@link CodecProfileLevel}.
* @return Whether the specified profile is supported at the specified level. * @return Whether the specified profile is supported at the specified level.
......
...@@ -373,6 +373,13 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer { ...@@ -373,6 +373,13 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer {
: holder.format.pixelWidthHeightRatio; : holder.format.pixelWidthHeightRatio;
} }
/**
* @return True if the first frame has been rendered (playback has not necessarily begun).
*/
protected final boolean haveRenderedFirstFrame() {
return renderedFirstFrame;
}
@Override @Override
protected void onOutputFormatChanged(android.media.MediaFormat format) { protected void onOutputFormatChanged(android.media.MediaFormat format) {
boolean hasCrop = format.containsKey(KEY_CROP_RIGHT) && format.containsKey(KEY_CROP_LEFT) boolean hasCrop = format.containsKey(KEY_CROP_RIGHT) && format.containsKey(KEY_CROP_LEFT)
...@@ -427,7 +434,6 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer { ...@@ -427,7 +434,6 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer {
if (!renderedFirstFrame) { if (!renderedFirstFrame) {
renderOutputBufferImmediate(codec, bufferIndex); renderOutputBufferImmediate(codec, bufferIndex);
renderedFirstFrame = true;
return true; return true;
} }
...@@ -463,14 +469,14 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer { ...@@ -463,14 +469,14 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer {
return false; return false;
} }
private void skipOutputBuffer(MediaCodec codec, int bufferIndex) { protected void skipOutputBuffer(MediaCodec codec, int bufferIndex) {
TraceUtil.beginSection("skipVideoBuffer"); TraceUtil.beginSection("skipVideoBuffer");
codec.releaseOutputBuffer(bufferIndex, false); codec.releaseOutputBuffer(bufferIndex, false);
TraceUtil.endSection(); TraceUtil.endSection();
codecCounters.skippedOutputBufferCount++; codecCounters.skippedOutputBufferCount++;
} }
private void dropOutputBuffer(MediaCodec codec, int bufferIndex) { protected void dropOutputBuffer(MediaCodec codec, int bufferIndex) {
TraceUtil.beginSection("dropVideoBuffer"); TraceUtil.beginSection("dropVideoBuffer");
codec.releaseOutputBuffer(bufferIndex, false); codec.releaseOutputBuffer(bufferIndex, false);
TraceUtil.endSection(); TraceUtil.endSection();
...@@ -481,22 +487,24 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer { ...@@ -481,22 +487,24 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer {
} }
} }
private void renderOutputBufferImmediate(MediaCodec codec, int bufferIndex) { protected void renderOutputBufferImmediate(MediaCodec codec, int bufferIndex) {
maybeNotifyVideoSizeChanged(); maybeNotifyVideoSizeChanged();
TraceUtil.beginSection("renderVideoBufferImmediate"); TraceUtil.beginSection("renderVideoBufferImmediate");
codec.releaseOutputBuffer(bufferIndex, true); codec.releaseOutputBuffer(bufferIndex, true);
TraceUtil.endSection(); TraceUtil.endSection();
codecCounters.renderedOutputBufferCount++; codecCounters.renderedOutputBufferCount++;
renderedFirstFrame = true;
maybeNotifyDrawnToSurface(); maybeNotifyDrawnToSurface();
} }
@TargetApi(21) @TargetApi(21)
private void renderOutputBufferTimedV21(MediaCodec codec, int bufferIndex, long releaseTimeNs) { protected void renderOutputBufferTimedV21(MediaCodec codec, int bufferIndex, long releaseTimeNs) {
maybeNotifyVideoSizeChanged(); maybeNotifyVideoSizeChanged();
TraceUtil.beginSection("releaseOutputBufferTimed"); TraceUtil.beginSection("releaseOutputBufferTimed");
codec.releaseOutputBuffer(bufferIndex, releaseTimeNs); codec.releaseOutputBuffer(bufferIndex, releaseTimeNs);
TraceUtil.endSection(); TraceUtil.endSection();
codecCounters.renderedOutputBufferCount++; codecCounters.renderedOutputBufferCount++;
renderedFirstFrame = true;
maybeNotifyDrawnToSurface(); maybeNotifyDrawnToSurface();
} }
......
...@@ -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},
* {@link android.media.MediaExtractor#SAMPLE_FLAG_SYNC} and * {@link C#SAMPLE_FLAG_ENCRYPTED} and {@link C#SAMPLE_FLAG_DECODE_ONLY}.
* {@link android.media.MediaExtractor#SAMPLE_FLAG_ENCRYPTED}
*/ */
public int flags; public int flags;
...@@ -61,11 +60,6 @@ public final class SampleHolder { ...@@ -61,11 +60,6 @@ public final class SampleHolder {
*/ */
public long timeUs; public long timeUs;
/**
* If true then the sample should be decoded, but should not be presented.
*/
public boolean decodeOnly;
private final int bufferReplacementMode; private final int bufferReplacementMode;
/** /**
...@@ -97,6 +91,27 @@ public final class SampleHolder { ...@@ -97,6 +91,27 @@ public final class SampleHolder {
} }
/** /**
* Returns whether {@link #flags} has {@link C#SAMPLE_FLAG_ENCRYPTED} set.
*/
public boolean isEncrypted() {
return (flags & C.SAMPLE_FLAG_ENCRYPTED) != 0;
}
/**
* Returns whether {@link #flags} has {@link C#SAMPLE_FLAG_DECODE_ONLY} set.
*/
public boolean isDecodeOnly() {
return (flags & C.SAMPLE_FLAG_DECODE_ONLY) != 0;
}
/**
* Returns whether {@link #flags} has {@link C#SAMPLE_FLAG_SYNC} set.
*/
public boolean isSyncFrame() {
return (flags & C.SAMPLE_FLAG_SYNC) != 0;
}
/**
* Clears {@link #data}. Does nothing if {@link #data} is null. * Clears {@link #data}. Does nothing if {@link #data} is null.
*/ */
public void clearData() { public void clearData() {
......
...@@ -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,13 +352,14 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback { ...@@ -352,13 +352,14 @@ 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;
} }
if (mediaChunk.read(sampleHolder)) { if (mediaChunk.read(sampleHolder)) {
sampleHolder.decodeOnly = frameAccurateSeeking && sampleHolder.timeUs < lastSeekPositionUs; boolean decodeOnly = frameAccurateSeeking && sampleHolder.timeUs < lastSeekPositionUs;
sampleHolder.flags |= decodeOnly ? C.SAMPLE_FLAG_DECODE_ONLY : 0;
onSampleRead(mediaChunk, sampleHolder); onSampleRead(mediaChunk, sampleHolder);
return SAMPLE_READ; return SAMPLE_READ;
} else { } else {
......
...@@ -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;
} }
} }
...@@ -85,14 +85,6 @@ public class Format { ...@@ -85,14 +85,6 @@ public class Format {
public final String language; public final String language;
/** /**
* The average bandwidth in bytes per second.
*
* @deprecated Use {@link #bitrate}. However note that the units of measurement are different.
*/
@Deprecated
public final int bandwidth;
/**
* @param id The format identifier. * @param id The format identifier.
* @param mimeType The format mime type. * @param mimeType The format mime type.
* @param width The width of the video in pixels, or -1 for non-video formats. * @param width The width of the video in pixels, or -1 for non-video formats.
...@@ -144,7 +136,6 @@ public class Format { ...@@ -144,7 +136,6 @@ public class Format {
this.bitrate = bitrate; this.bitrate = bitrate;
this.language = language; this.language = language;
this.codecs = codecs; this.codecs = codecs;
this.bandwidth = bitrate / 8;
} }
@Override @Override
......
...@@ -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,27 +21,23 @@ import com.google.android.exoplayer.ParserException; ...@@ -21,27 +21,23 @@ 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;
import com.google.android.exoplayer.mp4.CommonMp4AtomParsers; import com.google.android.exoplayer.mp4.CommonMp4AtomParsers;
import com.google.android.exoplayer.mp4.Mp4Util;
import com.google.android.exoplayer.mp4.Track; import com.google.android.exoplayer.mp4.Track;
import com.google.android.exoplayer.upstream.NonBlockingInputStream; import com.google.android.exoplayer.upstream.NonBlockingInputStream;
import com.google.android.exoplayer.util.H264Util;
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;
...@@ -161,11 +157,10 @@ public final class FragmentedMp4Extractor implements Extractor { ...@@ -161,11 +157,10 @@ public final class FragmentedMp4Extractor implements Extractor {
public FragmentedMp4Extractor(int workaroundFlags) { public FragmentedMp4Extractor(int workaroundFlags) {
this.workaroundFlags = workaroundFlags; this.workaroundFlags = workaroundFlags;
parserState = STATE_READING_ATOM_HEADER; parserState = STATE_READING_ATOM_HEADER;
atomHeader = new ParsableByteArray(Mp4Util.ATOM_HEADER_SIZE); atomHeader = new ParsableByteArray(Atom.ATOM_HEADER_SIZE);
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 {
...@@ -269,14 +259,14 @@ public final class FragmentedMp4Extractor implements Extractor { ...@@ -269,14 +259,14 @@ public final class FragmentedMp4Extractor implements Extractor {
} }
private int readAtomHeader(NonBlockingInputStream inputStream) { private int readAtomHeader(NonBlockingInputStream inputStream) {
int remainingBytes = Mp4Util.ATOM_HEADER_SIZE - atomBytesRead; int remainingBytes = Atom.ATOM_HEADER_SIZE - atomBytesRead;
int bytesRead = inputStream.read(atomHeader.data, atomBytesRead, remainingBytes); int bytesRead = inputStream.read(atomHeader.data, atomBytesRead, remainingBytes);
if (bytesRead == -1) { if (bytesRead == -1) {
return RESULT_END_OF_STREAM; return RESULT_END_OF_STREAM;
} }
rootAtomBytesRead += bytesRead; rootAtomBytesRead += bytesRead;
atomBytesRead += bytesRead; atomBytesRead += bytesRead;
if (atomBytesRead != Mp4Util.ATOM_HEADER_SIZE) { if (atomBytesRead != Atom.ATOM_HEADER_SIZE) {
return RESULT_NEED_MORE_DATA; return RESULT_NEED_MORE_DATA;
} }
...@@ -298,10 +288,10 @@ public final class FragmentedMp4Extractor implements Extractor { ...@@ -298,10 +288,10 @@ public final class FragmentedMp4Extractor implements Extractor {
if (CONTAINER_TYPES.contains(atomTypeInteger)) { if (CONTAINER_TYPES.contains(atomTypeInteger)) {
enterState(STATE_READING_ATOM_HEADER); enterState(STATE_READING_ATOM_HEADER);
containerAtoms.add(new ContainerAtom(atomType, containerAtoms.add(new ContainerAtom(atomType,
rootAtomBytesRead + atomSize - Mp4Util.ATOM_HEADER_SIZE)); rootAtomBytesRead + atomSize - Atom.ATOM_HEADER_SIZE));
} else { } else {
atomData = new ParsableByteArray(atomSize); atomData = new ParsableByteArray(atomSize);
System.arraycopy(atomHeader.data, 0, atomData.data, 0, Mp4Util.ATOM_HEADER_SIZE); System.arraycopy(atomHeader.data, 0, atomData.data, 0, Atom.ATOM_HEADER_SIZE);
enterState(STATE_READING_ATOM_PAYLOAD); enterState(STATE_READING_ATOM_PAYLOAD);
} }
} else { } else {
...@@ -370,12 +360,15 @@ public final class FragmentedMp4Extractor implements Extractor { ...@@ -370,12 +360,15 @@ public final class FragmentedMp4Extractor implements Extractor {
LeafAtom child = moovChildren.get(i); LeafAtom child = moovChildren.get(i);
if (child.type == Atom.TYPE_pssh) { if (child.type == Atom.TYPE_pssh) {
ParsableByteArray psshAtom = child.data; ParsableByteArray psshAtom = child.data;
psshAtom.setPosition(Mp4Util.FULL_ATOM_HEADER_SIZE); psshAtom.setPosition(Atom.FULL_ATOM_HEADER_SIZE);
UUID uuid = new UUID(psshAtom.readLong(), psshAtom.readLong()); UUID uuid = new UUID(psshAtom.readLong(), psshAtom.readLong());
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);
...@@ -406,7 +399,7 @@ public final class FragmentedMp4Extractor implements Extractor { ...@@ -406,7 +399,7 @@ public final class FragmentedMp4Extractor implements Extractor {
* Parses a trex atom (defined in 14496-12). * Parses a trex atom (defined in 14496-12).
*/ */
private static DefaultSampleValues parseTrex(ParsableByteArray trex) { private static DefaultSampleValues parseTrex(ParsableByteArray trex) {
trex.setPosition(Mp4Util.FULL_ATOM_HEADER_SIZE + 4); trex.setPosition(Atom.FULL_ATOM_HEADER_SIZE + 4);
int defaultSampleDescriptionIndex = trex.readUnsignedIntToInt() - 1; int defaultSampleDescriptionIndex = trex.readUnsignedIntToInt() - 1;
int defaultSampleDuration = trex.readUnsignedIntToInt(); int defaultSampleDuration = trex.readUnsignedIntToInt();
int defaultSampleSize = trex.readUnsignedIntToInt(); int defaultSampleSize = trex.readUnsignedIntToInt();
...@@ -460,9 +453,9 @@ public final class FragmentedMp4Extractor implements Extractor { ...@@ -460,9 +453,9 @@ public final class FragmentedMp4Extractor implements Extractor {
private static void parseSaiz(TrackEncryptionBox encryptionBox, ParsableByteArray saiz, private static void parseSaiz(TrackEncryptionBox encryptionBox, ParsableByteArray saiz,
TrackFragment out) { TrackFragment out) {
int vectorSize = encryptionBox.initializationVectorSize; int vectorSize = encryptionBox.initializationVectorSize;
saiz.setPosition(Mp4Util.ATOM_HEADER_SIZE); saiz.setPosition(Atom.ATOM_HEADER_SIZE);
int fullAtom = saiz.readInt(); int fullAtom = saiz.readInt();
int flags = Mp4Util.parseFullAtomFlags(fullAtom); int flags = Atom.parseFullAtomFlags(fullAtom);
if ((flags & 0x01) == 1) { if ((flags & 0x01) == 1) {
saiz.skip(8); saiz.skip(8);
} }
...@@ -497,9 +490,9 @@ public final class FragmentedMp4Extractor implements Extractor { ...@@ -497,9 +490,9 @@ public final class FragmentedMp4Extractor implements Extractor {
*/ */
private static DefaultSampleValues parseTfhd(DefaultSampleValues extendsDefaults, private static DefaultSampleValues parseTfhd(DefaultSampleValues extendsDefaults,
ParsableByteArray tfhd) { ParsableByteArray tfhd) {
tfhd.setPosition(Mp4Util.ATOM_HEADER_SIZE); tfhd.setPosition(Atom.ATOM_HEADER_SIZE);
int fullAtom = tfhd.readInt(); int fullAtom = tfhd.readInt();
int flags = Mp4Util.parseFullAtomFlags(fullAtom); int flags = Atom.parseFullAtomFlags(fullAtom);
tfhd.skip(4); // trackId tfhd.skip(4); // trackId
if ((flags & 0x01 /* base_data_offset_present */) != 0) { if ((flags & 0x01 /* base_data_offset_present */) != 0) {
...@@ -526,9 +519,9 @@ public final class FragmentedMp4Extractor implements Extractor { ...@@ -526,9 +519,9 @@ public final class FragmentedMp4Extractor implements Extractor {
* media, expressed in the media's timescale. * media, expressed in the media's timescale.
*/ */
private static long parseTfdt(ParsableByteArray tfdt) { private static long parseTfdt(ParsableByteArray tfdt) {
tfdt.setPosition(Mp4Util.ATOM_HEADER_SIZE); tfdt.setPosition(Atom.ATOM_HEADER_SIZE);
int fullAtom = tfdt.readInt(); int fullAtom = tfdt.readInt();
int version = Mp4Util.parseFullAtomVersion(fullAtom); int version = Atom.parseFullAtomVersion(fullAtom);
return version == 1 ? tfdt.readUnsignedLongToLong() : tfdt.readUnsignedInt(); return version == 1 ? tfdt.readUnsignedLongToLong() : tfdt.readUnsignedInt();
} }
...@@ -543,9 +536,9 @@ public final class FragmentedMp4Extractor implements Extractor { ...@@ -543,9 +536,9 @@ public final class FragmentedMp4Extractor implements Extractor {
*/ */
private static void parseTrun(Track track, DefaultSampleValues defaultSampleValues, private static void parseTrun(Track track, DefaultSampleValues defaultSampleValues,
long decodeTime, int workaroundFlags, ParsableByteArray trun, TrackFragment out) { long decodeTime, int workaroundFlags, ParsableByteArray trun, TrackFragment out) {
trun.setPosition(Mp4Util.ATOM_HEADER_SIZE); trun.setPosition(Atom.ATOM_HEADER_SIZE);
int fullAtom = trun.readInt(); int fullAtom = trun.readInt();
int flags = Mp4Util.parseFullAtomFlags(fullAtom); int flags = Atom.parseFullAtomFlags(fullAtom);
int sampleCount = trun.readUnsignedIntToInt(); int sampleCount = trun.readUnsignedIntToInt();
if ((flags & 0x01 /* data_offset_present */) != 0) { if ((flags & 0x01 /* data_offset_present */) != 0) {
...@@ -603,7 +596,7 @@ public final class FragmentedMp4Extractor implements Extractor { ...@@ -603,7 +596,7 @@ public final class FragmentedMp4Extractor implements Extractor {
private static void parseUuid(ParsableByteArray uuid, TrackFragment out, private static void parseUuid(ParsableByteArray uuid, TrackFragment out,
byte[] extendedTypeScratch) { byte[] extendedTypeScratch) {
uuid.setPosition(Mp4Util.ATOM_HEADER_SIZE); uuid.setPosition(Atom.ATOM_HEADER_SIZE);
uuid.readBytes(extendedTypeScratch, 0, 16); uuid.readBytes(extendedTypeScratch, 0, 16);
// Currently this parser only supports Microsoft's PIFF SampleEncryptionBox. // Currently this parser only supports Microsoft's PIFF SampleEncryptionBox.
...@@ -622,9 +615,9 @@ public final class FragmentedMp4Extractor implements Extractor { ...@@ -622,9 +615,9 @@ public final class FragmentedMp4Extractor implements Extractor {
} }
private static void parseSenc(ParsableByteArray senc, int offset, TrackFragment out) { private static void parseSenc(ParsableByteArray senc, int offset, TrackFragment out) {
senc.setPosition(Mp4Util.ATOM_HEADER_SIZE + offset); senc.setPosition(Atom.ATOM_HEADER_SIZE + offset);
int fullAtom = senc.readInt(); int fullAtom = senc.readInt();
int flags = Mp4Util.parseFullAtomFlags(fullAtom); int flags = Atom.parseFullAtomFlags(fullAtom);
if ((flags & 0x01 /* override_track_encryption_box_parameters */) != 0) { if ((flags & 0x01 /* override_track_encryption_box_parameters */) != 0) {
// TODO: Implement this. // TODO: Implement this.
...@@ -646,9 +639,9 @@ public final class FragmentedMp4Extractor implements Extractor { ...@@ -646,9 +639,9 @@ public final class FragmentedMp4Extractor implements Extractor {
* Parses a sidx atom (defined in 14496-12). * Parses a sidx atom (defined in 14496-12).
*/ */
private static SegmentIndex parseSidx(ParsableByteArray atom) { private static SegmentIndex parseSidx(ParsableByteArray atom) {
atom.setPosition(Mp4Util.ATOM_HEADER_SIZE); atom.setPosition(Atom.ATOM_HEADER_SIZE);
int fullAtom = atom.readInt(); int fullAtom = atom.readInt();
int version = Mp4Util.parseFullAtomVersion(fullAtom); int version = Atom.parseFullAtomVersion(fullAtom);
atom.skip(4); atom.skip(4);
long timescale = atom.readUnsignedInt(); long timescale = atom.readUnsignedInt();
...@@ -788,7 +781,7 @@ public final class FragmentedMp4Extractor implements Extractor { ...@@ -788,7 +781,7 @@ public final class FragmentedMp4Extractor implements Extractor {
if (track.type == Track.TYPE_VIDEO) { if (track.type == Track.TYPE_VIDEO) {
// The mp4 file contains length-prefixed NAL units, but the decoder wants start code // The mp4 file contains length-prefixed NAL units, but the decoder wants start code
// delimited content. // delimited content.
Mp4Util.replaceLengthPrefixesWithAvcStartCodes(outputData, sampleSize); H264Util.replaceLengthPrefixesWithAvcStartCodes(outputData, sampleSize);
} }
out.size = sampleSize; out.size = sampleSize;
} }
...@@ -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;
}
} }
} }
...@@ -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.upstream.NonBlockingInputStream; import com.google.android.exoplayer.upstream.NonBlockingInputStream;
import com.google.android.exoplayer.util.LongArray; import com.google.android.exoplayer.util.LongArray;
import com.google.android.exoplayer.util.MimeTypes; import com.google.android.exoplayer.util.MimeTypes;
...@@ -28,8 +29,6 @@ import com.google.android.exoplayer.util.MimeTypes; ...@@ -28,8 +29,6 @@ import com.google.android.exoplayer.util.MimeTypes;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
/** /**
...@@ -38,6 +37,8 @@ import java.util.concurrent.TimeUnit; ...@@ -38,6 +37,8 @@ import java.util.concurrent.TimeUnit;
* <p>WebM is a subset of the EBML elements defined for Matroska. More information about EBML and * <p>WebM is a subset of the EBML elements defined for Matroska. More information about EBML and
* Matroska is available <a href="http://www.matroska.org/technical/specs/index.html">here</a>. * Matroska is available <a href="http://www.matroska.org/technical/specs/index.html">here</a>.
* More info about WebM is <a href="http://www.webmproject.org/code/specs/container/">here</a>. * More info about WebM is <a href="http://www.webmproject.org/code/specs/container/">here</a>.
* RFC on encrypted WebM can be found
* <a href="http://wiki.webmproject.org/encryption/webm-encryption-rfc">here</a>.
*/ */
public final class WebmExtractor implements Extractor { public final class WebmExtractor implements Extractor {
...@@ -47,6 +48,7 @@ public final class WebmExtractor implements Extractor { ...@@ -47,6 +48,7 @@ public final class WebmExtractor implements Extractor {
private static final String CODEC_ID_OPUS = "A_OPUS"; private static final String CODEC_ID_OPUS = "A_OPUS";
private static final int VORBIS_MAX_INPUT_SIZE = 8192; private static final int VORBIS_MAX_INPUT_SIZE = 8192;
private static final int OPUS_MAX_INPUT_SIZE = 5760; private static final int OPUS_MAX_INPUT_SIZE = 5760;
private static final int BLOCK_COUNTER_SIZE = 16;
private static final int UNKNOWN = -1; private static final int UNKNOWN = -1;
// Element IDs // Element IDs
...@@ -80,23 +82,31 @@ public final class WebmExtractor implements Extractor { ...@@ -80,23 +82,31 @@ public final class WebmExtractor implements Extractor {
private static final int ID_CHANNELS = 0x9F; private static final int ID_CHANNELS = 0x9F;
private static final int ID_SAMPLING_FREQUENCY = 0xB5; private static final int ID_SAMPLING_FREQUENCY = 0xB5;
private static final int ID_CONTENT_ENCODINGS = 0x6D80;
private static final int ID_CONTENT_ENCODING = 0x6240;
private static final int ID_CONTENT_ENCODING_ORDER = 0x5031;
private static final int ID_CONTENT_ENCODING_SCOPE = 0x5032;
private static final int ID_CONTENT_ENCODING_TYPE = 0x5033;
private static final int ID_CONTENT_ENCRYPTION = 0x5035;
private static final int ID_CONTENT_ENCRYPTION_ALGORITHM = 0x47E1;
private static final int ID_CONTENT_ENCRYPTION_KEY_ID = 0x47E2;
private static final int ID_CONTENT_ENCRYPTION_AES_SETTINGS = 0x47E7;
private static final int ID_CONTENT_ENCRYPTION_AES_SETTINGS_CIPHER_MODE = 0x47E8;
private static final int ID_CUES = 0x1C53BB6B; private static final int ID_CUES = 0x1C53BB6B;
private static final int ID_CUE_POINT = 0xBB; private static final int ID_CUE_POINT = 0xBB;
private static final int ID_CUE_TIME = 0xB3; private static final int ID_CUE_TIME = 0xB3;
private static final int ID_CUE_TRACK_POSITIONS = 0xB7; private static final int ID_CUE_TRACK_POSITIONS = 0xB7;
private static final int ID_CUE_CLUSTER_POSITION = 0xF1; private static final int ID_CUE_CLUSTER_POSITION = 0xF1;
// SimpleBlock Lacing Values
private static final int LACING_NONE = 0; private static final int LACING_NONE = 0;
private static final int LACING_XIPH = 1;
private static final int LACING_FIXED = 2;
private static final int LACING_EBML = 3;
private static final int READ_TERMINATING_RESULTS = RESULT_NEED_MORE_DATA | RESULT_END_OF_STREAM private static final int READ_TERMINATING_RESULTS = RESULT_NEED_MORE_DATA | RESULT_END_OF_STREAM
| RESULT_READ_SAMPLE | RESULT_NEED_SAMPLE_HOLDER; | RESULT_READ_SAMPLE | RESULT_NEED_SAMPLE_HOLDER;
private final EbmlReader reader; private final EbmlReader reader;
private final byte[] simpleBlockTimecodeAndFlags = new byte[3]; private final byte[] simpleBlockTimecodeAndFlags = new byte[3];
private DrmInitData.Universal drmInitData;
private SampleHolder sampleHolder; private SampleHolder sampleHolder;
private int readResults; private int readResults;
...@@ -104,7 +114,7 @@ public final class WebmExtractor implements Extractor { ...@@ -104,7 +114,7 @@ public final class WebmExtractor implements Extractor {
private long segmentStartOffsetBytes = UNKNOWN; private long segmentStartOffsetBytes = UNKNOWN;
private long segmentEndOffsetBytes = UNKNOWN; private long segmentEndOffsetBytes = UNKNOWN;
private long timecodeScale = 1000000L; private long timecodeScale = 1000000L;
private long durationUs = UNKNOWN; private long durationUs = C.UNKNOWN_TIME_US;
private int pixelWidth = UNKNOWN; private int pixelWidth = UNKNOWN;
private int pixelHeight = UNKNOWN; private int pixelHeight = UNKNOWN;
private int channelCount = UNKNOWN; private int channelCount = UNKNOWN;
...@@ -113,7 +123,9 @@ public final class WebmExtractor implements Extractor { ...@@ -113,7 +123,9 @@ public final class WebmExtractor implements Extractor {
private String codecId; private String codecId;
private long codecDelayNs; private long codecDelayNs;
private long seekPreRollNs; private long seekPreRollNs;
private boolean seenAudioTrack; private boolean isAudioTrack;
private boolean hasContentEncryption;
private byte[] encryptionKeyId;
private long cuesSizeBytes = UNKNOWN; private long cuesSizeBytes = UNKNOWN;
private long clusterTimecodeUs = UNKNOWN; private long clusterTimecodeUs = UNKNOWN;
private long simpleBlockTimecodeUs = UNKNOWN; private long simpleBlockTimecodeUs = UNKNOWN;
...@@ -182,14 +194,8 @@ public final class WebmExtractor implements Extractor { ...@@ -182,14 +194,8 @@ public final class WebmExtractor implements Extractor {
} }
@Override @Override
public long getDurationUs() { public DrmInitData getDrmInitData() {
return durationUs == UNKNOWN ? C.UNKNOWN_TIME_US : durationUs; return drmInitData;
}
@Override
public Map<UUID, byte[]> getPsshInfo() {
// TODO: Parse pssh data from Webm streams.
return null;
} }
/* package */ int getElementType(int id) { /* package */ int getElementType(int id) {
...@@ -202,6 +208,10 @@ public final class WebmExtractor implements Extractor { ...@@ -202,6 +208,10 @@ public final class WebmExtractor implements Extractor {
case ID_TRACK_ENTRY: case ID_TRACK_ENTRY:
case ID_AUDIO: case ID_AUDIO:
case ID_VIDEO: case ID_VIDEO:
case ID_CONTENT_ENCODINGS:
case ID_CONTENT_ENCODING:
case ID_CONTENT_ENCRYPTION:
case ID_CONTENT_ENCRYPTION_AES_SETTINGS:
case ID_CUES: case ID_CUES:
case ID_CUE_POINT: case ID_CUE_POINT:
case ID_CUE_TRACK_POSITIONS: case ID_CUE_TRACK_POSITIONS:
...@@ -216,12 +226,18 @@ public final class WebmExtractor implements Extractor { ...@@ -216,12 +226,18 @@ public final class WebmExtractor implements Extractor {
case ID_CODEC_DELAY: case ID_CODEC_DELAY:
case ID_SEEK_PRE_ROLL: case ID_SEEK_PRE_ROLL:
case ID_CHANNELS: case ID_CHANNELS:
case ID_CONTENT_ENCODING_ORDER:
case ID_CONTENT_ENCODING_SCOPE:
case ID_CONTENT_ENCODING_TYPE:
case ID_CONTENT_ENCRYPTION_ALGORITHM:
case ID_CONTENT_ENCRYPTION_AES_SETTINGS_CIPHER_MODE:
case ID_CUE_TIME: case ID_CUE_TIME:
case ID_CUE_CLUSTER_POSITION: case ID_CUE_CLUSTER_POSITION:
return EbmlReader.TYPE_UNSIGNED_INT; return EbmlReader.TYPE_UNSIGNED_INT;
case ID_DOC_TYPE: case ID_DOC_TYPE:
case ID_CODEC_ID: case ID_CODEC_ID:
return EbmlReader.TYPE_STRING; return EbmlReader.TYPE_STRING;
case ID_CONTENT_ENCRYPTION_KEY_ID:
case ID_SIMPLE_BLOCK: case ID_SIMPLE_BLOCK:
case ID_BLOCK: case ID_BLOCK:
case ID_CODEC_PRIVATE: case ID_CODEC_PRIVATE:
...@@ -250,6 +266,12 @@ public final class WebmExtractor implements Extractor { ...@@ -250,6 +266,12 @@ public final class WebmExtractor implements Extractor {
cueTimesUs = new LongArray(); cueTimesUs = new LongArray();
cueClusterPositions = new LongArray(); cueClusterPositions = new LongArray();
break; break;
case ID_CONTENT_ENCODING:
// TODO: check and fail if more than one content encoding is present.
break;
case ID_CONTENT_ENCRYPTION:
hasContentEncryption = true;
break;
default: default:
// pass // pass
} }
...@@ -261,17 +283,24 @@ public final class WebmExtractor implements Extractor { ...@@ -261,17 +283,24 @@ public final class WebmExtractor implements Extractor {
case ID_CUES: case ID_CUES:
buildCues(); buildCues();
return false; return false;
case ID_VIDEO: case ID_CONTENT_ENCODING:
buildVideoFormat(); if (!hasContentEncryption) {
// We found a ContentEncoding other than Encryption.
throw new ParserException("Found an unsupported ContentEncoding");
}
if (encryptionKeyId == null) {
throw new ParserException("Encrypted Track found but ContentEncKeyID was not found");
}
drmInitData = new DrmInitData.Universal(MimeTypes.VIDEO_WEBM, encryptionKeyId);
return true; return true;
case ID_AUDIO: case ID_AUDIO:
seenAudioTrack = true; isAudioTrack = true;
return true; return true;
case ID_TRACK_ENTRY: case ID_TRACK_ENTRY:
if (seenAudioTrack) { if (isAudioTrack) {
// Audio format has to be built here since codec private may not be available at the end
// of ID_AUDIO.
buildAudioFormat(); buildAudioFormat();
} else {
buildVideoFormat();
} }
return true; return true;
default: default:
...@@ -311,6 +340,37 @@ public final class WebmExtractor implements Extractor { ...@@ -311,6 +340,37 @@ public final class WebmExtractor implements Extractor {
case ID_CHANNELS: case ID_CHANNELS:
channelCount = (int) value; channelCount = (int) value;
break; break;
case ID_CONTENT_ENCODING_ORDER:
// This extractor only supports one ContentEncoding element and hence the order has to be 0.
if (value != 0) {
throw new ParserException("ContentEncodingOrder " + value + " not supported");
}
break;
case ID_CONTENT_ENCODING_SCOPE:
// This extractor only supports the scope of all frames (since that's the only scope used
// for Encryption).
if (value != 1) {
throw new ParserException("ContentEncodingScope " + value + " not supported");
}
break;
case ID_CONTENT_ENCODING_TYPE:
// This extractor only supports Encrypted ContentEncodingType.
if (value != 1) {
throw new ParserException("ContentEncodingType " + value + " not supported");
}
break;
case ID_CONTENT_ENCRYPTION_ALGORITHM:
// Only the value 5 (AES) is allowed according to the WebM specification.
if (value != 5) {
throw new ParserException("ContentEncAlgo " + value + " not supported");
}
break;
case ID_CONTENT_ENCRYPTION_AES_SETTINGS_CIPHER_MODE:
// Only the value 1 is allowed according to the WebM specification.
if (value != 1) {
throw new ParserException("AESSettingsCipherMode " + value + " not supported");
}
break;
case ID_CUE_TIME: case ID_CUE_TIME:
cueTimesUs.add(scaleTimecodeToUs(value)); cueTimesUs.add(scaleTimecodeToUs(value));
break; break;
...@@ -402,22 +462,49 @@ public final class WebmExtractor implements Extractor { ...@@ -402,22 +462,49 @@ public final class WebmExtractor implements Extractor {
} }
boolean invisible = (simpleBlockTimecodeAndFlags[2] & 0x08) == 0x08; boolean invisible = (simpleBlockTimecodeAndFlags[2] & 0x08) == 0x08;
int lacing = (simpleBlockTimecodeAndFlags[2] & 0x06) >> 1; int lacing = (simpleBlockTimecodeAndFlags[2] & 0x06) >> 1;
if (lacing != LACING_NONE) {
// Validate lacing and set info into sample holder. throw new ParserException("Lacing mode " + lacing + " not supported");
switch (lacing) { }
case LACING_NONE: long elementEndOffsetBytes = elementOffsetBytes + headerSizeBytes + contentsSizeBytes;
long elementEndOffsetBytes = elementOffsetBytes + headerSizeBytes + contentsSizeBytes; simpleBlockTimecodeUs = clusterTimecodeUs + timecodeUs;
simpleBlockTimecodeUs = clusterTimecodeUs + timecodeUs; sampleHolder.flags = (keyframe ? C.SAMPLE_FLAG_SYNC : 0)
sampleHolder.flags = keyframe ? C.SAMPLE_FLAG_SYNC : 0; | (invisible ? C.SAMPLE_FLAG_DECODE_ONLY : 0);
sampleHolder.decodeOnly = invisible; sampleHolder.timeUs = clusterTimecodeUs + timecodeUs;
sampleHolder.timeUs = clusterTimecodeUs + timecodeUs; sampleHolder.size = (int) (elementEndOffsetBytes - reader.getBytesRead());
sampleHolder.size = (int) (elementEndOffsetBytes - reader.getBytesRead());
break; if (hasContentEncryption) {
case LACING_EBML: byte[] signalByte = new byte[1];
case LACING_FIXED: reader.readBytes(inputStream, signalByte, 1);
case LACING_XIPH: sampleHolder.size -= 1;
default: // First bit of the signalByte (extension bit) must be 0.
throw new ParserException("Lacing mode " + lacing + " not supported"); if ((signalByte[0] & 0x80) != 0) {
throw new ParserException("Extension bit is set in signal byte");
}
boolean isEncrypted = (signalByte[0] & 0x01) == 0x01;
if (isEncrypted) {
byte[] iv = null;
iv = sampleHolder.cryptoInfo.iv;
if (iv == null || iv.length != BLOCK_COUNTER_SIZE) {
iv = new byte[BLOCK_COUNTER_SIZE];
}
reader.readBytes(inputStream, iv, 8); // The container has only 8 bytes of IV.
sampleHolder.size -= 8;
int[] clearDataSizes = sampleHolder.cryptoInfo.numBytesOfClearData;
if (clearDataSizes == null || clearDataSizes.length < 1) {
clearDataSizes = new int[1];
}
int[] encryptedDataSizes = sampleHolder.cryptoInfo.numBytesOfEncryptedData;
if (encryptedDataSizes == null || encryptedDataSizes.length < 1) {
encryptedDataSizes = new int[1];
}
clearDataSizes[0] = 0;
encryptedDataSizes[0] = sampleHolder.size;
sampleHolder.cryptoInfo.set(1, clearDataSizes, encryptedDataSizes,
encryptionKeyId, iv, C.CRYPTO_MODE_AES_CTR);
sampleHolder.flags |= C.SAMPLE_FLAG_ENCRYPTED;
}
} }
if (sampleHolder.data == null || sampleHolder.data.capacity() < sampleHolder.size) { if (sampleHolder.data == null || sampleHolder.data.capacity() < sampleHolder.size) {
...@@ -437,6 +524,10 @@ public final class WebmExtractor implements Extractor { ...@@ -437,6 +524,10 @@ public final class WebmExtractor implements Extractor {
codecPrivate = new byte[contentsSizeBytes]; codecPrivate = new byte[contentsSizeBytes];
reader.readBytes(inputStream, codecPrivate, contentsSizeBytes); reader.readBytes(inputStream, codecPrivate, contentsSizeBytes);
break; break;
case ID_CONTENT_ENCRYPTION_KEY_ID:
encryptionKeyId = new byte[contentsSizeBytes];
reader.readBytes(inputStream, encryptionKeyId, contentsSizeBytes);
break;
default: default:
// pass // pass
} }
...@@ -463,8 +554,8 @@ public final class WebmExtractor implements Extractor { ...@@ -463,8 +554,8 @@ public final class WebmExtractor implements Extractor {
private void buildVideoFormat() throws ParserException { private void buildVideoFormat() throws ParserException {
if (pixelWidth != UNKNOWN && pixelHeight != UNKNOWN if (pixelWidth != UNKNOWN && pixelHeight != UNKNOWN
&& (format == null || format.width != pixelWidth || format.height != pixelHeight)) { && (format == null || format.width != pixelWidth || format.height != pixelHeight)) {
format = MediaFormat.createVideoFormat( format = MediaFormat.createVideoFormat(MimeTypes.VIDEO_VP9, MediaFormat.NO_VALUE, durationUs,
MimeTypes.VIDEO_VP9, MediaFormat.NO_VALUE, pixelWidth, pixelHeight, null); pixelWidth, pixelHeight, null);
readResults |= RESULT_READ_INIT; readResults |= RESULT_READ_INIT;
} else if (format == null) { } else if (format == null) {
throw new ParserException("Unable to build format"); throw new ParserException("Unable to build format");
...@@ -485,17 +576,15 @@ public final class WebmExtractor implements Extractor { ...@@ -485,17 +576,15 @@ public final class WebmExtractor implements Extractor {
&& (format == null || format.channelCount != channelCount && (format == null || format.channelCount != channelCount
|| format.sampleRate != sampleRate)) { || format.sampleRate != sampleRate)) {
if (CODEC_ID_VORBIS.equals(codecId)) { if (CODEC_ID_VORBIS.equals(codecId)) {
format = MediaFormat.createAudioFormat( format = MediaFormat.createAudioFormat(MimeTypes.AUDIO_VORBIS, VORBIS_MAX_INPUT_SIZE,
MimeTypes.AUDIO_VORBIS, VORBIS_MAX_INPUT_SIZE, durationUs, channelCount, sampleRate, parseVorbisCodecPrivate());
channelCount, sampleRate, parseVorbisCodecPrivate());
} else if (CODEC_ID_OPUS.equals(codecId)) { } else if (CODEC_ID_OPUS.equals(codecId)) {
ArrayList<byte[]> opusInitializationData = new ArrayList<byte[]>(3); ArrayList<byte[]> opusInitializationData = new ArrayList<byte[]>(3);
opusInitializationData.add(codecPrivate); opusInitializationData.add(codecPrivate);
opusInitializationData.add(ByteBuffer.allocate(Long.SIZE).putLong(codecDelayNs).array()); opusInitializationData.add(ByteBuffer.allocate(Long.SIZE).putLong(codecDelayNs).array());
opusInitializationData.add(ByteBuffer.allocate(Long.SIZE).putLong(seekPreRollNs).array()); opusInitializationData.add(ByteBuffer.allocate(Long.SIZE).putLong(seekPreRollNs).array());
format = MediaFormat.createAudioFormat( format = MediaFormat.createAudioFormat(MimeTypes.AUDIO_OPUS, OPUS_MAX_INPUT_SIZE,
MimeTypes.AUDIO_OPUS, OPUS_MAX_INPUT_SIZE, channelCount, sampleRate, durationUs, channelCount, sampleRate, opusInitializationData);
opusInitializationData);
} }
readResults |= RESULT_READ_INIT; readResults |= RESULT_READ_INIT;
} else if (format == null) { } else if (format == null) {
...@@ -512,7 +601,7 @@ public final class WebmExtractor implements Extractor { ...@@ -512,7 +601,7 @@ public final class WebmExtractor implements Extractor {
private void buildCues() throws ParserException { private void buildCues() throws ParserException {
if (segmentStartOffsetBytes == UNKNOWN) { if (segmentStartOffsetBytes == UNKNOWN) {
throw new ParserException("Segment start/end offsets unknown"); throw new ParserException("Segment start/end offsets unknown");
} else if (durationUs == UNKNOWN) { } else if (durationUs == C.UNKNOWN_TIME_US) {
throw new ParserException("Duration unknown"); throw new ParserException("Duration unknown");
} else if (cuesSizeBytes == UNKNOWN) { } else if (cuesSizeBytes == UNKNOWN) {
throw new ParserException("Cues size unknown"); throw new ParserException("Cues size unknown");
......
...@@ -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;
} }
} }
...@@ -581,7 +585,7 @@ public class DashChunkSource implements ChunkSource { ...@@ -581,7 +585,7 @@ public class DashChunkSource implements ChunkSource {
} }
if ((result & Extractor.RESULT_READ_INDEX) != 0) { if ((result & Extractor.RESULT_READ_INDEX) != 0) {
representationHolders.get(format.id).segmentIndex = representationHolders.get(format.id).segmentIndex =
new DashWrappingSegmentIndex(extractor.getIndex(), uri, indexAnchor); new DashWrappingSegmentIndex(extractor.getIndex(), uri.toString(), indexAnchor);
} }
} }
......
...@@ -19,8 +19,6 @@ import com.google.android.exoplayer.chunk.parser.SegmentIndex; ...@@ -19,8 +19,6 @@ import com.google.android.exoplayer.chunk.parser.SegmentIndex;
import com.google.android.exoplayer.dash.mpd.RangedUri; import com.google.android.exoplayer.dash.mpd.RangedUri;
import com.google.android.exoplayer.util.Util; import com.google.android.exoplayer.util.Util;
import android.net.Uri;
/** /**
* An implementation of {@link DashSegmentIndex} that wraps a {@link SegmentIndex} parsed from a * An implementation of {@link DashSegmentIndex} that wraps a {@link SegmentIndex} parsed from a
* media stream. * media stream.
...@@ -28,16 +26,16 @@ import android.net.Uri; ...@@ -28,16 +26,16 @@ import android.net.Uri;
public class DashWrappingSegmentIndex implements DashSegmentIndex { public class DashWrappingSegmentIndex implements DashSegmentIndex {
private final SegmentIndex segmentIndex; private final SegmentIndex segmentIndex;
private final Uri uri; private final String uri;
private final long indexAnchor; private final long indexAnchor;
/** /**
* @param segmentIndex The {@link SegmentIndex} to wrap. * @param segmentIndex The {@link SegmentIndex} to wrap.
* @param uri The {@link Uri} where the data is located. * @param uri The URI where the data is located.
* @param indexAnchor The index anchor point. This value is added to the byte offsets specified * @param indexAnchor The index anchor point. This value is added to the byte offsets specified
* in the wrapped {@link SegmentIndex}. * in the wrapped {@link SegmentIndex}.
*/ */
public DashWrappingSegmentIndex(SegmentIndex segmentIndex, Uri uri, long indexAnchor) { public DashWrappingSegmentIndex(SegmentIndex segmentIndex, String uri, long indexAnchor) {
this.segmentIndex = segmentIndex; this.segmentIndex = segmentIndex;
this.uri = uri; this.uri = uri;
this.indexAnchor = indexAnchor; this.indexAnchor = indexAnchor;
......
...@@ -15,6 +15,10 @@ ...@@ -15,6 +15,10 @@
*/ */
package com.google.android.exoplayer.dash.mpd; package com.google.android.exoplayer.dash.mpd;
import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.Util;
import java.util.Arrays;
import java.util.UUID; import java.util.UUID;
/** /**
...@@ -43,9 +47,38 @@ public class ContentProtection { ...@@ -43,9 +47,38 @@ public class ContentProtection {
* @param data Protection scheme specific initialization data. May be null. * @param data Protection scheme specific initialization data. May be null.
*/ */
public ContentProtection(String schemeUriId, UUID uuid, byte[] data) { public ContentProtection(String schemeUriId, UUID uuid, byte[] data) {
this.schemeUriId = schemeUriId; this.schemeUriId = Assertions.checkNotNull(schemeUriId);
this.uuid = uuid; this.uuid = uuid;
this.data = data; this.data = data;
} }
@Override
public boolean equals(Object obj) {
if (!(obj instanceof ContentProtection)) {
return false;
}
if (obj == this) {
return true;
}
ContentProtection other = (ContentProtection) obj;
return schemeUriId.equals(other.schemeUriId)
&& Util.areEqual(uuid, other.uuid)
&& Arrays.equals(data, other.data);
}
@Override
public int hashCode() {
int hashCode = 1;
hashCode = hashCode * 37 + schemeUriId.hashCode();
if (uuid != null) {
hashCode = hashCode * 37 + uuid.hashCode();
}
if (data != null) {
hashCode = hashCode * 37 + Arrays.hashCode(data);
}
return hashCode;
}
} }
...@@ -24,9 +24,9 @@ import com.google.android.exoplayer.dash.mpd.SegmentBase.SingleSegmentBase; ...@@ -24,9 +24,9 @@ import com.google.android.exoplayer.dash.mpd.SegmentBase.SingleSegmentBase;
import com.google.android.exoplayer.upstream.NetworkLoadable; import com.google.android.exoplayer.upstream.NetworkLoadable;
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.UriUtil;
import com.google.android.exoplayer.util.Util; import com.google.android.exoplayer.util.Util;
import android.net.Uri;
import android.text.TextUtils; import android.text.TextUtils;
import org.xml.sax.helpers.DefaultHandler; import org.xml.sax.helpers.DefaultHandler;
...@@ -38,6 +38,8 @@ import java.io.IOException; ...@@ -38,6 +38,8 @@ import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.text.ParseException; import java.text.ParseException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List; import java.util.List;
/** /**
...@@ -83,7 +85,7 @@ public class MediaPresentationDescriptionParser extends DefaultHandler ...@@ -83,7 +85,7 @@ public class MediaPresentationDescriptionParser extends DefaultHandler
throw new ParserException( throw new ParserException(
"inputStream does not contain a valid media presentation description"); "inputStream does not contain a valid media presentation description");
} }
return parseMediaPresentationDescription(xpp, Util.parseBaseUri(connectionUrl)); return parseMediaPresentationDescription(xpp, connectionUrl);
} catch (XmlPullParserException e) { } catch (XmlPullParserException e) {
throw new ParserException(e); throw new ParserException(e);
} catch (ParseException e) { } catch (ParseException e) {
...@@ -92,7 +94,7 @@ public class MediaPresentationDescriptionParser extends DefaultHandler ...@@ -92,7 +94,7 @@ public class MediaPresentationDescriptionParser extends DefaultHandler
} }
protected MediaPresentationDescription parseMediaPresentationDescription(XmlPullParser xpp, protected MediaPresentationDescription parseMediaPresentationDescription(XmlPullParser xpp,
Uri baseUrl) throws XmlPullParserException, IOException, ParseException { String baseUrl) throws XmlPullParserException, IOException, ParseException {
long availabilityStartTime = parseDateTime(xpp, "availabilityStartTime", -1); long availabilityStartTime = parseDateTime(xpp, "availabilityStartTime", -1);
long durationMs = parseDuration(xpp, "mediaPresentationDuration", -1); long durationMs = parseDuration(xpp, "mediaPresentationDuration", -1);
long minBufferTimeMs = parseDuration(xpp, "minBufferTime", -1); long minBufferTimeMs = parseDuration(xpp, "minBufferTime", -1);
...@@ -137,7 +139,7 @@ public class MediaPresentationDescriptionParser extends DefaultHandler ...@@ -137,7 +139,7 @@ public class MediaPresentationDescriptionParser extends DefaultHandler
return new UtcTimingElement(schemeIdUri, value); return new UtcTimingElement(schemeIdUri, value);
} }
protected Period parsePeriod(XmlPullParser xpp, Uri baseUrl, long mpdDurationMs) protected Period parsePeriod(XmlPullParser xpp, String baseUrl, long mpdDurationMs)
throws XmlPullParserException, IOException { throws XmlPullParserException, IOException {
String id = xpp.getAttributeValue(null, "id"); String id = xpp.getAttributeValue(null, "id");
long startMs = parseDuration(xpp, "start", 0); long startMs = parseDuration(xpp, "start", 0);
...@@ -170,7 +172,7 @@ public class MediaPresentationDescriptionParser extends DefaultHandler ...@@ -170,7 +172,7 @@ public class MediaPresentationDescriptionParser extends DefaultHandler
// AdaptationSet parsing. // AdaptationSet parsing.
protected AdaptationSet parseAdaptationSet(XmlPullParser xpp, Uri baseUrl, long periodStartMs, protected AdaptationSet parseAdaptationSet(XmlPullParser xpp, String baseUrl, long periodStartMs,
long periodDurationMs, SegmentBase segmentBase) throws XmlPullParserException, IOException { long periodDurationMs, SegmentBase segmentBase) throws XmlPullParserException, IOException {
String mimeType = xpp.getAttributeValue(null, "mimeType"); String mimeType = xpp.getAttributeValue(null, "mimeType");
...@@ -178,24 +180,22 @@ public class MediaPresentationDescriptionParser extends DefaultHandler ...@@ -178,24 +180,22 @@ public class MediaPresentationDescriptionParser extends DefaultHandler
int contentType = parseAdaptationSetTypeFromMimeType(mimeType); int contentType = parseAdaptationSetTypeFromMimeType(mimeType);
int id = -1; int id = -1;
List<ContentProtection> contentProtections = null; ContentProtectionsBuilder contentProtectionsBuilder = new ContentProtectionsBuilder();
List<Representation> representations = new ArrayList<Representation>(); List<Representation> representations = new ArrayList<Representation>();
do { do {
xpp.next(); xpp.next();
if (isStartTag(xpp, "BaseURL")) { if (isStartTag(xpp, "BaseURL")) {
baseUrl = parseBaseUrl(xpp, baseUrl); baseUrl = parseBaseUrl(xpp, baseUrl);
} else if (isStartTag(xpp, "ContentProtection")) { } else if (isStartTag(xpp, "ContentProtection")) {
if (contentProtections == null) { contentProtectionsBuilder.addAdaptationSetProtection(parseContentProtection(xpp));
contentProtections = new ArrayList<ContentProtection>();
}
contentProtections.add(parseContentProtection(xpp));
} else if (isStartTag(xpp, "ContentComponent")) { } else if (isStartTag(xpp, "ContentComponent")) {
id = Integer.parseInt(xpp.getAttributeValue(null, "id")); id = Integer.parseInt(xpp.getAttributeValue(null, "id"));
contentType = checkAdaptationSetTypeConsistency(contentType, contentType = checkAdaptationSetTypeConsistency(contentType,
parseAdaptationSetType(xpp.getAttributeValue(null, "contentType"))); parseAdaptationSetType(xpp.getAttributeValue(null, "contentType")));
} else if (isStartTag(xpp, "Representation")) { } else if (isStartTag(xpp, "Representation")) {
Representation representation = parseRepresentation(xpp, baseUrl, periodStartMs, Representation representation = parseRepresentation(xpp, baseUrl, periodStartMs,
periodDurationMs, mimeType, language, segmentBase); periodDurationMs, mimeType, language, segmentBase, contentProtectionsBuilder);
contentProtectionsBuilder.endRepresentation();
contentType = checkAdaptationSetTypeConsistency(contentType, contentType = checkAdaptationSetTypeConsistency(contentType,
parseAdaptationSetTypeFromMimeType(representation.format.mimeType)); parseAdaptationSetTypeFromMimeType(representation.format.mimeType));
representations.add(representation); representations.add(representation);
...@@ -211,7 +211,7 @@ public class MediaPresentationDescriptionParser extends DefaultHandler ...@@ -211,7 +211,7 @@ public class MediaPresentationDescriptionParser extends DefaultHandler
} }
} while (!isEndTag(xpp, "AdaptationSet")); } while (!isEndTag(xpp, "AdaptationSet"));
return buildAdaptationSet(id, contentType, representations, contentProtections); return buildAdaptationSet(id, contentType, representations, contentProtectionsBuilder.build());
} }
protected AdaptationSet buildAdaptationSet(int id, int contentType, protected AdaptationSet buildAdaptationSet(int id, int contentType,
...@@ -287,8 +287,9 @@ public class MediaPresentationDescriptionParser extends DefaultHandler ...@@ -287,8 +287,9 @@ public class MediaPresentationDescriptionParser extends DefaultHandler
// Representation parsing. // Representation parsing.
protected Representation parseRepresentation(XmlPullParser xpp, Uri baseUrl, long periodStartMs, protected Representation parseRepresentation(XmlPullParser xpp, String baseUrl,
long periodDurationMs, String mimeType, String language, SegmentBase segmentBase) long periodStartMs, long periodDurationMs, String mimeType, String language,
SegmentBase segmentBase, ContentProtectionsBuilder contentProtectionsBuilder)
throws XmlPullParserException, IOException { throws XmlPullParserException, IOException {
String id = xpp.getAttributeValue(null, "id"); String id = xpp.getAttributeValue(null, "id");
int bandwidth = parseInt(xpp, "bandwidth"); int bandwidth = parseInt(xpp, "bandwidth");
...@@ -312,6 +313,8 @@ public class MediaPresentationDescriptionParser extends DefaultHandler ...@@ -312,6 +313,8 @@ public class MediaPresentationDescriptionParser extends DefaultHandler
} else if (isStartTag(xpp, "SegmentTemplate")) { } else if (isStartTag(xpp, "SegmentTemplate")) {
segmentBase = parseSegmentTemplate(xpp, baseUrl, (SegmentTemplate) segmentBase, segmentBase = parseSegmentTemplate(xpp, baseUrl, (SegmentTemplate) segmentBase,
periodDurationMs); periodDurationMs);
} else if (isStartTag(xpp, "ContentProtection")) {
contentProtectionsBuilder.addRepresentationProtection(parseContentProtection(xpp));
} }
} while (!isEndTag(xpp, "Representation")); } while (!isEndTag(xpp, "Representation"));
...@@ -335,7 +338,7 @@ public class MediaPresentationDescriptionParser extends DefaultHandler ...@@ -335,7 +338,7 @@ public class MediaPresentationDescriptionParser extends DefaultHandler
// SegmentBase, SegmentList and SegmentTemplate parsing. // SegmentBase, SegmentList and SegmentTemplate parsing.
protected SingleSegmentBase parseSegmentBase(XmlPullParser xpp, Uri baseUrl, protected SingleSegmentBase parseSegmentBase(XmlPullParser xpp, String baseUrl,
SingleSegmentBase parent) throws XmlPullParserException, IOException { SingleSegmentBase parent) throws XmlPullParserException, IOException {
long timescale = parseLong(xpp, "timescale", parent != null ? parent.timescale : 1); long timescale = parseLong(xpp, "timescale", parent != null ? parent.timescale : 1);
...@@ -364,12 +367,12 @@ public class MediaPresentationDescriptionParser extends DefaultHandler ...@@ -364,12 +367,12 @@ public class MediaPresentationDescriptionParser extends DefaultHandler
} }
protected SingleSegmentBase buildSingleSegmentBase(RangedUri initialization, long timescale, protected SingleSegmentBase buildSingleSegmentBase(RangedUri initialization, long timescale,
long presentationTimeOffset, Uri baseUrl, long indexStart, long indexLength) { long presentationTimeOffset, String baseUrl, long indexStart, long indexLength) {
return new SingleSegmentBase(initialization, timescale, presentationTimeOffset, baseUrl, return new SingleSegmentBase(initialization, timescale, presentationTimeOffset, baseUrl,
indexStart, indexLength); indexStart, indexLength);
} }
protected SegmentList parseSegmentList(XmlPullParser xpp, Uri baseUrl, SegmentList parent, protected SegmentList parseSegmentList(XmlPullParser xpp, String baseUrl, SegmentList parent,
long periodDurationMs) throws XmlPullParserException, IOException { long periodDurationMs) throws XmlPullParserException, IOException {
long timescale = parseLong(xpp, "timescale", parent != null ? parent.timescale : 1); long timescale = parseLong(xpp, "timescale", parent != null ? parent.timescale : 1);
...@@ -413,7 +416,7 @@ public class MediaPresentationDescriptionParser extends DefaultHandler ...@@ -413,7 +416,7 @@ public class MediaPresentationDescriptionParser extends DefaultHandler
startNumber, duration, timeline, segments); startNumber, duration, timeline, segments);
} }
protected SegmentTemplate parseSegmentTemplate(XmlPullParser xpp, Uri baseUrl, protected SegmentTemplate parseSegmentTemplate(XmlPullParser xpp, String baseUrl,
SegmentTemplate parent, long periodDurationMs) throws XmlPullParserException, IOException { SegmentTemplate parent, long periodDurationMs) throws XmlPullParserException, IOException {
long timescale = parseLong(xpp, "timescale", parent != null ? parent.timescale : 1); long timescale = parseLong(xpp, "timescale", parent != null ? parent.timescale : 1);
...@@ -450,7 +453,7 @@ public class MediaPresentationDescriptionParser extends DefaultHandler ...@@ -450,7 +453,7 @@ public class MediaPresentationDescriptionParser extends DefaultHandler
protected SegmentTemplate buildSegmentTemplate(RangedUri initialization, long timescale, protected SegmentTemplate buildSegmentTemplate(RangedUri initialization, long timescale,
long presentationTimeOffset, long periodDurationMs, int startNumber, long duration, long presentationTimeOffset, long periodDurationMs, int startNumber, long duration,
List<SegmentTimelineElement> timeline, UrlTemplate initializationTemplate, List<SegmentTimelineElement> timeline, UrlTemplate initializationTemplate,
UrlTemplate mediaTemplate, Uri baseUrl) { UrlTemplate mediaTemplate, String baseUrl) {
return new SegmentTemplate(initialization, timescale, presentationTimeOffset, periodDurationMs, return new SegmentTemplate(initialization, timescale, presentationTimeOffset, periodDurationMs,
startNumber, duration, timeline, initializationTemplate, mediaTemplate, baseUrl); startNumber, duration, timeline, initializationTemplate, mediaTemplate, baseUrl);
} }
...@@ -487,15 +490,15 @@ public class MediaPresentationDescriptionParser extends DefaultHandler ...@@ -487,15 +490,15 @@ public class MediaPresentationDescriptionParser extends DefaultHandler
return defaultValue; return defaultValue;
} }
protected RangedUri parseInitialization(XmlPullParser xpp, Uri baseUrl) { protected RangedUri parseInitialization(XmlPullParser xpp, String baseUrl) {
return parseRangedUrl(xpp, baseUrl, "sourceURL", "range"); return parseRangedUrl(xpp, baseUrl, "sourceURL", "range");
} }
protected RangedUri parseSegmentUrl(XmlPullParser xpp, Uri baseUrl) { protected RangedUri parseSegmentUrl(XmlPullParser xpp, String baseUrl) {
return parseRangedUrl(xpp, baseUrl, "media", "mediaRange"); return parseRangedUrl(xpp, baseUrl, "media", "mediaRange");
} }
protected RangedUri parseRangedUrl(XmlPullParser xpp, Uri baseUrl, String urlAttribute, protected RangedUri parseRangedUrl(XmlPullParser xpp, String baseUrl, String urlAttribute,
String rangeAttribute) { String rangeAttribute) {
String urlText = xpp.getAttributeValue(null, urlAttribute); String urlText = xpp.getAttributeValue(null, urlAttribute);
long rangeStart = 0; long rangeStart = 0;
...@@ -509,7 +512,7 @@ public class MediaPresentationDescriptionParser extends DefaultHandler ...@@ -509,7 +512,7 @@ public class MediaPresentationDescriptionParser extends DefaultHandler
return buildRangedUri(baseUrl, urlText, rangeStart, rangeLength); return buildRangedUri(baseUrl, urlText, rangeStart, rangeLength);
} }
protected RangedUri buildRangedUri(Uri baseUrl, String urlText, long rangeStart, protected RangedUri buildRangedUri(String baseUrl, String urlText, long rangeStart,
long rangeLength) { long rangeLength) {
return new RangedUri(baseUrl, urlText, rangeStart, rangeLength); return new RangedUri(baseUrl, urlText, rangeStart, rangeLength);
} }
...@@ -548,15 +551,10 @@ public class MediaPresentationDescriptionParser extends DefaultHandler ...@@ -548,15 +551,10 @@ public class MediaPresentationDescriptionParser extends DefaultHandler
} }
} }
protected static Uri parseBaseUrl(XmlPullParser xpp, Uri parentBaseUrl) protected static String parseBaseUrl(XmlPullParser xpp, String parentBaseUrl)
throws XmlPullParserException, IOException { throws XmlPullParserException, IOException {
xpp.next(); xpp.next();
String newBaseUrlText = xpp.getText(); return UriUtil.resolve(parentBaseUrl, xpp.getText());
Uri newBaseUri = Uri.parse(newBaseUrlText);
if (!newBaseUri.isAbsolute()) {
newBaseUri = Uri.withAppendedPath(parentBaseUrl, newBaseUrlText);
}
return newBaseUri;
} }
protected static int parseInt(XmlPullParser xpp, String name) { protected static int parseInt(XmlPullParser xpp, String name) {
...@@ -582,4 +580,120 @@ public class MediaPresentationDescriptionParser extends DefaultHandler ...@@ -582,4 +580,120 @@ public class MediaPresentationDescriptionParser extends DefaultHandler
return value == null ? defaultValue : value; return value == null ? defaultValue : value;
} }
/**
* Builds a list of {@link ContentProtection} elements for an {@link AdaptationSet}.
* <p>
* If child Representation elements contain ContentProtection elements, then it is required that
* they all define the same ones. If they do, the ContentProtection elements are bubbled up to the
* AdaptationSet. Child Representation elements defining different ContentProtection elements is
* considered an error.
*/
protected static final class ContentProtectionsBuilder implements Comparator<ContentProtection> {
private ArrayList<ContentProtection> adaptationSetProtections;
private ArrayList<ContentProtection> representationProtections;
private ArrayList<ContentProtection> currentRepresentationProtections;
private boolean representationProtectionsSet;
/**
* Adds a {@link ContentProtection} found in the AdaptationSet element.
*
* @param contentProtection The {@link ContentProtection} to add.
*/
public void addAdaptationSetProtection(ContentProtection contentProtection) {
if (adaptationSetProtections == null) {
adaptationSetProtections = new ArrayList<ContentProtection>();
}
maybeAddContentProtection(adaptationSetProtections, contentProtection);
}
/**
* Adds a {@link ContentProtection} found in a child Representation element.
*
* @param contentProtection The {@link ContentProtection} to add.
*/
public void addRepresentationProtection(ContentProtection contentProtection) {
if (currentRepresentationProtections == null) {
currentRepresentationProtections = new ArrayList<ContentProtection>();
}
maybeAddContentProtection(currentRepresentationProtections, contentProtection);
}
/**
* Should be invoked after processing each child Representation element, in order to apply
* consistency checks.
*/
public void endRepresentation() {
if (!representationProtectionsSet) {
if (currentRepresentationProtections != null) {
Collections.sort(currentRepresentationProtections, this);
}
representationProtections = currentRepresentationProtections;
representationProtectionsSet = true;
} else {
// Assert that each Representation element defines the same ContentProtection elements.
if (currentRepresentationProtections == null) {
Assertions.checkState(representationProtections == null);
} else {
Collections.sort(currentRepresentationProtections, this);
Assertions.checkState(currentRepresentationProtections.equals(representationProtections));
}
}
currentRepresentationProtections = null;
}
/**
* Returns the final list of consistent {@link ContentProtection} elements.
*/
public ArrayList<ContentProtection> build() {
if (adaptationSetProtections == null) {
return representationProtections;
} else if (representationProtections == null) {
return adaptationSetProtections;
} else {
// Bubble up ContentProtection elements found in the child Representation elements.
for (int i = 0; i < representationProtections.size(); i++) {
maybeAddContentProtection(adaptationSetProtections, representationProtections.get(i));
}
return adaptationSetProtections;
}
}
/**
* Checks a ContentProtection for consistency with the given list, adding it if necessary.
* <ul>
* <li>If the new ContentProtection matches another in the list, it's consistent and is not
* added to the list.
* <li>If the new ContentProtection has the same schemeUriId as another ContentProtection in the
* list, but its other attributes do not match, then it's inconsistent and an
* {@link IllegalStateException} is thrown.
* <li>Else the new ContentProtection has a unique schemeUriId, it's consistent and is added.
* </ul>
*
* @param contentProtections The list of ContentProtection elements currently known.
* @param contentProtection The ContentProtection to add.
*/
private void maybeAddContentProtection(List<ContentProtection> contentProtections,
ContentProtection contentProtection) {
if (!contentProtections.contains(contentProtection)) {
for (int i = 0; i < contentProtections.size(); i++) {
// If contains returned false (no complete match), but find a matching schemeUriId, then
// the MPD contains inconsistent ContentProtection data.
Assertions.checkState(
!contentProtections.get(i).schemeUriId.equals(contentProtection.schemeUriId));
}
contentProtections.add(contentProtection);
}
}
// Comparator implementation.
@Override
public int compare(ContentProtection first, ContentProtection second) {
return first.schemeUriId.compareTo(second.schemeUriId);
}
}
} }
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
package com.google.android.exoplayer.dash.mpd; package com.google.android.exoplayer.dash.mpd;
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.UriUtil;
import android.net.Uri; import android.net.Uri;
...@@ -35,31 +35,28 @@ public final class RangedUri { ...@@ -35,31 +35,28 @@ public final class RangedUri {
*/ */
public final long length; public final long length;
// The {@link Uri} is stored internally in two parts, {@link #baseUri} and {@link uriString}. // The URI is stored internally in two parts: reference URI and a base URI to use when
// This helps optimize memory usage in the same way that DASH manifests allow many URLs to be // resolving it. This helps optimize memory usage in the same way that DASH manifests allow many
// expressed concisely in the form of a single BaseURL and many relative paths. Note that this // URLs to be expressed concisely in the form of a single BaseURL and many relative paths. Note
// optimization relies on the same {@code Uri} being passed as the {@link #baseUri} to many // that this optimization relies on the same object being passed as the base URI to many
// instances of this class. // instances of this class.
private final Uri baseUri; private final String baseUri;
private final String stringUri; private final String referenceUri;
private int hashCode; private int hashCode;
/** /**
* Constructs an ranged uri. * Constructs an ranged uri.
* <p>
* See {@link Util#getMergedUri(Uri, String)} for a description of how {@code baseUri} and
* {@code stringUri} are merged.
* *
* @param baseUri A uri that can form the base of the uri defined by the instance. * @param baseUri A uri that can form the base of the uri defined by the instance.
* @param stringUri A relative or absolute uri in string form. * @param referenceUri A reference uri that should be resolved with respect to {@code baseUri}.
* @param start The (zero based) index of the first byte of the range. * @param start The (zero based) index of the first byte of the range.
* @param length The length of the range, or -1 to indicate that the range is unbounded. * @param length The length of the range, or -1 to indicate that the range is unbounded.
*/ */
public RangedUri(Uri baseUri, String stringUri, long start, long length) { public RangedUri(String baseUri, String referenceUri, long start, long length) {
Assertions.checkArgument(baseUri != null || stringUri != null); Assertions.checkArgument(baseUri != null || referenceUri != null);
this.baseUri = baseUri; this.baseUri = baseUri;
this.stringUri = stringUri; this.referenceUri = referenceUri;
this.start = start; this.start = start;
this.length = length; this.length = length;
} }
...@@ -70,7 +67,16 @@ public final class RangedUri { ...@@ -70,7 +67,16 @@ public final class RangedUri {
* @return The {@link Uri} represented by the instance. * @return The {@link Uri} represented by the instance.
*/ */
public Uri getUri() { public Uri getUri() {
return Util.getMergedUri(baseUri, stringUri); return UriUtil.resolveToUri(baseUri, referenceUri);
}
/**
* Returns the uri represented by the instance as a string.
*
* @return The uri represented by the instance.
*/
public String getUriString() {
return UriUtil.resolve(baseUri, referenceUri);
} }
/** /**
...@@ -85,13 +91,13 @@ public final class RangedUri { ...@@ -85,13 +91,13 @@ public final class RangedUri {
* @return The merged {@link RangedUri} if the merge was successful. Null otherwise. * @return The merged {@link RangedUri} if the merge was successful. Null otherwise.
*/ */
public RangedUri attemptMerge(RangedUri other) { public RangedUri attemptMerge(RangedUri other) {
if (other == null || !getUri().equals(other.getUri())) { if (other == null || !getUriString().equals(other.getUriString())) {
return null; return null;
} else if (length != -1 && start + length == other.start) { } else if (length != -1 && start + length == other.start) {
return new RangedUri(baseUri, stringUri, start, return new RangedUri(baseUri, referenceUri, start,
other.length == -1 ? -1 : length + other.length); other.length == -1 ? -1 : length + other.length);
} else if (other.length != -1 && other.start + other.length == start) { } else if (other.length != -1 && other.start + other.length == start) {
return new RangedUri(baseUri, stringUri, other.start, return new RangedUri(baseUri, referenceUri, other.start,
length == -1 ? -1 : other.length + length); length == -1 ? -1 : other.length + length);
} else { } else {
return null; return null;
...@@ -104,7 +110,7 @@ public final class RangedUri { ...@@ -104,7 +110,7 @@ public final class RangedUri {
int result = 17; int result = 17;
result = 31 * result + (int) start; result = 31 * result + (int) start;
result = 31 * result + (int) length; result = 31 * result + (int) length;
result = 31 * result + getUri().hashCode(); result = 31 * result + getUriString().hashCode();
hashCode = result; hashCode = result;
} }
return hashCode; return hashCode;
...@@ -121,7 +127,7 @@ public final class RangedUri { ...@@ -121,7 +127,7 @@ public final class RangedUri {
RangedUri other = (RangedUri) obj; RangedUri other = (RangedUri) obj;
return this.start == other.start return this.start == other.start
&& this.length == other.length && this.length == other.length
&& getUri().equals(other.getUri()); && getUriString().equals(other.getUriString());
} }
} }
...@@ -147,7 +147,7 @@ public abstract class Representation { ...@@ -147,7 +147,7 @@ public abstract class Representation {
public static class SingleSegmentRepresentation extends Representation { public static class SingleSegmentRepresentation extends Representation {
/** /**
* The {@link Uri} of the single segment. * The uri of the single segment.
*/ */
public final Uri uri; public final Uri uri;
...@@ -174,7 +174,7 @@ public abstract class Representation { ...@@ -174,7 +174,7 @@ public abstract class Representation {
* @param contentLength The content length, or -1 if unknown. * @param contentLength The content length, or -1 if unknown.
*/ */
public static SingleSegmentRepresentation newInstance(long periodStartMs, long periodDurationMs, public static SingleSegmentRepresentation newInstance(long periodStartMs, long periodDurationMs,
String contentId, long revisionId, Format format, Uri uri, long initializationStart, String contentId, long revisionId, Format format, String uri, long initializationStart,
long initializationEnd, long indexStart, long indexEnd, long contentLength) { long initializationEnd, long indexStart, long indexEnd, long contentLength) {
RangedUri rangedUri = new RangedUri(uri, null, initializationStart, RangedUri rangedUri = new RangedUri(uri, null, initializationStart,
initializationEnd - initializationStart + 1); initializationEnd - initializationStart + 1);
...@@ -197,13 +197,13 @@ public abstract class Representation { ...@@ -197,13 +197,13 @@ public abstract class Representation {
public SingleSegmentRepresentation(long periodStartMs, long periodDurationMs, String contentId, public SingleSegmentRepresentation(long periodStartMs, long periodDurationMs, String contentId,
long revisionId, Format format, SingleSegmentBase segmentBase, long contentLength) { long revisionId, Format format, SingleSegmentBase segmentBase, long contentLength) {
super(periodStartMs, periodDurationMs, contentId, revisionId, format, segmentBase); super(periodStartMs, periodDurationMs, contentId, revisionId, format, segmentBase);
this.uri = segmentBase.uri; this.uri = Uri.parse(segmentBase.uri);
this.indexUri = segmentBase.getIndex(); this.indexUri = segmentBase.getIndex();
this.contentLength = contentLength; this.contentLength = contentLength;
// If we have an index uri then the index is defined externally, and we shouldn't return one // If we have an index uri then the index is defined externally, and we shouldn't return one
// directly. If we don't, then we can't do better than an index defining a single segment. // directly. If we don't, then we can't do better than an index defining a single segment.
segmentIndex = indexUri != null ? null : new DashSingleSegmentIndex(periodStartMs * 1000, segmentIndex = indexUri != null ? null : new DashSingleSegmentIndex(periodStartMs * 1000,
periodDurationMs * 1000, new RangedUri(uri, null, 0, -1)); periodDurationMs * 1000, new RangedUri(segmentBase.uri, null, 0, -1));
} }
@Override @Override
......
...@@ -19,8 +19,6 @@ import com.google.android.exoplayer.C; ...@@ -19,8 +19,6 @@ import com.google.android.exoplayer.C;
import com.google.android.exoplayer.dash.DashSegmentIndex; import com.google.android.exoplayer.dash.DashSegmentIndex;
import com.google.android.exoplayer.util.Util; import com.google.android.exoplayer.util.Util;
import android.net.Uri;
import java.util.List; import java.util.List;
/** /**
...@@ -73,7 +71,7 @@ public abstract class SegmentBase { ...@@ -73,7 +71,7 @@ public abstract class SegmentBase {
/** /**
* The uri of the segment. * The uri of the segment.
*/ */
public final Uri uri; public final String uri;
/* package */ final long indexStart; /* package */ final long indexStart;
/* package */ final long indexLength; /* package */ final long indexLength;
...@@ -89,7 +87,7 @@ public abstract class SegmentBase { ...@@ -89,7 +87,7 @@ public abstract class SegmentBase {
* @param indexLength The length of the index data in bytes. * @param indexLength The length of the index data in bytes.
*/ */
public SingleSegmentBase(RangedUri initialization, long timescale, long presentationTimeOffset, public SingleSegmentBase(RangedUri initialization, long timescale, long presentationTimeOffset,
Uri uri, long indexStart, long indexLength) { String uri, long indexStart, long indexLength) {
super(initialization, timescale, presentationTimeOffset); super(initialization, timescale, presentationTimeOffset);
this.uri = uri; this.uri = uri;
this.indexStart = indexStart; this.indexStart = indexStart;
...@@ -99,7 +97,7 @@ public abstract class SegmentBase { ...@@ -99,7 +97,7 @@ public abstract class SegmentBase {
/** /**
* @param uri The uri of the segment. * @param uri The uri of the segment.
*/ */
public SingleSegmentBase(Uri uri) { public SingleSegmentBase(String uri) {
this(null, 1, 0, uri, 0, -1); this(null, 1, 0, uri, 0, -1);
} }
...@@ -289,7 +287,7 @@ public abstract class SegmentBase { ...@@ -289,7 +287,7 @@ public abstract class SegmentBase {
/* package */ final UrlTemplate initializationTemplate; /* package */ final UrlTemplate initializationTemplate;
/* package */ final UrlTemplate mediaTemplate; /* package */ final UrlTemplate mediaTemplate;
private final Uri baseUrl; private final String baseUrl;
/** /**
* @param initialization A {@link RangedUri} corresponding to initialization data, if such data * @param initialization A {@link RangedUri} corresponding to initialization data, if such data
...@@ -315,7 +313,7 @@ public abstract class SegmentBase { ...@@ -315,7 +313,7 @@ public abstract class SegmentBase {
public SegmentTemplate(RangedUri initialization, long timescale, long presentationTimeOffset, public SegmentTemplate(RangedUri initialization, long timescale, long presentationTimeOffset,
long periodDurationMs, int startNumber, long duration, long periodDurationMs, int startNumber, long duration,
List<SegmentTimelineElement> segmentTimeline, UrlTemplate initializationTemplate, List<SegmentTimelineElement> segmentTimeline, UrlTemplate initializationTemplate,
UrlTemplate mediaTemplate, Uri baseUrl) { UrlTemplate mediaTemplate, String baseUrl) {
super(initialization, timescale, presentationTimeOffset, periodDurationMs, startNumber, super(initialization, timescale, presentationTimeOffset, periodDurationMs, startNumber,
duration, segmentTimeline); duration, segmentTimeline);
this.initializationTemplate = initializationTemplate; this.initializationTemplate = initializationTemplate;
......
...@@ -32,6 +32,7 @@ import java.io.InputStreamReader; ...@@ -32,6 +32,7 @@ import java.io.InputStreamReader;
import java.text.ParseException; import java.text.ParseException;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.Locale; import java.util.Locale;
import java.util.TimeZone;
import java.util.concurrent.CancellationException; import java.util.concurrent.CancellationException;
/** /**
...@@ -173,6 +174,7 @@ public class UtcTimingElementResolver implements Loader.Callback { ...@@ -173,6 +174,7 @@ public class UtcTimingElementResolver implements Loader.Callback {
try { try {
// TODO: It may be necessary to handle timestamp offsets from UTC. // TODO: It may be necessary to handle timestamp offsets from UTC.
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US); SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US);
format.setTimeZone(TimeZone.getTimeZone("UTC"));
return format.parse(firstLine).getTime(); return format.parse(firstLine).getTime();
} catch (ParseException e) { } catch (ParseException e) {
throw new ParserException(e); throw new ParserException(e);
......
/*
* 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.
......
/*
* 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;
/**
* Thrown when the drm keys loaded into an open session expire.
*/
public final class KeysExpiredException extends Exception {
}
...@@ -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;
/** /**
...@@ -82,15 +81,6 @@ public class StreamingDrmSessionManager implements DrmSessionManager { ...@@ -82,15 +81,6 @@ public class StreamingDrmSessionManager implements DrmSessionManager {
private byte[] sessionId; private byte[] sessionId;
/** /**
* @deprecated Use the other constructor, passing null as {@code optionalKeyRequestParameters}.
*/
@Deprecated
public StreamingDrmSessionManager(UUID uuid, Looper playbackLooper, MediaDrmCallback callback,
Handler eventHandler, EventListener eventListener) throws UnsupportedSchemeException {
this(uuid, playbackLooper, callback, null, eventHandler, eventListener);
}
/**
* @param uuid The UUID of the drm scheme. * @param uuid The UUID of the drm scheme.
* @param playbackLooper The looper associated with the media playback thread. Should usually be * @param playbackLooper The looper associated with the media playback thread. Should usually be
* obtained using {@link com.google.android.exoplayer.ExoPlayer#getPlaybackLooper()}. * obtained using {@link com.google.android.exoplayer.ExoPlayer#getPlaybackLooper()}.
...@@ -168,7 +158,7 @@ public class StreamingDrmSessionManager implements DrmSessionManager { ...@@ -168,7 +158,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 +168,8 @@ public class StreamingDrmSessionManager implements DrmSessionManager { ...@@ -178,8 +168,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;
...@@ -332,7 +322,7 @@ public class StreamingDrmSessionManager implements DrmSessionManager { ...@@ -332,7 +322,7 @@ public class StreamingDrmSessionManager implements DrmSessionManager {
return; return;
case MediaDrm.EVENT_KEY_EXPIRED: case MediaDrm.EVENT_KEY_EXPIRED:
state = STATE_OPENED; state = STATE_OPENED;
postKeyRequest(); onError(new KeysExpiredException());
return; return;
case MediaDrm.EVENT_PROVISION_REQUIRED: case MediaDrm.EVENT_PROVISION_REQUIRED:
state = STATE_OPENED; state = STATE_OPENED;
......
/*
* 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.extractor;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.upstream.DataSource;
import java.io.EOFException;
import java.io.IOException;
/**
* An {@link ExtractorInput} that wraps a {@link DataSource}.
*/
public final class DefaultExtractorInput implements ExtractorInput {
private static final byte[] SCRATCH_SPACE = new byte[4096];
private final DataSource dataSource;
private long position;
private long length;
/**
* @param dataSource The wrapped {@link DataSource}.
* @param position The initial position in the stream.
* @param length The length of the stream, or {@link C#LENGTH_UNBOUNDED} if it is unknown.
*/
public DefaultExtractorInput(DataSource dataSource, long position, long length) {
this.dataSource = dataSource;
this.position = position;
this.length = length;
}
@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 == C.RESULT_END_OF_INPUT) {
return C.RESULT_END_OF_INPUT;
}
position += bytesRead;
return bytesRead;
}
@Override
public boolean readFully(byte[] target, int offset, int length, boolean allowEndOfInput)
throws IOException, InterruptedException {
int remaining = length;
while (remaining > 0) {
if (Thread.interrupted()) {
throw new InterruptedException();
}
int bytesRead = dataSource.read(target, offset, remaining);
if (bytesRead == C.RESULT_END_OF_INPUT) {
if (allowEndOfInput && remaining == length) {
return false;
}
throw new EOFException();
}
offset += bytesRead;
remaining -= bytesRead;
}
position += length;
return true;
}
@Override
public void readFully(byte[] target, int offset, int length)
throws IOException, InterruptedException {
readFully(target, offset, length, false);
}
@Override
public void 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 == C.RESULT_END_OF_INPUT) {
throw new EOFException();
}
remaining -= bytesRead;
}
position += length;
}
@Override
public long getPosition() {
return position;
}
@Override
public long getLength() {
return length;
}
}
...@@ -13,20 +13,21 @@ ...@@ -13,20 +13,21 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package com.google.android.exoplayer.hls.parser; package com.google.android.exoplayer.extractor;
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.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 * A {@link TrackOutput} that buffers extracted samples in a queue, and allows for consumption from
* the first sample returned from the queue is a keyframe, allowing splicing to another queue, and * that queue.
* so on.
*/ */
/* package */ abstract class SampleQueue { public final class DefaultTrackOutput implements TrackOutput {
private final RollingSampleBuffer rollingBuffer; private final RollingSampleBuffer rollingBuffer;
private final SampleHolder sampleInfoHolder; private final SampleHolder sampleInfoHolder;
...@@ -36,14 +37,11 @@ import com.google.android.exoplayer.util.ParsableByteArray; ...@@ -36,14 +37,11 @@ import com.google.android.exoplayer.util.ParsableByteArray;
private long lastReadTimeUs; private long lastReadTimeUs;
private long spliceOutTimeUs; private long spliceOutTimeUs;
// Accessed only by the loading thread.
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 DefaultTrackOutput(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;
...@@ -52,24 +50,60 @@ import com.google.android.exoplayer.util.ParsableByteArray; ...@@ -52,24 +50,60 @@ import com.google.android.exoplayer.util.ParsableByteArray;
largestParsedTimestampUs = Long.MIN_VALUE; largestParsedTimestampUs = Long.MIN_VALUE;
} }
public void release() { // Called by the consuming thread, but only when there is no loading thread.
rollingBuffer.release();
/**
* Clears the queue, returning all allocations to the allocator.
*/
public void clear() {
rollingBuffer.clear();
needKeyframe = true;
lastReadTimeUs = Long.MIN_VALUE;
spliceOutTimeUs = Long.MIN_VALUE;
largestParsedTimestampUs = Long.MIN_VALUE;
}
/**
* Returns the current absolute write index.
*/
public int getWriteIndex() {
return rollingBuffer.getWriteIndex();
} }
// Called by the consuming thread. // Called by the consuming thread.
public long getLargestParsedTimestampUs() { /**
return largestParsedTimestampUs; * Returns the current absolute read index.
*/
public int getReadIndex() {
return rollingBuffer.getReadIndex();
} }
public boolean hasMediaFormat() { /**
return mediaFormat != null; * True if the output has received a format. False otherwise.
*/
public boolean hasFormat() {
return format != null;
} }
public MediaFormat getMediaFormat() { /**
return mediaFormat; * The format most recently received by the output, or null if a format has yet to be received.
*/
public MediaFormat getFormat() {
return format;
} }
/**
* The largest timestamp of any sample received by the output, or {@link Long#MIN_VALUE} if a
* sample has yet to be received.
*/
public long getLargestParsedTimestampUs() {
return largestParsedTimestampUs;
}
/**
* True if at least one sample can be read from the queue. False otherwise.
*/
public boolean isEmpty() { public boolean isEmpty() {
return !advanceToEligibleSample(); return !advanceToEligibleSample();
} }
...@@ -115,7 +149,7 @@ import com.google.android.exoplayer.util.ParsableByteArray; ...@@ -115,7 +149,7 @@ import com.google.android.exoplayer.util.ParsableByteArray;
* @param nextQueue The queue being spliced to. * @param nextQueue The queue being spliced to.
* @return Whether the splice was configured successfully. * @return Whether the splice was configured successfully.
*/ */
public boolean configureSpliceTo(SampleQueue nextQueue) { public boolean configureSpliceTo(DefaultTrackOutput nextQueue) {
if (spliceOutTimeUs != Long.MIN_VALUE) { if (spliceOutTimeUs != Long.MIN_VALUE) {
// We've already configured the splice. // We've already configured the splice.
return true; return true;
...@@ -128,8 +162,7 @@ import com.google.android.exoplayer.util.ParsableByteArray; ...@@ -128,8 +162,7 @@ import com.google.android.exoplayer.util.ParsableByteArray;
} }
RollingSampleBuffer nextRollingBuffer = nextQueue.rollingBuffer; RollingSampleBuffer nextRollingBuffer = nextQueue.rollingBuffer;
while (nextRollingBuffer.peekSample(sampleInfoHolder) while (nextRollingBuffer.peekSample(sampleInfoHolder)
&& (sampleInfoHolder.timeUs < firstPossibleSpliceTime && (sampleInfoHolder.timeUs < firstPossibleSpliceTime || !sampleInfoHolder.isSyncFrame())) {
|| (sampleInfoHolder.flags & C.SAMPLE_FLAG_SYNC) == 0)) {
// Discard samples from the next queue for as long as they are before the earliest possible // Discard samples from the next queue for as long as they are before the earliest possible
// splice time, or not keyframes. // splice time, or not keyframes.
nextRollingBuffer.skipSample(); nextRollingBuffer.skipSample();
...@@ -152,7 +185,7 @@ import com.google.android.exoplayer.util.ParsableByteArray; ...@@ -152,7 +185,7 @@ import com.google.android.exoplayer.util.ParsableByteArray;
private boolean advanceToEligibleSample() { private boolean advanceToEligibleSample() {
boolean haveNext = rollingBuffer.peekSample(sampleInfoHolder); boolean haveNext = rollingBuffer.peekSample(sampleInfoHolder);
if (needKeyframe) { if (needKeyframe) {
while (haveNext && (sampleInfoHolder.flags & C.SAMPLE_FLAG_SYNC) == 0) { while (haveNext && !sampleInfoHolder.isSyncFrame()) {
rollingBuffer.skipSample(); rollingBuffer.skipSample();
haveNext = rollingBuffer.peekSample(sampleInfoHolder); haveNext = rollingBuffer.peekSample(sampleInfoHolder);
} }
...@@ -168,35 +201,27 @@ import com.google.android.exoplayer.util.ParsableByteArray; ...@@ -168,35 +201,27 @@ import com.google.android.exoplayer.util.ParsableByteArray;
// Called by the loading thread. // Called by the loading thread.
protected boolean writingSample() { public int sampleData(DataSource dataSource, int length) throws IOException {
return writingSample; return rollingBuffer.appendData(dataSource, length);
}
protected void setMediaFormat(MediaFormat mediaFormat) {
this.mediaFormat = mediaFormat;
} }
protected void startSample(long sampleTimeUs) { // TrackOutput implementation. Called by the loading thread.
startSample(sampleTimeUs, 0);
}
protected void startSample(long sampleTimeUs, int offset) { @Override
writingSample = true; public void format(MediaFormat format) {
largestParsedTimestampUs = Math.max(largestParsedTimestampUs, sampleTimeUs); this.format = format;
rollingBuffer.startSample(sampleTimeUs, offset);
} }
protected void appendData(ParsableByteArray buffer, int length) { @Override
public void sampleData(ParsableByteArray buffer, int length) {
rollingBuffer.appendData(buffer, length); rollingBuffer.appendData(buffer, length);
} }
protected void commitSample(boolean isKeyframe) { @Override
commitSample(isKeyframe, 0); public void sampleMetadata(long timeUs, int flags, int size, int offset, byte[] encryptionKey) {
} largestParsedTimestampUs = Math.max(largestParsedTimestampUs, timeUs);
rollingBuffer.commitSample(timeUs, flags, rollingBuffer.getWritePosition() - size - offset,
protected void commitSample(boolean isKeyframe, int offset) { size, encryptionKey);
rollingBuffer.commitSample(isKeyframe, offset);
writingSample = 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.extractor;
import com.google.android.exoplayer.C;
import java.io.IOException;
/**
* Facilitates extraction of data from a container format.
*/
public interface Extractor {
/**
* Returned by {@link #read(ExtractorInput)} if the {@link ExtractorInput} passed to the next
* {@link #read(ExtractorInput)} is required to provide data continuing from the position in the
* stream reached by the returning call.
*/
public static final int RESULT_CONTINUE = 0;
/**
* Returned by {@link #read(ExtractorInput)} if the end of the {@link ExtractorInput} was reached.
* Equal to {@link C#RESULT_END_OF_INPUT}.
*/
public static final int RESULT_END_OF_INPUT = C.RESULT_END_OF_INPUT;
/**
* Initializes the extractor with an {@link ExtractorOutput}.
*
* @param output An {@link ExtractorOutput} to receive extracted data.
*/
void init(ExtractorOutput output);
/**
* Extracts data read from a provided {@link ExtractorInput}.
* <p>
* Each read will extract at most one sample from the stream before returning.
*
* @param input The {@link ExtractorInput} from which data should be read.
* @return One of the {@code RESULT_} values defined in this interface.
* @throws IOException If an error occurred reading from the input.
* @throws InterruptedException If the thread was interrupted.
*/
int 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.extractor;
import com.google.android.exoplayer.C;
import java.io.EOFException;
import java.io.IOException;
/**
* Provides data to be consumed by an {@link Extractor}.
*/
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 {@link C#RESULT_END_OF_INPUT} 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 reads the requested {@code length} in full.
* <p>
* If the end of the input is found having read no data, then behavior is dependent on
* {@code allowEndOfInput}. If {@code allowEndOfInput == true} then {@code false} is returned.
* Otherwise an {@link EOFException} is thrown.
* <p>
* Encountering the end of input having partially satisfied the read is always considered an
* error, and will result in an {@link EOFException} being 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 number of bytes to read from the input.
* @param allowEndOfInput True if encountering the end of the input having read no data is
* allowed, and should result in {@code false} being returned. False if it should be
* considered an error, causing an {@link EOFException} to be thrown.
* @return True if the read was successful. False if the end of the input was encountered having
* read no data.
* @throws EOFException If the end of input was encountered having partially satisfied the read
* (i.e. having read at least one byte, but fewer than {@code length}), or if no bytes were
* read and {@code allowEndOfInput} is false.
* @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, boolean allowEndOfInput)
throws IOException, InterruptedException;
/**
* Equivalent to {@code readFully(target, offset, length, false)}.
*
* @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.
* @throws EOFException If the end of input was encountered.
* @throws IOException If an error occurs reading from the input.
* @throws InterruptedException If the thread is interrupted.
*/
void readFully(byte[] target, int offset, int length) throws IOException, InterruptedException;
/**
* Like {@link #readFully(byte[], int, int)}, except the data is skipped instead of read.
* <p>
* Encountering the end of input is always considered an error, and will result in an
* {@link EOFException} being thrown.
*
* @param length The number of bytes to skip from the input.
* @throws EOFException If the end of input was encountered.
* @throws IOException If an error occurs reading from the input.
* @throws InterruptedException If the thread is interrupted.
*/
void skipFully(int length) throws IOException, InterruptedException;
/**
* The current position (byte offset) in the stream.
*
* @return The position (byte offset) in the stream.
*/
long getPosition();
/**
* Returns the length of the source stream, or {@link C#LENGTH_UNBOUNDED} if it is unknown.
*
* @return The length of the source stream, or {@link C#LENGTH_UNBOUNDED}.
*/
long getLength();
}
/*
* 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.extractor;
/**
* Receives stream level data extracted by an {@link Extractor}.
*/
public interface ExtractorOutput {
/**
* Invoked when the {@link Extractor} identifies the existence of a track in the stream.
* <p>
* Returns a {@link TrackOutput} that will receive track level data belonging to the track.
*
* @param trackId A track identifier.
* @return The {@link TrackOutput} that should receive track level data belonging to the track.
*/
TrackOutput track(int trackId);
/**
* Invoked when all tracks have been identified, meaning that {@link #track(int)} will not be
* invoked again.
*/
void endTracks();
}
/*
* 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.extractor;
import com.google.android.exoplayer.C;
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.ParsableByteArray;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.concurrent.ConcurrentLinkedQueue;
/**
* A rolling buffer of sample data and corresponding sample information.
*/
/* package */ final class RollingSampleBuffer {
private static final int INITIAL_SCRATCH_SIZE = 32;
private final BufferPool fragmentPool;
private final int fragmentLength;
private final InfoQueue infoQueue;
private final ConcurrentLinkedQueue<byte[]> dataQueue;
private final SampleExtrasHolder extrasHolder;
private final ParsableByteArray scratch;
// Accessed only by the consuming thread.
private long totalBytesDropped;
// Accessed only by the loading thread.
private long totalBytesWritten;
private byte[] lastFragment;
private int lastFragmentOffset;
public RollingSampleBuffer(BufferPool bufferPool) {
this.fragmentPool = bufferPool;
fragmentLength = bufferPool.bufferLength;
infoQueue = new InfoQueue();
dataQueue = new ConcurrentLinkedQueue<byte[]>();
extrasHolder = new SampleExtrasHolder();
scratch = new ParsableByteArray(INITIAL_SCRATCH_SIZE);
lastFragmentOffset = fragmentLength;
}
// Called by the consuming thread, but only when there is no loading thread.
/**
* Clears the buffer, returning all allocations to the allocator.
*/
public void clear() {
infoQueue.clear();
while (!dataQueue.isEmpty()) {
fragmentPool.releaseDirect(dataQueue.remove());
}
totalBytesDropped = 0;
totalBytesWritten = 0;
lastFragment = null;
lastFragmentOffset = fragmentLength;
}
/**
* Returns the current absolute write index.
*/
public int getWriteIndex() {
return infoQueue.getWriteIndex();
}
// Called by the consuming thread.
/**
* Returns the current absolute read index.
*/
public int getReadIndex() {
return infoQueue.getReadIndex();
}
/**
* Fills {@code holder} with information about the current sample, but does not write its data.
* <p>
* The fields set are {@link SampleHolder#size}, {@link SampleHolder#timeUs} and
* {@link SampleHolder#flags}.
*
* @param holder The holder into which the current sample information should be written.
* @return True if the holder was filled. False if there is no current sample.
*/
public boolean peekSample(SampleHolder holder) {
return infoQueue.peekSample(holder, extrasHolder);
}
/**
* Skips the current sample.
*/
public void skipSample() {
long nextOffset = infoQueue.moveToNextSample();
dropDownstreamTo(nextOffset);
}
/**
* Reads the current sample, advancing the read index to the next sample.
*
* @param sampleHolder The holder into which the current sample should be written.
* @return True if a sample was read. False if there is no current sample.
*/
public boolean readSample(SampleHolder sampleHolder) {
// Write the sample information into the holder and extrasHolder.
boolean haveSample = infoQueue.peekSample(sampleHolder, extrasHolder);
if (!haveSample) {
return false;
}
// Read encryption data if the sample is encrypted.
if (sampleHolder.isEncrypted()) {
readEncryptionData(sampleHolder, extrasHolder);
}
// Write the sample data into the holder.
if (sampleHolder.data == null || sampleHolder.data.capacity() < sampleHolder.size) {
sampleHolder.replaceBuffer(sampleHolder.size);
}
if (sampleHolder.data != null) {
readData(extrasHolder.offset, sampleHolder.data, sampleHolder.size);
}
// Advance the read head.
long nextOffset = infoQueue.moveToNextSample();
dropDownstreamTo(nextOffset);
return true;
}
/**
* Reads encryption data for the current sample.
* <p>
* The encryption data is written into {@code sampleHolder.cryptoInfo}, and
* {@code sampleHolder.size} is adjusted to subtract the number of bytes that were read. The
* same value is added to {@code extrasHolder.offset}.
*
* @param sampleHolder The holder into which the encryption data should be written.
* @param extrasHolder The extras holder whose offset should be read and subsequently adjusted.
*/
private void readEncryptionData(SampleHolder sampleHolder, SampleExtrasHolder extrasHolder) {
long offset = extrasHolder.offset;
// Read the signal byte.
readData(offset, scratch.data, 1);
offset++;
byte signalByte = scratch.data[0];
boolean subsampleEncryption = (signalByte & 0x80) != 0;
int ivSize = signalByte & 0x7F;
// Read the initialization vector.
if (sampleHolder.cryptoInfo.iv == null) {
sampleHolder.cryptoInfo.iv = new byte[16];
}
readData(offset, sampleHolder.cryptoInfo.iv, ivSize);
offset += ivSize;
// Read the subsample count, if present.
int subsampleCount;
if (subsampleEncryption) {
readData(offset, scratch.data, 2);
offset += 2;
scratch.setPosition(0);
subsampleCount = scratch.readUnsignedShort();
} else {
subsampleCount = 1;
}
// Write the clear and encrypted subsample sizes.
int[] clearDataSizes = sampleHolder.cryptoInfo.numBytesOfClearData;
if (clearDataSizes == null || clearDataSizes.length < subsampleCount) {
clearDataSizes = new int[subsampleCount];
}
int[] encryptedDataSizes = sampleHolder.cryptoInfo.numBytesOfEncryptedData;
if (encryptedDataSizes == null || encryptedDataSizes.length < subsampleCount) {
encryptedDataSizes = new int[subsampleCount];
}
if (subsampleEncryption) {
int subsampleDataLength = 6 * subsampleCount;
ensureCapacity(scratch, subsampleDataLength);
readData(offset, scratch.data, subsampleDataLength);
offset += subsampleDataLength;
scratch.setPosition(0);
for (int i = 0; i < subsampleCount; i++) {
clearDataSizes[i] = scratch.readUnsignedShort();
encryptedDataSizes[i] = scratch.readUnsignedIntToInt();
}
} else {
clearDataSizes[0] = 0;
encryptedDataSizes[0] = sampleHolder.size - (int) (offset - extrasHolder.offset);
}
// Populate the cryptoInfo.
sampleHolder.cryptoInfo.set(subsampleCount, clearDataSizes, encryptedDataSizes,
extrasHolder.encryptionKeyId, sampleHolder.cryptoInfo.iv, C.CRYPTO_MODE_AES_CTR);
// Adjust the offset and size to take into account the bytes read.
int bytesRead = (int) (offset - extrasHolder.offset);
extrasHolder.offset += bytesRead;
sampleHolder.size -= bytesRead;
}
/**
* Reads data from the front of the rolling buffer.
*
* @param absolutePosition The absolute position from which data should be read.
* @param target The buffer into which data should be written.
* @param length The number of bytes to read.
*/
private void readData(long absolutePosition, ByteBuffer target, int length) {
int remaining = length;
while (remaining > 0) {
dropDownstreamTo(absolutePosition);
int positionInFragment = (int) (absolutePosition - totalBytesDropped);
int toCopy = Math.min(remaining, fragmentLength - positionInFragment);
target.put(dataQueue.peek(), positionInFragment, toCopy);
absolutePosition += toCopy;
remaining -= toCopy;
}
}
/**
* Reads data from the front of the rolling buffer.
*
* @param absolutePosition The absolute position from which data should be read.
* @param target The array into which data should be written.
* @param length The number of bytes to read.
*/
// TODO: Consider reducing duplication of this method and the one above.
private void readData(long absolutePosition, byte[] target, int length) {
int bytesRead = 0;
while (bytesRead < length) {
dropDownstreamTo(absolutePosition);
int positionInFragment = (int) (absolutePosition - totalBytesDropped);
int toCopy = Math.min(length - bytesRead, fragmentLength - positionInFragment);
System.arraycopy(dataQueue.peek(), positionInFragment, target, bytesRead, toCopy);
absolutePosition += toCopy;
bytesRead += toCopy;
}
}
/**
* Discard any fragments that hold data prior to the specified absolute position, returning
* them to the pool.
*
* @param absolutePosition The absolute position up to which fragments can be discarded.
*/
private void dropDownstreamTo(long absolutePosition) {
int relativePosition = (int) (absolutePosition - totalBytesDropped);
int fragmentIndex = relativePosition / fragmentLength;
for (int i = 0; i < fragmentIndex; i++) {
fragmentPool.releaseDirect(dataQueue.remove());
totalBytesDropped += fragmentLength;
}
}
/**
* Ensure that the passed {@link ParsableByteArray} is of at least the specified limit.
*/
private static void ensureCapacity(ParsableByteArray byteArray, int limit) {
if (byteArray.limit() < limit) {
byteArray.reset(new byte[limit], limit);
}
}
// Called by the loading thread.
/**
* Returns the current write position in the rolling buffer.
*
* @return The current write position.
*/
public long getWritePosition() {
return totalBytesWritten;
}
/**
* Appends data to the rolling buffer.
*
* @param dataSource The source from which to read.
* @param length The maximum length of the read, or {@link C#LENGTH_UNBOUNDED} if the caller does
* not wish to impose a limit.
* @return The number of bytes appended.
* @throws IOException If an error occurs reading from the source.
*/
public int appendData(DataSource dataSource, int length) throws IOException {
ensureSpaceForWrite();
int remainingFragmentCapacity = fragmentLength - lastFragmentOffset;
length = length != C.LENGTH_UNBOUNDED ? Math.min(length, remainingFragmentCapacity)
: remainingFragmentCapacity;
int bytesRead = dataSource.read(lastFragment, lastFragmentOffset, length);
if (bytesRead == C.RESULT_END_OF_INPUT) {
return C.RESULT_END_OF_INPUT;
}
lastFragmentOffset += bytesRead;
totalBytesWritten += bytesRead;
return bytesRead;
}
/**
* Appends data to the rolling buffer.
*
* @param buffer A buffer containing the data to append.
* @param length The length of the data to append.
*/
public void appendData(ParsableByteArray buffer, int length) {
int remainingWriteLength = length;
while (remainingWriteLength > 0) {
ensureSpaceForWrite();
int thisWriteLength = Math.min(remainingWriteLength, fragmentLength - lastFragmentOffset);
buffer.readBytes(lastFragment, lastFragmentOffset, thisWriteLength);
lastFragmentOffset += thisWriteLength;
remainingWriteLength -= thisWriteLength;
}
totalBytesWritten += length;
}
/**
* Indicates the end point for the current sample, making it available for consumption.
*
* @param sampleTimeUs The sample timestamp.
* @param flags Flags that accompany the sample. See {@link SampleHolder#flags}.
* @param position The position of the sample data in the rolling buffer.
* @param size The size of the sample, in bytes.
* @param encryptionKey The encryption key associated with the sample, or null.
*/
public void commitSample(long sampleTimeUs, int flags, long position, int size,
byte[] encryptionKey) {
infoQueue.commitSample(sampleTimeUs, flags, position, size, encryptionKey);
}
/**
* Ensures at least one byte can be written, allocating a new fragment if necessary.
*/
private void ensureSpaceForWrite() {
if (lastFragmentOffset == fragmentLength) {
lastFragmentOffset = 0;
lastFragment = fragmentPool.allocateDirect();
dataQueue.add(lastFragment);
}
}
/**
* Holds information about the samples in the rolling buffer.
*/
private static final class InfoQueue {
private static final int SAMPLE_CAPACITY_INCREMENT = 1000;
private int capacity;
private long[] offsets;
private int[] sizes;
private int[] flags;
private long[] timesUs;
private byte[][] encryptionKeys;
private int queueSize;
private int absoluteReadIndex;
private int relativeReadIndex;
private int relativeWriteIndex;
public InfoQueue() {
capacity = SAMPLE_CAPACITY_INCREMENT;
offsets = new long[capacity];
timesUs = new long[capacity];
flags = new int[capacity];
sizes = new int[capacity];
encryptionKeys = new byte[capacity][];
}
// Called by the consuming thread, but only when there is no loading thread.
/**
* Clears the queue.
*/
public void clear() {
absoluteReadIndex = 0;
relativeReadIndex = 0;
relativeWriteIndex = 0;
queueSize = 0;
}
/**
* Returns the current absolute write index.
*/
public int getWriteIndex() {
return absoluteReadIndex + queueSize;
}
// Called by the consuming thread.
/**
* Returns the current absolute read index.
*/
public int getReadIndex() {
return absoluteReadIndex;
}
/**
* Fills {@code holder} with information about the current sample, but does not write its data.
* The first entry in {@code offsetHolder} is filled with the absolute position of the sample's
* data in the rolling buffer.
* <p>
* The fields set are {SampleHolder#size}, {SampleHolder#timeUs}, {SampleHolder#flags} and
* {@code offsetHolder[0]}.
*
* @param holder The holder into which the current sample information should be written.
* @param extrasHolder The holder into which extra sample information should be written.
* @return True if the holders were filled. False if there is no current sample.
*/
public synchronized boolean peekSample(SampleHolder holder, SampleExtrasHolder extrasHolder) {
if (queueSize == 0) {
return false;
}
holder.timeUs = timesUs[relativeReadIndex];
holder.size = sizes[relativeReadIndex];
holder.flags = flags[relativeReadIndex];
extrasHolder.offset = offsets[relativeReadIndex];
extrasHolder.encryptionKeyId = encryptionKeys[relativeReadIndex];
return true;
}
/**
* Advances the read index to the next sample.
*
* @return The absolute position of the first byte in the rolling buffer that may still be
* required after advancing the index. Data prior to this position can be dropped.
*/
public synchronized long moveToNextSample() {
queueSize--;
int lastReadIndex = relativeReadIndex++;
absoluteReadIndex++;
if (relativeReadIndex == capacity) {
// Wrap around.
relativeReadIndex = 0;
}
return queueSize > 0 ? offsets[relativeReadIndex]
: (sizes[lastReadIndex] + offsets[lastReadIndex]);
}
// Called by the loading thread.
public synchronized void commitSample(long timeUs, int sampleFlags, long offset, int size,
byte[] encryptionKey) {
timesUs[relativeWriteIndex] = timeUs;
offsets[relativeWriteIndex] = offset;
sizes[relativeWriteIndex] = size;
flags[relativeWriteIndex] = sampleFlags;
encryptionKeys[relativeWriteIndex] = encryptionKey;
// Increment the write index.
queueSize++;
if (queueSize == capacity) {
// Increase the capacity.
int newCapacity = capacity + SAMPLE_CAPACITY_INCREMENT;
long[] newOffsets = new long[newCapacity];
long[] newTimesUs = new long[newCapacity];
int[] newFlags = new int[newCapacity];
int[] newSizes = new int[newCapacity];
byte[][] newEncryptionKeys = new byte[newCapacity][];
int beforeWrap = capacity - relativeReadIndex;
System.arraycopy(offsets, relativeReadIndex, newOffsets, 0, beforeWrap);
System.arraycopy(timesUs, relativeReadIndex, newTimesUs, 0, beforeWrap);
System.arraycopy(flags, relativeReadIndex, newFlags, 0, beforeWrap);
System.arraycopy(sizes, relativeReadIndex, newSizes, 0, beforeWrap);
System.arraycopy(encryptionKeys, relativeReadIndex, newEncryptionKeys, 0, beforeWrap);
int afterWrap = relativeReadIndex;
System.arraycopy(offsets, 0, newOffsets, beforeWrap, afterWrap);
System.arraycopy(timesUs, 0, newTimesUs, beforeWrap, afterWrap);
System.arraycopy(flags, 0, newFlags, beforeWrap, afterWrap);
System.arraycopy(sizes, 0, newSizes, beforeWrap, afterWrap);
System.arraycopy(encryptionKeys, 0, newEncryptionKeys, beforeWrap, afterWrap);
offsets = newOffsets;
timesUs = newTimesUs;
flags = newFlags;
sizes = newSizes;
encryptionKeys = newEncryptionKeys;
relativeReadIndex = 0;
relativeWriteIndex = capacity;
queueSize = capacity;
capacity = newCapacity;
} else {
relativeWriteIndex++;
if (relativeWriteIndex == capacity) {
// Wrap around.
relativeWriteIndex = 0;
}
}
}
}
/**
* Holds additional sample information not held by {@link SampleHolder}.
*/
private static final class SampleExtrasHolder {
public long offset;
public byte[] encryptionKeyId;
}
}
/*
* 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.extractor;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.util.ParsableByteArray;
/**
* Receives track level data extracted by an {@link Extractor}.
*/
public interface TrackOutput {
/**
* Invoked when the {@link MediaFormat} of the track has been extracted from the stream.
*
* @param format The extracted {@link MediaFormat}.
*/
void format(MediaFormat format);
/**
* Invoked to write sample data to the output.
*
* @param data A {@link ParsableByteArray} from which to read the sample data.
* @param length The number of bytes to read.
*/
void sampleData(ParsableByteArray data, int length);
/**
* Invoked when metadata associated with a sample has been extracted from the stream.
* <p>
* The corresponding sample data will have already been passed to the output via calls to
* {@link #sampleData(ParsableByteArray, int)}.
*
* @param timeUs The media timestamp associated with the sample, in microseconds.
* @param flags Flags associated with the sample. See {@link SampleHolder#flags}.
* @param size The size of the sample data, in bytes.
* @param offset The number of bytes that have been passed to
* {@link #sampleData(ParsableByteArray, int)} since the last byte belonging to the sample
* whose metadata is being passed.
* @param encryptionKey The encryption key associated with the sample. May be null.
*/
void sampleMetadata(long timeUs, int flags, int size, int offset, byte[] encryptionKey);
}
...@@ -13,13 +13,11 @@ ...@@ -13,13 +13,11 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package com.google.android.exoplayer.hls.parser; package com.google.android.exoplayer.extractor.ts;
import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.extractor.Extractor;
import com.google.android.exoplayer.SampleHolder; import com.google.android.exoplayer.extractor.ExtractorInput;
import com.google.android.exoplayer.upstream.BufferPool; import com.google.android.exoplayer.extractor.ExtractorOutput;
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 +26,38 @@ import java.io.IOException; ...@@ -28,82 +26,38 @@ 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 Extractor {
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(ExtractorOutput output) {
Assertions.checkState(prepared); adtsReader = new AdtsReader(output.track(0));
return 1; output.endTracks();
} }
@Override @Override
public MediaFormat getFormat(int track) { public int read(ExtractorInput input)
Assertions.checkState(prepared); throws IOException, InterruptedException {
return adtsReader.getMediaFormat(); int bytesRead = input.read(packetBuffer.data, 0, MAX_PACKET_SIZE);
}
@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 RESULT_END_OF_INPUT;
} }
// 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 +65,7 @@ public class AdtsExtractor extends HlsExtractor { ...@@ -111,16 +65,7 @@ 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) { return RESULT_CONTINUE;
prepared = adtsReader.hasMediaFormat();
}
return bytesRead;
}
@Override
protected SampleQueue getSampleQueue(int track) {
Assertions.checkState(track == 0);
return adtsReader;
} }
} }
...@@ -13,11 +13,11 @@ ...@@ -13,11 +13,11 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package com.google.android.exoplayer.hls.parser; package com.google.android.exoplayer.extractor.ts;
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.extractor.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;
...@@ -48,15 +48,16 @@ import java.util.Collections; ...@@ -48,15 +48,16 @@ import java.util.Collections;
private boolean lastByteWasFF; private boolean lastByteWasFF;
private boolean hasCrc; private boolean hasCrc;
// Parsed from the header. // Used when parsing the header.
private boolean hasOutputFormat;
private long frameDurationUs; private long frameDurationUs;
private int sampleSize; private int sampleSize;
// 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 +79,16 @@ import java.util.Collections; ...@@ -78,17 +79,16 @@ 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);
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.sampleData(data, bytesToRead);
bytesRead += bytesToRead; bytesRead += bytesToRead;
if (bytesRead == sampleSize) { if (bytesRead == sampleSize) {
commitSample(true); output.sampleMetadata(timeUs, C.SAMPLE_FLAG_SYNC, sampleSize, 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 (!hasOutputFormat) {
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,8 @@ import java.util.Collections; ...@@ -167,7 +167,8 @@ 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.format(mediaFormat);
hasOutputFormat = true;
} else { } else {
adtsScratch.skipBits(10); adtsScratch.skipBits(10);
} }
......
...@@ -13,18 +13,23 @@ ...@@ -13,18 +13,23 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package com.google.android.exoplayer.hls.parser; package com.google.android.exoplayer.extractor.ts;
import com.google.android.exoplayer.upstream.BufferPool; import com.google.android.exoplayer.extractor.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;
} }
/** /**
......
...@@ -13,16 +13,19 @@ ...@@ -13,16 +13,19 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package com.google.android.exoplayer.hls.parser; package com.google.android.exoplayer.extractor.ts;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.mp4.Mp4Util; import com.google.android.exoplayer.extractor.TrackOutput;
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.H264Util;
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;
import com.google.android.exoplayer.util.ParsableByteArray; import com.google.android.exoplayer.util.ParsableByteArray;
import android.util.Log;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
...@@ -32,29 +35,59 @@ import java.util.List; ...@@ -32,29 +35,59 @@ import java.util.List;
*/ */
/* package */ class H264Reader extends ElementaryStreamReader { /* package */ class H264Reader extends ElementaryStreamReader {
private static final String TAG = "H264Reader";
private static final int NAL_UNIT_TYPE_IDR = 5; private static final int NAL_UNIT_TYPE_IDR = 5;
private static final int NAL_UNIT_TYPE_SEI = 6; private static final int NAL_UNIT_TYPE_SEI = 6;
private static final int NAL_UNIT_TYPE_SPS = 7; private static final int NAL_UNIT_TYPE_SPS = 7;
private static final int NAL_UNIT_TYPE_PPS = 8; private static final int NAL_UNIT_TYPE_PPS = 8;
private static final int NAL_UNIT_TYPE_AUD = 9; private static final int NAL_UNIT_TYPE_AUD = 9;
private static final int EXTENDED_SAR = 0xFF;
private static final float[] ASPECT_RATIO_IDC_VALUES = new float[] {
1f /* Unspecified. Assume square */,
1f,
12f / 11f,
10f / 11f,
16f / 11f,
40f / 33f,
24f / 11f,
20f / 11f,
32f / 11f,
80f / 33f,
18f / 11f,
15f / 11f,
64f / 33f,
160f / 99f,
4f / 3f,
3f / 2f,
2f
};
private final SeiReader seiReader; private final SeiReader seiReader;
private final boolean[] prefixFlags; private final boolean[] prefixFlags;
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 boolean hasOutputFormat;
private int scratchEscapeCount; private int scratchEscapeCount;
private int[] scratchEscapePositions; private int[] scratchEscapePositions;
private boolean writingSample;
private boolean isKeyframe; private boolean isKeyframe;
private long samplePosition;
private long sampleTimeUs;
private long totalBytesWritten;
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,11 +99,12 @@ import java.util.List; ...@@ -66,11 +99,12 @@ 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()); totalBytesWritten += data.bytesLeft();
output.sampleData(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) {
int nextNalUnitOffset = Mp4Util.findNalUnit(dataArray, offset, limit, prefixFlags); int nextNalUnitOffset = H264Util.findNalUnit(dataArray, offset, limit, prefixFlags);
if (nextNalUnitOffset < limit) { if (nextNalUnitOffset < limit) {
// We've seen the start of a NAL unit. // We've seen the start of a NAL unit.
...@@ -81,16 +115,21 @@ import java.util.List; ...@@ -81,16 +115,21 @@ import java.util.List;
feedNalUnitTargetBuffersData(dataArray, offset, nextNalUnitOffset); feedNalUnitTargetBuffersData(dataArray, offset, nextNalUnitOffset);
} }
int nalUnitType = Mp4Util.getNalUnitType(dataArray, nextNalUnitOffset); int nalUnitType = H264Util.getNalUnitType(dataArray, nextNalUnitOffset);
int nalUnitOffsetInData = nextNalUnitOffset - limit; int bytesWrittenPastNalUnit = limit - nextNalUnitOffset;
if (nalUnitType == NAL_UNIT_TYPE_AUD) { if (nalUnitType == NAL_UNIT_TYPE_AUD) {
if (writingSample()) { if (writingSample) {
if (isKeyframe && !hasMediaFormat() && sps.isCompleted() && pps.isCompleted()) { if (isKeyframe && !hasOutputFormat && sps.isCompleted() && pps.isCompleted()) {
parseMediaFormat(sps, pps); parseMediaFormat(sps, pps);
} }
commitSample(isKeyframe, nalUnitOffsetInData); int flags = isKeyframe ? C.SAMPLE_FLAG_SYNC : 0;
int size = (int) (totalBytesWritten - samplePosition) - bytesWrittenPastNalUnit;
output.sampleMetadata(sampleTimeUs, flags, size, bytesWrittenPastNalUnit, null);
writingSample = false;
} }
startSample(pesTimeUs, nalUnitOffsetInData); writingSample = true;
samplePosition = totalBytesWritten - bytesWrittenPastNalUnit;
sampleTimeUs = pesTimeUs;
isKeyframe = false; isKeyframe = false;
} else if (nalUnitType == NAL_UNIT_TYPE_IDR) { } else if (nalUnitType == NAL_UNIT_TYPE_IDR) {
isKeyframe = true; isKeyframe = true;
...@@ -117,7 +156,7 @@ import java.util.List; ...@@ -117,7 +156,7 @@ import java.util.List;
} }
private void feedNalUnitTargetBuffersStart(int nalUnitType) { private void feedNalUnitTargetBuffersStart(int nalUnitType) {
if (!hasMediaFormat()) { if (!hasOutputFormat) {
sps.startNalUnit(nalUnitType); sps.startNalUnit(nalUnitType);
pps.startNalUnit(nalUnitType); pps.startNalUnit(nalUnitType);
} }
...@@ -125,7 +164,7 @@ import java.util.List; ...@@ -125,7 +164,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 (!hasOutputFormat) {
sps.appendToNalUnit(dataArray, offset, limit); sps.appendToNalUnit(dataArray, offset, limit);
pps.appendToNalUnit(dataArray, offset, limit); pps.appendToNalUnit(dataArray, offset, limit);
} }
...@@ -137,7 +176,8 @@ import java.util.List; ...@@ -137,7 +176,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);
} }
} }
...@@ -228,9 +268,29 @@ import java.util.List; ...@@ -228,9 +268,29 @@ import java.util.List;
frameHeight -= (frameCropTopOffset + frameCropBottomOffset) * cropUnitY; frameHeight -= (frameCropTopOffset + frameCropBottomOffset) * cropUnitY;
} }
// Set the format. float pixelWidthHeightRatio = 1;
setMediaFormat(MediaFormat.createVideoFormat(MimeTypes.VIDEO_H264, MediaFormat.NO_VALUE, boolean vuiParametersPresentFlag = bitArray.readBit();
frameWidth, frameHeight, initializationData)); if (vuiParametersPresentFlag) {
boolean aspectRatioInfoPresentFlag = bitArray.readBit();
if (aspectRatioInfoPresentFlag) {
int aspectRatioIdc = bitArray.readBits(8);
if (aspectRatioIdc == EXTENDED_SAR) {
int sarWidth = bitArray.readBits(16);
int sarHeight = bitArray.readBits(16);
if (sarWidth != 0 && sarHeight != 0) {
pixelWidthHeightRatio = (float) sarWidth / sarHeight;
}
} else if (aspectRatioIdc < ASPECT_RATIO_IDC_VALUES.length) {
pixelWidthHeightRatio = ASPECT_RATIO_IDC_VALUES[aspectRatioIdc];
} else {
Log.w(TAG, "Unexpected aspect_ratio_idc value: " + aspectRatioIdc);
}
}
}
output.format(MediaFormat.createVideoFormat(MimeTypes.VIDEO_H264, MediaFormat.NO_VALUE,
C.UNKNOWN_TIME_US, frameWidth, frameHeight, pixelWidthHeightRatio, initializationData));
hasOutputFormat = true;
} }
private void skipScalingList(ParsableBitArray bitArray, int size) { private void skipScalingList(ParsableBitArray bitArray, int size) {
......
...@@ -13,10 +13,11 @@ ...@@ -13,10 +13,11 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package com.google.android.exoplayer.hls.parser; package com.google.android.exoplayer.extractor.ts;
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.extractor.TrackOutput;
import com.google.android.exoplayer.util.ParsableByteArray; import com.google.android.exoplayer.util.ParsableByteArray;
/** /**
...@@ -24,24 +25,32 @@ import com.google.android.exoplayer.util.ParsableByteArray; ...@@ -24,24 +25,32 @@ import com.google.android.exoplayer.util.ParsableByteArray;
*/ */
/* package */ class Id3Reader extends ElementaryStreamReader { /* package */ class Id3Reader extends ElementaryStreamReader {
public Id3Reader(BufferPool bufferPool) { private boolean writingSample;
super(bufferPool); private long sampleTimeUs;
setMediaFormat(MediaFormat.createId3Format()); private int sampleSize;
public Id3Reader(TrackOutput output) {
super(output);
output.format(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); writingSample = true;
sampleTimeUs = pesTimeUs;
sampleSize = 0;
} }
if (writingSample()) { if (writingSample) {
appendData(data, data.bytesLeft()); sampleSize += data.bytesLeft();
output.sampleData(data, data.bytesLeft());
} }
} }
@Override @Override
public void packetFinished() { public void packetFinished() {
commitSample(true); output.sampleMetadata(sampleTimeUs, C.SAMPLE_FLAG_SYNC, sampleSize, 0, null);
writingSample = false;
} }
} }
...@@ -13,11 +13,12 @@ ...@@ -13,11 +13,12 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package com.google.android.exoplayer.hls.parser; package com.google.android.exoplayer.extractor.ts;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.extractor.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.format(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,17 @@ import com.google.android.exoplayer.util.ParsableByteArray; ...@@ -57,13 +55,17 @@ 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.sampleData(seiBuffer, payloadSize);
appendData(seiBuffer, payloadSize); output.sampleMetadata(pesTimeUs, C.SAMPLE_FLAG_SYNC, payloadSize, 0, null);
commitSample(true);
} else { } else {
seiBuffer.skip(payloadSize); seiBuffer.skip(payloadSize);
} }
} }
} }
@Override
public void packetFinished() {
// Do nothing.
}
} }
...@@ -13,14 +13,12 @@ ...@@ -13,14 +13,12 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package com.google.android.exoplayer.hls.parser; package com.google.android.exoplayer.extractor.ts;
import com.google.android.exoplayer.C; import com.google.android.exoplayer.C;
import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.extractor.Extractor;
import com.google.android.exoplayer.SampleHolder; import com.google.android.exoplayer.extractor.ExtractorInput;
import com.google.android.exoplayer.upstream.BufferPool; import com.google.android.exoplayer.extractor.ExtractorOutput;
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 +30,7 @@ import java.io.IOException; ...@@ -32,7 +30,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 Extractor {
private static final String TAG = "TsExtractor"; private static final String TAG = "TsExtractor";
...@@ -50,119 +48,43 @@ public final class TsExtractor extends HlsExtractor { ...@@ -50,119 +48,43 @@ 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 ExtractorOutput 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(ExtractorOutput 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 @Override
public long getLargestSampleTimestamp() { public int read(ExtractorInput input)
long largestParsedTimestampUs = Long.MIN_VALUE; throws IOException, InterruptedException {
for (int i = 0; i < sampleQueues.size(); i++) { if (!input.readFully(tsPacketBuffer.data, 0, TS_PACKET_SIZE, true)) {
largestParsedTimestampUs = Math.max(largestParsedTimestampUs, return RESULT_END_OF_INPUT;
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
public int read(DataSource dataSource) throws IOException {
int bytesRead = dataSource.read(tsPacketBuffer.data, tsPacketBytesRead,
TS_PACKET_SIZE - tsPacketBytesRead);
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); 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 RESULT_CONTINUE;
} }
tsPacketBuffer.readBytes(tsScratch, 3); tsPacketBuffer.readBytes(tsScratch, 3);
...@@ -185,20 +107,11 @@ public final class TsExtractor extends HlsExtractor { ...@@ -185,20 +107,11 @@ 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) { return RESULT_CONTINUE;
prepared = checkPrepared();
}
return bytesRead;
}
@Override
protected SampleQueue getSampleQueue(int track) {
return sampleQueues.valueAt(track);
} }
/** /**
...@@ -233,7 +146,8 @@ public final class TsExtractor extends HlsExtractor { ...@@ -233,7 +146,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,
ExtractorOutput output);
} }
...@@ -249,7 +163,8 @@ public final class TsExtractor extends HlsExtractor { ...@@ -249,7 +163,8 @@ public final class TsExtractor extends HlsExtractor {
} }
@Override @Override
public void consume(ParsableByteArray data, boolean payloadUnitStartIndicator) { public void consume(ParsableByteArray data, boolean payloadUnitStartIndicator,
ExtractorOutput output) {
// Skip pointer. // Skip pointer.
if (payloadUnitStartIndicator) { if (payloadUnitStartIndicator) {
int pointerField = data.readUnsignedByte(); int pointerField = data.readUnsignedByte();
...@@ -288,7 +203,8 @@ public final class TsExtractor extends HlsExtractor { ...@@ -288,7 +203,8 @@ public final class TsExtractor extends HlsExtractor {
} }
@Override @Override
public void consume(ParsableByteArray data, boolean payloadUnitStartIndicator) { public void consume(ParsableByteArray data, boolean payloadUnitStartIndicator,
ExtractorOutput output) {
// Skip pointer. // Skip pointer.
if (payloadUnitStartIndicator) { if (payloadUnitStartIndicator) {
int pointerField = data.readUnsignedByte(); int pointerField = data.readUnsignedByte();
...@@ -325,7 +241,7 @@ public final class TsExtractor extends HlsExtractor { ...@@ -325,7 +241,7 @@ 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;
} }
...@@ -336,25 +252,26 @@ public final class TsExtractor extends HlsExtractor { ...@@ -336,25 +252,26 @@ public final class TsExtractor extends HlsExtractor {
pesPayloadReader = new MpaReader(bufferPool); pesPayloadReader = new MpaReader(bufferPool);
break; break;
case TS_STREAM_TYPE_AAC: case TS_STREAM_TYPE_AAC:
pesPayloadReader = new AdtsReader(bufferPool); pesPayloadReader = new AdtsReader(output.track(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.track(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.track(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.track(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.endTracks();
} }
} }
...@@ -393,7 +310,8 @@ public final class TsExtractor extends HlsExtractor { ...@@ -393,7 +310,8 @@ public final class TsExtractor extends HlsExtractor {
} }
@Override @Override
public void consume(ParsableByteArray data, boolean payloadUnitStartIndicator) { public void consume(ParsableByteArray data, boolean payloadUnitStartIndicator,
ExtractorOutput output) {
if (payloadUnitStartIndicator) { if (payloadUnitStartIndicator) {
switch (state) { switch (state) {
case STATE_FINDING_HEADER: case STATE_FINDING_HEADER:
......
...@@ -17,9 +17,9 @@ package com.google.android.exoplayer.hls; ...@@ -17,9 +17,9 @@ package com.google.android.exoplayer.hls;
import com.google.android.exoplayer.C; import com.google.android.exoplayer.C;
import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.hls.parser.AdtsExtractor; import com.google.android.exoplayer.extractor.Extractor;
import com.google.android.exoplayer.hls.parser.HlsExtractor; import com.google.android.exoplayer.extractor.ts.AdtsExtractor;
import com.google.android.exoplayer.hls.parser.TsExtractor; import com.google.android.exoplayer.extractor.ts.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;
import com.google.android.exoplayer.upstream.BufferPool; import com.google.android.exoplayer.upstream.BufferPool;
...@@ -27,6 +27,7 @@ import com.google.android.exoplayer.upstream.DataSource; ...@@ -27,6 +27,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.upstream.HttpDataSource.InvalidResponseCodeException; import com.google.android.exoplayer.upstream.HttpDataSource.InvalidResponseCodeException;
import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.UriUtil;
import com.google.android.exoplayer.util.Util; import com.google.android.exoplayer.util.Util;
import android.net.Uri; import android.net.Uri;
...@@ -107,10 +108,9 @@ public class HlsChunkSource { ...@@ -107,10 +108,9 @@ public class HlsChunkSource {
public static final long DEFAULT_MAX_BUFFER_TO_SWITCH_DOWN_MS = 20000; public static final long DEFAULT_MAX_BUFFER_TO_SWITCH_DOWN_MS = 20000;
/** /**
* The default maximum time a media playlist is blacklisted without * The default time for which a media playlist should be blacklisted.
* rechecking if it is alive again (because an encoder reset, for example)
*/ */
public static final long DEFAULT_MAX_TIME_MEDIA_PLAYLIST_BLACKLISTED_MS = 60000; public static final long DEFAULT_PLAYLIST_BLACKLIST_MS = 60000;
private static final String TAG = "HlsChunkSource"; private static final String TAG = "HlsChunkSource";
private static final String AAC_FILE_EXTENSION = ".aac"; private static final String AAC_FILE_EXTENSION = ".aac";
...@@ -122,7 +122,7 @@ public class HlsChunkSource { ...@@ -122,7 +122,7 @@ public class HlsChunkSource {
private final Variant[] enabledVariants; private final Variant[] enabledVariants;
private final BandwidthMeter bandwidthMeter; private final BandwidthMeter bandwidthMeter;
private final int adaptiveMode; private final int adaptiveMode;
private final Uri baseUri; private final String baseUri;
private final int maxWidth; private final int maxWidth;
private final int maxHeight; private final int maxHeight;
private final int targetBufferSize; private final int targetBufferSize;
...@@ -132,8 +132,7 @@ public class HlsChunkSource { ...@@ -132,8 +132,7 @@ public class HlsChunkSource {
/* package */ byte[] scratchSpace; /* package */ byte[] scratchSpace;
/* package */ final HlsMediaPlaylist[] mediaPlaylists; /* package */ final HlsMediaPlaylist[] mediaPlaylists;
/* package */ final boolean[] mediaPlaylistBlacklistFlags; /* package */ final long[] mediaPlaylistBlacklistTimesMs;
/* package */ final long[] mediaPlaylistBlacklistedTimeMs;
/* package */ final long[] lastMediaPlaylistLoadTimesMs; /* package */ final long[] lastMediaPlaylistLoadTimesMs;
/* package */ boolean live; /* package */ boolean live;
/* package */ long durationUs; /* package */ long durationUs;
...@@ -188,16 +187,14 @@ public class HlsChunkSource { ...@@ -188,16 +187,14 @@ public class HlsChunkSource {
if (playlist.type == HlsPlaylist.TYPE_MEDIA) { if (playlist.type == HlsPlaylist.TYPE_MEDIA) {
enabledVariants = new Variant[] {new Variant(0, playlistUrl, 0, null, -1, -1)}; enabledVariants = new Variant[] {new Variant(0, playlistUrl, 0, null, -1, -1)};
mediaPlaylists = new HlsMediaPlaylist[1]; mediaPlaylists = new HlsMediaPlaylist[1];
mediaPlaylistBlacklistFlags = new boolean[1]; mediaPlaylistBlacklistTimesMs = new long[1];
mediaPlaylistBlacklistedTimeMs = new long[1];
lastMediaPlaylistLoadTimesMs = new long[1]; lastMediaPlaylistLoadTimesMs = new long[1];
setMediaPlaylist(0, (HlsMediaPlaylist) playlist); setMediaPlaylist(0, (HlsMediaPlaylist) playlist);
} else { } else {
Assertions.checkState(playlist.type == HlsPlaylist.TYPE_MASTER); Assertions.checkState(playlist.type == HlsPlaylist.TYPE_MASTER);
enabledVariants = filterVariants((HlsMasterPlaylist) playlist, variantIndices); enabledVariants = filterVariants((HlsMasterPlaylist) playlist, variantIndices);
mediaPlaylists = new HlsMediaPlaylist[enabledVariants.length]; mediaPlaylists = new HlsMediaPlaylist[enabledVariants.length];
mediaPlaylistBlacklistFlags = new boolean[enabledVariants.length]; mediaPlaylistBlacklistTimesMs = new long[enabledVariants.length];
mediaPlaylistBlacklistedTimeMs = new long[enabledVariants.length];
lastMediaPlaylistLoadTimesMs = new long[enabledVariants.length]; lastMediaPlaylistLoadTimesMs = new long[enabledVariants.length];
} }
...@@ -305,11 +302,11 @@ public class HlsChunkSource { ...@@ -305,11 +302,11 @@ public class HlsChunkSource {
} }
HlsMediaPlaylist.Segment segment = mediaPlaylist.segments.get(chunkIndex); HlsMediaPlaylist.Segment segment = mediaPlaylist.segments.get(chunkIndex);
Uri chunkUri = Util.getMergedUri(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 = Util.getMergedUri(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.
HlsChunk toReturn = newEncryptionKeyChunk(keyUri, segment.encryptionIV); HlsChunk toReturn = newEncryptionKeyChunk(keyUri, segment.encryptionIV);
...@@ -344,16 +341,17 @@ public class HlsChunkSource { ...@@ -344,16 +341,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) Extractor 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);
} }
...@@ -370,8 +368,7 @@ public class HlsChunkSource { ...@@ -370,8 +368,7 @@ public class HlsChunkSource {
int responseCode = responseCodeException.responseCode; int responseCode = responseCodeException.responseCode;
if (responseCode == 404 || responseCode == 410) { if (responseCode == 404 || responseCode == 410) {
MediaPlaylistChunk playlistChunk = (MediaPlaylistChunk) chunk; MediaPlaylistChunk playlistChunk = (MediaPlaylistChunk) chunk;
mediaPlaylistBlacklistFlags[playlistChunk.variantIndex] = true; mediaPlaylistBlacklistTimesMs[playlistChunk.variantIndex] = SystemClock.elapsedRealtime();
mediaPlaylistBlacklistedTimeMs[playlistChunk.variantIndex] = SystemClock.elapsedRealtime();
if (!allPlaylistsBlacklisted()) { if (!allPlaylistsBlacklisted()) {
// We've handled the 404/410 by blacklisting the playlist. // We've handled the 404/410 by blacklisting the playlist.
Log.w(TAG, "Blacklisted playlist (" + responseCode + "): " Log.w(TAG, "Blacklisted playlist (" + responseCode + "): "
...@@ -381,8 +378,7 @@ public class HlsChunkSource { ...@@ -381,8 +378,7 @@ public class HlsChunkSource {
// This was the last non-blacklisted playlist. Don't blacklist it. // This was the last non-blacklisted playlist. Don't blacklist it.
Log.w(TAG, "Final playlist not blacklisted (" + responseCode + "): " Log.w(TAG, "Final playlist not blacklisted (" + responseCode + "): "
+ playlistChunk.dataSpec.uri); + playlistChunk.dataSpec.uri);
mediaPlaylistBlacklistFlags[playlistChunk.variantIndex] = false; mediaPlaylistBlacklistTimesMs[playlistChunk.variantIndex] = 0;
mediaPlaylistBlacklistedTimeMs[playlistChunk.variantIndex] = 0;
return false; return false;
} }
} }
...@@ -392,19 +388,27 @@ public class HlsChunkSource { ...@@ -392,19 +388,27 @@ 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 (mediaPlaylistBlacklistFlags[variantIndex] if (mediaPlaylistBlacklistTimesMs[variantIndex] != 0
|| (idealVariantIndex > variantIndex && bufferedUs < maxBufferDurationToSwitchDownUs) || (idealVariantIndex > variantIndex && bufferedUs < maxBufferDurationToSwitchDownUs)
|| (idealVariantIndex < variantIndex && bufferedUs > minBufferDurationToSwitchUpUs)) { || (idealVariantIndex < variantIndex && bufferedUs > minBufferDurationToSwitchUpUs)) {
// Switch variant. // Switch variant.
...@@ -417,7 +421,7 @@ public class HlsChunkSource { ...@@ -417,7 +421,7 @@ public class HlsChunkSource {
private int getVariantIndexForBandwdith(int bandwidth) { private int getVariantIndexForBandwdith(int bandwidth) {
int lowestQualityEnabledVariant = 0; int lowestQualityEnabledVariant = 0;
for (int i = 0; i < enabledVariants.length; i++) { for (int i = 0; i < enabledVariants.length; i++) {
if (!mediaPlaylistBlacklistFlags[i]) { if (mediaPlaylistBlacklistTimesMs[i] == 0) {
if (enabledVariants[i].bandwidth <= bandwidth) { if (enabledVariants[i].bandwidth <= bandwidth) {
return i; return i;
} }
...@@ -443,14 +447,15 @@ public class HlsChunkSource { ...@@ -443,14 +447,15 @@ public class HlsChunkSource {
} }
private MediaPlaylistChunk newMediaPlaylistChunk(int variantIndex) { private MediaPlaylistChunk newMediaPlaylistChunk(int variantIndex) {
Uri mediaPlaylistUri = Util.getMergedUri(baseUri, enabledVariants[variantIndex].url); Uri mediaPlaylistUri = UriUtil.resolveToUri(baseUri, enabledVariants[variantIndex].url);
DataSpec dataSpec = new DataSpec(mediaPlaylistUri, 0, C.LENGTH_UNBOUNDED, null); DataSpec dataSpec = new DataSpec(mediaPlaylistUri, 0, C.LENGTH_UNBOUNDED, null,
DataSpec.FLAG_ALLOW_GZIP);
return new MediaPlaylistChunk(variantIndex, upstreamDataSource, dataSpec, return new MediaPlaylistChunk(variantIndex, upstreamDataSource, dataSpec,
mediaPlaylistUri.toString()); mediaPlaylistUri.toString());
} }
private EncryptionKeyChunk newEncryptionKeyChunk(Uri keyUri, String iv) { private EncryptionKeyChunk newEncryptionKeyChunk(Uri keyUri, String iv) {
DataSpec dataSpec = new DataSpec(keyUri, 0, C.LENGTH_UNBOUNDED, null); DataSpec dataSpec = new DataSpec(keyUri, 0, C.LENGTH_UNBOUNDED, null, DataSpec.FLAG_ALLOW_GZIP);
return new EncryptionKeyChunk(upstreamDataSource, dataSpec, iv); return new EncryptionKeyChunk(upstreamDataSource, dataSpec, iv);
} }
...@@ -468,7 +473,7 @@ public class HlsChunkSource { ...@@ -468,7 +473,7 @@ public class HlsChunkSource {
System.arraycopy(ivData, offset, ivDataWithPadding, ivDataWithPadding.length - ivData.length System.arraycopy(ivData, offset, ivDataWithPadding, ivDataWithPadding.length - ivData.length
+ offset, ivData.length - offset); + offset, ivData.length - offset);
encryptedDataSource = new Aes128DataSource(secretKey, ivDataWithPadding, upstreamDataSource); encryptedDataSource = new Aes128DataSource(upstreamDataSource, secretKey, ivDataWithPadding);
encryptionKeyUri = keyUri; encryptionKeyUri = keyUri;
encryptedDataSourceIv = iv; encryptedDataSourceIv = iv;
encryptedDataSourceSecretKey = secretKey; encryptedDataSourceSecretKey = secretKey;
...@@ -545,8 +550,8 @@ public class HlsChunkSource { ...@@ -545,8 +550,8 @@ public class HlsChunkSource {
} }
private boolean allPlaylistsBlacklisted() { private boolean allPlaylistsBlacklisted() {
for (int i = 0; i < mediaPlaylistBlacklistFlags.length; i++) { for (int i = 0; i < mediaPlaylistBlacklistTimesMs.length; i++) {
if (!mediaPlaylistBlacklistFlags[i]) { if (mediaPlaylistBlacklistTimesMs[i] == 0) {
return false; return false;
} }
} }
...@@ -555,11 +560,10 @@ public class HlsChunkSource { ...@@ -555,11 +560,10 @@ public class HlsChunkSource {
private void clearStaleBlacklistedPlaylists() { private void clearStaleBlacklistedPlaylists() {
long currentTime = SystemClock.elapsedRealtime(); long currentTime = SystemClock.elapsedRealtime();
for (int i = 0; i < mediaPlaylistBlacklistFlags.length; i++) { for (int i = 0; i < mediaPlaylistBlacklistTimesMs.length; i++) {
if (mediaPlaylistBlacklistFlags[i] && if (mediaPlaylistBlacklistTimesMs[i] != 0
currentTime - mediaPlaylistBlacklistedTimeMs[i] > DEFAULT_MAX_TIME_MEDIA_PLAYLIST_BLACKLISTED_MS) { && currentTime - mediaPlaylistBlacklistTimesMs[i] > DEFAULT_PLAYLIST_BLACKLIST_MS) {
mediaPlaylistBlacklistFlags[i] = false; mediaPlaylistBlacklistTimesMs[i] = 0;
mediaPlaylistBlacklistedTimeMs[i] = 0;
} }
} }
} }
......
...@@ -13,27 +13,44 @@ ...@@ -13,27 +13,44 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package com.google.android.exoplayer.hls.parser; package com.google.android.exoplayer.hls;
import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.SampleHolder; import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.extractor.DefaultTrackOutput;
import com.google.android.exoplayer.extractor.Extractor;
import com.google.android.exoplayer.extractor.ExtractorInput;
import com.google.android.exoplayer.extractor.ExtractorOutput;
import com.google.android.exoplayer.extractor.TrackOutput;
import com.google.android.exoplayer.upstream.BufferPool;
import com.google.android.exoplayer.util.Assertions;
import android.util.SparseArray;
import java.io.IOException; import java.io.IOException;
/** /**
* Facilitates extraction of media samples for HLS playbacks. * Wraps a {@link Extractor}, adding functionality to enable reading of the extracted samples.
*/ */
// TODO: Consider consolidating more common logic in this base class. public final class HlsExtractorWrapper implements ExtractorOutput {
public abstract class HlsExtractor {
private final BufferPool bufferPool;
private final Extractor extractor;
private final SparseArray<DefaultTrackOutput> sampleQueues;
private final boolean shouldSpliceIn; private final boolean shouldSpliceIn;
private volatile boolean tracksBuilt;
// Accessed only by the consuming thread. // Accessed only by the consuming thread.
private boolean prepared;
private boolean spliceConfigured; private boolean spliceConfigured;
public HlsExtractor(boolean shouldSpliceIn) { public HlsExtractorWrapper(BufferPool bufferPool, Extractor extractor, boolean shouldSpliceIn) {
this.bufferPool = bufferPool;
this.extractor = extractor;
this.shouldSpliceIn = shouldSpliceIn; this.shouldSpliceIn = shouldSpliceIn;
sampleQueues = new SparseArray<DefaultTrackOutput>();
extractor.init(this);
} }
/** /**
...@@ -50,7 +67,7 @@ public abstract class HlsExtractor { ...@@ -50,7 +67,7 @@ public abstract class HlsExtractor {
* *
* @param nextExtractor The extractor being spliced to. * @param nextExtractor The extractor being spliced to.
*/ */
public final void configureSpliceTo(HlsExtractor nextExtractor) { public final void configureSpliceTo(HlsExtractorWrapper nextExtractor) {
if (spliceConfigured || !nextExtractor.shouldSpliceIn || !nextExtractor.isPrepared()) { if (spliceConfigured || !nextExtractor.shouldSpliceIn || !nextExtractor.isPrepared()) {
// The splice is already configured, or the next extractor doesn't want to be spliced in, or // 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. // the next extractor isn't ready to be spliced in.
...@@ -59,7 +76,9 @@ public abstract class HlsExtractor { ...@@ -59,7 +76,9 @@ public abstract class HlsExtractor {
boolean spliceConfigured = true; boolean spliceConfigured = true;
int trackCount = getTrackCount(); int trackCount = getTrackCount();
for (int i = 0; i < trackCount; i++) { for (int i = 0; i < trackCount; i++) {
spliceConfigured &= getSampleQueue(i).configureSpliceTo(nextExtractor.getSampleQueue(i)); DefaultTrackOutput currentSampleQueue = sampleQueues.valueAt(i);
DefaultTrackOutput nextSampleQueue = nextExtractor.sampleQueues.valueAt(i);
spliceConfigured &= currentSampleQueue.configureSpliceTo(nextSampleQueue);
} }
this.spliceConfigured = spliceConfigured; this.spliceConfigured = spliceConfigured;
return; return;
...@@ -72,7 +91,9 @@ public abstract class HlsExtractor { ...@@ -72,7 +91,9 @@ public abstract class HlsExtractor {
* *
* @return The number of available tracks. * @return The number of available tracks.
*/ */
public abstract int getTrackCount(); public int getTrackCount() {
return sampleQueues.size();
}
/** /**
* Gets the format of the specified track. * Gets the format of the specified track.
...@@ -82,28 +103,49 @@ public abstract class HlsExtractor { ...@@ -82,28 +103,49 @@ public abstract class HlsExtractor {
* @param track The track index. * @param track The track index.
* @return The corresponding format. * @return The corresponding format.
*/ */
public abstract MediaFormat getFormat(int track); public MediaFormat getFormat(int track) {
return sampleQueues.valueAt(track).getFormat();
}
/** /**
* Whether the extractor is prepared. * Whether the extractor is prepared.
* *
* @return True if the extractor is prepared. False otherwise. * @return True if the extractor is prepared. False otherwise.
*/ */
public abstract boolean isPrepared(); public boolean isPrepared() {
if (!prepared && tracksBuilt) {
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. * Clears queues for all tracks, returning all allocations to the buffer pool.
* <p>
* This method should not be called whilst {@link #read(DataSource)} is also being invoked.
*/ */
public abstract void release(); public void clear() {
for (int i = 0; i < sampleQueues.size(); i++) {
sampleQueues.valueAt(i).clear();
}
}
/** /**
* Gets the largest timestamp of any sample parsed by the extractor. * 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. * @return The largest timestamp, or {@link Long#MIN_VALUE} if no samples have been parsed.
*/ */
public abstract long getLargestSampleTimestamp(); public long getLargestParsedTimestampUs() {
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. * Gets the next sample for the specified track.
...@@ -112,7 +154,10 @@ public abstract class HlsExtractor { ...@@ -112,7 +154,10 @@ public abstract class HlsExtractor {
* @param holder A {@link SampleHolder} into which the sample should be read. * @param holder A {@link SampleHolder} into which the sample should be read.
* @return True if a sample was read. False otherwise. * @return True if a sample was read. False otherwise.
*/ */
public abstract boolean getSample(int track, SampleHolder holder); 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. * Discards samples for the specified track up to the specified time.
...@@ -120,7 +165,10 @@ public abstract class HlsExtractor { ...@@ -120,7 +165,10 @@ public abstract class HlsExtractor {
* @param track The track from which samples should be discarded. * @param track The track from which samples should be discarded.
* @param timeUs The time up to which samples should be discarded, in microseconds. * @param timeUs The time up to which samples should be discarded, in microseconds.
*/ */
public abstract void discardUntil(int track, long timeUs); 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 * Whether samples are available for reading from {@link #getSample(int, SampleHolder)} for the
...@@ -129,23 +177,36 @@ public abstract class HlsExtractor { ...@@ -129,23 +177,36 @@ public abstract class HlsExtractor {
* @return True if samples are available for reading from {@link #getSample(int, SampleHolder)} * @return True if samples are available for reading from {@link #getSample(int, SampleHolder)}
* for the specified track. False otherwise. * for the specified track. False otherwise.
*/ */
public abstract boolean hasSamples(int track); public boolean hasSamples(int track) {
Assertions.checkState(isPrepared());
return !sampleQueues.valueAt(track).isEmpty();
}
/** /**
* Reads up to a single TS packet. * Reads from the provided {@link ExtractorInput}.
* *
* @param dataSource The {@link DataSource} from which to read. * @param input The {@link ExtractorInput} from which to read.
* @return One of {@link Extractor#RESULT_CONTINUE} and {@link Extractor#RESULT_END_OF_INPUT}.
* @throws IOException If an error occurred reading from the source. * @throws IOException If an error occurred reading from the source.
* @return The number of bytes read from the source. * @throws InterruptedException If the thread was interrupted.
*/ */
public abstract int read(DataSource dataSource) throws IOException; public int read(ExtractorInput input) throws IOException, InterruptedException {
int result = extractor.read(input);
return result;
}
/** // ExtractorOutput implementation.
* Gets the {@link SampleQueue} for the specified track.
* @Override
* @param track The track index. public TrackOutput track(int id) {
* @return The corresponding sample queue. DefaultTrackOutput sampleQueue = new DefaultTrackOutput(bufferPool);
*/ sampleQueues.put(id, sampleQueue);
protected abstract SampleQueue getSampleQueue(int track); return sampleQueue;
}
@Override
public void endTracks() {
this.tracksBuilt = true;
}
} }
...@@ -15,8 +15,6 @@ ...@@ -15,8 +15,6 @@
*/ */
package com.google.android.exoplayer.hls; package com.google.android.exoplayer.hls;
import android.net.Uri;
import java.util.List; import java.util.List;
/** /**
...@@ -25,10 +23,12 @@ import java.util.List; ...@@ -25,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(Uri 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;
} }
} }
...@@ -17,8 +17,6 @@ package com.google.android.exoplayer.hls; ...@@ -17,8 +17,6 @@ package com.google.android.exoplayer.hls;
import com.google.android.exoplayer.C; import com.google.android.exoplayer.C;
import android.net.Uri;
import java.util.List; import java.util.List;
/** /**
...@@ -30,24 +28,25 @@ public final class HlsMediaPlaylist extends HlsPlaylist { ...@@ -30,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;
...@@ -70,7 +69,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist { ...@@ -70,7 +69,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
public final boolean live; public final boolean live;
public final long durationUs; public final long durationUs;
public HlsMediaPlaylist(Uri baseUri, int mediaSequence, int targetDurationSecs, int version, public HlsMediaPlaylist(String baseUri, int mediaSequence, int targetDurationSecs, int version,
boolean live, List<Segment> segments) { boolean live, List<Segment> segments) {
super(baseUri, HlsPlaylist.TYPE_MEDIA); super(baseUri, HlsPlaylist.TYPE_MEDIA);
this.mediaSequence = mediaSequence; this.mediaSequence = mediaSequence;
......
...@@ -23,7 +23,10 @@ import java.util.regex.Pattern; ...@@ -23,7 +23,10 @@ import java.util.regex.Pattern;
/** /**
* Utility methods for HLS manifest parsing. * Utility methods for HLS manifest parsing.
*/ */
/* package */ class HlsParserUtil { /* package */ final class HlsParserUtil {
private static final String BOOLEAN_YES = "YES";
private static final String BOOLEAN_NO = "NO";
private HlsParserUtil() {} private HlsParserUtil() {}
...@@ -36,6 +39,16 @@ import java.util.regex.Pattern; ...@@ -36,6 +39,16 @@ 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 int parseIntAttr(String line, Pattern pattern, String tag)
throws ParserException {
return Integer.parseInt(parseStringAttr(line, pattern, tag));
}
public static double parseDoubleAttr(String line, Pattern pattern, String tag)
throws ParserException {
return Double.parseDouble(parseStringAttr(line, pattern, tag));
}
public static String parseOptionalStringAttr(String line, Pattern pattern) { public static String parseOptionalStringAttr(String line, Pattern pattern) {
Matcher matcher = pattern.matcher(line); Matcher matcher = pattern.matcher(line);
if (matcher.find() && matcher.groupCount() == 1) { if (matcher.find() && matcher.groupCount() == 1) {
...@@ -44,14 +57,16 @@ import java.util.regex.Pattern; ...@@ -44,14 +57,16 @@ import java.util.regex.Pattern;
return null; return null;
} }
public static int parseIntAttr(String line, Pattern pattern, String tag) public static boolean parseOptionalBooleanAttr(String line, Pattern pattern) {
throws ParserException { Matcher matcher = pattern.matcher(line);
return Integer.parseInt(parseStringAttr(line, pattern, tag)); if (matcher.find() && matcher.groupCount() == 1) {
return BOOLEAN_YES.equals(matcher.group(1));
}
return false;
} }
public static double parseDoubleAttr(String line, Pattern pattern, String tag) public static Pattern compileBooleanAttrPattern(String attrName) {
throws ParserException { return Pattern.compile(attrName + "=(" + BOOLEAN_YES + "|" + BOOLEAN_NO + ")");
return Double.parseDouble(parseStringAttr(line, pattern, tag));
} }
} }
...@@ -15,7 +15,6 @@ ...@@ -15,7 +15,6 @@
*/ */
package com.google.android.exoplayer.hls; package com.google.android.exoplayer.hls;
import android.net.Uri;
/** /**
* Represents an HLS playlist. * Represents an HLS playlist.
...@@ -25,10 +24,10 @@ public abstract class HlsPlaylist { ...@@ -25,10 +24,10 @@ public abstract class HlsPlaylist {
public final static int TYPE_MASTER = 0; public final static int TYPE_MASTER = 0;
public final static int TYPE_MEDIA = 1; public final static int TYPE_MEDIA = 1;
public final Uri baseUri; public final String baseUri;
public final int type; public final int type;
protected HlsPlaylist(Uri baseUri, int type) { protected HlsPlaylist(String baseUri, int type) {
this.baseUri = baseUri; this.baseUri = baseUri;
this.type = type; this.type = type;
} }
......
...@@ -19,9 +19,6 @@ import com.google.android.exoplayer.C; ...@@ -19,9 +19,6 @@ import com.google.android.exoplayer.C;
import com.google.android.exoplayer.ParserException; import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.hls.HlsMediaPlaylist.Segment; import com.google.android.exoplayer.hls.HlsMediaPlaylist.Segment;
import com.google.android.exoplayer.upstream.NetworkLoadable; import com.google.android.exoplayer.upstream.NetworkLoadable;
import com.google.android.exoplayer.util.Util;
import android.net.Uri;
import java.io.BufferedReader; import java.io.BufferedReader;
import java.io.IOException; import java.io.IOException;
...@@ -40,12 +37,8 @@ import java.util.regex.Pattern; ...@@ -40,12 +37,8 @@ import java.util.regex.Pattern;
public final class HlsPlaylistParser implements NetworkLoadable.Parser<HlsPlaylist> { public final class HlsPlaylistParser implements NetworkLoadable.Parser<HlsPlaylist> {
private static final String VERSION_TAG = "#EXT-X-VERSION"; private static final String VERSION_TAG = "#EXT-X-VERSION";
private static final String STREAM_INF_TAG = "#EXT-X-STREAM-INF"; private static final String STREAM_INF_TAG = "#EXT-X-STREAM-INF";
private static final String BANDWIDTH_ATTR = "BANDWIDTH"; private static final String MEDIA_TAG = "#EXT-X-MEDIA";
private static final String CODECS_ATTR = "CODECS";
private static final String RESOLUTION_ATTR = "RESOLUTION";
private static final String DISCONTINUITY_TAG = "#EXT-X-DISCONTINUITY"; private static final String DISCONTINUITY_TAG = "#EXT-X-DISCONTINUITY";
private static final String MEDIA_DURATION_TAG = "#EXTINF"; private static final String MEDIA_DURATION_TAG = "#EXTINF";
private static final String MEDIA_SEQUENCE_TAG = "#EXT-X-MEDIA-SEQUENCE"; private static final String MEDIA_SEQUENCE_TAG = "#EXT-X-MEDIA-SEQUENCE";
...@@ -54,17 +47,32 @@ public final class HlsPlaylistParser implements NetworkLoadable.Parser<HlsPlayli ...@@ -54,17 +47,32 @@ public final class HlsPlaylistParser implements NetworkLoadable.Parser<HlsPlayli
private static final String KEY_TAG = "#EXT-X-KEY"; private static final String KEY_TAG = "#EXT-X-KEY";
private static final String BYTERANGE_TAG = "#EXT-X-BYTERANGE"; private static final String BYTERANGE_TAG = "#EXT-X-BYTERANGE";
private static final String BANDWIDTH_ATTR = "BANDWIDTH";
private static final String CODECS_ATTR = "CODECS";
private static final String RESOLUTION_ATTR = "RESOLUTION";
private static final String LANGUAGE_ATTR = "LANGUAGE";
private static final String NAME_ATTR = "NAME";
private static final String AUTOSELECT_ATTR = "AUTOSELECT";
private static final String DEFAULT_ATTR = "DEFAULT";
private static final String TYPE_ATTR = "TYPE";
private static final String METHOD_ATTR = "METHOD"; private static final String METHOD_ATTR = "METHOD";
private static final String URI_ATTR = "URI"; private static final String URI_ATTR = "URI";
private static final String IV_ATTR = "IV"; private static final String IV_ATTR = "IV";
private static final String AUDIO_TYPE = "AUDIO";
private static final String VIDEO_TYPE = "VIDEO";
private static final String SUBTITLES_TYPE = "SUBTITLES";
private static final String CLOSED_CAPTIONS_TYPE = "CLOSED-CAPTIONS";
private static final String METHOD_NONE = "NONE";
private static final String METHOD_AES128 = "AES-128";
private static final Pattern BANDWIDTH_ATTR_REGEX = private static final Pattern BANDWIDTH_ATTR_REGEX =
Pattern.compile(BANDWIDTH_ATTR + "=(\\d+)\\b"); Pattern.compile(BANDWIDTH_ATTR + "=(\\d+)\\b");
private static final Pattern CODECS_ATTR_REGEX = private static final Pattern CODECS_ATTR_REGEX =
Pattern.compile(CODECS_ATTR + "=\"(.+?)\""); Pattern.compile(CODECS_ATTR + "=\"(.+?)\"");
private static final Pattern RESOLUTION_ATTR_REGEX = private static final Pattern RESOLUTION_ATTR_REGEX =
Pattern.compile(RESOLUTION_ATTR + "=(\\d+x\\d+)"); Pattern.compile(RESOLUTION_ATTR + "=(\\d+x\\d+)");
private static final Pattern MEDIA_DURATION_REGEX = private static final Pattern MEDIA_DURATION_REGEX =
Pattern.compile(MEDIA_DURATION_TAG + ":([\\d.]+),"); Pattern.compile(MEDIA_DURATION_TAG + ":([\\d.]+),");
private static final Pattern MEDIA_SEQUENCE_REGEX = private static final Pattern MEDIA_SEQUENCE_REGEX =
...@@ -77,16 +85,26 @@ public final class HlsPlaylistParser implements NetworkLoadable.Parser<HlsPlayli ...@@ -77,16 +85,26 @@ public final class HlsPlaylistParser implements NetworkLoadable.Parser<HlsPlayli
Pattern.compile(BYTERANGE_TAG + ":(\\d+(?:@\\d+)?)\\b"); Pattern.compile(BYTERANGE_TAG + ":(\\d+(?:@\\d+)?)\\b");
private static final Pattern METHOD_ATTR_REGEX = private static final Pattern METHOD_ATTR_REGEX =
Pattern.compile(METHOD_ATTR + "=([^,.*]+)"); Pattern.compile(METHOD_ATTR + "=(" + METHOD_NONE + "|" + METHOD_AES128 + ")");
private static final Pattern URI_ATTR_REGEX = private static final Pattern URI_ATTR_REGEX =
Pattern.compile(URI_ATTR + "=\"(.+)\""); Pattern.compile(URI_ATTR + "=\"(.+)\"");
private static final Pattern IV_ATTR_REGEX = private static final Pattern IV_ATTR_REGEX =
Pattern.compile(IV_ATTR + "=([^,.*]+)"); Pattern.compile(IV_ATTR + "=([^,.*]+)");
private static final Pattern TYPE_ATTR_REGEX =
Pattern.compile(TYPE_ATTR + "=(" + AUDIO_TYPE + "|" + VIDEO_TYPE + "|" + SUBTITLES_TYPE + "|"
+ CLOSED_CAPTIONS_TYPE + ")");
private static final Pattern LANGUAGE_ATTR_REGEX =
Pattern.compile(LANGUAGE_ATTR + "=\"(.+?)\"");
private static final Pattern NAME_ATTR_REGEX =
Pattern.compile(NAME_ATTR + "=\"(.+?)\"");
private static final Pattern AUTOSELECT_ATTR_REGEX =
HlsParserUtil.compileBooleanAttrPattern(AUTOSELECT_ATTR);
private static final Pattern DEFAULT_ATTR_REGEX =
HlsParserUtil.compileBooleanAttrPattern(DEFAULT_ATTR);
@Override @Override
public HlsPlaylist parse(String connectionUrl, InputStream inputStream) public HlsPlaylist parse(String connectionUrl, InputStream inputStream)
throws IOException, ParserException { throws IOException, ParserException {
Uri baseUri = Util.parseBaseUri(connectionUrl);
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
Queue<String> extraLines = new LinkedList<String>(); Queue<String> extraLines = new LinkedList<String>();
String line; String line;
...@@ -97,7 +115,7 @@ public final class HlsPlaylistParser implements NetworkLoadable.Parser<HlsPlayli ...@@ -97,7 +115,7 @@ public final class HlsPlaylistParser implements NetworkLoadable.Parser<HlsPlayli
// Do nothing. // Do nothing.
} else if (line.startsWith(STREAM_INF_TAG)) { } else if (line.startsWith(STREAM_INF_TAG)) {
extraLines.add(line); extraLines.add(line);
return parseMasterPlaylist(new LineIterator(extraLines, reader), baseUri); return parseMasterPlaylist(new LineIterator(extraLines, reader), connectionUrl);
} else if (line.startsWith(TARGET_DURATION_TAG) } else if (line.startsWith(TARGET_DURATION_TAG)
|| line.startsWith(MEDIA_SEQUENCE_TAG) || line.startsWith(MEDIA_SEQUENCE_TAG)
|| line.startsWith(MEDIA_DURATION_TAG) || line.startsWith(MEDIA_DURATION_TAG)
...@@ -106,11 +124,9 @@ public final class HlsPlaylistParser implements NetworkLoadable.Parser<HlsPlayli ...@@ -106,11 +124,9 @@ public final class HlsPlaylistParser implements NetworkLoadable.Parser<HlsPlayli
|| line.equals(DISCONTINUITY_TAG) || line.equals(DISCONTINUITY_TAG)
|| line.equals(ENDLIST_TAG)) { || line.equals(ENDLIST_TAG)) {
extraLines.add(line); extraLines.add(line);
return parseMediaPlaylist(new LineIterator(extraLines, reader), baseUri); return parseMediaPlaylist(new LineIterator(extraLines, reader), connectionUrl);
} else if (line.startsWith(VERSION_TAG)) { } else {
extraLines.add(line); extraLines.add(line);
} else if (!line.startsWith("#")) {
throw new ParserException("Missing a tag before URL.");
} }
} }
} finally { } finally {
...@@ -119,19 +135,34 @@ public final class HlsPlaylistParser implements NetworkLoadable.Parser<HlsPlayli ...@@ -119,19 +135,34 @@ public final class HlsPlaylistParser implements NetworkLoadable.Parser<HlsPlayli
throw new ParserException("Failed to parse the playlist, could not identify any tags."); throw new ParserException("Failed to parse the playlist, could not identify any tags.");
} }
private static HlsMasterPlaylist parseMasterPlaylist(LineIterator iterator, Uri baseUri) private static HlsMasterPlaylist parseMasterPlaylist(LineIterator iterator, String baseUri)
throws IOException { throws IOException {
List<Variant> variants = new ArrayList<Variant>(); ArrayList<Variant> variants = new ArrayList<Variant>();
ArrayList<Subtitle> subtitles = new ArrayList<Subtitle>();
int bandwidth = 0; int bandwidth = 0;
String[] codecs = null; String[] codecs = null;
int width = -1; int width = -1;
int height = -1; int height = -1;
int variantIndex = 0; int variantIndex = 0;
boolean expectingStreamInfUrl = false;
String line; String line;
while (iterator.hasNext()) { while (iterator.hasNext()) {
line = iterator.next(); line = iterator.next();
if (line.startsWith(STREAM_INF_TAG)) { if (line.startsWith(MEDIA_TAG)) {
String type = HlsParserUtil.parseStringAttr(line, TYPE_ATTR_REGEX, TYPE_ATTR);
if (SUBTITLES_TYPE.equals(type)) {
// We assume all subtitles belong to the same group.
String name = HlsParserUtil.parseStringAttr(line, NAME_ATTR_REGEX, NAME_ATTR);
String uri = HlsParserUtil.parseStringAttr(line, URI_ATTR_REGEX, URI_ATTR);
String language = HlsParserUtil.parseOptionalStringAttr(line, LANGUAGE_ATTR_REGEX);
boolean isDefault = HlsParserUtil.parseOptionalBooleanAttr(line, DEFAULT_ATTR_REGEX);
boolean autoSelect = HlsParserUtil.parseOptionalBooleanAttr(line, AUTOSELECT_ATTR_REGEX);
subtitles.add(new Subtitle(name, uri, language, isDefault, autoSelect));
} else {
// TODO: Support other types of media tag.
}
} else if (line.startsWith(STREAM_INF_TAG)) {
bandwidth = HlsParserUtil.parseIntAttr(line, BANDWIDTH_ATTR_REGEX, BANDWIDTH_ATTR); bandwidth = HlsParserUtil.parseIntAttr(line, BANDWIDTH_ATTR_REGEX, BANDWIDTH_ATTR);
String codecsString = HlsParserUtil.parseOptionalStringAttr(line, CODECS_ATTR_REGEX); String codecsString = HlsParserUtil.parseOptionalStringAttr(line, CODECS_ATTR_REGEX);
if (codecsString != null) { if (codecsString != null) {
...@@ -149,18 +180,21 @@ public final class HlsPlaylistParser implements NetworkLoadable.Parser<HlsPlayli ...@@ -149,18 +180,21 @@ public final class HlsPlaylistParser implements NetworkLoadable.Parser<HlsPlayli
width = -1; width = -1;
height = -1; height = -1;
} }
} else if (!line.startsWith("#")) { expectingStreamInfUrl = true;
} else if (!line.startsWith("#") && expectingStreamInfUrl) {
variants.add(new Variant(variantIndex++, line, bandwidth, codecs, width, height)); variants.add(new Variant(variantIndex++, line, bandwidth, codecs, width, height));
bandwidth = 0; bandwidth = 0;
codecs = null; codecs = null;
width = -1; width = -1;
height = -1; height = -1;
expectingStreamInfUrl = false;
} }
} }
return new HlsMasterPlaylist(baseUri, Collections.unmodifiableList(variants)); return new HlsMasterPlaylist(baseUri, Collections.unmodifiableList(variants),
Collections.unmodifiableList(subtitles));
} }
private static HlsMediaPlaylist parseMediaPlaylist(LineIterator iterator, Uri baseUri) private static HlsMediaPlaylist parseMediaPlaylist(LineIterator iterator, String baseUri)
throws IOException { throws IOException {
int mediaSequence = 0; int mediaSequence = 0;
int targetDurationSecs = 0; int targetDurationSecs = 0;
...@@ -171,14 +205,14 @@ public final class HlsPlaylistParser implements NetworkLoadable.Parser<HlsPlayli ...@@ -171,14 +205,14 @@ public final class HlsPlaylistParser implements NetworkLoadable.Parser<HlsPlayli
double segmentDurationSecs = 0.0; double segmentDurationSecs = 0.0;
boolean segmentDiscontinuity = false; boolean segmentDiscontinuity = false;
long segmentStartTimeUs = 0; long segmentStartTimeUs = 0;
String segmentEncryptionMethod = null;
String segmentEncryptionKeyUri = null;
String segmentEncryptionIV = null;
int segmentByterangeOffset = 0; int segmentByterangeOffset = 0;
int segmentByterangeLength = C.LENGTH_UNBOUNDED; int segmentByterangeLength = C.LENGTH_UNBOUNDED;
int segmentMediaSequence = 0; int segmentMediaSequence = 0;
boolean isEncrypted = false;
String encryptionKeyUri = null;
String encryptionIV = null;
String line; String line;
while (iterator.hasNext()) { while (iterator.hasNext()) {
line = iterator.next(); line = iterator.next();
...@@ -194,18 +228,14 @@ public final class HlsPlaylistParser implements NetworkLoadable.Parser<HlsPlayli ...@@ -194,18 +228,14 @@ public final class HlsPlaylistParser implements NetworkLoadable.Parser<HlsPlayli
segmentDurationSecs = HlsParserUtil.parseDoubleAttr(line, MEDIA_DURATION_REGEX, segmentDurationSecs = HlsParserUtil.parseDoubleAttr(line, MEDIA_DURATION_REGEX,
MEDIA_DURATION_TAG); MEDIA_DURATION_TAG);
} else if (line.startsWith(KEY_TAG)) { } else if (line.startsWith(KEY_TAG)) {
segmentEncryptionMethod = HlsParserUtil.parseStringAttr(line, METHOD_ATTR_REGEX, String method = HlsParserUtil.parseStringAttr(line, METHOD_ATTR_REGEX, METHOD_ATTR);
METHOD_ATTR); isEncrypted = METHOD_AES128.equals(method);
if (segmentEncryptionMethod.equals(HlsMediaPlaylist.ENCRYPTION_METHOD_NONE)) { if (isEncrypted) {
segmentEncryptionKeyUri = null; encryptionKeyUri = HlsParserUtil.parseStringAttr(line, URI_ATTR_REGEX, URI_ATTR);
segmentEncryptionIV = null; encryptionIV = HlsParserUtil.parseOptionalStringAttr(line, IV_ATTR_REGEX);
} else { } else {
segmentEncryptionKeyUri = HlsParserUtil.parseStringAttr(line, URI_ATTR_REGEX, encryptionKeyUri = null;
URI_ATTR); encryptionIV = null;
segmentEncryptionIV = HlsParserUtil.parseOptionalStringAttr(line, IV_ATTR_REGEX);
if (segmentEncryptionIV == null) {
segmentEncryptionIV = Integer.toHexString(segmentMediaSequence);
}
} }
} else if (line.startsWith(BYTERANGE_TAG)) { } else if (line.startsWith(BYTERANGE_TAG)) {
String byteRange = HlsParserUtil.parseStringAttr(line, BYTERANGE_REGEX, BYTERANGE_TAG); String byteRange = HlsParserUtil.parseStringAttr(line, BYTERANGE_REGEX, BYTERANGE_TAG);
...@@ -217,13 +247,21 @@ public final class HlsPlaylistParser implements NetworkLoadable.Parser<HlsPlayli ...@@ -217,13 +247,21 @@ public final class HlsPlaylistParser implements NetworkLoadable.Parser<HlsPlayli
} else if (line.equals(DISCONTINUITY_TAG)) { } else if (line.equals(DISCONTINUITY_TAG)) {
segmentDiscontinuity = true; segmentDiscontinuity = true;
} else if (!line.startsWith("#")) { } else if (!line.startsWith("#")) {
String segmentEncryptionIV;
if (!isEncrypted) {
segmentEncryptionIV = null;
} else if (encryptionIV != null) {
segmentEncryptionIV = encryptionIV;
} else {
segmentEncryptionIV = Integer.toHexString(segmentMediaSequence);
}
segmentMediaSequence++; segmentMediaSequence++;
if (segmentByterangeLength == C.LENGTH_UNBOUNDED) { if (segmentByterangeLength == C.LENGTH_UNBOUNDED) {
segmentByterangeOffset = 0; segmentByterangeOffset = 0;
} }
segments.add(new Segment(line, segmentDurationSecs, segmentDiscontinuity, segments.add(new Segment(line, segmentDurationSecs, segmentDiscontinuity,
segmentStartTimeUs, segmentEncryptionMethod, segmentEncryptionKeyUri, segmentStartTimeUs, isEncrypted, encryptionKeyUri, segmentEncryptionIV,
segmentEncryptionIV, segmentByterangeOffset, segmentByterangeLength)); segmentByterangeOffset, segmentByterangeLength));
segmentStartTimeUs += (long) (segmentDurationSecs * C.MICROS_PER_SECOND); segmentStartTimeUs += (long) (segmentDurationSecs * C.MICROS_PER_SECOND);
segmentDiscontinuity = false; segmentDiscontinuity = false;
segmentDurationSecs = 0.0; segmentDurationSecs = 0.0;
......
...@@ -15,13 +15,13 @@ ...@@ -15,13 +15,13 @@
*/ */
package com.google.android.exoplayer.hls; package com.google.android.exoplayer.hls;
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.MediaFormatHolder;
import com.google.android.exoplayer.SampleHolder; import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.SampleSource; import com.google.android.exoplayer.SampleSource;
import com.google.android.exoplayer.TrackInfo; import com.google.android.exoplayer.TrackInfo;
import com.google.android.exoplayer.TrackRenderer; import com.google.android.exoplayer.TrackRenderer;
import com.google.android.exoplayer.hls.parser.HlsExtractor;
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,8 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { ...@@ -83,7 +83,8 @@ 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>(); this.pendingResetPositionUs = NO_RESET_PENDING;
extractors = new LinkedList<HlsExtractorWrapper>();
} }
@Override @Override
...@@ -96,7 +97,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { ...@@ -96,7 +97,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];
...@@ -190,12 +191,16 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { ...@@ -190,12 +191,16 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
return DISCONTINUITY_READ; return DISCONTINUITY_READ;
} }
if (onlyReadDiscontinuity || isPendingReset() || extractors.isEmpty()) { if (onlyReadDiscontinuity) {
return NOTHING_READ;
}
if (isPendingReset()) {
maybeThrowLoadableException(); maybeThrowLoadableException();
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.
...@@ -223,7 +228,8 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { ...@@ -223,7 +228,8 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
} }
if (extractor.getSample(track, sampleHolder)) { if (extractor.getSample(track, sampleHolder)) {
sampleHolder.decodeOnly = frameAccurateSeeking && sampleHolder.timeUs < lastSeekPositionUs; boolean decodeOnly = frameAccurateSeeking && sampleHolder.timeUs < lastSeekPositionUs;
sampleHolder.flags |= decodeOnly ? C.SAMPLE_FLAG_DECODE_ONLY : 0;
return SAMPLE_READ; return SAMPLE_READ;
} }
...@@ -240,10 +246,11 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { ...@@ -240,10 +246,11 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
Assertions.checkState(prepared); Assertions.checkState(prepared);
Assertions.checkState(enabledTrackCount > 0); Assertions.checkState(enabledTrackCount > 0);
lastSeekPositionUs = positionUs; lastSeekPositionUs = positionUs;
if (pendingResetPositionUs == positionUs || downstreamPositionUs == positionUs) { if ((isPendingReset() ? pendingResetPositionUs : downstreamPositionUs) == positionUs) {
downstreamPositionUs = positionUs;
return; return;
} }
// TODO: Optimize the seek for the case where the position is already buffered.
downstreamPositionUs = positionUs; downstreamPositionUs = positionUs;
for (int i = 0; i < pendingDiscontinuities.length; i++) { for (int i = 0; i < pendingDiscontinuities.length; i++) {
pendingDiscontinuities[i] = true; pendingDiscontinuities[i] = true;
...@@ -260,9 +267,9 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { ...@@ -260,9 +267,9 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
} else if (loadingFinished) { } else if (loadingFinished) {
return TrackRenderer.END_OF_TRACK_US; return TrackRenderer.END_OF_TRACK_US;
} else { } else {
long largestSampleTimestamp = extractors.getLast().getLargestSampleTimestamp(); long largestParsedTimestampUs = extractors.getLast().getLargestParsedTimestampUs();
return largestSampleTimestamp == Long.MIN_VALUE ? downstreamPositionUs return largestParsedTimestampUs == Long.MIN_VALUE ? downstreamPositionUs
: largestSampleTimestamp; : largestParsedTimestampUs;
} }
} }
...@@ -328,17 +335,17 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { ...@@ -328,17 +335,17 @@ 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().clear();
extractor = extractors.getFirst(); extractor = extractors.getFirst();
} }
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 +356,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { ...@@ -349,7 +356,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;
} }
...@@ -381,7 +388,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { ...@@ -381,7 +388,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
private void clearState() { private void clearState() {
for (int i = 0; i < extractors.size(); i++) { for (int i = 0; i < extractors.size(); i++) {
extractors.get(i).release(); extractors.get(i).clear();
} }
extractors.clear(); extractors.clear();
clearCurrentLoadable(); clearCurrentLoadable();
......
/*
* 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.extractor.DefaultExtractorInput;
import com.google.android.exoplayer.extractor.Extractor;
import com.google.android.exoplayer.extractor.ExtractorInput;
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;
try { try {
dataSource.open(dataSpec); input = new DefaultExtractorInput(dataSource, 0, 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); int result = Extractor.RESULT_CONTINUE;
if (bytesRead != -1) { while (result == Extractor.RESULT_CONTINUE && !loadCanceled) {
bytesSkipped += bytesRead; result = extractor.read(input);
}
}
// 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;
} finally { } finally {
dataSource.close(); dataSource.close();
} }
......
/*
* 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.C;
import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.upstream.BufferPool;
import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.ParsableByteArray;
import java.nio.ByteBuffer;
import java.util.concurrent.ConcurrentLinkedQueue;
/**
* A rolling buffer of sample data and corresponding sample information.
*/
/* package */ final class RollingSampleBuffer {
private final BufferPool fragmentPool;
private final int fragmentLength;
private final InfoQueue infoQueue;
private final ConcurrentLinkedQueue<byte[]> dataQueue;
private final long[] dataOffsetHolder;
// Accessed only by the consuming thread.
private long totalBytesDropped;
// Accessed only by the loading thread.
private long totalBytesWritten;
private byte[] lastFragment;
private int lastFragmentOffset;
private long pendingSampleTimeUs;
private long pendingSampleOffset;
public RollingSampleBuffer(BufferPool bufferPool) {
this.fragmentPool = bufferPool;
fragmentLength = bufferPool.bufferLength;
infoQueue = new InfoQueue();
dataQueue = new ConcurrentLinkedQueue<byte[]>();
dataOffsetHolder = new long[1];
}
public void release() {
while (!dataQueue.isEmpty()) {
fragmentPool.releaseDirect(dataQueue.remove());
}
}
// Called by the consuming thread.
/**
* Fills {@code holder} with information about the current sample, but does not write its data.
* <p>
* The fields set are {SampleHolder#size}, {SampleHolder#timeUs} and {SampleHolder#flags}.
*
* @param holder The holder into which the current sample information should be written.
* @return True if the holder was filled. False if there is no current sample.
*/
public boolean peekSample(SampleHolder holder) {
return infoQueue.peekSample(holder, dataOffsetHolder);
}
/**
* Skips the current sample.
*/
public void skipSample() {
long nextOffset = infoQueue.moveToNextSample();
dropFragmentsTo(nextOffset);
}
/**
* Reads the current sample, advancing the read index to the next sample.
*
* @param holder The holder into which the current sample should be written.
*/
public void readSample(SampleHolder holder) {
// Write the sample information into the holder.
infoQueue.peekSample(holder, dataOffsetHolder);
// Write the sample data into the holder.
if (holder.data == null || holder.data.capacity() < holder.size) {
holder.replaceBuffer(holder.size);
}
if (holder.data != null) {
readData(dataOffsetHolder[0], holder.data, holder.size);
}
// Advance the read head.
long nextOffset = infoQueue.moveToNextSample();
dropFragmentsTo(nextOffset);
}
/**
* Reads data from the front of the rolling buffer.
*
* @param absolutePosition The absolute position from which data should be read.
* @param target The buffer into which data should be written.
* @param length The number of bytes to read.
*/
private void readData(long absolutePosition, ByteBuffer target, int length) {
int remaining = length;
while (remaining > 0) {
dropFragmentsTo(absolutePosition);
int positionInFragment = (int) (absolutePosition - totalBytesDropped);
int toCopy = Math.min(remaining, fragmentLength - positionInFragment);
target.put(dataQueue.peek(), positionInFragment, toCopy);
absolutePosition += toCopy;
remaining -= toCopy;
}
}
/**
* Discard any fragments that hold data prior to the specified absolute position, returning
* them to the pool.
*
* @param absolutePosition The absolute position up to which fragments can be discarded.
*/
private void dropFragmentsTo(long absolutePosition) {
int relativePosition = (int) (absolutePosition - totalBytesDropped);
int fragmentIndex = relativePosition / fragmentLength;
for (int i = 0; i < fragmentIndex; i++) {
fragmentPool.releaseDirect(dataQueue.remove());
totalBytesDropped += fragmentLength;
}
}
// Called by the loading thread.
/**
* Indicates the start point for the next sample.
*
* @param sampleTimeUs The sample timestamp.
* @param offset The offset of the sample's data, relative to the total number of bytes written
* to the buffer. Must be negative or zero.
*/
public void startSample(long sampleTimeUs, int offset) {
Assertions.checkState(offset <= 0);
pendingSampleTimeUs = sampleTimeUs;
pendingSampleOffset = totalBytesWritten + offset;
}
/**
* Appends data to the rolling buffer.
*
* @param buffer A buffer containing the data to append.
* @param length The length of the data to append.
*/
public void appendData(ParsableByteArray buffer, int length) {
int remainingWriteLength = length;
while (remainingWriteLength > 0) {
if (dataQueue.isEmpty() || lastFragmentOffset == fragmentLength) {
lastFragmentOffset = 0;
lastFragment = fragmentPool.allocateDirect();
dataQueue.add(lastFragment);
}
int thisWriteLength = Math.min(remainingWriteLength, fragmentLength - lastFragmentOffset);
buffer.readBytes(lastFragment, lastFragmentOffset, thisWriteLength);
lastFragmentOffset += thisWriteLength;
remainingWriteLength -= thisWriteLength;
}
totalBytesWritten += length;
}
/**
* Indicates the end point for the current sample, making it available for consumption.
*
* @param isKeyframe True if the sample being committed is a keyframe. False otherwise.
* @param offset The offset of the first byte after the end of the sample's data, relative to
* the total number of bytes written to the buffer. Must be negative or zero.
*/
public void commitSample(boolean isKeyframe, int offset) {
Assertions.checkState(offset <= 0);
int sampleSize = (int) (totalBytesWritten + offset - pendingSampleOffset);
infoQueue.commitSample(pendingSampleTimeUs, pendingSampleOffset, sampleSize,
isKeyframe ? C.SAMPLE_FLAG_SYNC : 0);
}
/**
* Holds information about the samples in the rolling buffer.
*/
private static class InfoQueue {
private static final int SAMPLE_CAPACITY_INCREMENT = 1000;
private int capacity;
private long[] offsets;
private int[] sizes;
private int[] flags;
private long[] timesUs;
private int queueSize;
private int readIndex;
private int writeIndex;
public InfoQueue() {
capacity = SAMPLE_CAPACITY_INCREMENT;
offsets = new long[capacity];
timesUs = new long[capacity];
flags = new int[capacity];
sizes = new int[capacity];
}
// Called by the consuming thread.
/**
* Fills {@code holder} with information about the current sample, but does not write its data.
* The first entry in {@code offsetHolder} is filled with the absolute position of the sample's
* data in the rolling buffer.
* <p>
* The fields set are {SampleHolder#size}, {SampleHolder#timeUs}, {SampleHolder#flags} and
* {@code offsetHolder[0]}.
*
* @param holder The holder into which the current sample information should be written.
* @param offsetHolder The holder into which the absolute position of the sample's data should
* be written.
* @return True if the holders were filled. False if there is no current sample.
*/
public synchronized boolean peekSample(SampleHolder holder, long[] offsetHolder) {
if (queueSize == 0) {
return false;
}
holder.timeUs = timesUs[readIndex];
holder.size = sizes[readIndex];
holder.flags = flags[readIndex];
offsetHolder[0] = offsets[readIndex];
return true;
}
/**
* Advances the read index to the next sample.
*
* @return The absolute position of the first byte in the rolling buffer that may still be
* required after advancing the index. Data prior to this position can be dropped.
*/
public synchronized long moveToNextSample() {
queueSize--;
int lastReadIndex = readIndex++;
if (readIndex == capacity) {
// Wrap around.
readIndex = 0;
}
return queueSize > 0 ? offsets[readIndex] : (sizes[lastReadIndex] + offsets[lastReadIndex]);
}
// Called by the loading thread.
public synchronized void commitSample(long timeUs, long offset, int size, int sampleFlags) {
timesUs[writeIndex] = timeUs;
offsets[writeIndex] = offset;
sizes[writeIndex] = size;
flags[writeIndex] = sampleFlags;
// Increment the write index.
queueSize++;
if (queueSize == capacity) {
// Increase the capacity.
int newCapacity = capacity + SAMPLE_CAPACITY_INCREMENT;
long[] newOffsets = new long[newCapacity];
long[] newTimesUs = new long[newCapacity];
int[] newFlags = new int[newCapacity];
int[] newSizes = new int[newCapacity];
int beforeWrap = capacity - readIndex;
System.arraycopy(offsets, readIndex, newOffsets, 0, beforeWrap);
System.arraycopy(timesUs, readIndex, newTimesUs, 0, beforeWrap);
System.arraycopy(flags, readIndex, newFlags, 0, beforeWrap);
System.arraycopy(sizes, readIndex, newSizes, 0, beforeWrap);
int afterWrap = readIndex;
System.arraycopy(offsets, 0, newOffsets, beforeWrap, afterWrap);
System.arraycopy(timesUs, 0, newTimesUs, beforeWrap, afterWrap);
System.arraycopy(flags, 0, newFlags, beforeWrap, afterWrap);
System.arraycopy(sizes, 0, newSizes, beforeWrap, afterWrap);
offsets = newOffsets;
timesUs = newTimesUs;
flags = newFlags;
sizes = newSizes;
readIndex = 0;
writeIndex = capacity;
queueSize = capacity;
capacity = newCapacity;
} else {
writeIndex++;
if (writeIndex == capacity) {
// Wrap around.
writeIndex = 0;
}
}
}
}
}
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.metadata;
/**
* A metadata that contains parsed ID3 GEOB (General Encapsulated Object) frame data associated
* with time indices.
*/
public class GeobMetadata {
public static final String TYPE = "GEOB";
public final String mimeType;
public final String filename;
public final String description;
public final byte[] data;
public GeobMetadata(String mimeType, String filename, String description, byte[] data) {
this.mimeType = mimeType;
this.filename = filename;
this.description = description;
this.data = data;
}
}
...@@ -29,6 +29,11 @@ import java.util.Map; ...@@ -29,6 +29,11 @@ import java.util.Map;
*/ */
public class Id3Parser implements MetadataParser<Map<String, Object>> { public class Id3Parser implements MetadataParser<Map<String, Object>> {
private static final int ID3_TEXT_ENCODING_ISO_8859_1 = 0;
private static final int ID3_TEXT_ENCODING_UTF_16 = 1;
private static final int ID3_TEXT_ENCODING_UTF_16BE = 2;
private static final int ID3_TEXT_ENCODING_UTF_8 = 3;
@Override @Override
public boolean canParse(String mimeType) { public boolean canParse(String mimeType) {
return mimeType.equals(MimeTypes.APPLICATION_ID3); return mimeType.equals(MimeTypes.APPLICATION_ID3);
...@@ -60,13 +65,48 @@ public class Id3Parser implements MetadataParser<Map<String, Object>> { ...@@ -60,13 +65,48 @@ public class Id3Parser implements MetadataParser<Map<String, Object>> {
byte[] frame = new byte[frameSize - 1]; byte[] frame = new byte[frameSize - 1];
id3Data.readBytes(frame, 0, frameSize - 1); id3Data.readBytes(frame, 0, frameSize - 1);
int firstZeroIndex = indexOf(frame, 0, (byte) 0); int firstZeroIndex = indexOfEOS(frame, 0, encoding);
String description = new String(frame, 0, firstZeroIndex, charset); String description = new String(frame, 0, firstZeroIndex, charset);
int valueStartIndex = indexOfNot(frame, firstZeroIndex, (byte) 0); int valueStartIndex = firstZeroIndex + delimiterLength(encoding);
int valueEndIndex = indexOf(frame, valueStartIndex, (byte) 0); int valueEndIndex = indexOfEOS(frame, valueStartIndex, encoding);
String value = new String(frame, valueStartIndex, valueEndIndex - valueStartIndex, String value = new String(frame, valueStartIndex, valueEndIndex - valueStartIndex,
charset); charset);
metadata.put(TxxxMetadata.TYPE, new TxxxMetadata(description, value)); metadata.put(TxxxMetadata.TYPE, new TxxxMetadata(description, value));
} else if (frameId0 == 'P' && frameId1 == 'R' && frameId2 == 'I' && frameId3 == 'V') {
// Check frame ID == PRIV
byte[] frame = new byte[frameSize];
id3Data.readBytes(frame, 0, frameSize);
int firstZeroIndex = indexOf(frame, 0, (byte) 0);
String owner = new String(frame, 0, firstZeroIndex, "ISO-8859-1");
byte[] privateData = new byte[frameSize - firstZeroIndex - 1];
System.arraycopy(frame, firstZeroIndex + 1, privateData, 0, frameSize - firstZeroIndex - 1);
metadata.put(PrivMetadata.TYPE, new PrivMetadata(owner, privateData));
} else if (frameId0 == 'G' && frameId1 == 'E' && frameId2 == 'O' && frameId3 == 'B') {
// Check frame ID == GEOB
int encoding = id3Data.readUnsignedByte();
String charset = getCharsetName(encoding);
byte[] frame = new byte[frameSize - 1];
id3Data.readBytes(frame, 0, frameSize - 1);
int firstZeroIndex = indexOf(frame, 0, (byte) 0);
String mimeType = new String(frame, 0, firstZeroIndex, "ISO-8859-1");
int filenameStartIndex = firstZeroIndex + 1;
int filenameEndIndex = indexOfEOS(frame, filenameStartIndex, encoding);
String filename = new String(frame, filenameStartIndex,
filenameEndIndex - filenameStartIndex, charset);
int descriptionStartIndex = filenameEndIndex + delimiterLength(encoding);
int descriptionEndIndex = indexOfEOS(frame, descriptionStartIndex, encoding);
String description = new String(frame, descriptionStartIndex,
descriptionEndIndex - descriptionStartIndex, charset);
int objectDataSize = frameSize - 1 /* encoding byte */ - descriptionEndIndex
- delimiterLength(encoding);
byte[] objectData = new byte[objectDataSize];
System.arraycopy(frame, descriptionEndIndex + delimiterLength(encoding), objectData, 0,
objectDataSize);
metadata.put(GeobMetadata.TYPE, new GeobMetadata(mimeType, filename,
description, objectData));
} else { } else {
String type = String.format("%c%c%c%c", frameId0, frameId1, frameId2, frameId3); String type = String.format("%c%c%c%c", frameId0, frameId1, frameId2, frameId3);
byte[] frame = new byte[frameSize]; byte[] frame = new byte[frameSize];
...@@ -89,15 +129,30 @@ public class Id3Parser implements MetadataParser<Map<String, Object>> { ...@@ -89,15 +129,30 @@ public class Id3Parser implements MetadataParser<Map<String, Object>> {
return data.length; return data.length;
} }
private static int indexOfNot(byte[] data, int fromIndex, byte key) { private static int indexOfEOS(byte[] data, int fromIndex, int encodingByte) {
for (int i = fromIndex; i < data.length; i++) { int terminationPos = indexOf(data, fromIndex, (byte) 0);
if (data[i] != key) {
return i; // For single byte encoding charsets, we are done
if (encodingByte == ID3_TEXT_ENCODING_ISO_8859_1 || encodingByte == ID3_TEXT_ENCODING_UTF_8) {
return terminationPos;
}
// Otherwise, look for a two zero bytes
while (terminationPos < data.length - 1) {
if (data[terminationPos + 1] == (byte) 0) {
return terminationPos;
} }
terminationPos = indexOf(data, terminationPos + 1, (byte) 0);
} }
return data.length; return data.length;
} }
private static int delimiterLength(int encodingByte) {
return (encodingByte == ID3_TEXT_ENCODING_ISO_8859_1
|| encodingByte == ID3_TEXT_ENCODING_UTF_8) ? 1 : 2;
}
/** /**
* Parses an ID3 header. * Parses an ID3 header.
* *
...@@ -142,13 +197,13 @@ public class Id3Parser implements MetadataParser<Map<String, Object>> { ...@@ -142,13 +197,13 @@ public class Id3Parser implements MetadataParser<Map<String, Object>> {
*/ */
private static String getCharsetName(int encodingByte) { private static String getCharsetName(int encodingByte) {
switch (encodingByte) { switch (encodingByte) {
case 0: case ID3_TEXT_ENCODING_ISO_8859_1:
return "ISO-8859-1"; return "ISO-8859-1";
case 1: case ID3_TEXT_ENCODING_UTF_16:
return "UTF-16"; return "UTF-16";
case 2: case ID3_TEXT_ENCODING_UTF_16BE:
return "UTF-16BE"; return "UTF-16BE";
case 3: case ID3_TEXT_ENCODING_UTF_8:
return "UTF-8"; return "UTF-8";
default: default:
return "ISO-8859-1"; return "ISO-8859-1";
......
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.metadata;
/**
* A metadata that contains parsed ID3 PRIV (Private) frame data associated
* with time indices.
*/
public class PrivMetadata {
public static final String TYPE = "PRIV";
public final String owner;
public final byte[] privateData;
public PrivMetadata(String owner, byte[] privateData) {
this.owner = owner;
this.privateData = privateData;
}
}
...@@ -24,6 +24,19 @@ import java.util.List; ...@@ -24,6 +24,19 @@ import java.util.List;
public abstract class Atom { public abstract class Atom {
/** Size of an atom header, in bytes. */
public static final int ATOM_HEADER_SIZE = 8;
/** Size of a long atom header, in bytes. */
public static final int LONG_ATOM_HEADER_SIZE = 16;
/** Size of a full atom header, in bytes. */
public static final int FULL_ATOM_HEADER_SIZE = 12;
/** Value for the first 32 bits of atomSize when the atom size is actually a long value. */
public static final int LONG_SIZE_PREFIX = 1;
public static final int TYPE_ftyp = getAtomTypeInteger("ftyp");
public static final int TYPE_avc1 = getAtomTypeInteger("avc1"); public static final int TYPE_avc1 = getAtomTypeInteger("avc1");
public static final int TYPE_avc3 = getAtomTypeInteger("avc3"); public static final int TYPE_avc3 = getAtomTypeInteger("avc3");
public static final int TYPE_esds = getAtomTypeInteger("esds"); public static final int TYPE_esds = getAtomTypeInteger("esds");
...@@ -153,6 +166,20 @@ public abstract class Atom { ...@@ -153,6 +166,20 @@ public abstract class Atom {
} }
/**
* Parses the version number out of the additional integer component of a full atom.
*/
public static int parseFullAtomVersion(int fullAtomInt) {
return 0x000000FF & (fullAtomInt >> 24);
}
/**
* Parses the atom flags out of the additional integer component of a full atom.
*/
public static int parseFullAtomFlags(int fullAtomInt) {
return 0x00FFFFFF & fullAtomInt;
}
private static String getAtomTypeString(int type) { private static String getAtomTypeString(int type) {
return "" + (char) (type >> 24) return "" + (char) (type >> 24)
+ (char) ((type >> 16) & 0xFF) + (char) ((type >> 16) & 0xFF)
......
...@@ -20,6 +20,7 @@ import com.google.android.exoplayer.MediaFormat; ...@@ -20,6 +20,7 @@ import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.chunk.parser.mp4.TrackEncryptionBox; import com.google.android.exoplayer.chunk.parser.mp4.TrackEncryptionBox;
import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.CodecSpecificDataUtil; import com.google.android.exoplayer.util.CodecSpecificDataUtil;
import com.google.android.exoplayer.util.H264Util;
import com.google.android.exoplayer.util.MimeTypes; 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;
...@@ -67,7 +68,7 @@ public final class CommonMp4AtomParsers { ...@@ -67,7 +68,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);
} }
...@@ -102,7 +103,7 @@ public final class CommonMp4AtomParsers { ...@@ -102,7 +103,7 @@ public final class CommonMp4AtomParsers {
ParsableByteArray ctts = cttsAtom != null ? cttsAtom.data : null; ParsableByteArray ctts = cttsAtom != null ? cttsAtom.data : null;
// Skip full atom. // Skip full atom.
stsz.setPosition(Mp4Util.FULL_ATOM_HEADER_SIZE); stsz.setPosition(Atom.FULL_ATOM_HEADER_SIZE);
int fixedSampleSize = stsz.readUnsignedIntToInt(); int fixedSampleSize = stsz.readUnsignedIntToInt();
int sampleCount = stsz.readUnsignedIntToInt(); int sampleCount = stsz.readUnsignedIntToInt();
...@@ -112,10 +113,10 @@ public final class CommonMp4AtomParsers { ...@@ -112,10 +113,10 @@ public final class CommonMp4AtomParsers {
int[] flags = new int[sampleCount]; int[] flags = new int[sampleCount];
// Prepare to read chunk offsets. // Prepare to read chunk offsets.
chunkOffsets.setPosition(Mp4Util.FULL_ATOM_HEADER_SIZE); chunkOffsets.setPosition(Atom.FULL_ATOM_HEADER_SIZE);
int chunkCount = chunkOffsets.readUnsignedIntToInt(); int chunkCount = chunkOffsets.readUnsignedIntToInt();
stsc.setPosition(Mp4Util.FULL_ATOM_HEADER_SIZE); stsc.setPosition(Atom.FULL_ATOM_HEADER_SIZE);
int remainingSamplesPerChunkChanges = stsc.readUnsignedIntToInt() - 1; int remainingSamplesPerChunkChanges = stsc.readUnsignedIntToInt() - 1;
Assertions.checkState(stsc.readInt() == 1, "stsc first chunk must be 1"); Assertions.checkState(stsc.readInt() == 1, "stsc first chunk must be 1");
int samplesPerChunk = stsc.readUnsignedIntToInt(); int samplesPerChunk = stsc.readUnsignedIntToInt();
...@@ -130,7 +131,7 @@ public final class CommonMp4AtomParsers { ...@@ -130,7 +131,7 @@ public final class CommonMp4AtomParsers {
int remainingSamplesInChunk = samplesPerChunk; int remainingSamplesInChunk = samplesPerChunk;
// Prepare to read sample timestamps. // Prepare to read sample timestamps.
stts.setPosition(Mp4Util.FULL_ATOM_HEADER_SIZE); stts.setPosition(Atom.FULL_ATOM_HEADER_SIZE);
int remainingTimestampDeltaChanges = stts.readUnsignedIntToInt() - 1; int remainingTimestampDeltaChanges = stts.readUnsignedIntToInt() - 1;
int remainingSamplesAtTimestampDelta = stts.readUnsignedIntToInt(); int remainingSamplesAtTimestampDelta = stts.readUnsignedIntToInt();
int timestampDeltaInTimeUnits = stts.readUnsignedIntToInt(); int timestampDeltaInTimeUnits = stts.readUnsignedIntToInt();
...@@ -141,8 +142,8 @@ public final class CommonMp4AtomParsers { ...@@ -141,8 +142,8 @@ public final class CommonMp4AtomParsers {
int remainingTimestampOffsetChanges = 0; int remainingTimestampOffsetChanges = 0;
int timestampOffset = 0; int timestampOffset = 0;
if (ctts != null) { if (ctts != null) {
ctts.setPosition(Mp4Util.ATOM_HEADER_SIZE); ctts.setPosition(Atom.ATOM_HEADER_SIZE);
cttsHasSignedOffsets = Mp4Util.parseFullAtomVersion(ctts.readInt()) == 1; cttsHasSignedOffsets = Atom.parseFullAtomVersion(ctts.readInt()) == 1;
remainingTimestampOffsetChanges = ctts.readUnsignedIntToInt() - 1; remainingTimestampOffsetChanges = ctts.readUnsignedIntToInt() - 1;
remainingSamplesAtTimestampOffset = ctts.readUnsignedIntToInt(); remainingSamplesAtTimestampOffset = ctts.readUnsignedIntToInt();
timestampOffset = cttsHasSignedOffsets ? ctts.readInt() : ctts.readUnsignedIntToInt(); timestampOffset = cttsHasSignedOffsets ? ctts.readInt() : ctts.readUnsignedIntToInt();
...@@ -151,7 +152,7 @@ public final class CommonMp4AtomParsers { ...@@ -151,7 +152,7 @@ public final class CommonMp4AtomParsers {
int nextSynchronizationSampleIndex = -1; int nextSynchronizationSampleIndex = -1;
int remainingSynchronizationSamples = 0; int remainingSynchronizationSamples = 0;
if (stss != null) { if (stss != null) {
stss.setPosition(Mp4Util.FULL_ATOM_HEADER_SIZE); stss.setPosition(Atom.FULL_ATOM_HEADER_SIZE);
remainingSynchronizationSamples = stss.readUnsignedIntToInt(); remainingSynchronizationSamples = stss.readUnsignedIntToInt();
nextSynchronizationSampleIndex = stss.readUnsignedIntToInt() - 1; nextSynchronizationSampleIndex = stss.readUnsignedIntToInt() - 1;
} }
...@@ -249,10 +250,10 @@ public final class CommonMp4AtomParsers { ...@@ -249,10 +250,10 @@ public final class CommonMp4AtomParsers {
* @return Timescale for the movie. * @return Timescale for the movie.
*/ */
private static long parseMvhd(ParsableByteArray mvhd) { private static long parseMvhd(ParsableByteArray mvhd) {
mvhd.setPosition(Mp4Util.ATOM_HEADER_SIZE); mvhd.setPosition(Atom.ATOM_HEADER_SIZE);
int fullAtom = mvhd.readInt(); int fullAtom = mvhd.readInt();
int version = Mp4Util.parseFullAtomVersion(fullAtom); int version = Atom.parseFullAtomVersion(fullAtom);
mvhd.skip(version == 0 ? 8 : 16); mvhd.skip(version == 0 ? 8 : 16);
...@@ -266,9 +267,9 @@ public final class CommonMp4AtomParsers { ...@@ -266,9 +267,9 @@ public final class CommonMp4AtomParsers {
* the movie header box). The duration is set to -1 if the duration is unspecified. * the movie header box). The duration is set to -1 if the duration is unspecified.
*/ */
private static Pair<Integer, Long> parseTkhd(ParsableByteArray tkhd) { private static Pair<Integer, Long> parseTkhd(ParsableByteArray tkhd) {
tkhd.setPosition(Mp4Util.ATOM_HEADER_SIZE); tkhd.setPosition(Atom.ATOM_HEADER_SIZE);
int fullAtom = tkhd.readInt(); int fullAtom = tkhd.readInt();
int version = Mp4Util.parseFullAtomVersion(fullAtom); int version = Atom.parseFullAtomVersion(fullAtom);
tkhd.skip(version == 0 ? 8 : 16); tkhd.skip(version == 0 ? 8 : 16);
...@@ -302,7 +303,7 @@ public final class CommonMp4AtomParsers { ...@@ -302,7 +303,7 @@ public final class CommonMp4AtomParsers {
* @return The track type. * @return The track type.
*/ */
private static int parseHdlr(ParsableByteArray hdlr) { private static int parseHdlr(ParsableByteArray hdlr) {
hdlr.setPosition(Mp4Util.FULL_ATOM_HEADER_SIZE + 4); hdlr.setPosition(Atom.FULL_ATOM_HEADER_SIZE + 4);
return hdlr.readInt(); return hdlr.readInt();
} }
...@@ -313,16 +314,17 @@ public final class CommonMp4AtomParsers { ...@@ -313,16 +314,17 @@ public final class CommonMp4AtomParsers {
* @return The media timescale, defined as the number of time units that pass in one second. * @return The media timescale, defined as the number of time units that pass in one second.
*/ */
private static long parseMdhd(ParsableByteArray mdhd) { private static long parseMdhd(ParsableByteArray mdhd) {
mdhd.setPosition(Mp4Util.ATOM_HEADER_SIZE); mdhd.setPosition(Atom.ATOM_HEADER_SIZE);
int fullAtom = mdhd.readInt(); int fullAtom = mdhd.readInt();
int version = Mp4Util.parseFullAtomVersion(fullAtom); int version = Atom.parseFullAtomVersion(fullAtom);
mdhd.skip(version == 0 ? 8 : 16); mdhd.skip(version == 0 ? 8 : 16);
return mdhd.readUnsignedInt(); return mdhd.readUnsignedInt();
} }
private static Pair<MediaFormat, TrackEncryptionBox[]> parseStsd(ParsableByteArray stsd) { private static Pair<MediaFormat, TrackEncryptionBox[]> parseStsd(
stsd.setPosition(Mp4Util.FULL_ATOM_HEADER_SIZE); ParsableByteArray stsd, long durationUs) {
stsd.setPosition(Atom.FULL_ATOM_HEADER_SIZE);
int numberOfEntries = stsd.readInt(); int numberOfEntries = stsd.readInt();
MediaFormat mediaFormat = null; MediaFormat mediaFormat = null;
TrackEncryptionBox[] trackEncryptionBoxes = new TrackEncryptionBox[numberOfEntries]; TrackEncryptionBox[] trackEncryptionBoxes = new TrackEncryptionBox[numberOfEntries];
...@@ -334,19 +336,19 @@ public final class CommonMp4AtomParsers { ...@@ -334,19 +336,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,8 +357,8 @@ public final class CommonMp4AtomParsers { ...@@ -355,8 +357,8 @@ 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 + Atom.ATOM_HEADER_SIZE);
parent.skip(24); parent.skip(24);
int width = parent.readUnsignedShort(); int width = parent.readUnsignedShort();
...@@ -388,12 +390,12 @@ public final class CommonMp4AtomParsers { ...@@ -388,12 +390,12 @@ 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);
} }
private static List<byte[]> parseAvcCFromParent(ParsableByteArray parent, int position) { private static List<byte[]> parseAvcCFromParent(ParsableByteArray parent, int position) {
parent.setPosition(position + Mp4Util.ATOM_HEADER_SIZE + 4); parent.setPosition(position + Atom.ATOM_HEADER_SIZE + 4);
// Start of the AVCDecoderConfigurationRecord (defined in 14496-15) // Start of the AVCDecoderConfigurationRecord (defined in 14496-15)
int nalUnitLength = (parent.readUnsignedByte() & 0x3) + 1; int nalUnitLength = (parent.readUnsignedByte() & 0x3) + 1;
if (nalUnitLength != 4) { if (nalUnitLength != 4) {
...@@ -406,18 +408,18 @@ public final class CommonMp4AtomParsers { ...@@ -406,18 +408,18 @@ public final class CommonMp4AtomParsers {
// expose the AVC profile and level somewhere useful; Most likely in MediaFormat. // expose the AVC profile and level somewhere useful; Most likely in MediaFormat.
int numSequenceParameterSets = parent.readUnsignedByte() & 0x1F; int numSequenceParameterSets = parent.readUnsignedByte() & 0x1F;
for (int j = 0; j < numSequenceParameterSets; j++) { for (int j = 0; j < numSequenceParameterSets; j++) {
initializationData.add(Mp4Util.parseChildNalUnit(parent)); initializationData.add(H264Util.parseChildNalUnit(parent));
} }
int numPictureParameterSets = parent.readUnsignedByte(); int numPictureParameterSets = parent.readUnsignedByte();
for (int j = 0; j < numPictureParameterSets; j++) { for (int j = 0; j < numPictureParameterSets; j++) {
initializationData.add(Mp4Util.parseChildNalUnit(parent)); initializationData.add(H264Util.parseChildNalUnit(parent));
} }
return initializationData; return initializationData;
} }
private static TrackEncryptionBox parseSinfFromParent(ParsableByteArray parent, int position, private static TrackEncryptionBox parseSinfFromParent(ParsableByteArray parent, int position,
int size) { int size) {
int childPosition = position + Mp4Util.ATOM_HEADER_SIZE; int childPosition = position + Atom.ATOM_HEADER_SIZE;
TrackEncryptionBox trackEncryptionBox = null; TrackEncryptionBox trackEncryptionBox = null;
while (childPosition - position < size) { while (childPosition - position < size) {
...@@ -440,7 +442,7 @@ public final class CommonMp4AtomParsers { ...@@ -440,7 +442,7 @@ public final class CommonMp4AtomParsers {
} }
private static float parsePaspFromParent(ParsableByteArray parent, int position) { private static float parsePaspFromParent(ParsableByteArray parent, int position) {
parent.setPosition(position + Mp4Util.ATOM_HEADER_SIZE); parent.setPosition(position + Atom.ATOM_HEADER_SIZE);
int hSpacing = parent.readUnsignedIntToInt(); int hSpacing = parent.readUnsignedIntToInt();
int vSpacing = parent.readUnsignedIntToInt(); int vSpacing = parent.readUnsignedIntToInt();
return (float) hSpacing / vSpacing; return (float) hSpacing / vSpacing;
...@@ -448,7 +450,7 @@ public final class CommonMp4AtomParsers { ...@@ -448,7 +450,7 @@ public final class CommonMp4AtomParsers {
private static TrackEncryptionBox parseSchiFromParent(ParsableByteArray parent, int position, private static TrackEncryptionBox parseSchiFromParent(ParsableByteArray parent, int position,
int size) { int size) {
int childPosition = position + Mp4Util.ATOM_HEADER_SIZE; int childPosition = position + Atom.ATOM_HEADER_SIZE;
while (childPosition - position < size) { while (childPosition - position < size) {
parent.setPosition(childPosition); parent.setPosition(childPosition);
int childAtomSize = parent.readInt(); int childAtomSize = parent.readInt();
...@@ -468,9 +470,9 @@ public final class CommonMp4AtomParsers { ...@@ -468,9 +470,9 @@ 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 + Atom.ATOM_HEADER_SIZE);
parent.skip(24); parent.skip(24);
int width = parent.readUnsignedShort(); int width = parent.readUnsignedShort();
...@@ -492,12 +494,12 @@ public final class CommonMp4AtomParsers { ...@@ -492,12 +494,12 @@ 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 + Atom.ATOM_HEADER_SIZE);
parent.skip(16); parent.skip(16);
int channelCount = parent.readUnsignedShort(); int channelCount = parent.readUnsignedShort();
int sampleSize = parent.readUnsignedShort(); int sampleSize = parent.readUnsignedShort();
...@@ -555,14 +557,14 @@ public final class CommonMp4AtomParsers { ...@@ -555,14 +557,14 @@ 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);
} }
/** Returns codec-specific initialization data contained in an esds box. */ /** Returns codec-specific initialization data contained in an esds box. */
private static byte[] parseEsdsFromParent(ParsableByteArray parent, int position) { private static byte[] parseEsdsFromParent(ParsableByteArray parent, int position) {
parent.setPosition(position + Mp4Util.ATOM_HEADER_SIZE + 4); parent.setPosition(position + Atom.ATOM_HEADER_SIZE + 4);
// Start of the ES_Descriptor (defined in 14496-1) // Start of the ES_Descriptor (defined in 14496-1)
parent.skip(1); // ES_Descriptor tag parent.skip(1); // ES_Descriptor tag
int varIntByte = parent.readUnsignedByte(); int varIntByte = parent.readUnsignedByte();
...@@ -606,7 +608,7 @@ public final class CommonMp4AtomParsers { ...@@ -606,7 +608,7 @@ public final class CommonMp4AtomParsers {
private static Ac3Format parseAc3SpecificBoxFromParent(ParsableByteArray parent, int position) { private static Ac3Format parseAc3SpecificBoxFromParent(ParsableByteArray parent, int position) {
// Start of the dac3 atom (defined in ETSI TS 102 366) // Start of the dac3 atom (defined in ETSI TS 102 366)
parent.setPosition(position + Mp4Util.ATOM_HEADER_SIZE); parent.setPosition(position + Atom.ATOM_HEADER_SIZE);
// fscod (sample rate code) // fscod (sample rate code)
int fscod = (parent.readUnsignedByte() & 0xC0) >> 6; int fscod = (parent.readUnsignedByte() & 0xC0) >> 6;
...@@ -644,7 +646,7 @@ public final class CommonMp4AtomParsers { ...@@ -644,7 +646,7 @@ public final class CommonMp4AtomParsers {
private static int parseEc3SpecificBoxFromParent(ParsableByteArray parent, int position) { private static int parseEc3SpecificBoxFromParent(ParsableByteArray parent, int position) {
// Start of the dec3 atom (defined in ETSI TS 102 366) // Start of the dec3 atom (defined in ETSI TS 102 366)
parent.setPosition(position + Mp4Util.ATOM_HEADER_SIZE); parent.setPosition(position + Atom.ATOM_HEADER_SIZE);
// TODO: Implement parsing for enhanced AC-3 with multiple sub-streams. // TODO: Implement parsing for enhanced AC-3 with multiple sub-streams.
return 0; return 0;
} }
......
...@@ -22,6 +22,9 @@ import com.google.android.exoplayer.util.Util; ...@@ -22,6 +22,9 @@ import com.google.android.exoplayer.util.Util;
/** Sample table for a track in an MP4 file. */ /** Sample table for a track in an MP4 file. */
public final class Mp4TrackSampleTable { public final class Mp4TrackSampleTable {
/** Sample index when no sample is available. */
public static final int NO_SAMPLE = -1;
/** Sample offsets in bytes. */ /** Sample offsets in bytes. */
public final long[] offsets; public final long[] offsets;
/** Sample sizes in bytes. */ /** Sample sizes in bytes. */
...@@ -53,7 +56,7 @@ public final class Mp4TrackSampleTable { ...@@ -53,7 +56,7 @@ public final class Mp4TrackSampleTable {
* timestamp, if one is available. * timestamp, if one is available.
* *
* @param timeUs Timestamp adjacent to which to find a synchronization sample. * @param timeUs Timestamp adjacent to which to find a synchronization sample.
* @return Index of the synchronization sample, or {@link Mp4Util#NO_SAMPLE} if none. * @return Index of the synchronization sample, or {@link #NO_SAMPLE} if none.
*/ */
public int getIndexOfEarlierOrEqualSynchronizationSample(long timeUs) { public int getIndexOfEarlierOrEqualSynchronizationSample(long timeUs) {
int startIndex = Util.binarySearchFloor(timestampsUs, timeUs, true, false); int startIndex = Util.binarySearchFloor(timestampsUs, timeUs, true, false);
...@@ -63,7 +66,7 @@ public final class Mp4TrackSampleTable { ...@@ -63,7 +66,7 @@ public final class Mp4TrackSampleTable {
} }
} }
return Mp4Util.NO_SAMPLE; return NO_SAMPLE;
} }
/** /**
...@@ -71,7 +74,7 @@ public final class Mp4TrackSampleTable { ...@@ -71,7 +74,7 @@ public final class Mp4TrackSampleTable {
* if one is available. * if one is available.
* *
* @param timeUs Timestamp adjacent to which to find a synchronization sample. * @param timeUs Timestamp adjacent to which to find a synchronization sample.
* @return index Index of the synchronization sample, or {@link Mp4Util#NO_SAMPLE} if none. * @return index Index of the synchronization sample, or {@link #NO_SAMPLE} if none.
*/ */
public int getIndexOfLaterOrEqualSynchronizationSample(long timeUs) { public int getIndexOfLaterOrEqualSynchronizationSample(long timeUs) {
int startIndex = Util.binarySearchCeil(timestampsUs, timeUs, true, false); int startIndex = Util.binarySearchCeil(timestampsUs, timeUs, true, false);
...@@ -81,7 +84,7 @@ public final class Mp4TrackSampleTable { ...@@ -81,7 +84,7 @@ public final class Mp4TrackSampleTable {
} }
} }
return Mp4Util.NO_SAMPLE; return NO_SAMPLE;
} }
} }
...@@ -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) {
......
...@@ -17,6 +17,7 @@ package com.google.android.exoplayer.smoothstreaming; ...@@ -17,6 +17,7 @@ package com.google.android.exoplayer.smoothstreaming;
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.UriUtil;
import com.google.android.exoplayer.util.Util; import com.google.android.exoplayer.util.Util;
import android.net.Uri; import android.net.Uri;
...@@ -197,14 +198,14 @@ public class SmoothStreamingManifest { ...@@ -197,14 +198,14 @@ public class SmoothStreamingManifest {
public final TrackElement[] tracks; public final TrackElement[] tracks;
public final int chunkCount; public final int chunkCount;
private final Uri baseUri; private final String baseUri;
private final String chunkTemplate; private final String chunkTemplate;
private final List<Long> chunkStartTimes; private final List<Long> chunkStartTimes;
private final long[] chunkStartTimesUs; private final long[] chunkStartTimesUs;
private final long lastChunkDurationUs; private final long lastChunkDurationUs;
public StreamElement(Uri baseUri, String chunkTemplate, int type, String subType, public StreamElement(String baseUri, String chunkTemplate, int type, String subType,
long timescale, String name, int qualityLevels, int maxWidth, int maxHeight, long timescale, String name, int qualityLevels, int maxWidth, int maxHeight,
int displayWidth, int displayHeight, String language, TrackElement[] tracks, int displayWidth, int displayHeight, String language, TrackElement[] tracks,
List<Long> chunkStartTimes, long lastChunkDuration) { List<Long> chunkStartTimes, long lastChunkDuration) {
...@@ -274,7 +275,7 @@ public class SmoothStreamingManifest { ...@@ -274,7 +275,7 @@ public class SmoothStreamingManifest {
String chunkUrl = chunkTemplate String chunkUrl = chunkTemplate
.replace(URL_PLACEHOLDER_BITRATE, Integer.toString(tracks[track].bitrate)) .replace(URL_PLACEHOLDER_BITRATE, Integer.toString(tracks[track].bitrate))
.replace(URL_PLACEHOLDER_START_TIME, chunkStartTimes.get(chunkIndex).toString()); .replace(URL_PLACEHOLDER_START_TIME, chunkStartTimes.get(chunkIndex).toString());
return Util.getMergedUri(baseUri, chunkUrl); return UriUtil.resolveToUri(baseUri, chunkUrl);
} }
} }
......
...@@ -23,9 +23,7 @@ import com.google.android.exoplayer.upstream.NetworkLoadable; ...@@ -23,9 +23,7 @@ import com.google.android.exoplayer.upstream.NetworkLoadable;
import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.Assertions;
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.Util;
import android.net.Uri;
import android.util.Base64; import android.util.Base64;
import android.util.Pair; import android.util.Pair;
...@@ -65,8 +63,8 @@ public class SmoothStreamingManifestParser implements ...@@ -65,8 +63,8 @@ public class SmoothStreamingManifestParser implements
try { try {
XmlPullParser xmlParser = xmlParserFactory.newPullParser(); XmlPullParser xmlParser = xmlParserFactory.newPullParser();
xmlParser.setInput(inputStream, null); xmlParser.setInput(inputStream, null);
SmoothStreamMediaParser smoothStreamMediaParser = new SmoothStreamMediaParser(null, SmoothStreamMediaParser smoothStreamMediaParser =
Util.parseBaseUri(connectionUrl)); new SmoothStreamMediaParser(null, connectionUrl);
return (SmoothStreamingManifest) smoothStreamMediaParser.parse(xmlParser); return (SmoothStreamingManifest) smoothStreamMediaParser.parse(xmlParser);
} catch (XmlPullParserException e) { } catch (XmlPullParserException e) {
throw new ParserException(e); throw new ParserException(e);
...@@ -89,13 +87,13 @@ public class SmoothStreamingManifestParser implements ...@@ -89,13 +87,13 @@ public class SmoothStreamingManifestParser implements
*/ */
private static abstract class ElementParser { private static abstract class ElementParser {
private final Uri baseUri; private final String baseUri;
private final String tag; private final String tag;
private final ElementParser parent; private final ElementParser parent;
private final List<Pair<String, Object>> normalizedAttributes; private final List<Pair<String, Object>> normalizedAttributes;
public ElementParser(ElementParser parent, Uri baseUri, String tag) { public ElementParser(ElementParser parent, String baseUri, String tag) {
this.parent = parent; this.parent = parent;
this.baseUri = baseUri; this.baseUri = baseUri;
this.tag = tag; this.tag = tag;
...@@ -158,7 +156,7 @@ public class SmoothStreamingManifestParser implements ...@@ -158,7 +156,7 @@ public class SmoothStreamingManifestParser implements
} }
} }
private ElementParser newChildParser(ElementParser parent, String name, Uri baseUri) { private ElementParser newChildParser(ElementParser parent, String name, String baseUri) {
if (TrackElementParser.TAG.equals(name)) { if (TrackElementParser.TAG.equals(name)) {
return new TrackElementParser(parent, baseUri); return new TrackElementParser(parent, baseUri);
} else if (ProtectionElementParser.TAG.equals(name)) { } else if (ProtectionElementParser.TAG.equals(name)) {
...@@ -342,7 +340,7 @@ public class SmoothStreamingManifestParser implements ...@@ -342,7 +340,7 @@ public class SmoothStreamingManifestParser implements
private ProtectionElement protectionElement; private ProtectionElement protectionElement;
private List<StreamElement> streamElements; private List<StreamElement> streamElements;
public SmoothStreamMediaParser(ElementParser parent, Uri baseUri) { public SmoothStreamMediaParser(ElementParser parent, String baseUri) {
super(parent, baseUri, TAG); super(parent, baseUri, TAG);
lookAheadCount = -1; lookAheadCount = -1;
protectionElement = null; protectionElement = null;
...@@ -392,7 +390,7 @@ public class SmoothStreamingManifestParser implements ...@@ -392,7 +390,7 @@ public class SmoothStreamingManifestParser implements
private UUID uuid; private UUID uuid;
private byte[] initData; private byte[] initData;
public ProtectionElementParser(ElementParser parent, Uri baseUri) { public ProtectionElementParser(ElementParser parent, String baseUri) {
super(parent, baseUri, TAG); super(parent, baseUri, TAG);
} }
...@@ -455,7 +453,7 @@ public class SmoothStreamingManifestParser implements ...@@ -455,7 +453,7 @@ public class SmoothStreamingManifestParser implements
private static final String KEY_FRAGMENT_START_TIME = "t"; private static final String KEY_FRAGMENT_START_TIME = "t";
private static final String KEY_FRAGMENT_REPEAT_COUNT = "r"; private static final String KEY_FRAGMENT_REPEAT_COUNT = "r";
private final Uri baseUri; private final String baseUri;
private final List<TrackElement> tracks; private final List<TrackElement> tracks;
private int type; private int type;
...@@ -473,7 +471,7 @@ public class SmoothStreamingManifestParser implements ...@@ -473,7 +471,7 @@ public class SmoothStreamingManifestParser implements
private long lastChunkDuration; private long lastChunkDuration;
public StreamElementParser(ElementParser parent, Uri baseUri) { public StreamElementParser(ElementParser parent, String baseUri) {
super(parent, baseUri, TAG); super(parent, baseUri, TAG);
this.baseUri = baseUri; this.baseUri = baseUri;
tracks = new LinkedList<TrackElement>(); tracks = new LinkedList<TrackElement>();
...@@ -615,7 +613,7 @@ public class SmoothStreamingManifestParser implements ...@@ -615,7 +613,7 @@ public class SmoothStreamingManifestParser implements
private int nalUnitLengthField; private int nalUnitLengthField;
private String content; private String content;
public TrackElementParser(ElementParser parent, Uri baseUri) { public TrackElementParser(ElementParser parent, String baseUri) {
super(parent, baseUri, TAG); super(parent, baseUri, TAG);
this.csd = new LinkedList<byte[]>(); this.csd = new LinkedList<byte[]>();
} }
......
...@@ -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;
} }
......
...@@ -15,14 +15,13 @@ ...@@ -15,14 +15,13 @@
*/ */
package com.google.android.exoplayer.source; package com.google.android.exoplayer.source;
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 +52,6 @@ public final class FrameworkSampleExtractor implements SampleExtractor { ...@@ -53,8 +52,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 +103,10 @@ public final class FrameworkSampleExtractor implements SampleExtractor { ...@@ -106,25 +103,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 +133,18 @@ public final class FrameworkSampleExtractor implements SampleExtractor { ...@@ -151,10 +133,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 +163,7 @@ public final class FrameworkSampleExtractor implements SampleExtractor { ...@@ -173,7 +163,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.isEncrypted()) {
sampleHolder.cryptoInfo.setFromExtractorV16(mediaExtractor); sampleHolder.cryptoInfo.setFromExtractorV16(mediaExtractor);
} }
...@@ -188,9 +178,15 @@ public final class FrameworkSampleExtractor implements SampleExtractor { ...@@ -188,9 +178,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;
} }
} }
/*
* 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 com.google.android.exoplayer.C;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.SampleSource;
import com.google.android.exoplayer.TrackRenderer;
import com.google.android.exoplayer.drm.DrmInitData;
import com.google.android.exoplayer.mp4.Atom;
import com.google.android.exoplayer.mp4.Atom.ContainerAtom;
import com.google.android.exoplayer.mp4.CommonMp4AtomParsers;
import com.google.android.exoplayer.mp4.Mp4TrackSampleTable;
import com.google.android.exoplayer.mp4.Track;
import com.google.android.exoplayer.upstream.BufferPool;
import com.google.android.exoplayer.upstream.BufferedNonBlockingInputStream;
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DataSourceStream;
import com.google.android.exoplayer.upstream.DataSpec;
import com.google.android.exoplayer.upstream.Loader;
import com.google.android.exoplayer.upstream.Loader.Loadable;
import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.H264Util;
import com.google.android.exoplayer.util.MimeTypes;
import com.google.android.exoplayer.util.ParsableByteArray;
import com.google.android.exoplayer.util.Util;
import android.util.Log;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.Stack;
/**
* Extracts data from a {@link DataSpec} in unfragmented MP4 format (ISO 14496-12).
*/
public final class Mp4SampleExtractor implements SampleExtractor, Loader.Callback {
private static final String TAG = "Mp4SampleExtractor";
private static final String LOADER_THREAD_NAME = "Mp4SampleExtractor";
private static final int NO_TRACK = -1;
// Reading results
private static final int RESULT_NEED_MORE_DATA = 1;
private static final int RESULT_END_OF_STREAM = 2;
// Parser states
private static final int STATE_READING_ATOM_HEADER = 0;
private static final int STATE_READING_ATOM_PAYLOAD = 1;
/** Set of atom types that contain data to be parsed. */
private static final Set<Integer> LEAF_ATOM_TYPES = getAtomTypeSet(
Atom.TYPE_mdhd, Atom.TYPE_mvhd, Atom.TYPE_hdlr, Atom.TYPE_vmhd, Atom.TYPE_smhd,
Atom.TYPE_stsd, Atom.TYPE_avc1, Atom.TYPE_avcC, Atom.TYPE_mp4a, Atom.TYPE_esds,
Atom.TYPE_stts, Atom.TYPE_stss, Atom.TYPE_ctts, Atom.TYPE_stsc, Atom.TYPE_stsz,
Atom.TYPE_stco, Atom.TYPE_co64, Atom.TYPE_tkhd);
/** Set of atom types that contain other atoms that need to be parsed. */
private static final Set<Integer> CONTAINER_TYPES = getAtomTypeSet(
Atom.TYPE_moov, Atom.TYPE_trak, Atom.TYPE_mdia, Atom.TYPE_minf, Atom.TYPE_stbl);
/** Default number of times to retry loading data prior to failing. */
private static final int DEFAULT_LOADABLE_RETRY_COUNT = 3;
private final DataSource dataSource;
private final DataSpec dataSpec;
private final int readAheadAllocationSize;
private final int reloadMinimumSeekDistance;
private final int maximumTrackSampleInterval;
private final int loadRetryCount;
private final BufferPool bufferPool;
private final Loader loader;
private final ParsableByteArray atomHeader;
private final Stack<Atom.ContainerAtom> containerAtoms;
private DataSourceStream dataSourceStream;
private BufferedNonBlockingInputStream inputStream;
private long inputStreamOffset;
private long rootAtomBytesRead;
private boolean loadCompleted;
private int parserState;
private int atomBytesRead;
private int atomType;
private long atomSize;
private ParsableByteArray atomData;
private boolean prepared;
private int loadErrorCount;
private Mp4Track[] tracks;
/** An exception from {@link #inputStream}'s callbacks, or {@code null} if there was no error. */
private IOException lastLoadError;
private long loadErrorPosition;
/** If handling a call to {@link #seekTo}, the new required stream offset, or -1 otherwise. */
private long pendingSeekPosition;
/** If the input stream is being reopened at a new position, the new offset, or -1 otherwise. */
private long pendingLoadPosition;
/**
* Creates a new sample extractor for reading {@code dataSource} and {@code dataSpec} as an
* unfragmented MP4 file with default settings.
*
* <p>The default settings read ahead by 5 MiB, handle maximum offsets between samples at the same
* timestamp in different tracks of 3 MiB and restart loading when seeking forward by >= 256 KiB.
*
* @param dataSource Data source used to read from {@code dataSpec}.
* @param dataSpec Data specification specifying what to read.
*/
public Mp4SampleExtractor(DataSource dataSource, DataSpec dataSpec) {
this(dataSource, dataSpec, 5 * 1024 * 1024, 3 * 1024 * 1024, 256 * 1024,
DEFAULT_LOADABLE_RETRY_COUNT);
}
/**
* Creates a new sample extractor for reading {@code dataSource} and {@code dataSpec} as an
* unfragmented MP4 file.
*
* @param dataSource Data source used to read from {@code dataSpec}.
* @param dataSpec Data specification specifying what to read.
* @param readAheadAllocationSize Size of the allocation that buffers the stream, in bytes. The
* value must exceed the maximum sample size, so that a sample can be read in its entirety.
* @param maximumTrackSampleInterval Size of the buffer that handles reading from any selected
* track. The value should be chosen so that the buffer is as big as the interval in bytes
* between the start of the earliest and the end of the latest sample required to render media
* from all selected tracks, at any timestamp in the data source.
* @param reloadMinimumSeekDistance Determines when {@code dataSource} is reopened while seeking:
* if the number of bytes between the current position and the new position is greater than or
* equal to this value, or the new position is before the current position, loading will
* restart. The value should be set to the number of bytes that can be loaded/consumed from an
* existing connection in the time it takes to start a new connection.
* @param loadableRetryCount The number of times to retry loading if an error occurs.
*/
public Mp4SampleExtractor(DataSource dataSource, DataSpec dataSpec, int readAheadAllocationSize,
int maximumTrackSampleInterval, int reloadMinimumSeekDistance, int loadableRetryCount) {
// TODO: Handle minimumTrackSampleInterval specified in time not bytes.
this.dataSource = Assertions.checkNotNull(dataSource);
this.dataSpec = Assertions.checkNotNull(dataSpec);
this.readAheadAllocationSize = readAheadAllocationSize;
this.maximumTrackSampleInterval = maximumTrackSampleInterval;
this.reloadMinimumSeekDistance = reloadMinimumSeekDistance;
this.loadRetryCount = loadableRetryCount;
// TODO: Implement Allocator here so it is possible to check there is only one buffer at a time.
bufferPool = new BufferPool(readAheadAllocationSize);
loader = new Loader(LOADER_THREAD_NAME);
atomHeader = new ParsableByteArray(Atom.LONG_ATOM_HEADER_SIZE);
containerAtoms = new Stack<Atom.ContainerAtom>();
parserState = STATE_READING_ATOM_HEADER;
pendingLoadPosition = -1;
pendingSeekPosition = -1;
loadErrorPosition = -1;
}
@Override
public boolean prepare() throws IOException {
if (inputStream == null) {
loadFromOffset(0L);
}
if (!prepared) {
if (readHeaders() && !prepared) {
throw new IOException("moov atom not found.");
}
if (!prepared) {
maybeThrowLoadError();
}
}
return prepared;
}
@Override
public void selectTrack(int trackIndex) {
Assertions.checkState(prepared);
if (tracks[trackIndex].selected) {
return;
}
tracks[trackIndex].selected = true;
// Get the timestamp of the earliest currently-selected sample.
int earliestSampleTrackIndex = getTrackIndexOfEarliestCurrentSample();
if (earliestSampleTrackIndex == NO_TRACK) {
tracks[trackIndex].sampleIndex = 0;
return;
}
if (earliestSampleTrackIndex == Mp4TrackSampleTable.NO_SAMPLE) {
tracks[trackIndex].sampleIndex = Mp4TrackSampleTable.NO_SAMPLE;
return;
}
long timestampUs =
tracks[earliestSampleTrackIndex].sampleTable.timestampsUs[earliestSampleTrackIndex];
// Find the latest sync sample in the new track that has an earlier or equal timestamp.
tracks[trackIndex].sampleIndex =
tracks[trackIndex].sampleTable.getIndexOfEarlierOrEqualSynchronizationSample(timestampUs);
}
@Override
public void deselectTrack(int trackIndex) {
Assertions.checkState(prepared);
tracks[trackIndex].selected = false;
}
@Override
public long getBufferedPositionUs() {
Assertions.checkState(prepared);
if (pendingLoadPosition != -1) {
return TrackRenderer.UNKNOWN_TIME_US;
}
if (loadCompleted) {
return TrackRenderer.END_OF_TRACK_US;
}
// Get the absolute position to which there is data buffered.
long bufferedPosition =
inputStreamOffset + inputStream.getReadPosition() + inputStream.getAvailableByteCount();
// Find the timestamp of the latest sample that does not exceed the buffered position.
long latestTimestampBeforeEnd = Long.MIN_VALUE;
for (int trackIndex = 0; trackIndex < tracks.length; trackIndex++) {
if (!tracks[trackIndex].selected) {
continue;
}
Mp4TrackSampleTable sampleTable = tracks[trackIndex].sampleTable;
int sampleIndex = Util.binarySearchFloor(sampleTable.offsets, bufferedPosition, false, true);
if (sampleIndex > 0
&& sampleTable.offsets[sampleIndex] + sampleTable.sizes[sampleIndex] > bufferedPosition) {
sampleIndex--;
}
// Update the latest timestamp if this is greater.
long timestamp = sampleTable.timestampsUs[sampleIndex];
if (timestamp > latestTimestampBeforeEnd) {
latestTimestampBeforeEnd = timestamp;
}
}
return latestTimestampBeforeEnd < 0L ? C.UNKNOWN_TIME_US : latestTimestampBeforeEnd;
}
@Override
public void seekTo(long positionUs) {
Assertions.checkState(prepared);
long earliestSamplePosition = Long.MAX_VALUE;
for (int trackIndex = 0; trackIndex < tracks.length; trackIndex++) {
if (!tracks[trackIndex].selected) {
continue;
}
Mp4TrackSampleTable sampleTable = tracks[trackIndex].sampleTable;
int sampleIndex = sampleTable.getIndexOfEarlierOrEqualSynchronizationSample(positionUs);
if (sampleIndex == Mp4TrackSampleTable.NO_SAMPLE) {
sampleIndex = sampleTable.getIndexOfLaterOrEqualSynchronizationSample(positionUs);
}
tracks[trackIndex].sampleIndex = sampleIndex;
long offset = sampleTable.offsets[tracks[trackIndex].sampleIndex];
if (offset < earliestSamplePosition) {
earliestSamplePosition = offset;
}
}
pendingSeekPosition = earliestSamplePosition;
if (pendingLoadPosition != -1) {
loadFromOffset(earliestSamplePosition);
return;
}
inputStream.returnToMark();
long earliestOffset = inputStreamOffset + inputStream.getReadPosition();
long latestOffset = earliestOffset + inputStream.getAvailableByteCount();
if (earliestSamplePosition < earliestOffset
|| earliestSamplePosition >= latestOffset + reloadMinimumSeekDistance) {
loadFromOffset(earliestSamplePosition);
}
}
@Override
public int getTrackCount() {
Assertions.checkState(prepared);
return tracks.length;
}
@Override
public MediaFormat getMediaFormat(int track) {
Assertions.checkState(prepared);
return tracks[track].track.mediaFormat;
}
@Override
public DrmInitData getDrmInitData(int track) {
return null;
}
@Override
public int readSample(int trackIndex, SampleHolder sampleHolder) throws IOException {
Assertions.checkState(prepared);
Mp4Track track = tracks[trackIndex];
Assertions.checkState(track.selected);
int sampleIndex = track.sampleIndex;
// Check for the end of the stream.
if (sampleIndex == Mp4TrackSampleTable.NO_SAMPLE) {
// TODO: Should END_OF_STREAM be returned as soon as this track has no more samples, or as
// soon as no tracks have a sample (as implemented here)?
return hasSampleInAnySelectedTrack() ? SampleSource.NOTHING_READ : SampleSource.END_OF_STREAM;
}
// Return if the input stream will be reopened at the requested position.
if (pendingLoadPosition != -1) {
return SampleSource.NOTHING_READ;
}
// If there was a seek request, try to skip forwards to the requested position.
if (pendingSeekPosition != -1) {
int bytesToSeekPosition =
(int) (pendingSeekPosition - (inputStreamOffset + inputStream.getReadPosition()));
int skippedByteCount = inputStream.skip(bytesToSeekPosition);
if (skippedByteCount == -1) {
throw new IOException("Unexpected end-of-stream while seeking to sample.");
}
bytesToSeekPosition -= skippedByteCount;
inputStream.mark();
if (bytesToSeekPosition == 0) {
pendingSeekPosition = -1;
} else {
maybeThrowLoadError();
return SampleSource.NOTHING_READ;
}
}
// Return if the sample offset hasn't been loaded yet.
inputStream.returnToMark();
long sampleOffset = track.sampleTable.offsets[sampleIndex];
long seekOffsetLong = (sampleOffset - inputStreamOffset) - inputStream.getReadPosition();
Assertions.checkState(seekOffsetLong <= Integer.MAX_VALUE);
int seekOffset = (int) seekOffsetLong;
if (inputStream.skip(seekOffset) != seekOffset) {
maybeThrowLoadError();
return SampleSource.NOTHING_READ;
}
// Return if the sample has been loaded.
int sampleSize = track.sampleTable.sizes[sampleIndex];
if (inputStream.getAvailableByteCount() < sampleSize) {
maybeThrowLoadError();
return SampleSource.NOTHING_READ;
}
if (sampleHolder.data == null || sampleHolder.data.capacity() < sampleSize) {
sampleHolder.replaceBuffer(sampleSize);
}
ByteBuffer data = sampleHolder.data;
if (data == null) {
inputStream.skip(sampleSize);
sampleHolder.size = 0;
} else {
int bytesRead = inputStream.read(data, sampleSize);
Assertions.checkState(bytesRead == sampleSize);
if (MimeTypes.VIDEO_H264.equals(tracks[trackIndex].track.mediaFormat.mimeType)) {
// The mp4 file contains length-prefixed access units, but the decoder wants start code
// delimited content.
H264Util.replaceLengthPrefixesWithAvcStartCodes(sampleHolder.data, sampleSize);
}
sampleHolder.size = sampleSize;
}
// Move the input stream mark forwards if the earliest current sample was just read.
if (getTrackIndexOfEarliestCurrentSample() == trackIndex) {
inputStream.mark();
}
// TODO: Read encryption data.
sampleHolder.timeUs = track.sampleTable.timestampsUs[sampleIndex];
sampleHolder.flags = track.sampleTable.flags[sampleIndex];
// Advance to the next sample, checking if this was the last sample.
track.sampleIndex =
sampleIndex + 1 == track.sampleTable.getSampleCount() ? Mp4TrackSampleTable.NO_SAMPLE : sampleIndex + 1;
// Reset the loading error counter if we read past the offset at which the error was thrown.
if (dataSourceStream.getReadPosition() > loadErrorPosition) {
loadErrorCount = 0;
loadErrorPosition = -1;
}
return SampleSource.SAMPLE_READ;
}
@Override
public void release() {
pendingLoadPosition = -1;
loader.release();
if (inputStream != null) {
inputStream.close();
}
}
@Override
public void onLoadError(Loadable loadable, IOException exception) {
lastLoadError = exception;
loadErrorCount++;
if (loadErrorPosition == -1) {
loadErrorPosition = dataSourceStream.getLoadPosition();
}
int delayMs = getRetryDelayMs(loadErrorCount);
Log.w(TAG, "Retry loading (delay " + delayMs + " ms).");
loader.startLoading(dataSourceStream, this, delayMs);
}
@Override
public void onLoadCompleted(Loadable loadable) {
loadCompleted = true;
}
@Override
public void onLoadCanceled(Loadable loadable) {
if (pendingLoadPosition != -1) {
loadFromOffset(pendingLoadPosition);
pendingLoadPosition = -1;
}
}
private void loadFromOffset(long offsetBytes) {
inputStreamOffset = offsetBytes;
rootAtomBytesRead = offsetBytes;
if (loader.isLoading()) {
// Wait for loading to be canceled before proceeding.
pendingLoadPosition = offsetBytes;
loader.cancelLoading();
return;
}
if (inputStream != null) {
inputStream.close();
}
DataSpec dataSpec = new DataSpec(
this.dataSpec.uri, offsetBytes, C.LENGTH_UNBOUNDED, this.dataSpec.key);
dataSourceStream =
new DataSourceStream(dataSource, dataSpec, bufferPool, readAheadAllocationSize);
loader.startLoading(dataSourceStream, this);
// Wrap the input stream with a buffering stream so that it is possible to read from any track.
inputStream =
new BufferedNonBlockingInputStream(dataSourceStream, maximumTrackSampleInterval);
loadCompleted = false;
loadErrorCount = 0;
loadErrorPosition = -1;
}
/**
* Returns the index of the track that contains the earliest current sample, or {@link #NO_TRACK}
* if no track is selected, or {@link Mp4TrackSampleTable#NO_SAMPLE} if no samples remain in
* selected tracks.
*/
private int getTrackIndexOfEarliestCurrentSample() {
int earliestSampleTrackIndex = NO_TRACK;
long earliestSampleOffset = Long.MAX_VALUE;
for (int trackIndex = 0; trackIndex < tracks.length; trackIndex++) {
Mp4Track track = tracks[trackIndex];
if (!track.selected) {
continue;
}
int sampleIndex = track.sampleIndex;
if (sampleIndex == Mp4TrackSampleTable.NO_SAMPLE) {
if (earliestSampleTrackIndex == NO_TRACK) {
// A track is selected, but it has no more samples.
earliestSampleTrackIndex = Mp4TrackSampleTable.NO_SAMPLE;
}
continue;
}
long trackSampleOffset = track.sampleTable.offsets[sampleIndex];
if (trackSampleOffset < earliestSampleOffset) {
earliestSampleOffset = trackSampleOffset;
earliestSampleTrackIndex = trackIndex;
}
}
return earliestSampleTrackIndex;
}
private boolean hasSampleInAnySelectedTrack() {
boolean hasSample = false;
for (int trackIndex = 0; trackIndex < tracks.length; trackIndex++) {
if (tracks[trackIndex].selected && tracks[trackIndex].sampleIndex
!= Mp4TrackSampleTable.NO_SAMPLE) {
hasSample = true;
break;
}
}
return hasSample;
}
/** Reads headers, returning whether the end of the stream was reached. */
private boolean readHeaders() {
int results = 0;
while (!prepared && (results & (RESULT_NEED_MORE_DATA | RESULT_END_OF_STREAM)) == 0) {
switch (parserState) {
case STATE_READING_ATOM_HEADER:
results |= readAtomHeader();
break;
case STATE_READING_ATOM_PAYLOAD:
results |= readAtomPayload();
break;
}
}
return (results & RESULT_END_OF_STREAM) != 0;
}
private int readAtomHeader() {
if (pendingLoadPosition != -1) {
return RESULT_NEED_MORE_DATA;
}
// The size value is either 4 or 8 bytes long (in which case atomSize = Mp4Util.LONG_ATOM_SIZE).
int remainingBytes;
if (atomSize != Atom.LONG_SIZE_PREFIX) {
remainingBytes = Atom.ATOM_HEADER_SIZE - atomBytesRead;
} else {
remainingBytes = Atom.LONG_ATOM_HEADER_SIZE - atomBytesRead;
}
int bytesRead = inputStream.read(atomHeader.data, atomBytesRead, remainingBytes);
if (bytesRead == -1) {
return RESULT_END_OF_STREAM;
}
rootAtomBytesRead += bytesRead;
atomBytesRead += bytesRead;
if (atomBytesRead < Atom.ATOM_HEADER_SIZE
|| (atomSize == Atom.LONG_SIZE_PREFIX && atomBytesRead < Atom.LONG_ATOM_HEADER_SIZE)) {
return RESULT_NEED_MORE_DATA;
}
atomHeader.setPosition(0);
atomSize = atomHeader.readUnsignedInt();
atomType = atomHeader.readInt();
if (atomSize == Atom.LONG_SIZE_PREFIX) {
// The extended atom size is contained in the next 8 bytes, so try to read it now.
if (atomBytesRead < Atom.LONG_ATOM_HEADER_SIZE) {
return readAtomHeader();
}
atomSize = atomHeader.readLong();
}
Integer atomTypeInteger = atomType; // Avoids boxing atomType twice.
if (CONTAINER_TYPES.contains(atomTypeInteger)) {
if (atomSize == Atom.LONG_SIZE_PREFIX) {
containerAtoms.add(new ContainerAtom(
atomType, rootAtomBytesRead + atomSize - Atom.LONG_ATOM_HEADER_SIZE));
} else {
containerAtoms.add(new ContainerAtom(
atomType, rootAtomBytesRead + atomSize - Atom.ATOM_HEADER_SIZE));
}
enterState(STATE_READING_ATOM_HEADER);
} else if (LEAF_ATOM_TYPES.contains(atomTypeInteger)) {
Assertions.checkState(atomSize <= Integer.MAX_VALUE);
atomData = new ParsableByteArray((int) atomSize);
System.arraycopy(atomHeader.data, 0, atomData.data, 0, Atom.ATOM_HEADER_SIZE);
enterState(STATE_READING_ATOM_PAYLOAD);
} else {
atomData = null;
enterState(STATE_READING_ATOM_PAYLOAD);
}
return 0;
}
private int readAtomPayload() {
int bytesRead;
if (atomData != null) {
bytesRead = inputStream.read(atomData.data, atomBytesRead, (int) atomSize - atomBytesRead);
} else {
if (atomSize >= reloadMinimumSeekDistance || atomSize > Integer.MAX_VALUE) {
loadFromOffset(rootAtomBytesRead + atomSize - atomBytesRead);
onContainerAtomRead();
enterState(STATE_READING_ATOM_HEADER);
return 0;
} else {
bytesRead = inputStream.skip((int) atomSize - atomBytesRead);
}
}
if (bytesRead == -1) {
return RESULT_END_OF_STREAM;
}
rootAtomBytesRead += bytesRead;
atomBytesRead += bytesRead;
if (atomBytesRead != atomSize) {
return RESULT_NEED_MORE_DATA;
}
if (atomData != null && !containerAtoms.isEmpty()) {
containerAtoms.peek().add(new Atom.LeafAtom(atomType, atomData));
}
onContainerAtomRead();
enterState(STATE_READING_ATOM_HEADER);
return 0;
}
private void onContainerAtomRead() {
while (!containerAtoms.isEmpty() && containerAtoms.peek().endByteOffset == rootAtomBytesRead) {
Atom.ContainerAtom containerAtom = containerAtoms.pop();
if (containerAtom.type == Atom.TYPE_moov) {
processMoovAtom(containerAtom);
} else if (!containerAtoms.isEmpty()) {
containerAtoms.peek().add(containerAtom);
}
}
}
private void enterState(int state) {
switch (state) {
case STATE_READING_ATOM_HEADER:
atomBytesRead = 0;
atomSize = 0;
break;
}
parserState = state;
inputStream.mark();
}
/** Updates the stored track metadata to reflect the contents on the specified moov atom. */
private void processMoovAtom(Atom.ContainerAtom moov) {
List<Mp4Track> tracks = new ArrayList<Mp4Track>();
long earliestSampleOffset = Long.MAX_VALUE;
for (int i = 0; i < moov.containerChildren.size(); i++) {
Atom.ContainerAtom atom = moov.containerChildren.get(i);
if (atom.type != Atom.TYPE_trak) {
continue;
}
Track track = CommonMp4AtomParsers.parseTrak(atom, moov.getLeafAtomOfType(Atom.TYPE_mvhd));
if (track.type != Track.TYPE_AUDIO && track.type != Track.TYPE_VIDEO) {
continue;
}
Atom.ContainerAtom stblAtom = atom.getContainerAtomOfType(Atom.TYPE_mdia)
.getContainerAtomOfType(Atom.TYPE_minf).getContainerAtomOfType(Atom.TYPE_stbl);
Mp4TrackSampleTable trackSampleTable = CommonMp4AtomParsers.parseStbl(track, stblAtom);
if (trackSampleTable.getSampleCount() == 0) {
continue;
}
tracks.add(new Mp4Track(track, trackSampleTable));
// Keep track of the byte offset of the earliest sample.
long firstSampleOffset = trackSampleTable.offsets[0];
if (firstSampleOffset < earliestSampleOffset) {
earliestSampleOffset = firstSampleOffset;
}
}
this.tracks = tracks.toArray(new Mp4Track[0]);
if (earliestSampleOffset < inputStream.getReadPosition()) {
loadFromOffset(earliestSampleOffset);
}
prepared = true;
}
/** Returns an unmodifiable set of atom types. */
private static Set<Integer> getAtomTypeSet(int... atomTypes) {
Set<Integer> atomTypeSet = new HashSet<Integer>();
for (int atomType : atomTypes) {
atomTypeSet.add(atomType);
}
return Collections.unmodifiableSet(atomTypeSet);
}
private int getRetryDelayMs(int errorCount) {
return Math.min((errorCount - 1) * 1000, 5000);
}
private void maybeThrowLoadError() throws IOException {
if (loadErrorCount > loadRetryCount) {
throw lastLoadError;
}
}
private static final class Mp4Track {
public final Track track;
public final Mp4TrackSampleTable sampleTable;
public boolean selected;
public int sampleIndex;
public Mp4Track(Track track, Mp4TrackSampleTable sampleTable) {
this.track = track;
this.sampleTable = sampleTable;
}
}
}
...@@ -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
......
...@@ -179,7 +179,7 @@ public class TextTrackRenderer extends TrackRenderer implements Callback { ...@@ -179,7 +179,7 @@ public class TextTrackRenderer extends TrackRenderer implements Callback {
SampleHolder sampleHolder = parserHelper.getSampleHolder(); SampleHolder sampleHolder = parserHelper.getSampleHolder();
sampleHolder.clearData(); sampleHolder.clearData();
int result = source.readData(trackIndex, positionUs, formatHolder, sampleHolder, false); int result = source.readData(trackIndex, positionUs, formatHolder, sampleHolder, false);
if (result == SampleSource.SAMPLE_READ && !sampleHolder.decodeOnly) { if (result == SampleSource.SAMPLE_READ && !sampleHolder.isDecodeOnly()) {
parserHelper.startParseOperation(); parserHelper.startParseOperation();
textRendererNeedsUpdate = false; textRendererNeedsUpdate = false;
} else if (result == SampleSource.END_OF_STREAM) { } else if (result == SampleSource.END_OF_STREAM) {
......
...@@ -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;
...@@ -166,7 +200,7 @@ public class Eia608Parser { ...@@ -166,7 +200,7 @@ public class Eia608Parser {
ClosedCaption[] captionArray = new ClosedCaption[captions.size()]; ClosedCaption[] captionArray = new ClosedCaption[captions.size()];
captions.toArray(captionArray); captions.toArray(captionArray);
return new ClosedCaptionList(sampleHolder.timeUs, sampleHolder.decodeOnly, captionArray); return new ClosedCaptionList(sampleHolder.timeUs, sampleHolder.isDecodeOnly(), captionArray);
} }
private static char getChar(byte ccData) { private static char getChar(byte ccData) {
...@@ -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;
} }
} }
......
...@@ -34,20 +34,24 @@ import javax.crypto.spec.SecretKeySpec; ...@@ -34,20 +34,24 @@ import javax.crypto.spec.SecretKeySpec;
/** /**
* A {@link DataSource} that decrypts the data read from an upstream source, encrypted with AES-128 * A {@link DataSource} that decrypts the data read from an upstream source, encrypted with AES-128
* with a 128-bit key and PKCS7 padding. * with a 128-bit key and PKCS7 padding.
*
*/ */
public class Aes128DataSource implements DataSource { public class Aes128DataSource implements DataSource {
private final DataSource upstream; private final DataSource upstream;
private final byte[] secretKey; private final byte[] encryptionKey;
private final byte[] iv; private final byte[] encryptionIv;
private CipherInputStream cipherInputStream; private CipherInputStream cipherInputStream;
public Aes128DataSource(byte[] secretKey, byte[] iv, DataSource upstream) { /**
* @param upstream The upstream {@link DataSource}.
* @param encryptionKey The encryption key.
* @param encryptionIv The encryption initialization vector.
*/
public Aes128DataSource(DataSource upstream, byte[] encryptionKey, byte[] encryptionIv) {
this.upstream = upstream; this.upstream = upstream;
this.secretKey = secretKey; this.encryptionKey = encryptionKey;
this.iv = iv; this.encryptionIv = encryptionIv;
} }
@Override @Override
...@@ -61,8 +65,8 @@ public class Aes128DataSource implements DataSource { ...@@ -61,8 +65,8 @@ public class Aes128DataSource implements DataSource {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
Key cipherKey = new SecretKeySpec(secretKey, "AES"); Key cipherKey = new SecretKeySpec(encryptionKey, "AES");
AlgorithmParameterSpec cipherIV = new IvParameterSpec(iv); AlgorithmParameterSpec cipherIV = new IvParameterSpec(encryptionIv);
try { try {
cipher.init(Cipher.DECRYPT_MODE, cipherKey, cipherIV); cipher.init(Cipher.DECRYPT_MODE, cipherKey, cipherIV);
......
/*
* 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,16 @@ public interface DataSource { ...@@ -55,13 +55,16 @@ 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 {@link C#RESULT_END_OF_INPUT} 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));
} }
...@@ -230,14 +286,18 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream ...@@ -230,14 +286,18 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream
long remainingLength = resolvedLength != C.LENGTH_UNBOUNDED long remainingLength = resolvedLength != C.LENGTH_UNBOUNDED
? resolvedLength - loadPosition : C.LENGTH_UNBOUNDED; ? resolvedLength - loadPosition : C.LENGTH_UNBOUNDED;
loadDataSpec = new DataSpec(dataSpec.uri, dataSpec.position + loadPosition, loadDataSpec = new DataSpec(dataSpec.uri, dataSpec.position + loadPosition,
remainingLength, dataSpec.key); remainingLength, dataSpec.key, dataSpec.flags);
dataSource.open(loadDataSpec); dataSource.open(loadDataSpec);
} }
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;
} }
......
...@@ -26,21 +26,31 @@ import android.net.Uri; ...@@ -26,21 +26,31 @@ import android.net.Uri;
public final class DataSpec { public final class DataSpec {
/** /**
* Identifies the source from which data should be read. * Permits an underlying network stack to request that the server use gzip compression.
* <p>
* Should not typically be set if the data being requested is already compressed (e.g. most audio
* and video requests). May be set when requesting other data.
* <p>
* When a {@link DataSource} is used to request data with this flag set, and if the
* {@link DataSource} does make a network request, then the value returned from
* {@link DataSource#open(DataSpec)} will typically be {@link C#LENGTH_UNBOUNDED}. The data read
* from {@link DataSource#read(byte[], int, int)} will be the decompressed data.
*/ */
public final Uri uri; public static final int FLAG_ALLOW_GZIP = 1;
/** /**
* True if the data at {@link #uri} is the full stream. False otherwise. An example where this * Identifies the source from which data should be read.
* may be false is if {@link #uri} defines the location of a cached part of the stream.
*/ */
public final boolean uriIsFullStream; public final Uri uri;
/** /**
* The absolute position of the data in the full stream. * The absolute position of the data in the full stream.
*/ */
public final long absoluteStreamPosition; public final long absoluteStreamPosition;
/** /**
* The position of the data when read from {@link #uri}. Always equal to * The position of the data when read from {@link #uri}.
* {@link #absoluteStreamPosition} if {@link #uriIsFullStream}. * <p>
* Always equal to {@link #absoluteStreamPosition} unless the {@link #uri} defines the location
* of a subset of the underyling data.
*/ */
public final long position; public final long position;
/** /**
...@@ -52,6 +62,10 @@ public final class DataSpec { ...@@ -52,6 +62,10 @@ public final class DataSpec {
* {@link DataSpec} is not intended to be used in conjunction with a cache. * {@link DataSpec} is not intended to be used in conjunction with a cache.
*/ */
public final String key; public final String key;
/**
* Request flags. Currently {@link #FLAG_ALLOW_GZIP} is the only supported flag.
*/
public final int flags;
/** /**
* Construct a {@link DataSpec} for the given uri and with {@link #key} set to null. * Construct a {@link DataSpec} for the given uri and with {@link #key} set to null.
...@@ -59,11 +73,21 @@ public final class DataSpec { ...@@ -59,11 +73,21 @@ public final class DataSpec {
* @param uri {@link #uri}. * @param uri {@link #uri}.
*/ */
public DataSpec(Uri uri) { public DataSpec(Uri uri) {
this(uri, 0, C.LENGTH_UNBOUNDED, null); this(uri, 0);
} }
/** /**
* Construct a {@link DataSpec} for which {@link #uriIsFullStream} is true. * Construct a {@link DataSpec} for the given uri and with {@link #key} set to null.
*
* @param uri {@link #uri}.
* @param flags {@link #flags}.
*/
public DataSpec(Uri uri, int flags) {
this(uri, 0, C.LENGTH_UNBOUNDED, null, flags);
}
/**
* Construct a {@link DataSpec} where {@link #position} equals {@link #absoluteStreamPosition}.
* *
* @param uri {@link #uri}. * @param uri {@link #uri}.
* @param absoluteStreamPosition {@link #absoluteStreamPosition}, equal to {@link #position}. * @param absoluteStreamPosition {@link #absoluteStreamPosition}, equal to {@link #position}.
...@@ -71,50 +95,50 @@ public final class DataSpec { ...@@ -71,50 +95,50 @@ public final class DataSpec {
* @param key {@link #key}. * @param key {@link #key}.
*/ */
public DataSpec(Uri uri, long absoluteStreamPosition, long length, String key) { public DataSpec(Uri uri, long absoluteStreamPosition, long length, String key) {
this(uri, absoluteStreamPosition, length, key, absoluteStreamPosition, true); this(uri, absoluteStreamPosition, absoluteStreamPosition, length, key, 0);
} }
/** /**
* Construct a {@link DataSpec} for which {@link #uriIsFullStream} is false. * Construct a {@link DataSpec} where {@link #position} equals {@link #absoluteStreamPosition}.
* *
* @param uri {@link #uri}. * @param uri {@link #uri}.
* @param absoluteStreamPosition {@link #absoluteStreamPosition}. * @param absoluteStreamPosition {@link #absoluteStreamPosition}, equal to {@link #position}.
* @param length {@link #length}. * @param length {@link #length}.
* @param key {@link #key}. * @param key {@link #key}.
* @param position {@link #position}. * @param flags {@link #flags}.
*/ */
public DataSpec(Uri uri, long absoluteStreamPosition, long length, String key, long position) { public DataSpec(Uri uri, long absoluteStreamPosition, long length, String key, int flags) {
this(uri, absoluteStreamPosition, length, key, position, false); this(uri, absoluteStreamPosition, absoluteStreamPosition, length, key, flags);
} }
/** /**
* Construct a {@link DataSpec}. * Construct a {@link DataSpec} where {@link #position} may differ from
* {@link #absoluteStreamPosition}.
* *
* @param uri {@link #uri}. * @param uri {@link #uri}.
* @param absoluteStreamPosition {@link #absoluteStreamPosition}. * @param absoluteStreamPosition {@link #absoluteStreamPosition}.
* @param position {@link #position}.
* @param length {@link #length}. * @param length {@link #length}.
* @param key {@link #key}. * @param key {@link #key}.
* @param position {@link #position}. * @param flags {@link #flags}.
* @param uriIsFullStream {@link #uriIsFullStream}.
*/ */
public DataSpec(Uri uri, long absoluteStreamPosition, long length, String key, long position, public DataSpec(Uri uri, long absoluteStreamPosition, long position, long length, String key,
boolean uriIsFullStream) { int flags) {
Assertions.checkArgument(absoluteStreamPosition >= 0); Assertions.checkArgument(absoluteStreamPosition >= 0);
Assertions.checkArgument(position >= 0); Assertions.checkArgument(position >= 0);
Assertions.checkArgument(length > 0 || length == C.LENGTH_UNBOUNDED); Assertions.checkArgument(length > 0 || length == C.LENGTH_UNBOUNDED);
Assertions.checkArgument(absoluteStreamPosition == position || !uriIsFullStream);
this.uri = uri; this.uri = uri;
this.uriIsFullStream = uriIsFullStream;
this.absoluteStreamPosition = absoluteStreamPosition; this.absoluteStreamPosition = absoluteStreamPosition;
this.position = position; this.position = position;
this.length = length; this.length = length;
this.key = key; this.key = key;
this.flags = flags;
} }
@Override @Override
public String toString() { public String toString() {
return "DataSpec[" + uri + ", " + uriIsFullStream + ", " + absoluteStreamPosition + ", " + return "DataSpec[" + uri + ", " + ", " + absoluteStreamPosition + ", " +
position + ", " + length + ", " + key + "]"; position + ", " + length + ", " + key + ", " + flags + "]";
} }
} }
...@@ -18,17 +18,21 @@ package com.google.android.exoplayer.upstream; ...@@ -18,17 +18,21 @@ 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;
import java.io.EOFException;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.InterruptedIOException;
import java.net.HttpURLConnection; import java.net.HttpURLConnection;
import java.net.URL; import java.net.URL;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
...@@ -43,6 +47,7 @@ public class DefaultHttpDataSource implements HttpDataSource { ...@@ -43,6 +47,7 @@ public class DefaultHttpDataSource implements HttpDataSource {
private static final String TAG = "HttpDataSource"; private static final String TAG = "HttpDataSource";
private static final Pattern CONTENT_RANGE_HEADER = private static final Pattern CONTENT_RANGE_HEADER =
Pattern.compile("^bytes (\\d+)-(\\d+)/(\\d+)$"); Pattern.compile("^bytes (\\d+)-(\\d+)/(\\d+)$");
private static final AtomicReference<byte[]> skipBufferReference = new AtomicReference<byte[]>();
private final int connectTimeoutMillis; private final int connectTimeoutMillis;
private final int readTimeoutMillis; private final int readTimeoutMillis;
...@@ -56,7 +61,10 @@ public class DefaultHttpDataSource implements HttpDataSource { ...@@ -56,7 +61,10 @@ public class DefaultHttpDataSource implements HttpDataSource {
private InputStream inputStream; private InputStream inputStream;
private boolean opened; private boolean opened;
private long dataLength; private long bytesToSkip;
private long bytesToRead;
private long bytesSkipped;
private long bytesRead; private long bytesRead;
/** /**
...@@ -132,23 +140,11 @@ public class DefaultHttpDataSource implements HttpDataSource { ...@@ -132,23 +140,11 @@ public class DefaultHttpDataSource implements HttpDataSource {
} }
} }
/*
* TODO: If the server uses gzip compression when serving the response, this may end up returning
* the size of the compressed response, where-as it should be returning the decompressed size or
* -1. See: developer.android.com/reference/java/net/HttpURLConnection.html
*
* To fix this we should:
*
* 1. Explicitly require no compression for media requests (since media should be compressed
* already) by setting the Accept-Encoding header to "identity"
* 2. In other cases, for example when requesting manifests, we don't want to disable compression.
* For these cases we should ensure that we return -1 here (and avoid performing any sanity
* checks on the content length).
*/
@Override @Override
public long open(DataSpec dataSpec) throws HttpDataSourceException { public long open(DataSpec dataSpec) throws HttpDataSourceException {
this.dataSpec = dataSpec; this.dataSpec = dataSpec;
this.bytesRead = 0; this.bytesRead = 0;
this.bytesSkipped = 0;
try { try {
connection = makeConnection(dataSpec); connection = makeConnection(dataSpec);
} catch (IOException e) { } catch (IOException e) {
...@@ -156,14 +152,16 @@ public class DefaultHttpDataSource implements HttpDataSource { ...@@ -156,14 +152,16 @@ public class DefaultHttpDataSource implements HttpDataSource {
dataSpec); dataSpec);
} }
// Check for a valid response code.
int responseCode; int responseCode;
try { try {
responseCode = connection.getResponseCode(); responseCode = connection.getResponseCode();
} catch (IOException e) { } catch (IOException e) {
closeConnection();
throw new HttpDataSourceException("Unable to connect to " + dataSpec.uri.toString(), e, throw new HttpDataSourceException("Unable to connect to " + dataSpec.uri.toString(), e,
dataSpec); dataSpec);
} }
// Check for a valid response code.
if (responseCode < 200 || responseCode > 299) { if (responseCode < 200 || responseCode > 299) {
Map<String, List<String>> headers = connection.getHeaderFields(); Map<String, List<String>> headers = connection.getHeaderFields();
closeConnection(); closeConnection();
...@@ -177,16 +175,23 @@ public class DefaultHttpDataSource implements HttpDataSource { ...@@ -177,16 +175,23 @@ public class DefaultHttpDataSource implements HttpDataSource {
throw new InvalidContentTypeException(contentType, dataSpec); throw new InvalidContentTypeException(contentType, dataSpec);
} }
long contentLength = getContentLength(connection); // If we requested a range starting from a non-zero position and received a 200 rather than a
dataLength = dataSpec.length == C.LENGTH_UNBOUNDED ? contentLength : dataSpec.length; // 206, then the server does not support partial requests. We'll need to manually skip to the
// requested position.
if (dataSpec.length != C.LENGTH_UNBOUNDED && contentLength != C.LENGTH_UNBOUNDED bytesToSkip = responseCode == 200 && dataSpec.position != 0 ? dataSpec.position : 0;
&& contentLength != dataSpec.length) {
// The DataSpec specified a length and we resolved a length from the response headers, but // Determine the length of the data to be read, after skipping.
// the two lengths do not match. if ((dataSpec.flags & DataSpec.FLAG_ALLOW_GZIP) == 0) {
closeConnection(); long contentLength = getContentLength(connection);
throw new HttpDataSourceException( bytesToRead = dataSpec.length != C.LENGTH_UNBOUNDED ? dataSpec.length
new UnexpectedLengthException(dataSpec.length, contentLength), dataSpec); : contentLength != C.LENGTH_UNBOUNDED ? contentLength - bytesToSkip
: C.LENGTH_UNBOUNDED;
} else {
// Gzip is enabled. If the server opts to use gzip then the content length in the response
// will be that of the compressed data, which isn't what we want. Furthermore, there isn't a
// reliable way to determine whether the gzip was used or not. Always use the dataSpec length
// in this case.
bytesToRead = dataSpec.length;
} }
try { try {
...@@ -201,37 +206,24 @@ public class DefaultHttpDataSource implements HttpDataSource { ...@@ -201,37 +206,24 @@ public class DefaultHttpDataSource implements HttpDataSource {
listener.onTransferStart(); listener.onTransferStart();
} }
return dataLength; return bytesToRead;
} }
@Override @Override
public int read(byte[] buffer, int offset, int readLength) throws HttpDataSourceException { public int read(byte[] buffer, int offset, int readLength) throws HttpDataSourceException {
int read = 0;
try { try {
read = inputStream.read(buffer, offset, readLength); skipInternal();
return readInternal(buffer, offset, readLength);
} catch (IOException e) { } catch (IOException e) {
throw new HttpDataSourceException(e, dataSpec); throw new HttpDataSourceException(e, dataSpec);
} }
if (read > 0) {
bytesRead += read;
if (listener != null) {
listener.onBytesTransferred(read);
}
} else if (dataLength != C.LENGTH_UNBOUNDED && dataLength != bytesRead) {
// Check for cases where the server closed the connection having not sent the correct amount
// of data. We can only do this if we know the length of the data we were expecting.
throw new HttpDataSourceException(new UnexpectedLengthException(dataLength, bytesRead),
dataSpec);
}
return read;
} }
@Override @Override
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) {
...@@ -250,13 +242,6 @@ public class DefaultHttpDataSource implements HttpDataSource { ...@@ -250,13 +242,6 @@ public class DefaultHttpDataSource implements HttpDataSource {
} }
} }
private void closeConnection() {
if (connection != null) {
connection.disconnect();
connection = null;
}
}
/** /**
* Returns the current connection, or null if the source is not currently opened. * Returns the current connection, or null if the source is not currently opened.
* *
...@@ -267,6 +252,16 @@ public class DefaultHttpDataSource implements HttpDataSource { ...@@ -267,6 +252,16 @@ public class DefaultHttpDataSource implements HttpDataSource {
} }
/** /**
* Returns the number of bytes that have been skipped since the most recent call to
* {@link #open(DataSpec)}.
*
* @return The number of bytes skipped.
*/
protected final long bytesSkipped() {
return bytesSkipped;
}
/**
* Returns the number of bytes that have been read since the most recent call to * Returns the number of bytes that have been read since the most recent call to
* {@link #open(DataSpec)}. * {@link #open(DataSpec)}.
* *
...@@ -285,7 +280,7 @@ public class DefaultHttpDataSource implements HttpDataSource { ...@@ -285,7 +280,7 @@ public class DefaultHttpDataSource implements HttpDataSource {
* @return The remaining length, or {@link C#LENGTH_UNBOUNDED}. * @return The remaining length, or {@link C#LENGTH_UNBOUNDED}.
*/ */
protected final long bytesRemaining() { protected final long bytesRemaining() {
return dataLength == C.LENGTH_UNBOUNDED ? dataLength : dataLength - bytesRead; return bytesToRead == C.LENGTH_UNBOUNDED ? bytesToRead : bytesToRead - bytesRead;
} }
private HttpURLConnection makeConnection(DataSpec dataSpec) throws IOException { private HttpURLConnection makeConnection(DataSpec dataSpec) throws IOException {
...@@ -301,6 +296,9 @@ public class DefaultHttpDataSource implements HttpDataSource { ...@@ -301,6 +296,9 @@ public class DefaultHttpDataSource implements HttpDataSource {
} }
setRangeHeader(connection, dataSpec); setRangeHeader(connection, dataSpec);
connection.setRequestProperty("User-Agent", userAgent); connection.setRequestProperty("User-Agent", userAgent);
if ((dataSpec.flags & DataSpec.FLAG_ALLOW_GZIP) == 0) {
connection.setRequestProperty("Accept-Encoding", "identity");
}
connection.connect(); connection.connect();
return connection; return connection;
} }
...@@ -355,4 +353,87 @@ public class DefaultHttpDataSource implements HttpDataSource { ...@@ -355,4 +353,87 @@ public class DefaultHttpDataSource implements HttpDataSource {
return contentLength; return contentLength;
} }
/**
* Skips any bytes that need skipping. Else does nothing.
* <p>
* This implementation is based roughly on {@code libcore.io.Streams.skipByReading()}.
*
* @throws InterruptedIOException If the thread is interrupted during the operation.
* @throws EOFException If the end of the input stream is reached before the bytes are skipped.
*/
private void skipInternal() throws IOException {
if (bytesSkipped == bytesToSkip) {
return;
}
// Acquire the shared skip buffer.
byte[] skipBuffer = skipBufferReference.getAndSet(null);
if (skipBuffer == null) {
skipBuffer = new byte[4096];
}
while (bytesSkipped != bytesToSkip) {
int readLength = (int) Math.min(bytesToSkip - bytesSkipped, skipBuffer.length);
int read = inputStream.read(skipBuffer, 0, readLength);
if (Thread.interrupted()) {
throw new InterruptedIOException();
}
if (read == -1) {
throw new EOFException();
}
bytesSkipped += read;
if (listener != null) {
listener.onBytesTransferred(read);
}
}
// Release the shared skip buffer.
skipBufferReference.set(skipBuffer);
}
/**
* Reads up to {@code length} bytes of data and stores them into {@code buffer}, starting at
* index {@code offset}.
* <p>
* This method blocks until at least one byte of data can be read, the end of the opened range is
* detected, or an exception is thrown.
*
* @param buffer The buffer into which the read data should be stored.
* @param offset The start offset into {@code buffer} at which data should be written.
* @param readLength The maximum number of bytes to read.
* @return The number of bytes read, or {@link C#RESULT_END_OF_INPUT} if the end of the opened
* range is reached.
* @throws IOException If an error occurs reading from the source.
*/
private int readInternal(byte[] buffer, int offset, int readLength) throws IOException {
readLength = bytesToRead == C.LENGTH_UNBOUNDED ? readLength
: (int) Math.min(readLength, bytesToRead - bytesRead);
if (readLength == 0) {
// We've read all of the requested data.
return C.RESULT_END_OF_INPUT;
}
int read = inputStream.read(buffer, offset, readLength);
if (read == -1) {
if (bytesToRead != C.LENGTH_UNBOUNDED && bytesToRead != bytesRead) {
// The server closed the connection having not sent sufficient data.
throw new EOFException();
}
return C.RESULT_END_OF_INPUT;
}
bytesRead += read;
if (listener != null) {
listener.onBytesTransferred(read);
}
return read;
}
private void closeConnection() {
if (connection != null) {
connection.disconnect();
connection = null;
}
}
} }
...@@ -17,6 +17,7 @@ package com.google.android.exoplayer.upstream; ...@@ -17,6 +17,7 @@ package com.google.android.exoplayer.upstream;
import com.google.android.exoplayer.C; import com.google.android.exoplayer.C;
import java.io.EOFException;
import java.io.IOException; import java.io.IOException;
import java.io.RandomAccessFile; import java.io.RandomAccessFile;
...@@ -65,6 +66,9 @@ public final class FileDataSource implements DataSource { ...@@ -65,6 +66,9 @@ public final class FileDataSource implements DataSource {
file.seek(dataSpec.position); file.seek(dataSpec.position);
bytesRemaining = dataSpec.length == C.LENGTH_UNBOUNDED ? file.length() - dataSpec.position bytesRemaining = dataSpec.length == C.LENGTH_UNBOUNDED ? file.length() - dataSpec.position
: dataSpec.length; : dataSpec.length;
if (bytesRemaining < 0) {
throw new EOFException();
}
} catch (IOException e) { } catch (IOException e) {
throw new FileDataSourceException(e); throw new FileDataSourceException(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();
} }
......
...@@ -63,7 +63,7 @@ public final class NetworkLoadable<T> implements Loadable { ...@@ -63,7 +63,7 @@ public final class NetworkLoadable<T> implements Loadable {
public NetworkLoadable(String url, HttpDataSource httpDataSource, Parser<T> parser) { public NetworkLoadable(String url, HttpDataSource httpDataSource, Parser<T> parser) {
this.httpDataSource = httpDataSource; this.httpDataSource = httpDataSource;
this.parser = parser; this.parser = parser;
dataSpec = new DataSpec(Uri.parse(url)); dataSpec = new DataSpec(Uri.parse(url), DataSpec.FLAG_ALLOW_GZIP);
} }
/** /**
......
...@@ -42,8 +42,8 @@ public final class TeeDataSource implements DataSource { ...@@ -42,8 +42,8 @@ public final class TeeDataSource implements DataSource {
long dataLength = upstream.open(dataSpec); long dataLength = upstream.open(dataSpec);
if (dataSpec.length == C.LENGTH_UNBOUNDED && dataLength != C.LENGTH_UNBOUNDED) { if (dataSpec.length == C.LENGTH_UNBOUNDED && dataLength != C.LENGTH_UNBOUNDED) {
// Reconstruct dataSpec in order to provide the resolved length to the sink. // Reconstruct dataSpec in order to provide the resolved length to the sink.
dataSpec = new DataSpec(dataSpec.uri, dataSpec.absoluteStreamPosition, dataLength, dataSpec = new DataSpec(dataSpec.uri, dataSpec.absoluteStreamPosition, dataSpec.position,
dataSpec.key, dataSpec.position, dataSpec.uriIsFullStream); dataLength, dataSpec.key, dataSpec.flags);
} }
dataSink.open(dataSpec); dataSink.open(dataSpec);
return dataLength; return dataLength;
...@@ -61,8 +61,11 @@ public final class TeeDataSource implements DataSource { ...@@ -61,8 +61,11 @@ public final class TeeDataSource implements DataSource {
@Override @Override
public void close() throws IOException { public void close() throws IOException {
upstream.close(); try {
dataSink.close(); upstream.close();
} finally {
dataSink.close();
}
} }
} }
...@@ -20,6 +20,7 @@ import java.io.IOException; ...@@ -20,6 +20,7 @@ import java.io.IOException;
/** /**
* Thrown when the length of some data does not match an expected length. * Thrown when the length of some data does not match an expected length.
*/ */
@Deprecated
public final class UnexpectedLengthException extends IOException { public final class UnexpectedLengthException extends IOException {
/** /**
......
...@@ -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.upstream.DataSink; import com.google.android.exoplayer.upstream.DataSink;
import com.google.android.exoplayer.upstream.DataSpec; import com.google.android.exoplayer.upstream.DataSpec;
import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.Util;
import java.io.File; import java.io.File;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
...@@ -115,11 +116,23 @@ public class CacheDataSink implements DataSink { ...@@ -115,11 +116,23 @@ public class CacheDataSink implements DataSink {
} }
private void closeCurrentOutputStream() throws IOException { private void closeCurrentOutputStream() throws IOException {
if (outputStream != null) { if (outputStream == null) {
return;
}
boolean success = false;
try {
outputStream.flush(); outputStream.flush();
outputStream.close(); outputStream.getFD().sync();
success = true;
} finally {
Util.closeQuietly(outputStream);
if (success) {
cache.commitFile(file);
} else {
file.delete();
}
outputStream = null; outputStream = null;
cache.commitFile(file);
file = null; file = null;
} }
} }
......
...@@ -22,7 +22,6 @@ import com.google.android.exoplayer.upstream.DataSpec; ...@@ -22,7 +22,6 @@ import com.google.android.exoplayer.upstream.DataSpec;
import com.google.android.exoplayer.upstream.FileDataSource; import com.google.android.exoplayer.upstream.FileDataSource;
import com.google.android.exoplayer.upstream.TeeDataSource; import com.google.android.exoplayer.upstream.TeeDataSource;
import com.google.android.exoplayer.upstream.cache.CacheDataSink.CacheDataSinkException; import com.google.android.exoplayer.upstream.cache.CacheDataSink.CacheDataSinkException;
import com.google.android.exoplayer.util.Assertions;
import android.net.Uri; import android.net.Uri;
import android.util.Log; import android.util.Log;
...@@ -64,6 +63,7 @@ public final class CacheDataSource implements DataSource { ...@@ -64,6 +63,7 @@ public final class CacheDataSource implements DataSource {
private DataSource currentDataSource; private DataSource currentDataSource;
private Uri uri; private Uri uri;
private int flags;
private String key; private String key;
private long readPosition; private long readPosition;
private long bytesRemaining; private long bytesRemaining;
...@@ -125,9 +125,9 @@ public final class CacheDataSource implements DataSource { ...@@ -125,9 +125,9 @@ public final class CacheDataSource implements DataSource {
@Override @Override
public long open(DataSpec dataSpec) throws IOException { public long open(DataSpec dataSpec) throws IOException {
Assertions.checkState(dataSpec.uriIsFullStream);
try { try {
uri = dataSpec.uri; uri = dataSpec.uri;
flags = dataSpec.flags;
key = dataSpec.key; key = dataSpec.key;
readPosition = dataSpec.position; readPosition = dataSpec.position;
bytesRemaining = dataSpec.length; bytesRemaining = dataSpec.length;
...@@ -201,19 +201,19 @@ public final class CacheDataSource implements DataSource { ...@@ -201,19 +201,19 @@ public final class CacheDataSource implements DataSource {
// The data is locked in the cache, or we're ignoring the cache. Bypass the cache and read // The data is locked in the cache, or we're ignoring the cache. Bypass the cache and read
// from upstream. // from upstream.
currentDataSource = upstreamDataSource; currentDataSource = upstreamDataSource;
dataSpec = new DataSpec(uri, readPosition, bytesRemaining, key); dataSpec = new DataSpec(uri, readPosition, bytesRemaining, key, flags);
} else if (span.isCached) { } else if (span.isCached) {
// Data is cached, read from cache. // Data is cached, read from cache.
Uri fileUri = Uri.fromFile(span.file); Uri fileUri = Uri.fromFile(span.file);
long filePosition = readPosition - span.position; long filePosition = readPosition - span.position;
long length = Math.min(span.length - filePosition, bytesRemaining); long length = Math.min(span.length - filePosition, bytesRemaining);
dataSpec = new DataSpec(fileUri, readPosition, length, key, filePosition); dataSpec = new DataSpec(fileUri, readPosition, filePosition, length, key, flags);
currentDataSource = cacheReadDataSource; currentDataSource = cacheReadDataSource;
} else { } else {
// Data is not cached, and data is not locked, read from upstream with cache backing. // Data is not cached, and data is not locked, read from upstream with cache backing.
lockedSpan = span; lockedSpan = span;
long length = span.isOpenEnded() ? bytesRemaining : Math.min(span.length, bytesRemaining); long length = span.isOpenEnded() ? bytesRemaining : Math.min(span.length, bytesRemaining);
dataSpec = new DataSpec(uri, readPosition, length, key); dataSpec = new DataSpec(uri, readPosition, length, key, flags);
currentDataSource = cacheWriteDataSource != null ? cacheWriteDataSource currentDataSource = cacheWriteDataSource != null ? cacheWriteDataSource
: upstreamDataSource; : upstreamDataSource;
} }
......
...@@ -13,67 +13,17 @@ ...@@ -13,67 +13,17 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package com.google.android.exoplayer.mp4; package com.google.android.exoplayer.util;
import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.CodecSpecificDataUtil;
import com.google.android.exoplayer.util.ParsableByteArray;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
/** /**
* Utility methods and constants for parsing fragmented and unfragmented MP4 files. * Utility methods for handling H264 data.
*/ */
public final class Mp4Util { public final class H264Util {
/** Size of an atom header, in bytes. */
public static final int ATOM_HEADER_SIZE = 8;
/** Size of a long atom header, in bytes. */
public static final int LONG_ATOM_HEADER_SIZE = 16;
/** Size of a full atom header, in bytes. */
public static final int FULL_ATOM_HEADER_SIZE = 12;
/** Value for the first 32 bits of atomSize when the atom size is actually a long value. */
public static final int LONG_ATOM_SIZE = 1;
/** Sample index when no sample is available. */
public static final int NO_SAMPLE = -1;
/** Track index when no track is selected. */
public static final int NO_TRACK = -1;
/** Four initial bytes that must prefix H.264/AVC NAL units for decoding. */ /** Four initial bytes that must prefix H.264/AVC NAL units for decoding. */
private static final byte[] NAL_START_CODE = new byte[] {0, 0, 0, 1}; public static final byte[] NAL_START_CODE = new byte[] {0, 0, 0, 1};
/** Parses the version number out of the additional integer component of a full atom. */
public static int parseFullAtomVersion(int fullAtomInt) {
return 0x000000FF & (fullAtomInt >> 24);
}
/** Parses the atom flags out of the additional integer component of a full atom. */
public static int parseFullAtomFlags(int fullAtomInt) {
return 0x00FFFFFF & fullAtomInt;
}
/**
* Reads an unsigned integer into an integer. This method is suitable for use when it can be
* assumed that the top bit will always be set to zero.
*
* @throws IllegalArgumentException If the top bit of the input data is set.
*/
public static int readUnsignedIntToInt(ByteBuffer data) {
int result = 0xFF & data.get();
for (int i = 1; i < 4; i++) {
result <<= 8;
result |= 0xFF & data.get();
}
if (result < 0) {
throw new IllegalArgumentException("Top bit not zero: " + result);
}
return result;
}
/** /**
* Replaces length prefixes of NAL units in {@code buffer} with start code prefixes, within the * Replaces length prefixes of NAL units in {@code buffer} with start code prefixes, within the
...@@ -92,7 +42,9 @@ public final class Mp4Util { ...@@ -92,7 +42,9 @@ public final class Mp4Util {
buffer.position(sampleOffset + size); buffer.position(sampleOffset + size);
} }
/** Constructs and returns a NAL unit with a start code followed by the data in {@code atom}. */ /**
* Constructs and returns a NAL unit with a start code followed by the data in {@code atom}.
*/
public static byte[] parseChildNalUnit(ParsableByteArray atom) { public static byte[] parseChildNalUnit(ParsableByteArray atom) {
int length = atom.readUnsignedShort(); int length = atom.readUnsignedShort();
int offset = atom.getPosition(); int offset = atom.getPosition();
...@@ -101,43 +53,39 @@ public final class Mp4Util { ...@@ -101,43 +53,39 @@ public final class Mp4Util {
} }
/** /**
* Finds the first NAL unit in {@code data}. * Gets the type of the NAL unit in {@code data} that starts at {@code offset}.
* <p>
* For a NAL unit to be found, its first four bytes must be contained within the part of the
* array being searched.
* *
* @param data The data to search. * @param data The data to search.
* @param startOffset The offset (inclusive) in the data to start the search. * @param offset The start offset of a NAL unit. Must lie between {@code -3} (inclusive) and
* @param endOffset The offset (exclusive) in the data to end the search. * {@code data.length - 3} (exclusive).
* @param type The type of the NAL unit to search for, or -1 for any NAL unit. * @return The type of the unit.
* @return The offset of the NAL unit, or {@code endOffset} if a NAL unit was not found.
*/ */
public static int findNalUnit(byte[] data, int startOffset, int endOffset, int type) { public static int getNalUnitType(byte[] data, int offset) {
return findNalUnit(data, startOffset, endOffset, type, null); return data[offset + 3] & 0x1F;
} }
/** /**
* Like {@link #findNalUnit(byte[], int, int, int)}, but supports finding of NAL units across * Finds the first NAL unit in {@code data}.
* array boundaries.
* <p> * <p>
* To use this method, pass the same {@code prefixFlags} parameter to successive calls where the * If {@code prefixFlags} is null then the first four bytes of a NAL unit must be entirely
* data passed represents a contiguous stream. The state maintained in this parameter allows the * contained within the part of the array being searched in order for it to be found.
* detection of NAL units where the NAL unit prefix spans array boundaries.
* <p> * <p>
* Note that when using {@code prefixFlags} the return value may be 3, 2 or 1 less than * When {@code prefixFlags} is non-null, this method supports finding NAL units whose first four
* {@code startOffset}, to indicate a NAL unit starting 3, 2 or 1 bytes before the first byte in * bytes span {@code data} arrays passed to successive calls. To use this feature, pass the same
* the current array. * {@code prefixFlags} parameter to successive calls. State maintained in this parameter enables
* the detection of such NAL units. Note that when using this feature, the return value may be 3,
* 2 or 1 less than {@code startOffset}, to indicate a NAL unit starting 3, 2 or 1 bytes before
* the first byte in the current array.
* *
* @param data The data to search. * @param data The data to search.
* @param startOffset The offset (inclusive) in the data to start the search. * @param startOffset The offset (inclusive) in the data to start the search.
* @param endOffset The offset (exclusive) in the data to end the search. * @param endOffset The offset (exclusive) in the data to end the search.
* @param type The type of the NAL unit to search for, or -1 for any NAL unit.
* @param prefixFlags A boolean array whose first three elements are used to store the state * @param prefixFlags A boolean array whose first three elements are used to store the state
* required to detect NAL units where the NAL unit prefix spans array boundaries. The array * required to detect NAL units where the NAL unit prefix spans array boundaries. The array
* must be at least 3 elements long. * must be at least 3 elements long.
* @return The offset of the NAL unit, or {@code endOffset} if a NAL unit was not found. * @return The offset of the NAL unit, or {@code endOffset} if a NAL unit was not found.
*/ */
public static int findNalUnit(byte[] data, int startOffset, int endOffset, int type, public static int findNalUnit(byte[] data, int startOffset, int endOffset,
boolean[] prefixFlags) { boolean[] prefixFlags) {
int length = endOffset - startOffset; int length = endOffset - startOffset;
...@@ -147,15 +95,14 @@ public final class Mp4Util { ...@@ -147,15 +95,14 @@ public final class Mp4Util {
} }
if (prefixFlags != null) { if (prefixFlags != null) {
if (prefixFlags[0] && matchesType(data, startOffset, type)) { if (prefixFlags[0]) {
clearPrefixFlags(prefixFlags); clearPrefixFlags(prefixFlags);
return startOffset - 3; return startOffset - 3;
} else if (length > 1 && prefixFlags[1] && data[startOffset] == 1 } else if (length > 1 && prefixFlags[1] && data[startOffset] == 1) {
&& matchesType(data, startOffset + 1, type)) {
clearPrefixFlags(prefixFlags); clearPrefixFlags(prefixFlags);
return startOffset - 2; return startOffset - 2;
} else if (length > 2 && prefixFlags[2] && data[startOffset] == 0 } else if (length > 2 && prefixFlags[2] && data[startOffset] == 0
&& data[startOffset + 1] == 1 && matchesType(data, startOffset + 2, type)) { && data[startOffset + 1] == 1) {
clearPrefixFlags(prefixFlags); clearPrefixFlags(prefixFlags);
return startOffset - 1; return startOffset - 1;
} }
...@@ -169,8 +116,7 @@ public final class Mp4Util { ...@@ -169,8 +116,7 @@ public final class Mp4Util {
if ((data[i] & 0xFE) != 0) { if ((data[i] & 0xFE) != 0) {
// There isn't a NAL prefix here, or at the next two positions. Do nothing and let the // There isn't a NAL prefix here, or at the next two positions. Do nothing and let the
// loop advance the index by three. // loop advance the index by three.
} else if (data[i - 2] == 0 && data[i - 1] == 0 && data[i] == 1 } else if (data[i - 2] == 0 && data[i - 1] == 0 && data[i] == 1) {
&& matchesType(data, i + 1, type)) {
if (prefixFlags != null) { if (prefixFlags != null) {
clearPrefixFlags(prefixFlags); clearPrefixFlags(prefixFlags);
} }
...@@ -199,45 +145,25 @@ public final class Mp4Util { ...@@ -199,45 +145,25 @@ public final class Mp4Util {
} }
/** /**
* Like {@link #findNalUnit(byte[], int, int, int)} with {@code type == -1}. * Reads an unsigned integer into an integer. This method is suitable for use when it can be
* * assumed that the top bit will always be set to zero.
* @param data The data to search.
* @param startOffset The offset (inclusive) in the data to start the search.
* @param endOffset The offset (exclusive) in the data to end the search.
* @return The offset of the NAL unit, or {@code endOffset} if a NAL unit was not found.
*/
public static int findNalUnit(byte[] data, int startOffset, int endOffset) {
return findNalUnit(data, startOffset, endOffset, null);
}
/**
* Like {@link #findNalUnit(byte[], int, int, int, boolean[])} with {@code type == -1}.
*
* @param data The data to search.
* @param startOffset The offset (inclusive) in the data to start the search.
* @param endOffset The offset (exclusive) in the data to end the search.
* @param prefixFlags A boolean array of length at least 3.
* @return The offset of the NAL unit, or {@code endOffset} if a NAL unit was not found.
*/
public static int findNalUnit(byte[] data, int startOffset, int endOffset,
boolean[] prefixFlags) {
return findNalUnit(data, startOffset, endOffset, -1, prefixFlags);
}
/**
* Gets the type of the NAL unit in {@code data} that starts at {@code offset}.
* *
* @param data The data to search. * @throws IllegalArgumentException If the top bit of the input data is set.
* @param offset The start offset of a NAL unit. Must lie between {@code -3} (inclusive) and
* {@code data.length - 3} (exclusive).
* @return The type of the unit.
*/ */
public static int getNalUnitType(byte[] data, int offset) { private static int readUnsignedIntToInt(ByteBuffer data) {
return data[offset + 3] & 0x1F; int result = 0xFF & data.get();
for (int i = 1; i < 4; i++) {
result <<= 8;
result |= 0xFF & data.get();
}
if (result < 0) {
throw new IllegalArgumentException("Top bit not zero: " + result);
}
return result;
} }
/** /**
* Clears prefix flags, as used by {@link #findNalUnit(byte[], int, int, int, boolean[])}. * Clears prefix flags, as used by {@link #findNalUnit(byte[], int, int, boolean[])}.
* *
* @param prefixFlags The flags to clear. * @param prefixFlags The flags to clear.
*/ */
...@@ -247,11 +173,4 @@ public final class Mp4Util { ...@@ -247,11 +173,4 @@ public final class Mp4Util {
prefixFlags[2] = false; prefixFlags[2] = false;
} }
/**
* Returns true if the type at {@code offset} is equal to {@code type}, or if {@code type == -1}.
*/
private static boolean matchesType(byte[] data, int offset, int type) {
return type == -1 || (data[offset] & 0x1F) == type;
}
} }
...@@ -37,6 +37,10 @@ public class MimeTypes { ...@@ -37,6 +37,10 @@ public class MimeTypes {
public static final String AUDIO_AC3 = BASE_TYPE_AUDIO + "/ac3"; public static final String AUDIO_AC3 = BASE_TYPE_AUDIO + "/ac3";
public static final String AUDIO_EC3 = BASE_TYPE_AUDIO + "/eac3"; public static final String AUDIO_EC3 = BASE_TYPE_AUDIO + "/eac3";
public static final String AUDIO_WEBM = BASE_TYPE_AUDIO + "/webm"; public static final String AUDIO_WEBM = BASE_TYPE_AUDIO + "/webm";
public static final String AUDIO_MPEG = BASE_TYPE_AUDIO + "/mpeg";
public static final String AUDIO_MPEG_L1 = BASE_TYPE_AUDIO + "/mpeg-L1";
public static final String AUDIO_MPEG_L2 = BASE_TYPE_AUDIO + "/mpeg-L2";
public static final String AUDIO_VORBIS = BASE_TYPE_AUDIO + "/vorbis"; public static final String AUDIO_VORBIS = BASE_TYPE_AUDIO + "/vorbis";
public static final String AUDIO_OPUS = BASE_TYPE_AUDIO + "/opus"; public static final String AUDIO_OPUS = BASE_TYPE_AUDIO + "/opus";
...@@ -45,6 +49,7 @@ public class MimeTypes { ...@@ -45,6 +49,7 @@ public class MimeTypes {
public static final String APPLICATION_ID3 = BASE_TYPE_APPLICATION + "/id3"; public static final String APPLICATION_ID3 = BASE_TYPE_APPLICATION + "/id3";
public static final String APPLICATION_EIA608 = BASE_TYPE_APPLICATION + "/eia-608"; public static final String APPLICATION_EIA608 = BASE_TYPE_APPLICATION + "/eia-608";
public static final String APPLICATION_TTML = BASE_TYPE_APPLICATION + "/ttml+xml"; public static final String APPLICATION_TTML = BASE_TYPE_APPLICATION + "/ttml+xml";
public static final String APPLICATION_M3U8 = BASE_TYPE_APPLICATION + "/x-mpegURL";
private MimeTypes() {} private MimeTypes() {}
......
...@@ -37,6 +37,12 @@ public final class ParsableByteArray { ...@@ -37,6 +37,12 @@ public final class ParsableByteArray {
limit = data.length; limit = data.length;
} }
/** Creates a new instance wrapping {@code data}. */
public ParsableByteArray(byte[] data) {
this.data = data;
limit = data.length;
}
/** /**
* Creates a new instance that wraps an existing array. * Creates a new instance that wraps an existing array.
* *
...@@ -171,6 +177,13 @@ public final class ParsableByteArray { ...@@ -171,6 +177,13 @@ public final class ParsableByteArray {
return result; return result;
} }
/** Reads the next three bytes as an unsigned value. */
public int readUnsignedInt24() {
int result = shiftIntoInt(data, position, 3);
position += 3;
return result;
}
/** Reads the next four bytes as an unsigned value. */ /** Reads the next four bytes as an unsigned value. */
public long readUnsignedInt() { public long readUnsignedInt() {
long result = shiftIntoLong(data, position, 4); long result = shiftIntoLong(data, position, 4);
...@@ -180,9 +193,11 @@ public final class ParsableByteArray { ...@@ -180,9 +193,11 @@ public final class ParsableByteArray {
/** Reads the next four bytes as a signed value. */ /** Reads the next four bytes as a signed value. */
public int readInt() { public int readInt() {
int result = shiftIntoInt(data, position, 4); // shiftIntoInt inlined as performance optimization.
position += 4; return (data[position++] & 0xFF) << 24
return result; | (data[position++] & 0xFF) << 16
| (data[position++] & 0xFF) << 8
| data[position++] & 0xFF;
} }
/** Reads the next eight bytes as a signed value. */ /** Reads the next eight bytes as a signed value. */
...@@ -221,8 +236,11 @@ public final class ParsableByteArray { ...@@ -221,8 +236,11 @@ public final class ParsableByteArray {
* @throws IllegalArgumentException Thrown if the top bit of the input data is set. * @throws IllegalArgumentException Thrown if the top bit of the input data is set.
*/ */
public int readUnsignedIntToInt() { public int readUnsignedIntToInt() {
int result = shiftIntoInt(data, position, 4); // shiftIntoInt inlined as performance optimization.
position += 4; final int result = (data[position++] & 0xFF) << 24
| (data[position++] & 0xFF) << 16
| (data[position++] & 0xFF) << 8
| data[position++] & 0xFF;
if (result < 0) { if (result < 0) {
throw new IllegalArgumentException("Top bit not zero: " + result); throw new IllegalArgumentException("Top bit not zero: " + result);
} }
......
/*
* 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.util;
import android.net.Uri;
import android.text.TextUtils;
/**
* Utility methods for manipulating URIs.
*/
public final class UriUtil {
/**
* The length of arrays returned by {@link #getUriIndices(String)}.
*/
private static final int INDEX_COUNT = 4;
/**
* An index into an array returned by {@link #getUriIndices(String)}.
* <p>
* The value at this position in the array is the index of the ':' after the scheme. Equals -1 if
* the URI is a relative reference (no scheme). The hier-part starts at (schemeColon + 1),
* including when the URI has no scheme.
*/
private static final int SCHEME_COLON = 0;
/**
* An index into an array returned by {@link #getUriIndices(String)}.
* <p>
* The value at this position in the array is the index of the path part. Equals (schemeColon + 1)
* if no authority part, (schemeColon + 3) if the authority part consists of just "//", and
* (query) if no path part. The characters starting at this index can be "//" only if the
* authority part is non-empty (in this case the double-slash means the first segment is empty).
*/
private static final int PATH = 1;
/**
* An index into an array returned by {@link #getUriIndices(String)}.
* <p>
* The value at this position in the array is the index of the query part, including the '?'
* before the query. Equals fragment if no query part, and (fragment - 1) if the query part is a
* single '?' with no data.
*/
private static final int QUERY = 2;
/**
* An index into an array returned by {@link #getUriIndices(String)}.
* <p>
* The value at this position in the array is the index of the fragment part, including the '#'
* before the fragment. Equal to the length of the URI if no fragment part, and (length - 1) if
* the fragment part is a single '#' with no data.
*/
private static final int FRAGMENT = 3;
private UriUtil() {}
/**
* Like {@link #resolve(String, String)}, but returns a {@link Uri} instead of a {@link String}.
*
* @param baseUri The base URI.
* @param referenceUri The reference URI to resolve.
*/
public static Uri resolveToUri(String baseUri, String referenceUri) {
return Uri.parse(resolve(baseUri, referenceUri));
}
/**
* Performs relative resolution of a {@code referenceUri} with respect to a {@code baseUri}.
* <p>
* The resolution is performed as specified by RFC-3986.
*
* @param baseUri The base URI.
* @param referenceUri The reference URI to resolve.
*/
public static String resolve(String baseUri, String referenceUri) {
StringBuilder uri = new StringBuilder();
// Map null onto empty string, to make the following logic simpler.
baseUri = baseUri == null ? "" : baseUri;
referenceUri = referenceUri == null ? "" : referenceUri;
int[] refIndices = getUriIndices(referenceUri);
if (refIndices[SCHEME_COLON] != -1) {
// The reference is absolute. The target Uri is the reference.
uri.append(referenceUri);
removeDotSegments(uri, refIndices[PATH], refIndices[QUERY]);
return uri.toString();
}
int[] baseIndices = getUriIndices(baseUri);
if (refIndices[FRAGMENT] == 0) {
// The reference is empty or contains just the fragment part, then the target Uri is the
// concatenation of the base Uri without its fragment, and the reference.
return uri.append(baseUri, 0, baseIndices[FRAGMENT]).append(referenceUri).toString();
}
if (refIndices[QUERY] == 0) {
// The reference starts with the query part. The target is the base up to (but excluding) the
// query, plus the reference.
return uri.append(baseUri, 0, baseIndices[QUERY]).append(referenceUri).toString();
}
if (refIndices[PATH] != 0) {
// The reference has authority. The target is the base scheme plus the reference.
int baseLimit = baseIndices[SCHEME_COLON] + 1;
uri.append(baseUri, 0, baseLimit).append(referenceUri);
return removeDotSegments(uri, baseLimit + refIndices[PATH], baseLimit + refIndices[QUERY]);
}
if (refIndices[PATH] != refIndices[QUERY] && referenceUri.charAt(refIndices[PATH]) == '/') {
// The reference path is rooted. The target is the base scheme and authority (if any), plus
// the reference.
uri.append(baseUri, 0, baseIndices[PATH]).append(referenceUri);
return removeDotSegments(uri, baseIndices[PATH], baseIndices[PATH] + refIndices[QUERY]);
}
// The target Uri is the concatenation of the base Uri up to (but excluding) the last segment,
// and the reference. This can be split into 2 cases:
if (baseIndices[SCHEME_COLON] + 2 < baseIndices[PATH]
&& baseIndices[PATH] == baseIndices[QUERY]) {
// Case 1: The base hier-part is just the authority, with an empty path. An additional '/' is
// needed after the authority, before appending the reference.
uri.append(baseUri, 0, baseIndices[PATH]).append('/').append(referenceUri);
return removeDotSegments(uri, baseIndices[PATH], baseIndices[PATH] + refIndices[QUERY] + 1);
} else {
// Case 2: Otherwise, find the last '/' in the base hier-part and append the reference after
// it. If base hier-part has no '/', it could only mean that it is completely empty or
// contains only one segment, in which case the whole hier-part is excluded and the reference
// is appended right after the base scheme colon without an added '/'.
int lastSlashIndex = baseUri.lastIndexOf('/', baseIndices[QUERY] - 1);
int baseLimit = lastSlashIndex == -1 ? baseIndices[PATH] : lastSlashIndex + 1;
uri.append(baseUri, 0, baseLimit).append(referenceUri);
return removeDotSegments(uri, baseIndices[PATH], baseLimit + refIndices[QUERY]);
}
}
/**
* Removes dot segments from the path of a URI.
*
* @param uri A {@link StringBuilder} containing the URI.
* @param offset The index of the start of the path in {@code uri}.
* @param limit The limit (exclusive) of the path in {@code uri}.
*/
private static String removeDotSegments(StringBuilder uri, int offset, int limit) {
if (offset >= limit) {
// Nothing to do.
return uri.toString();
}
if (uri.charAt(offset) == '/') {
// If the path starts with a /, always retain it.
offset++;
}
// The first character of the current path segment.
int segmentStart = offset;
int i = offset;
while (i <= limit) {
int nextSegmentStart = -1;
if (i == limit) {
nextSegmentStart = i;
} else if (uri.charAt(i) == '/') {
nextSegmentStart = i + 1;
} else {
i++;
continue;
}
// We've encountered the end of a segment or the end of the path. If the final segment was
// "." or "..", remove the appropriate segments of the path.
if (i == segmentStart + 1 && uri.charAt(segmentStart) == '.') {
// Given "abc/def/./ghi", remove "./" to get "abc/def/ghi".
uri.delete(segmentStart, nextSegmentStart);
limit -= nextSegmentStart - segmentStart;
i = segmentStart;
} else if (i == segmentStart + 2 && uri.charAt(segmentStart) == '.'
&& uri.charAt(segmentStart + 1) == '.') {
// Given "abc/def/../ghi", remove "def/../" to get "abc/ghi".
int prevSegmentStart = uri.lastIndexOf("/", segmentStart - 2) + 1;
int removeFrom = prevSegmentStart > offset ? prevSegmentStart : offset;
uri.delete(removeFrom, nextSegmentStart);
limit -= nextSegmentStart - removeFrom;
segmentStart = prevSegmentStart;
i = prevSegmentStart;
} else {
i++;
segmentStart = i;
}
}
return uri.toString();
}
/**
* Calculates indices of the constituent components of a URI.
*
* @param uriString The URI as a string.
* @return The corresponding indices.
*/
private static int[] getUriIndices(String uriString) {
int[] indices = new int[INDEX_COUNT];
if (TextUtils.isEmpty(uriString)) {
indices[SCHEME_COLON] = -1;
return indices;
}
// Determine outer structure from right to left.
// Uri = scheme ":" hier-part [ "?" query ] [ "#" fragment ]
int length = uriString.length();
int fragmentIndex = uriString.indexOf('#');
if (fragmentIndex == -1) {
fragmentIndex = length;
}
int queryIndex = uriString.indexOf('?');
if (queryIndex == -1 || queryIndex > fragmentIndex) {
// '#' before '?': '?' is within the fragment.
queryIndex = fragmentIndex;
}
// Slashes are allowed only in hier-part so any colon after the first slash is part of the
// hier-part, not the scheme colon separator.
int schemeIndexLimit = uriString.indexOf('/');
if (schemeIndexLimit == -1 || schemeIndexLimit > queryIndex) {
schemeIndexLimit = queryIndex;
}
int schemeIndex = uriString.indexOf(':');
if (schemeIndex > schemeIndexLimit) {
// '/' before ':'
schemeIndex = -1;
}
// Determine hier-part structure: hier-part = "//" authority path / path
// This block can also cope with schemeIndex == -1.
boolean hasAuthority = schemeIndex + 2 < queryIndex
&& uriString.charAt(schemeIndex + 1) == '/'
&& uriString.charAt(schemeIndex + 2) == '/';
int pathIndex;
if (hasAuthority) {
pathIndex = uriString.indexOf('/', schemeIndex + 3); // find first '/' after "://"
if (pathIndex == -1 || pathIndex > queryIndex) {
pathIndex = queryIndex;
}
} else {
pathIndex = schemeIndex + 1;
}
indices[SCHEME_COLON] = schemeIndex;
indices[PATH] = pathIndex;
indices[QUERY] = queryIndex;
indices[FRAGMENT] = fragmentIndex;
return indices;
}
}
...@@ -15,13 +15,17 @@ ...@@ -15,13 +15,17 @@
*/ */
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.net.Uri;
import android.text.TextUtils; import android.text.TextUtils;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
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;
...@@ -58,6 +62,8 @@ public final class Util { ...@@ -58,6 +62,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() {}
/** /**
...@@ -125,61 +131,26 @@ public final class Util { ...@@ -125,61 +131,26 @@ public final class Util {
} }
/** /**
* Converts text to lower case using {@link Locale#US}. * Closes an {@link OutputStream}, suppressing any {@link IOException} that may occur.
* *
* @param text The text to convert. * @param outputStream The {@link OutputStream} to close.
* @return The lower case text, or null if {@code text} is null.
*/ */
public static String toLowerInvariant(String text) { public static void closeQuietly(OutputStream outputStream) {
return text == null ? null : text.toLowerCase(Locale.US); try {
} outputStream.close();
} catch (IOException e) {
/** // Ignore.
* Like {@link Uri#parse(String)}, but discards the part of the uri that follows the final }
* forward slash.
*
* @param uriString An RFC 2396-compliant, encoded uri.
* @return The parsed base uri.
*/
public static Uri parseBaseUri(String uriString) {
return Uri.parse(uriString.substring(0, uriString.lastIndexOf('/')));
} }
/** /**
* Merges a uri and a string to produce a new uri. * Converts text to lower case using {@link Locale#US}.
* <p>
* The uri is built according to the following rules:
* <ul>
* <li>If {@code baseUri} is null or if {@code stringUri} is absolute, then {@code baseUri} is
* ignored and the uri consists solely of {@code stringUri}.
* <li>If {@code stringUri} is null, then the uri consists solely of {@code baseUrl}.
* <li>Otherwise, the uri consists of the concatenation of {@code baseUri} and {@code stringUri}.
* </ul>
* *
* @param baseUri A uri that can form the base of the merged uri. * @param text The text to convert.
* @param stringUri A relative or absolute uri in string form. * @return The lower case text, or null if {@code text} is null.
* @return The merged uri.
*/ */
public static Uri getMergedUri(Uri baseUri, String stringUri) { public static String toLowerInvariant(String text) {
if (stringUri == null) { return text == null ? null : text.toLowerCase(Locale.US);
return baseUri;
}
if (baseUri == null) {
return Uri.parse(stringUri);
}
if (stringUri.startsWith("/")) {
stringUri = stringUri.substring(1);
return new Uri.Builder()
.scheme(baseUri.getScheme())
.authority(baseUri.getAuthority())
.appendEncodedPath(stringUri)
.build();
}
Uri uri = Uri.parse(stringUri);
if (uri.isAbsolute()) {
return uri;
}
return Uri.withAppendedPath(baseUri, stringUri);
} }
/** /**
...@@ -445,4 +416,48 @@ public final class Util { ...@@ -445,4 +416,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.
}
}
} }
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" path="gen"/>
<classpathentry kind="src" path="java"/>
<classpathentry kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.LIBRARIES"/>
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.DEPENDENCIES"/>
<classpathentry combineaccessrules="false" kind="src" path="/ExoPlayerDemo"/>
<classpathentry kind="output" path="bin/classes"/>
</classpath>
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>ExoPlayerTests</name>
<comment></comment>
<projects>
<project>ExoPlayerLib</project>
</projects>
<buildSpec>
<buildCommand>
<name>com.android.ide.eclipse.adt.ResourceManagerBuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>com.android.ide.eclipse.adt.PreCompilerBuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>com.android.ide.eclipse.adt.ApkBuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>com.android.ide.eclipse.adt.AndroidNature</nature>
<nature>org.eclipse.jdt.core.javanature</nature>
</natures>
<linkedResources>
<link>
<name>libs/dexmaker-1.2.jar</name>
<type>1</type>
<locationURI>$%7BPARENT-3-PROJECT_LOC%7D/third_party/dexmaker/dexmaker-1.2.jar</locationURI>
</link>
<link>
<name>libs/dexmaker-mockito-1.2.jar</name>
<type>1</type>
<locationURI>$%7BPARENT-3-PROJECT_LOC%7D/third_party/dexmaker/dexmaker-mockito-1.2.jar</locationURI>
</link>
<link>
<name>libs/mockito-all-1.9.5.jar</name>
<type>1</type>
<locationURI>$%7BPARENT-3-PROJECT_LOC%7D/third_party/mockito/mockito-all-1.9.5.jar</locationURI>
</link>
</linkedResources>
<filteredResources>
<filter>
<id>1425657306619</id>
<name></name>
<type>14</type>
<matcher>
<id>org.eclipse.ui.ide.multiFilter</id>
<arguments>1.0-name-matches-true-false-BUILD</arguments>
</matcher>
</filter>
</filteredResources>
</projectDescription>
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.google.android.exoplayer.tests">
<uses-sdk android:minSdkVersion="9" android:targetSdkVersion="21"/>
<application>
<uses-library android:name="android.test.runner"/>
</application>
<instrumentation
android:targetPackage="com.google.android.exoplayer.demo"
android:name="android.test.InstrumentationTestRunner"/>
</manifest>
<?xml version="1.0" encoding="UTF-8"?>
<MPD xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
availabilityStartTime="2014-06-19T23:07:42"
minBufferTime="PT1.500S"
minimumUpdatePeriod="PT5.000S"
profiles="urn:mpeg:dash:profile:isoff-main:2011"
timeShiftBufferDepth="PT129600.000S"
type="dynamic"
xmlns="urn:mpeg:DASH:schema:MPD:2011"
xsi:schemaLocation="urn:mpeg:DASH:schema:MPD:2011 DASH-MPD.xsd"
yt:earliestMediaSequence="1266404" >
<Period start="PT6462826.784S" >
<SegmentList
presentationTimeOffset="34740095"
startNumber="1292317"
timescale="1000" >
<SegmentTimeline>
<S d="4804" />
<S d="5338" />
<S d="4938" />
</SegmentTimeline>
</SegmentList>
<AdaptationSet
mimeType="audio/mp4"
subsegmentAlignment="true" >
<Role
schemeIdUri="urn:mpeg:DASH:role:2011"
value="main" />
<Representation
id="141"
audioSamplingRate="48000"
bandwidth="272000"
codecs="mp4a.40.2"
startWithSAP="1" >
<AudioChannelConfiguration
schemeIdUri="urn:mpeg:dash:23003:3:audio_channel_configuration:2011"
value="2" />
<BaseURL>
http://www.test.com/141
</BaseURL>
<SegmentList>
<Initialization
range="0-591"
sourceURL="sq/0/clen/79480/lmt/1403219262956762/dur/4.805" />
<SegmentURL media="sq/1292317/clen/77447/lmt/1409671169987621/dur/4.805" />
<SegmentURL media="sq/1292318/clen/86958/lmt/1409671174832549/dur/5.339" />
<SegmentURL media="sq/1292319/clen/85018/lmt/1409671179719956/dur/4.938" />
</SegmentList>
</Representation>
</AdaptationSet>
<AdaptationSet
mimeType="video/mp4"
subsegmentAlignment="true" >
<Role
schemeIdUri="urn:mpeg:DASH:role:2011"
value="main" />
<Representation
id="135"
bandwidth="1116000"
codecs="avc1.42c01f"
height="480"
startWithSAP="1"
width="854" >
<BaseURL>
http://www.test.com/135
</BaseURL>
<SegmentList>
<Initialization
range="0-671"
sourceURL="sq/0/clen/1221137/lmt/1403219262956762/dur/4.805" />
<SegmentURL media="sq/1292317/clen/1279915/lmt/1409671169987621/dur/4.805" />
<SegmentURL media="sq/1292318/clen/1310650/lmt/1409671174832549/dur/5.339" />
<SegmentURL media="sq/1292319/clen/1486558/lmt/1409671179719956/dur/4.938" />
</SegmentList>
</Representation>
</AdaptationSet>
<AdaptationSet
lang="en"
mimeType="text/vtt" >
<Role
schemeIdUri="urn:mpeg:DASH:role:2011"
value="caption" />
<Representation
id="en"
bandwidth="0"
codecs="" >
<BaseURL>
http://www.test.com/vtt
</BaseURL>
<SegmentList>
<SegmentURL media="sq/1292317" />
<SegmentURL media="sq/1292318" />
<SegmentURL media="sq/1292319" />
</SegmentList>
</Representation>
</AdaptationSet>
</Period>
</MPD>
WEBVTT
X-TIMESTAMP-MAP=LOCAL:00:00.000,MPEGTS:450000
00:00.000 --> 00:01.234
This is the first subtitle.
00:02.345 --> 00:03.456
This is the second subtitle.
WEBVTT
X-TIMESTAMP-MAP=LOCAL:00:00.000,MPEGTS:450000
1
00:00.000 --> 00:01.234
This is the first subtitle.
2
00:02.345 --> 00:03.456
This is the second subtitle.
WEBVTT
X-TIMESTAMP-MAP=LOCAL:00:00.000,MPEGTS:450000
00:00.000 --> 00:01.234
This is the <i>first</i> subtitle.
00:02.345 --> 00:03.456
This is the <b><i>second</b></i> subtitle.
00:04.000 --> 00:05.000
This is the <c.red.caps>third</c> subtitle.
00:06.000 --> 00:07.000
This is&nbsp;the &lt;fourth&gt; &amp;subtitle.
/*
* 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;
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);
}
}
/*
* 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;
import com.google.android.exoplayer.util.Util;
import android.annotation.TargetApi;
import junit.framework.TestCase;
import java.util.ArrayList;
import java.util.List;
/**
* Unit test for {@link MediaFormat}.
*/
public class MediaFormatTest extends TestCase {
public void testConversionToFrameworkFormat() {
if (Util.SDK_INT < 16) {
// Test doesn't apply.
return;
}
byte[] initData1 = new byte[] {1, 2, 3};
byte[] initData2 = new byte[] {4, 5, 6};
List<byte[]> initData = new ArrayList<byte[]>();
initData.add(initData1);
initData.add(initData2);
testConversionToFrameworkFormatV16(
MediaFormat.createVideoFormat("video/xyz", 102400, 1000L, 1280, 720, 1.5f, initData));
testConversionToFrameworkFormatV16(
MediaFormat.createAudioFormat("audio/xyz", 102400, 1000L, 5, 44100, initData));
}
@TargetApi(16)
private void testConversionToFrameworkFormatV16(MediaFormat format) {
// Convert to a framework MediaFormat and back again.
MediaFormat convertedFormat = MediaFormat.createFromFrameworkMediaFormatV16(
format.getFrameworkMediaFormatV16());
// Assert that we end up with an equivalent object to the one we started with.
assertEquals(format.hashCode(), convertedFormat.hashCode());
assertEquals(format, convertedFormat);
}
}
/*
* 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.chunk.parser.webm;
import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.upstream.ByteArrayNonBlockingInputStream;
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
import junit.framework.TestCase;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* Tests {@link DefaultEbmlReader}.
*/
public class DefaultEbmlReaderTest extends TestCase {
private final EventCapturingEbmlEventHandler eventHandler =
new EventCapturingEbmlEventHandler();
public void testNothing() {
NonBlockingInputStream input = createTestInputStream();
assertNoEvents(input, EbmlReader.READ_RESULT_END_OF_STREAM);
}
public void testMasterElement() {
NonBlockingInputStream input =
createTestInputStream(0x1A, 0x45, 0xDF, 0xA3, 0x84, 0x42, 0x85, 0x81, 0x01);
EventCapturingEbmlEventHandler expected = new EventCapturingEbmlEventHandler();
expected.onMasterElementStart(EventCapturingEbmlEventHandler.ID_EBML, 0, 5, 4);
expected.onIntegerElement(EventCapturingEbmlEventHandler.ID_DOC_TYPE_READ_VERSION, 1);
expected.onMasterElementEnd(EventCapturingEbmlEventHandler.ID_EBML);
assertEvents(input, EbmlReader.READ_RESULT_END_OF_STREAM, expected.events);
}
public void testMasterElementEmpty() {
NonBlockingInputStream input = createTestInputStream(0x18, 0x53, 0x80, 0x67, 0x80);
EventCapturingEbmlEventHandler expected = new EventCapturingEbmlEventHandler();
expected.onMasterElementStart(EventCapturingEbmlEventHandler.ID_SEGMENT, 0, 5, 0);
expected.onMasterElementEnd(EventCapturingEbmlEventHandler.ID_SEGMENT);
assertEvents(input, EbmlReader.READ_RESULT_END_OF_STREAM, expected.events);
}
public void testUnsignedIntegerElement() {
// 0xFE is chosen because for signed integers it should be interpreted as -2
NonBlockingInputStream input = createTestInputStream(0x42, 0xF7, 0x81, 0xFE);
EventCapturingEbmlEventHandler expected = new EventCapturingEbmlEventHandler();
expected.onIntegerElement(EventCapturingEbmlEventHandler.ID_EBML_READ_VERSION, 254);
assertEvents(input, EbmlReader.READ_RESULT_END_OF_STREAM, expected.events);
}
public void testUnsignedIntegerElementLarge() {
NonBlockingInputStream input =
createTestInputStream(0x42, 0xF7, 0x88, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF);
EventCapturingEbmlEventHandler expected = new EventCapturingEbmlEventHandler();
expected.onIntegerElement(EventCapturingEbmlEventHandler.ID_EBML_READ_VERSION, Long.MAX_VALUE);
assertEvents(input, EbmlReader.READ_RESULT_END_OF_STREAM, expected.events);
}
public void testUnsignedIntegerElementTooLargeBecomesNegative() {
NonBlockingInputStream input =
createTestInputStream(0x42, 0xF7, 0x88, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF);
EventCapturingEbmlEventHandler expected = new EventCapturingEbmlEventHandler();
expected.onIntegerElement(EventCapturingEbmlEventHandler.ID_EBML_READ_VERSION, -1);
assertEvents(input, EbmlReader.READ_RESULT_END_OF_STREAM, expected.events);
}
public void testStringElement() {
NonBlockingInputStream input =
createTestInputStream(0x42, 0x82, 0x86, 0x41, 0x62, 0x63, 0x31, 0x32, 0x33);
EventCapturingEbmlEventHandler expected = new EventCapturingEbmlEventHandler();
expected.onStringElement(EventCapturingEbmlEventHandler.ID_DOC_TYPE, "Abc123");
assertEvents(input, EbmlReader.READ_RESULT_END_OF_STREAM, expected.events);
}
public void testStringElementEmpty() {
NonBlockingInputStream input = createTestInputStream(0x42, 0x82, 0x80);
EventCapturingEbmlEventHandler expected = new EventCapturingEbmlEventHandler();
expected.onStringElement(EventCapturingEbmlEventHandler.ID_DOC_TYPE, "");
assertEvents(input, EbmlReader.READ_RESULT_END_OF_STREAM, expected.events);
}
public void testFloatElementThreeBytes() {
try {
eventHandler.read(createTestInputStream(0x44, 0x89, 0x83, 0x3F, 0x80, 0x00));
fail();
} catch (IllegalStateException exception) {
// Expected
}
assertNoEvents();
}
public void testFloatElementFourBytes() {
NonBlockingInputStream input =
createTestInputStream(0x44, 0x89, 0x84, 0x3F, 0x80, 0x00, 0x00);
EventCapturingEbmlEventHandler expected = new EventCapturingEbmlEventHandler();
expected.onFloatElement(EventCapturingEbmlEventHandler.ID_DURATION, 1.0);
assertEvents(input, EbmlReader.READ_RESULT_END_OF_STREAM, expected.events);
}
public void testFloatElementEightBytes() {
NonBlockingInputStream input =
createTestInputStream(0x44, 0x89, 0x88, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00);
EventCapturingEbmlEventHandler expected = new EventCapturingEbmlEventHandler();
expected.onFloatElement(EventCapturingEbmlEventHandler.ID_DURATION, -2.0);
assertEvents(input, EbmlReader.READ_RESULT_END_OF_STREAM, expected.events);
}
public void testBinaryElementReadBytes() {
eventHandler.binaryElementHandler = EventCapturingEbmlEventHandler.HANDLER_READ_BYTES;
NonBlockingInputStream input =
createTestInputStream(0xA3, 0x88, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08);
EventCapturingEbmlEventHandler expected = new EventCapturingEbmlEventHandler();
expected.binaryElementHandler = EventCapturingEbmlEventHandler.HANDLER_READ_BYTES;
expected.onBinaryElement(
EventCapturingEbmlEventHandler.ID_SIMPLE_BLOCK, 0, 0, 8,
createTestInputStream(0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08));
assertEvents(input, EbmlReader.READ_RESULT_END_OF_STREAM, expected.events);
}
public void testBinaryElementReadVarint() {
eventHandler.binaryElementHandler = EventCapturingEbmlEventHandler.HANDLER_READ_VARINT;
NonBlockingInputStream input = createTestInputStream(0xA3, 0x82, 0x40, 0x2A);
EventCapturingEbmlEventHandler expected = new EventCapturingEbmlEventHandler();
expected.binaryElementHandler = EventCapturingEbmlEventHandler.HANDLER_READ_VARINT;
expected.onBinaryElement(
EventCapturingEbmlEventHandler.ID_SIMPLE_BLOCK, 0, 0, 0,
createTestInputStream(0x40, 0x2A));
assertEvents(input, EbmlReader.READ_RESULT_END_OF_STREAM, expected.events);
}
public void testBinaryElementSkipBytes() {
eventHandler.binaryElementHandler = EventCapturingEbmlEventHandler.HANDLER_SKIP_BYTES;
NonBlockingInputStream input =
createTestInputStream(0xA3, 0x88, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08);
EventCapturingEbmlEventHandler expected = new EventCapturingEbmlEventHandler();
expected.binaryElementHandler = EventCapturingEbmlEventHandler.HANDLER_SKIP_BYTES;
expected.onBinaryElement(
EventCapturingEbmlEventHandler.ID_SIMPLE_BLOCK, 0, 0, 8,
createTestInputStream(0, 0, 0, 0, 0, 0, 0, 0));
assertEvents(input, EbmlReader.READ_RESULT_END_OF_STREAM, expected.events);
}
public void testBinaryElementDoNothing() {
eventHandler.binaryElementHandler = EventCapturingEbmlEventHandler.HANDLER_DO_NOTHING;
try {
eventHandler.read(
createTestInputStream(0xA3, 0x88, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08));
fail();
} catch (IllegalStateException exception) {
// Expected
}
assertNoEvents();
}
public void testBinaryElementNotEnoughBytes() {
NonBlockingInputStream input = createTestInputStream(0xA3, 0x88, 0x01, 0x02, 0x03);
assertNoEvents(input, EbmlReader.READ_RESULT_NEED_MORE_DATA);
}
public void testUnknownElement() {
NonBlockingInputStream input = createTestInputStream(0xEC, 0x81, 0x00);
assertNoEvents(input, EbmlReader.READ_RESULT_END_OF_STREAM);
}
/**
* Helper to build a {@link ByteArrayNonBlockingInputStream} quickly from zero or more
* integer arguments.
*
* <p>Each argument must be able to cast to a byte value.
*
* @param data Zero or more integers with values between {@code 0x00} and {@code 0xFF}
* @return A {@link ByteArrayNonBlockingInputStream} containing the given byte values
*/
private NonBlockingInputStream createTestInputStream(int... data) {
byte[] bytes = new byte[data.length];
for (int i = 0; i < data.length; i++) {
bytes[i] = (byte) data[i];
}
return new ByteArrayNonBlockingInputStream(bytes);
}
private void assertReads(NonBlockingInputStream input, int continues, int finalResult) {
for (int i = 0; i < continues; i++) {
assertEquals(EbmlReader.READ_RESULT_CONTINUE, eventHandler.read(input));
}
assertEquals(finalResult, eventHandler.read(input));
}
private void assertNoEvents() {
assertEvents(Collections.<String>emptyList());
}
private void assertEvents(List<String> events) {
assertEquals(events.size(), eventHandler.events.size());
for (int i = 0; i < events.size(); i++) {
assertEquals(events.get(i), eventHandler.events.get(i));
}
}
private void assertNoEvents(NonBlockingInputStream input, int finalResult) {
assertReads(input, 0, finalResult);
assertNoEvents();
}
private void assertEvents(NonBlockingInputStream input, int finalResult, List<String> events) {
assertReads(input, events.size(), finalResult);
assertEvents(events);
}
/**
* An {@link EbmlEventHandler} which captures all event callbacks made by
* {@link DefaultEbmlReader} for testing purposes.
*/
private static final class EventCapturingEbmlEventHandler implements EbmlEventHandler {
// Element IDs
private static final int ID_EBML = 0x1A45DFA3;
private static final int ID_EBML_READ_VERSION = 0x42F7;
private static final int ID_DOC_TYPE = 0x4282;
private static final int ID_DOC_TYPE_READ_VERSION = 0x4285;
private static final int ID_SEGMENT = 0x18538067;
private static final int ID_DURATION = 0x4489;
private static final int ID_SIMPLE_BLOCK = 0xA3;
// Various ways to handle things in onBinaryElement()
private static final int HANDLER_DO_NOTHING = 0;
private static final int HANDLER_READ_BYTES = 1;
private static final int HANDLER_READ_VARINT = 2;
private static final int HANDLER_SKIP_BYTES = 3;
private final EbmlReader reader = new DefaultEbmlReader();
private final List<String> events = new ArrayList<String>();
private int binaryElementHandler;
private EventCapturingEbmlEventHandler() {
reader.setEventHandler(this);
}
private int read(NonBlockingInputStream inputStream) {
try {
return reader.read(inputStream);
} catch (ParserException e) {
// should never happen.
fail();
return -1;
}
}
@Override
public int getElementType(int id) {
switch (id) {
case ID_EBML:
case ID_SEGMENT:
return EbmlReader.TYPE_MASTER;
case ID_EBML_READ_VERSION:
case ID_DOC_TYPE_READ_VERSION:
return EbmlReader.TYPE_UNSIGNED_INT;
case ID_DOC_TYPE:
return EbmlReader.TYPE_STRING;
case ID_SIMPLE_BLOCK:
return EbmlReader.TYPE_BINARY;
case ID_DURATION:
return EbmlReader.TYPE_FLOAT;
default:
return EbmlReader.TYPE_UNKNOWN;
}
}
@Override
public void onMasterElementStart(
int id, long elementOffset, int headerSize, long contentsSize) {
events.add(formatEvent(id, "start elementOffset=" + elementOffset
+ " headerSize=" + headerSize + " contentsSize=" + contentsSize));
}
@Override
public void onMasterElementEnd(int id) {
events.add(formatEvent(id, "end"));
}
@Override
public void onIntegerElement(int id, long value) {
events.add(formatEvent(id, "integer=" + String.valueOf(value)));
}
@Override
public void onFloatElement(int id, double value) {
events.add(formatEvent(id, "float=" + String.valueOf(value)));
}
@Override
public void onStringElement(int id, String value) {
events.add(formatEvent(id, "string=" + value));
}
@Override
public boolean onBinaryElement(
int id, long elementOffset, int headerSize, int contentsSize,
NonBlockingInputStream inputStream) {
switch (binaryElementHandler) {
case HANDLER_READ_BYTES:
byte[] bytes = new byte[contentsSize];
reader.readBytes(inputStream, bytes, contentsSize);
events.add(formatEvent(id, "bytes=" + Arrays.toString(bytes)));
break;
case HANDLER_READ_VARINT:
long value = reader.readVarint(inputStream);
events.add(formatEvent(id, "varint=" + String.valueOf(value)));
break;
case HANDLER_SKIP_BYTES:
reader.skipBytes(inputStream, contentsSize);
events.add(formatEvent(id, "skipped " + contentsSize + " byte(s)"));
break;
case HANDLER_DO_NOTHING:
default:
// pass
}
return true;
}
private static String formatEvent(int id, String event) {
return "[" + Integer.toHexString(id) + "] " + event;
}
}
}
/*
* 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.chunk.parser.webm;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.chunk.parser.SegmentIndex;
import com.google.android.exoplayer.drm.DrmInitData;
import com.google.android.exoplayer.upstream.ByteArrayNonBlockingInputStream;
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
import com.google.android.exoplayer.util.MimeTypes;
import android.test.InstrumentationTestCase;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.UUID;
public class WebmExtractorTest extends InstrumentationTestCase {
private static final int INFO_ELEMENT_BYTE_SIZE = 31;
private static final int TRACKS_ELEMENT_BYTE_SIZE = 48;
private static final int CUES_ELEMENT_BYTE_SIZE = 12;
private static final int CUE_POINT_ELEMENT_BYTE_SIZE = 31;
private static final int DEFAULT_TIMECODE_SCALE = 1000000;
private static final long TEST_DURATION_US = 9920000L;
private static final int TEST_WIDTH = 1280;
private static final int TEST_HEIGHT = 720;
private static final int TEST_CHANNEL_COUNT = 1;
private static final int TEST_SAMPLE_RATE = 48000;
private static final long TEST_CODEC_DELAY = 6500000;
private static final long TEST_SEEK_PRE_ROLL = 80000000;
private static final int TEST_OPUS_CODEC_PRIVATE_SIZE = 2;
private static final String TEST_VORBIS_CODEC_PRIVATE = "webm/vorbis_codec_private";
private static final int TEST_VORBIS_INFO_SIZE = 30;
private static final int TEST_VORBIS_BOOKS_SIZE = 4140;
private static final byte[] TEST_ENCRYPTION_KEY_ID = { 0x00, 0x01, 0x02, 0x03 };
private static final UUID WIDEVINE_UUID = new UUID(0xEDEF8BA979D64ACEL, 0xA3C827DCD51D21EDL);
private static final UUID ZERO_UUID = new UUID(0, 0);
// First 8 bytes of IV come from the container, last 8 bytes are always initialized to 0.
private static final byte[] TEST_INITIALIZATION_VECTOR = {
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
private static final int ID_VP9 = 0;
private static final int ID_OPUS = 1;
private static final int ID_VORBIS = 2;
private static final int EXPECTED_INIT_RESULT = WebmExtractor.RESULT_READ_INIT
| WebmExtractor.RESULT_READ_INDEX | WebmExtractor.RESULT_END_OF_STREAM;
private static final int EXPECTED_INIT_AND_SAMPLE_RESULT = WebmExtractor.RESULT_READ_INIT
| WebmExtractor.RESULT_READ_INDEX | WebmExtractor.RESULT_READ_SAMPLE;
private final WebmExtractor extractor = new WebmExtractor();
private final SampleHolder sampleHolder =
new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_DISABLED);
@Override
public void setUp() {
sampleHolder.data = ByteBuffer.allocate(1024);
}
public void testPrepare() throws ParserException {
NonBlockingInputStream testInputStream = new ByteArrayNonBlockingInputStream(
createInitializationSegment(1, 0, true, DEFAULT_TIMECODE_SCALE, ID_VP9, null));
assertEquals(EXPECTED_INIT_RESULT, extractor.read(testInputStream, sampleHolder));
assertFormat();
assertIndex(new IndexPoint(0, 0, TEST_DURATION_US));
}
public void testPrepareOpus() throws ParserException {
NonBlockingInputStream testInputStream = new ByteArrayNonBlockingInputStream(
createInitializationSegment(1, 0, true, DEFAULT_TIMECODE_SCALE, ID_OPUS, null));
assertEquals(EXPECTED_INIT_RESULT, extractor.read(testInputStream, sampleHolder));
assertAudioFormat(ID_OPUS);
assertIndex(new IndexPoint(0, 0, TEST_DURATION_US));
}
public void testPrepareVorbis() throws ParserException {
NonBlockingInputStream testInputStream = new ByteArrayNonBlockingInputStream(
createInitializationSegment(1, 0, true, DEFAULT_TIMECODE_SCALE, ID_VORBIS, null));
assertEquals(EXPECTED_INIT_RESULT, extractor.read(testInputStream, sampleHolder));
assertAudioFormat(ID_VORBIS);
assertIndex(new IndexPoint(0, 0, TEST_DURATION_US));
}
public void testPrepareContentEncodingEncryption() throws ParserException {
ContentEncodingSettings settings = new ContentEncodingSettings(0, 1, 1, 5, 1);
NonBlockingInputStream testInputStream = new ByteArrayNonBlockingInputStream(
createInitializationSegment(1, 0, true, DEFAULT_TIMECODE_SCALE, ID_VP9, settings));
assertEquals(EXPECTED_INIT_RESULT, extractor.read(testInputStream, sampleHolder));
assertFormat();
assertIndex(new IndexPoint(0, 0, TEST_DURATION_US));
DrmInitData drmInitData = extractor.getDrmInitData();
assertNotNull(drmInitData);
android.test.MoreAsserts.assertEquals(TEST_ENCRYPTION_KEY_ID, drmInitData.get(WIDEVINE_UUID));
android.test.MoreAsserts.assertEquals(TEST_ENCRYPTION_KEY_ID, drmInitData.get(ZERO_UUID));
}
public void testPrepareThreeCuePoints() throws ParserException {
NonBlockingInputStream testInputStream = new ByteArrayNonBlockingInputStream(
createInitializationSegment(3, 0, true, DEFAULT_TIMECODE_SCALE, ID_VP9, null));
assertEquals(EXPECTED_INIT_RESULT, extractor.read(testInputStream, sampleHolder));
assertFormat();
assertIndex(
new IndexPoint(0, 0, 10000),
new IndexPoint(10000, 0, 10000),
new IndexPoint(20000, 0, TEST_DURATION_US - 20000));
}
public void testPrepareCustomTimecodeScale() throws ParserException {
NonBlockingInputStream testInputStream = new ByteArrayNonBlockingInputStream(
createInitializationSegment(3, 0, true, 1000, ID_VP9, null));
assertEquals(EXPECTED_INIT_RESULT, extractor.read(testInputStream, sampleHolder));
assertFormat();
assertIndex(
new IndexPoint(0, 0, 10),
new IndexPoint(10, 0, 10),
new IndexPoint(20, 0, (TEST_DURATION_US / 1000) - 20));
}
public void testPrepareNoCuePoints() {
NonBlockingInputStream testInputStream = new ByteArrayNonBlockingInputStream(
createInitializationSegment(0, 0, true, DEFAULT_TIMECODE_SCALE, ID_VP9, null));
try {
extractor.read(testInputStream, sampleHolder);
fail();
} catch (ParserException exception) {
assertEquals("Invalid/missing cue points", exception.getMessage());
}
}
public void testPrepareInvalidDocType() {
NonBlockingInputStream testInputStream = new ByteArrayNonBlockingInputStream(
createInitializationSegment(1, 0, false, DEFAULT_TIMECODE_SCALE, ID_VP9, null));
try {
extractor.read(testInputStream, sampleHolder);
fail();
} catch (ParserException exception) {
assertEquals("DocType webB not supported", exception.getMessage());
}
}
public void testPrepareInvalidContentEncodingOrder() {
ContentEncodingSettings settings = new ContentEncodingSettings(1, 1, 1, 5, 1);
NonBlockingInputStream testInputStream = new ByteArrayNonBlockingInputStream(
createInitializationSegment(1, 0, true, DEFAULT_TIMECODE_SCALE, ID_VP9, settings));
try {
extractor.read(testInputStream, sampleHolder);
fail();
} catch (ParserException exception) {
assertEquals("ContentEncodingOrder 1 not supported", exception.getMessage());
}
}
public void testPrepareInvalidContentEncodingScope() {
ContentEncodingSettings settings = new ContentEncodingSettings(0, 0, 1, 5, 1);
NonBlockingInputStream testInputStream = new ByteArrayNonBlockingInputStream(
createInitializationSegment(1, 0, true, DEFAULT_TIMECODE_SCALE, ID_VP9, settings));
try {
extractor.read(testInputStream, sampleHolder);
fail();
} catch (ParserException exception) {
assertEquals("ContentEncodingScope 0 not supported", exception.getMessage());
}
}
public void testPrepareInvalidContentEncodingType() {
ContentEncodingSettings settings = new ContentEncodingSettings(0, 1, 0, 5, 1);
NonBlockingInputStream testInputStream = new ByteArrayNonBlockingInputStream(
createInitializationSegment(1, 0, true, DEFAULT_TIMECODE_SCALE, ID_VP9, settings));
try {
extractor.read(testInputStream, sampleHolder);
fail();
} catch (ParserException exception) {
assertEquals("ContentEncodingType 0 not supported", exception.getMessage());
}
}
public void testPrepareInvalidContentEncAlgo() {
ContentEncodingSettings settings = new ContentEncodingSettings(0, 1, 1, 4, 1);
NonBlockingInputStream testInputStream = new ByteArrayNonBlockingInputStream(
createInitializationSegment(1, 0, true, DEFAULT_TIMECODE_SCALE, ID_VP9, settings));
try {
extractor.read(testInputStream, sampleHolder);
fail();
} catch (ParserException exception) {
assertEquals("ContentEncAlgo 4 not supported", exception.getMessage());
}
}
public void testPrepareInvalidAESSettingsCipherMode() {
ContentEncodingSettings settings = new ContentEncodingSettings(0, 1, 1, 5, 0);
NonBlockingInputStream testInputStream = new ByteArrayNonBlockingInputStream(
createInitializationSegment(1, 0, true, DEFAULT_TIMECODE_SCALE, ID_VP9, settings));
try {
extractor.read(testInputStream, sampleHolder);
fail();
} catch (ParserException exception) {
assertEquals("AESSettingsCipherMode 0 not supported", exception.getMessage());
}
}
public void testReadSampleKeyframe() throws ParserException {
MediaSegment mediaSegment = createMediaSegment(100, 0, 0, true, false, true, false, false);
byte[] testInputData = joinByteArrays(
createInitializationSegment(
1, mediaSegment.clusterBytes.length, true, DEFAULT_TIMECODE_SCALE, ID_VP9, null),
mediaSegment.clusterBytes);
NonBlockingInputStream testInputStream = new ByteArrayNonBlockingInputStream(testInputData);
assertEquals(EXPECTED_INIT_AND_SAMPLE_RESULT, extractor.read(testInputStream, sampleHolder));
assertFormat();
assertSample(mediaSegment, 0, true, false, false);
assertEquals(WebmExtractor.RESULT_END_OF_STREAM, extractor.read(testInputStream, sampleHolder));
}
public void testReadBlock() throws ParserException {
MediaSegment mediaSegment = createMediaSegment(100, 0, 0, true, false, false, false, false);
byte[] testInputData = joinByteArrays(
createInitializationSegment(
1, mediaSegment.clusterBytes.length, true, DEFAULT_TIMECODE_SCALE, ID_OPUS, null),
mediaSegment.clusterBytes);
NonBlockingInputStream testInputStream = new ByteArrayNonBlockingInputStream(testInputData);
assertEquals(EXPECTED_INIT_AND_SAMPLE_RESULT, extractor.read(testInputStream, sampleHolder));
assertAudioFormat(ID_OPUS);
assertSample(mediaSegment, 0, true, false, false);
assertEquals(WebmExtractor.RESULT_END_OF_STREAM, extractor.read(testInputStream, sampleHolder));
}
public void testReadEncryptedFrame() throws ParserException {
MediaSegment mediaSegment = createMediaSegment(100, 0, 0, true, false, true, true, true);
ContentEncodingSettings settings = new ContentEncodingSettings(0, 1, 1, 5, 1);
byte[] testInputData = joinByteArrays(
createInitializationSegment(
1, mediaSegment.clusterBytes.length, true, DEFAULT_TIMECODE_SCALE, ID_VP9, settings),
mediaSegment.clusterBytes);
NonBlockingInputStream testInputStream = new ByteArrayNonBlockingInputStream(testInputData);
assertEquals(EXPECTED_INIT_AND_SAMPLE_RESULT, extractor.read(testInputStream, sampleHolder));
assertFormat();
assertSample(mediaSegment, 0, true, false, true);
assertEquals(WebmExtractor.RESULT_END_OF_STREAM, extractor.read(testInputStream, sampleHolder));
}
public void testReadEncryptedFrameWithInvalidSignalByte() {
MediaSegment mediaSegment = createMediaSegment(100, 0, 0, true, false, true, true, false);
ContentEncodingSettings settings = new ContentEncodingSettings(0, 1, 1, 5, 1);
byte[] testInputData = joinByteArrays(
createInitializationSegment(
1, mediaSegment.clusterBytes.length, true, DEFAULT_TIMECODE_SCALE, ID_VP9, settings),
mediaSegment.clusterBytes);
NonBlockingInputStream testInputStream = new ByteArrayNonBlockingInputStream(testInputData);
try {
extractor.read(testInputStream, sampleHolder);
fail();
} catch (ParserException exception) {
assertEquals("Extension bit is set in signal byte", exception.getMessage());
}
}
public void testReadSampleInvisible() throws ParserException {
MediaSegment mediaSegment = createMediaSegment(100, 12, 13, false, true, true, false, false);
byte[] testInputData = joinByteArrays(
createInitializationSegment(
1, mediaSegment.clusterBytes.length, true, DEFAULT_TIMECODE_SCALE, ID_VP9, null),
mediaSegment.clusterBytes);
NonBlockingInputStream testInputStream = new ByteArrayNonBlockingInputStream(testInputData);
assertEquals(EXPECTED_INIT_AND_SAMPLE_RESULT, extractor.read(testInputStream, sampleHolder));
assertFormat();
assertSample(mediaSegment, 25000, false, true, false);
assertEquals(WebmExtractor.RESULT_END_OF_STREAM, extractor.read(testInputStream, sampleHolder));
}
public void testReadSampleCustomTimescale() throws ParserException {
MediaSegment mediaSegment = createMediaSegment(100, 12, 13, false, false, true, false, false);
byte[] testInputData = joinByteArrays(
createInitializationSegment(
1, mediaSegment.clusterBytes.length, true, 1000, ID_VP9, null),
mediaSegment.clusterBytes);
NonBlockingInputStream testInputStream = new ByteArrayNonBlockingInputStream(testInputData);
assertEquals(EXPECTED_INIT_AND_SAMPLE_RESULT, extractor.read(testInputStream, sampleHolder));
assertFormat();
assertSample(mediaSegment, 25, false, false, false);
assertEquals(WebmExtractor.RESULT_END_OF_STREAM, extractor.read(testInputStream, sampleHolder));
}
public void testReadSampleNegativeSimpleBlockTimecode() throws ParserException {
MediaSegment mediaSegment = createMediaSegment(100, 13, -12, true, true, true, false, false);
byte[] testInputData = joinByteArrays(
createInitializationSegment(
1, mediaSegment.clusterBytes.length, true, DEFAULT_TIMECODE_SCALE, ID_VP9, null),
mediaSegment.clusterBytes);
NonBlockingInputStream testInputStream = new ByteArrayNonBlockingInputStream(testInputData);
assertEquals(EXPECTED_INIT_AND_SAMPLE_RESULT, extractor.read(testInputStream, sampleHolder));
assertFormat();
assertSample(mediaSegment, 1000, true, true, false);
assertEquals(WebmExtractor.RESULT_END_OF_STREAM, extractor.read(testInputStream, sampleHolder));
}
private void assertFormat() {
MediaFormat format = extractor.getFormat();
assertEquals(TEST_WIDTH, format.width);
assertEquals(TEST_HEIGHT, format.height);
assertEquals(MimeTypes.VIDEO_VP9, format.mimeType);
}
private void assertAudioFormat(int codecId) {
MediaFormat format = extractor.getFormat();
assertEquals(TEST_CHANNEL_COUNT, format.channelCount);
assertEquals(TEST_SAMPLE_RATE, format.sampleRate);
if (codecId == ID_OPUS) {
assertEquals(MimeTypes.AUDIO_OPUS, format.mimeType);
assertEquals(3, format.initializationData.size());
assertEquals(TEST_OPUS_CODEC_PRIVATE_SIZE, format.initializationData.get(0).length);
assertEquals(TEST_CODEC_DELAY, ByteBuffer.wrap(format.initializationData.get(1)).getLong());
assertEquals(TEST_SEEK_PRE_ROLL, ByteBuffer.wrap(format.initializationData.get(2)).getLong());
} else if (codecId == ID_VORBIS) {
assertEquals(MimeTypes.AUDIO_VORBIS, format.mimeType);
assertEquals(2, format.initializationData.size());
assertEquals(TEST_VORBIS_INFO_SIZE, format.initializationData.get(0).length);
assertEquals(TEST_VORBIS_BOOKS_SIZE, format.initializationData.get(1).length);
}
}
private void assertIndex(IndexPoint... indexPoints) {
SegmentIndex index = extractor.getIndex();
assertEquals(CUES_ELEMENT_BYTE_SIZE + CUE_POINT_ELEMENT_BYTE_SIZE * indexPoints.length,
index.sizeBytes);
assertEquals(indexPoints.length, index.length);
for (int i = 0; i < indexPoints.length; i++) {
IndexPoint indexPoint = indexPoints[i];
assertEquals(indexPoint.timeUs, index.timesUs[i]);
assertEquals(indexPoint.size, index.sizes[i]);
assertEquals(indexPoint.durationUs, index.durationsUs[i]);
}
}
private void assertSample(
MediaSegment mediaSegment, int timeUs, boolean keyframe, boolean invisible,
boolean encrypted) {
assertTrue(Arrays.equals(
mediaSegment.videoBytes, Arrays.copyOf(sampleHolder.data.array(), sampleHolder.size)));
assertEquals(timeUs, sampleHolder.timeUs);
assertEquals(keyframe, sampleHolder.isSyncFrame());
assertEquals(invisible, sampleHolder.isDecodeOnly());
assertEquals(encrypted, sampleHolder.isEncrypted());
if (encrypted) {
android.test.MoreAsserts.assertEquals(TEST_INITIALIZATION_VECTOR, sampleHolder.cryptoInfo.iv);
assertEquals(C.CRYPTO_MODE_AES_CTR, sampleHolder.cryptoInfo.mode);
assertEquals(1, sampleHolder.cryptoInfo.numSubSamples);
assertEquals(100, sampleHolder.cryptoInfo.numBytesOfEncryptedData[0]);
assertEquals(0, sampleHolder.cryptoInfo.numBytesOfClearData[0]);
}
}
private byte[] createInitializationSegment(
int cuePoints, int mediaSegmentSize, boolean docTypeIsWebm, int timecodeScale,
int codecId, ContentEncodingSettings contentEncodingSettings) {
int initalizationSegmentSize = INFO_ELEMENT_BYTE_SIZE + TRACKS_ELEMENT_BYTE_SIZE
+ CUES_ELEMENT_BYTE_SIZE + CUE_POINT_ELEMENT_BYTE_SIZE * cuePoints;
byte[] tracksElement = null;
switch (codecId) {
case ID_VP9:
tracksElement = createTracksElementWithVideo(
true, TEST_WIDTH, TEST_HEIGHT, contentEncodingSettings);
break;
case ID_OPUS:
tracksElement = createTracksElementWithOpusAudio(TEST_CHANNEL_COUNT);
break;
case ID_VORBIS:
tracksElement = createTracksElementWithVorbisAudio(TEST_CHANNEL_COUNT);
break;
}
byte[] bytes = joinByteArrays(createEbmlElement(1, docTypeIsWebm, 2),
createSegmentElement(initalizationSegmentSize + mediaSegmentSize),
createInfoElement(timecodeScale),
tracksElement,
createCuesElement(CUE_POINT_ELEMENT_BYTE_SIZE * cuePoints));
for (int i = 0; i < cuePoints; i++) {
bytes = joinByteArrays(bytes, createCuePointElement(10 * i, initalizationSegmentSize));
}
return bytes;
}
private static MediaSegment createMediaSegment(int videoBytesLength, int clusterTimecode,
int blockTimecode, boolean keyframe, boolean invisible, boolean simple,
boolean encrypted, boolean validSignalByte) {
byte[] videoBytes = createVideoBytes(videoBytesLength);
byte[] blockBytes;
if (simple) {
blockBytes = createSimpleBlockElement(videoBytes.length, blockTimecode,
keyframe, invisible, true, encrypted, validSignalByte);
} else {
blockBytes = createBlockElement(videoBytes.length, blockTimecode, invisible, true);
}
byte[] clusterBytes =
createClusterElement(blockBytes.length + videoBytes.length, clusterTimecode);
return new MediaSegment(joinByteArrays(clusterBytes, blockBytes, videoBytes), videoBytes);
}
private static byte[] joinByteArrays(byte[]... byteArrays) {
int length = 0;
for (byte[] byteArray : byteArrays) {
length += byteArray.length;
}
byte[] joined = new byte[length];
length = 0;
for (byte[] byteArray : byteArrays) {
System.arraycopy(byteArray, 0, joined, length, byteArray.length);
length += byteArray.length;
}
return joined;
}
private static byte[] createEbmlElement(
int ebmlReadVersion, boolean docTypeIsWebm, int docTypeReadVersion) {
return createByteArray(
0x1A, 0x45, 0xDF, 0xA3, // EBML
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, // size=15
0x42, 0xF7, // EBMLReadVersion
0x81, ebmlReadVersion, // size=1
0x42, 0x82, // DocType
0x84, 0x77, 0x65, 0x62, docTypeIsWebm ? 0x6D : 0x42, // size=4 value=webm/B
0x42, 0x85, // DocTypeReadVersion
0x81, docTypeReadVersion); // size=1
}
private static byte[] createSegmentElement(int size) {
byte[] sizeBytes = getIntegerBytes(size);
return createByteArray(
0x18, 0x53, 0x80, 0x67, // Segment
0x01, 0x00, 0x00, 0x00, sizeBytes[0], sizeBytes[1], sizeBytes[2], sizeBytes[3]);
}
private static byte[] createInfoElement(int timecodeScale) {
byte[] scaleBytes = getIntegerBytes(timecodeScale);
return createByteArray(
0x15, 0x49, 0xA9, 0x66, // Info
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x13, // size=19
0x2A, 0xD7, 0xB1, // TimecodeScale
0x84, scaleBytes[0], scaleBytes[1], scaleBytes[2], scaleBytes[3], // size=4
0x44, 0x89, // Duration
0x88, 0x40, 0xC3, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00); // size=8 value=9920.0
}
private static byte[] createTracksElementWithVideo(
boolean codecIsVp9, int pixelWidth, int pixelHeight,
ContentEncodingSettings contentEncodingSettings) {
byte[] widthBytes = getIntegerBytes(pixelWidth);
byte[] heightBytes = getIntegerBytes(pixelHeight);
if (contentEncodingSettings != null) {
byte[] orderBytes = getIntegerBytes(contentEncodingSettings.order);
byte[] scopeBytes = getIntegerBytes(contentEncodingSettings.scope);
byte[] typeBytes = getIntegerBytes(contentEncodingSettings.type);
byte[] algorithmBytes = getIntegerBytes(contentEncodingSettings.algorithm);
byte[] cipherModeBytes = getIntegerBytes(contentEncodingSettings.aesCipherMode);
return createByteArray(
0x16, 0x54, 0xAE, 0x6B, // Tracks
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x48, // size=72
0xAE, // TrackEntry
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, // size=63
0x86, // CodecID
0x85, 0x56, 0x5F, 0x56, 0x50, codecIsVp9 ? 0x39 : 0x30, // size=5 value=V_VP9/0
0x6D, 0x80, // ContentEncodings
0xA4, // size=36
0x62, 0x40, // ContentEncoding
0xA1, // size=33
0x50, 0x31, // ContentEncodingOrder
0x81, orderBytes[3],
0x50, 0x32, // ContentEncodingScope
0x81, scopeBytes[3],
0x50, 0x33, // ContentEncodingType
0x81, typeBytes[3],
0x50, 0x35, // ContentEncryption
0x92, // size=18
0x47, 0xE1, // ContentEncAlgo
0x81, algorithmBytes[3],
0x47, 0xE2, // ContentEncKeyID
0x84, // size=4
TEST_ENCRYPTION_KEY_ID[0], TEST_ENCRYPTION_KEY_ID[1],
TEST_ENCRYPTION_KEY_ID[2], TEST_ENCRYPTION_KEY_ID[3], // value=binary
0x47, 0xE7, // ContentEncAESSettings
0x84, // size=4
0x47, 0xE8, // AESSettingsCipherMode
0x81, cipherModeBytes[3],
0xE0, // Video
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, // size=8
0xB0, // PixelWidth
0x82, widthBytes[2], widthBytes[3], // size=2
0xBA, // PixelHeight
0x82, heightBytes[2], heightBytes[3]); // size=2
} else {
return createByteArray(
0x16, 0x54, 0xAE, 0x6B, // Tracks
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x24, // size=36
0xAE, // TrackEntry
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1B, // size=27
0x86, // CodecID
0x85, 0x56, 0x5F, 0x56, 0x50, codecIsVp9 ? 0x39 : 0x30, // size=5 value=V_VP9/0
0xE0, // Video
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, // size=8
0xB0, // PixelWidth
0x82, widthBytes[2], widthBytes[3], // size=2
0xBA, // PixelHeight
0x82, heightBytes[2], heightBytes[3]); // size=2
}
}
private static byte[] createTracksElementWithOpusAudio(int channelCount) {
byte[] channelCountBytes = getIntegerBytes(channelCount);
return createByteArray(
0x16, 0x54, 0xAE, 0x6B, // Tracks
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x39, // size=57
0xAE, // TrackEntry
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, // size=48
0x86, // CodecID
0x86, 0x41, 0x5F, 0x4F, 0x50, 0x55, 0x53, // size=6 value=A_OPUS
0x56, 0xAA, // CodecDelay
0x83, 0x63, 0x2E, 0xA0, // size=3 value=6500000
0x56, 0xBB, // SeekPreRoll
0x84, 0x04, 0xC4, 0xB4, 0x00, // size=4 value=80000000
0xE1, // Audio
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0D, // size=13
0x9F, // Channels
0x81, channelCountBytes[3], // size=1
0xB5, // SamplingFrequency
0x88, 0x40, 0xE7, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, // size=8 value=48000
0x63, 0xA2, // CodecPrivate
0x82, 0x00, 0x00); // size=2
}
private byte[] createTracksElementWithVorbisAudio(int channelCount) {
byte[] channelCountBytes = getIntegerBytes(channelCount);
byte[] tracksElement = createByteArray(
0x16, 0x54, 0xAE, 0x6B, // Tracks
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x9C, // size=4252
0xAE, // TrackEntry
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x93, // size=4243 (36+4207)
0x86, // CodecID
0x88, 0x41, 0x5f, 0x56, 0x4f, 0x52, 0x42, 0x49, 0x53, // size=8 value=A_VORBIS
0xE1, // Audio
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0D, // size=13
0x9F, // Channels
0x81, channelCountBytes[3], // size=1
0xB5, // SamplingFrequency
0x88, 0x40, 0xE7, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, // size=8 value=48000
0x63, 0xA2, // CodecPrivate
0x50, 0x6F); // size=4207
byte[] codecPrivate = new byte[4207];
try {
getInstrumentation().getContext().getResources().getAssets().open(TEST_VORBIS_CODEC_PRIVATE)
.read(codecPrivate);
} catch (IOException e) {
fail(); // should never happen
}
return joinByteArrays(tracksElement, codecPrivate);
}
private static byte[] createCuesElement(int size) {
byte[] sizeBytes = getIntegerBytes(size);
return createByteArray(
0x1C, 0x53, 0xBB, 0x6B, // Cues
0x01, 0x00, 0x00, 0x00, sizeBytes[0], sizeBytes[1], sizeBytes[2], sizeBytes[3]); // size=31
}
private static byte[] createCuePointElement(int cueTime, int cueClusterPosition) {
byte[] positionBytes = getIntegerBytes(cueClusterPosition);
return createByteArray(
0xBB, // CuePoint
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, // size=22
0xB3, // CueTime
0x81, cueTime, // size=1
0xB7, // CueTrackPositions
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0A, // size=10
0xF1, // CueClusterPosition
0x88, 0x00, 0x00, 0x00, 0x00, positionBytes[0], positionBytes[1],
positionBytes[2], positionBytes[3]); // size=8
}
private static byte[] createClusterElement(int size, int timecode) {
byte[] sizeBytes = getIntegerBytes(size);
byte[] timeBytes = getIntegerBytes(timecode);
return createByteArray(
0x1F, 0x43, 0xB6, 0x75, // Cluster
0x01, 0x00, 0x00, 0x00, sizeBytes[0], sizeBytes[1], sizeBytes[2], sizeBytes[3],
0xE7, // Timecode
0x84, timeBytes[0], timeBytes[1], timeBytes[2], timeBytes[3]); // size=4
}
private static byte[] createSimpleBlockElement(
int size, int timecode, boolean keyframe, boolean invisible, boolean noLacing,
boolean encrypted, boolean validSignalByte) {
byte[] sizeBytes = getIntegerBytes(size + 4 + (encrypted ? 9 : 0));
byte[] timeBytes = getIntegerBytes(timecode);
byte flags = (byte)
((keyframe ? 0x80 : 0x00) | (invisible ? 0x08 : 0x00) | (noLacing ? 0x00 : 0x06));
byte[] simpleBlock = createByteArray(
0xA3, // SimpleBlock
0x01, 0x00, 0x00, 0x00, sizeBytes[0], sizeBytes[1], sizeBytes[2], sizeBytes[3],
0x81, // Track number value=1
timeBytes[2], timeBytes[3], flags); // Timecode and flags
if (encrypted) {
simpleBlock = joinByteArrays(
simpleBlock, createByteArray(validSignalByte ? 0x01 : 0x80),
Arrays.copyOfRange(TEST_INITIALIZATION_VECTOR, 0, 8));
}
return simpleBlock;
}
private static byte[] createBlockElement(
int size, int timecode, boolean invisible, boolean noLacing) {
int blockSize = size + 4;
byte[] blockSizeBytes = getIntegerBytes(blockSize);
byte[] timeBytes = getIntegerBytes(timecode);
int blockElementSize = 1 + 8 + blockSize; // id + size + length of data
byte[] sizeBytes = getIntegerBytes(blockElementSize);
byte flags = (byte) ((invisible ? 0x08 : 0x00) | (noLacing ? 0x00 : 0x06));
return createByteArray(
0xA0, // BlockGroup
0x01, 0x00, 0x00, 0x00, sizeBytes[0], sizeBytes[1], sizeBytes[2], sizeBytes[3],
0xA1, // Block
0x01, 0x00, 0x00, 0x00,
blockSizeBytes[0], blockSizeBytes[1], blockSizeBytes[2], blockSizeBytes[3],
0x81, // Track number value=1
timeBytes[2], timeBytes[3], flags); // Timecode and flags
}
private static byte[] createVideoBytes(int size) {
byte[] videoBytes = new byte[size];
for (int i = 0; i < size; i++) {
videoBytes[i] = (byte) i;
}
return videoBytes;
}
private static byte[] getIntegerBytes(int value) {
return createByteArray(
(value & 0xFF000000) >> 24,
(value & 0x00FF0000) >> 16,
(value & 0x0000FF00) >> 8,
(value & 0x000000FF));
}
private static byte[] createByteArray(int... intArray) {
byte[] byteArray = new byte[intArray.length];
for (int i = 0; i < byteArray.length; i++) {
byteArray[i] = (byte) intArray[i];
}
return byteArray;
}
/** Used by {@link #createMediaSegment} to return both cluster and video bytes together. */
private static final class MediaSegment {
private final byte[] clusterBytes;
private final byte[] videoBytes;
private MediaSegment(byte[] clusterBytes, byte[] videoBytes) {
this.clusterBytes = clusterBytes;
this.videoBytes = videoBytes;
}
}
/** Used by {@link #assertIndex(IndexPoint...)} to validate index elements. */
private static final class IndexPoint {
private final long timeUs;
private final int size;
private final long durationUs;
private IndexPoint(long timeUs, int size, long durationUs) {
this.timeUs = timeUs;
this.size = size;
this.durationUs = durationUs;
}
}
/** Used by {@link #createTracksElementWithVideo} to create a Track header with Encryption. */
private static final class ContentEncodingSettings {
private final int order;
private final int scope;
private final int type;
private final int algorithm;
private final int aesCipherMode;
private ContentEncodingSettings(int order, int scope, int type, int algorithm,
int aesCipherMode) {
this.order = order;
this.scope = scope;
this.type = type;
this.algorithm = algorithm;
this.aesCipherMode = aesCipherMode;
}
}
}
/*
* 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.dash;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.chunk.Format;
import com.google.android.exoplayer.dash.mpd.Representation;
import com.google.android.exoplayer.dash.mpd.SegmentBase.SingleSegmentBase;
import junit.framework.TestCase;
/**
* Tests {@link DashChunkSource}.
*/
public class DashChunkSourceTest extends TestCase {
public void testMaxVideoDimensions() {
SingleSegmentBase segmentBase1 = new SingleSegmentBase("https://example.com/1.mp4");
Format format1 = new Format("1", "video/mp4", 100, 200, -1, -1, 1000);
Representation representation1 =
Representation.newInstance(0, 0, null, 0, format1, segmentBase1);
SingleSegmentBase segmentBase2 = new SingleSegmentBase("https://example.com/2.mp4");
Format format2 = new Format("2", "video/mp4", 400, 50, -1, -1, 1000);
Representation representation2 =
Representation.newInstance(0, 0, null, 0, format2, segmentBase2);
DashChunkSource chunkSource = new DashChunkSource(null, null, representation1, representation2);
MediaFormat out = MediaFormat.createVideoFormat("video/h264", 1, 1, 1, 1, null);
chunkSource.getMaxVideoDimensions(out);
assertEquals(400, out.getMaxVideoWidth());
assertEquals(200, out.getMaxVideoHeight());
}
}
/*
* 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.dash.mpd;
import android.test.InstrumentationTestCase;
import java.io.IOException;
import java.io.InputStream;
/**
* Unit tests for {@link MediaPresentationDescriptionParser}.
*/
public class MediaPresentationDescriptionParserTest extends InstrumentationTestCase {
private static final String SAMPLE_MPD_1 = "dash/sample_mpd_1";
public void testParseMediaPresentationDescription() throws IOException {
MediaPresentationDescriptionParser parser = new MediaPresentationDescriptionParser();
InputStream inputStream =
getInstrumentation().getContext().getResources().getAssets().open(SAMPLE_MPD_1);
// Simple test to ensure that the sample manifest parses without throwing any exceptions.
parser.parse("https://example.com/test.mpd", inputStream);
}
}
/*
* 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.dash.mpd;
import junit.framework.TestCase;
/**
* Unit test for {@link RangedUri}.
*/
public class RangedUriTest extends TestCase {
private static final String FULL_URI = "http://www.test.com/path/file.ext";
public void testMerge() {
RangedUri rangeA = new RangedUri(null, FULL_URI, 0, 10);
RangedUri rangeB = new RangedUri(null, FULL_URI, 10, 10);
RangedUri expected = new RangedUri(null, FULL_URI, 0, 20);
assertMerge(rangeA, rangeB, expected);
}
public void testMergeUnbounded() {
RangedUri rangeA = new RangedUri(null, FULL_URI, 0, 10);
RangedUri rangeB = new RangedUri(null, FULL_URI, 10, -1);
RangedUri expected = new RangedUri(null, FULL_URI, 0, -1);
assertMerge(rangeA, rangeB, expected);
}
public void testNonMerge() {
// A and B do not overlap, so should not merge
RangedUri rangeA = new RangedUri(null, FULL_URI, 0, 10);
RangedUri rangeB = new RangedUri(null, FULL_URI, 11, 10);
assertNonMerge(rangeA, rangeB);
// A and B do not overlap, so should not merge
rangeA = new RangedUri(null, FULL_URI, 0, 10);
rangeB = new RangedUri(null, FULL_URI, 11, -1);
assertNonMerge(rangeA, rangeB);
// A and B are bounded but overlap, so should not merge
rangeA = new RangedUri(null, FULL_URI, 0, 11);
rangeB = new RangedUri(null, FULL_URI, 10, 10);
assertNonMerge(rangeA, rangeB);
// A and B overlap due to unboundedness, so should not merge
rangeA = new RangedUri(null, FULL_URI, 0, -1);
rangeB = new RangedUri(null, FULL_URI, 10, -1);
assertNonMerge(rangeA, rangeB);
}
private void assertMerge(RangedUri rangeA, RangedUri rangeB, RangedUri expected) {
RangedUri merged = rangeA.attemptMerge(rangeB);
assertEquals(expected, merged);
merged = rangeB.attemptMerge(rangeA);
assertEquals(expected, merged);
}
private void assertNonMerge(RangedUri rangeA, RangedUri rangeB) {
RangedUri merged = rangeA.attemptMerge(rangeB);
assertNull(merged);
merged = rangeB.attemptMerge(rangeA);
assertNull(merged);
}
}
/*
* 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.dash.mpd;
import com.google.android.exoplayer.chunk.Format;
import com.google.android.exoplayer.dash.mpd.SegmentBase.SingleSegmentBase;
import com.google.android.exoplayer.util.MimeTypes;
import junit.framework.TestCase;
/**
* Unit test for {@link Representation}.
*/
public class RepresentationTest extends TestCase {
public void testGetCacheKey() {
String uri = "http://www.google.com";
SegmentBase base = new SingleSegmentBase(new RangedUri(uri, null, 0, 1), 1, 0, uri, 1, 1);
Format format = new Format("0", MimeTypes.VIDEO_MP4, 1920, 1080, 0, 0, 2500000);
Representation representation = Representation.newInstance(-1, -1, "test_stream_1", 3,
format, base);
assertEquals("test_stream_1.0.3", representation.getCacheKey());
format = new Format("150", MimeTypes.VIDEO_MP4, 1920, 1080, 0, 0, 2500000);
representation = Representation.newInstance(-1, -1, "test_stream_1", -1, format, base);
assertEquals("test_stream_1.150.-1", representation.getCacheKey());
}
}
/*
* 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.dash.mpd;
import junit.framework.TestCase;
/**
* Unit test for {@link UrlTemplate}.
*/
public class UrlTemplateTest extends TestCase {
public void testRealExamples() {
String template = "QualityLevels($Bandwidth$)/Fragments(video=$Time$,format=mpd-time-csf)";
UrlTemplate urlTemplate = UrlTemplate.compile(template);
String url = urlTemplate.buildUri("abc1", 10, 650000, 5000);
assertEquals("QualityLevels(650000)/Fragments(video=5000,format=mpd-time-csf)", url);
template = "$RepresentationID$/$Number$";
urlTemplate = UrlTemplate.compile(template);
url = urlTemplate.buildUri("abc1", 10, 650000, 5000);
assertEquals("abc1/10", url);
template = "chunk_ctvideo_cfm4s_rid$RepresentationID$_cn$Number$_w2073857842_mpd.m4s";
urlTemplate = UrlTemplate.compile(template);
url = urlTemplate.buildUri("abc1", 10, 650000, 5000);
assertEquals("chunk_ctvideo_cfm4s_ridabc1_cn10_w2073857842_mpd.m4s", url);
}
public void testFull() {
String template = "$Bandwidth$_a_$RepresentationID$_b_$Time$_c_$Number$";
UrlTemplate urlTemplate = UrlTemplate.compile(template);
String url = urlTemplate.buildUri("abc1", 10, 650000, 5000);
assertEquals("650000_a_abc1_b_5000_c_10", url);
}
public void testFullWithDollarEscaping() {
String template = "$$$Bandwidth$$$_a$$_$RepresentationID$_b_$Time$_c_$Number$$$";
UrlTemplate urlTemplate = UrlTemplate.compile(template);
String url = urlTemplate.buildUri("abc1", 10, 650000, 5000);
assertEquals("$650000$_a$_abc1_b_5000_c_10$", url);
}
public void testInvalidSubstitution() {
String template = "$IllegalId$";
try {
UrlTemplate.compile(template);
assertTrue(false);
} catch (IllegalArgumentException e) {
// Expected.
}
}
}
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.hls;
import com.google.android.exoplayer.C;
import junit.framework.TestCase;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.List;
/**
* Test for {@link HlsMasterPlaylistParserTest}
*/
public class HlsMasterPlaylistParserTest extends TestCase {
public void testParseMasterPlaylist() {
String playlistUrl = "https://example.com/test.m3u8";
String playlistString = "#EXTM3U\n"
+ "\n"
+ "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128\n"
+ "http://example.com/low.m3u8\n"
+ "\n"
+ "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2 , avc1.66.30 \"\n"
+ "http://example.com/spaces_in_codecs.m3u8\n"
+ "\n"
+ "#EXT-X-STREAM-INF:BANDWIDTH=2560000,RESOLUTION=384x160\n"
+ "http://example.com/mid.m3u8\n"
+ "\n"
+ "#EXT-X-STREAM-INF:BANDWIDTH=7680000\n"
+ "http://example.com/hi.m3u8\n"
+ "\n"
+ "#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS=\"mp4a.40.5\"\n"
+ "http://example.com/audio-only.m3u8";
ByteArrayInputStream inputStream = new ByteArrayInputStream(
playlistString.getBytes(Charset.forName(C.UTF8_NAME)));
try {
HlsPlaylist playlist = new HlsPlaylistParser().parse(playlistUrl, inputStream);
assertNotNull(playlist);
assertEquals(HlsPlaylist.TYPE_MASTER, playlist.type);
HlsMasterPlaylist masterPlaylist = (HlsMasterPlaylist) playlist;
List<Variant> variants = masterPlaylist.variants;
assertNotNull(variants);
assertEquals(5, variants.size());
assertEquals(0, variants.get(0).index);
assertEquals(1280000, variants.get(0).bandwidth);
assertNotNull(variants.get(0).codecs);
assertEquals(2, variants.get(0).codecs.length);
assertEquals("mp4a.40.2", variants.get(0).codecs[0]);
assertEquals("avc1.66.30", variants.get(0).codecs[1]);
assertEquals(304, variants.get(0).width);
assertEquals(128, variants.get(0).height);
assertEquals("http://example.com/low.m3u8", variants.get(0).url);
assertEquals(1, variants.get(1).index);
assertEquals(1280000, variants.get(1).bandwidth);
assertNotNull(variants.get(1).codecs);
assertEquals(2, variants.get(1).codecs.length);
assertEquals("mp4a.40.2", variants.get(1).codecs[0]);
assertEquals("avc1.66.30", variants.get(1).codecs[1]);
assertEquals("http://example.com/spaces_in_codecs.m3u8", variants.get(1).url);
assertEquals(2, variants.get(2).index);
assertEquals(2560000, variants.get(2).bandwidth);
assertEquals(null, variants.get(2).codecs);
assertEquals(384, variants.get(2).width);
assertEquals(160, variants.get(2).height);
assertEquals("http://example.com/mid.m3u8", variants.get(2).url);
assertEquals(3, variants.get(3).index);
assertEquals(7680000, variants.get(3).bandwidth);
assertEquals(null, variants.get(3).codecs);
assertEquals(-1, variants.get(3).width);
assertEquals(-1, variants.get(3).height);
assertEquals("http://example.com/hi.m3u8", variants.get(3).url);
assertEquals(4, variants.get(4).index);
assertEquals(65000, variants.get(4).bandwidth);
assertNotNull(variants.get(4).codecs);
assertEquals(1, variants.get(4).codecs.length);
assertEquals("mp4a.40.5", variants.get(4).codecs[0]);
assertEquals(-1, variants.get(4).width);
assertEquals(-1, variants.get(4).height);
assertEquals("http://example.com/audio-only.m3u8", variants.get(4).url);
} catch (IOException exception) {
fail(exception.getMessage());
}
}
}
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.hls;
import com.google.android.exoplayer.C;
import junit.framework.TestCase;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.util.List;
import java.util.Locale;
/**
* Test for {@link HlsMediaPlaylistParserTest}
*/
public class HlsMediaPlaylistParserTest extends TestCase {
public void testParseMediaPlaylist() {
String playlistUrl = "https://example.com/test.m3u8";
String playlistString = "#EXTM3U\n"
+ "#EXT-X-VERSION:3\n"
+ "#EXT-X-TARGETDURATION:8\n"
+ "#EXT-X-MEDIA-SEQUENCE:2679\n"
+ "#EXT-X-ALLOW-CACHE:YES\n"
+ "\n"
+ "#EXTINF:7.975,\n"
+ "#EXT-X-BYTERANGE:51370@0\n"
+ "https://priv.example.com/fileSequence2679.ts\n"
+ "\n"
+ "#EXT-X-KEY:METHOD=AES-128,URI=\"https://priv.example.com/key.php?r=2680\",IV=0x1566B\n"
+ "#EXTINF:7.975,\n"
+ "#EXT-X-BYTERANGE:51501@51370\n"
+ "https://priv.example.com/fileSequence2680.ts\n"
+ "\n"
+ "#EXT-X-KEY:METHOD=NONE\n"
+ "#EXTINF:7.941,\n"
+ "#EXT-X-BYTERANGE:51501\n" // @102871
+ "https://priv.example.com/fileSequence2681.ts\n"
+ "\n"
+ "#EXT-X-DISCONTINUITY\n"
+ "#EXT-X-KEY:METHOD=AES-128,URI=\"https://priv.example.com/key.php?r=2682\"\n"
+ "#EXTINF:7.975,\n"
+ "#EXT-X-BYTERANGE:51740\n" // @154372
+ "https://priv.example.com/fileSequence2682.ts\n"
+ "\n"
+ "#EXTINF:7.975,\n"
+ "https://priv.example.com/fileSequence2683.ts\n"
+ "#EXT-X-ENDLIST";
InputStream inputStream = new ByteArrayInputStream(
playlistString.getBytes(Charset.forName(C.UTF8_NAME)));
try {
HlsPlaylist playlist = new HlsPlaylistParser().parse(playlistUrl, inputStream);
assertNotNull(playlist);
assertEquals(HlsPlaylist.TYPE_MEDIA, playlist.type);
HlsMediaPlaylist mediaPlaylist = (HlsMediaPlaylist) playlist;
assertEquals(2679, mediaPlaylist.mediaSequence);
assertEquals(8, mediaPlaylist.targetDurationSecs);
assertEquals(3, mediaPlaylist.version);
assertEquals(false, mediaPlaylist.live);
List<HlsMediaPlaylist.Segment> segments = mediaPlaylist.segments;
assertNotNull(segments);
assertEquals(5, segments.size());
assertEquals(false, segments.get(0).discontinuity);
assertEquals(7.975, segments.get(0).durationSecs);
assertEquals(false, segments.get(0).isEncrypted);
assertEquals(null, segments.get(0).encryptionKeyUri);
assertEquals(null, segments.get(0).encryptionIV);
assertEquals(51370, segments.get(0).byterangeLength);
assertEquals(0, segments.get(0).byterangeOffset);
assertEquals("https://priv.example.com/fileSequence2679.ts", segments.get(0).url);
assertEquals(false, segments.get(1).discontinuity);
assertEquals(7.975, segments.get(1).durationSecs);
assertEquals(true, segments.get(1).isEncrypted);
assertEquals("https://priv.example.com/key.php?r=2680", segments.get(1).encryptionKeyUri);
assertEquals("0x1566B", segments.get(1).encryptionIV);
assertEquals(51501, segments.get(1).byterangeLength);
assertEquals(51370, segments.get(1).byterangeOffset);
assertEquals("https://priv.example.com/fileSequence2680.ts", segments.get(1).url);
assertEquals(false, segments.get(2).discontinuity);
assertEquals(7.941, segments.get(2).durationSecs);
assertEquals(false, segments.get(2).isEncrypted);
assertEquals(null, segments.get(2).encryptionKeyUri);
assertEquals(null, segments.get(2).encryptionIV);
assertEquals(51501, segments.get(2).byterangeLength);
assertEquals(102871, segments.get(2).byterangeOffset);
assertEquals("https://priv.example.com/fileSequence2681.ts", segments.get(2).url);
assertEquals(true, segments.get(3).discontinuity);
assertEquals(7.975, segments.get(3).durationSecs);
assertEquals(true, segments.get(3).isEncrypted);
assertEquals("https://priv.example.com/key.php?r=2682", segments.get(3).encryptionKeyUri);
// 0xA7A == 2682.
assertNotNull(segments.get(3).encryptionIV);
assertEquals("A7A", segments.get(3).encryptionIV.toUpperCase(Locale.getDefault()));
assertEquals(51740, segments.get(3).byterangeLength);
assertEquals(154372, segments.get(3).byterangeOffset);
assertEquals("https://priv.example.com/fileSequence2682.ts", segments.get(3).url);
assertEquals(false, segments.get(4).discontinuity);
assertEquals(7.975, segments.get(4).durationSecs);
assertEquals(true, segments.get(4).isEncrypted);
assertEquals("https://priv.example.com/key.php?r=2682", segments.get(4).encryptionKeyUri);
// 0xA7B == 2683.
assertNotNull(segments.get(4).encryptionIV);
assertEquals("A7B", segments.get(4).encryptionIV.toUpperCase(Locale.getDefault()));
assertEquals(C.LENGTH_UNBOUNDED, segments.get(4).byterangeLength);
assertEquals(0, segments.get(4).byterangeOffset);
assertEquals("https://priv.example.com/fileSequence2683.ts", segments.get(4).url);
} catch (IOException exception) {
fail(exception.getMessage());
}
}
}
/*
* 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.metadata;
import junit.framework.TestCase;
import java.util.Map;
/**
* Test for {@link Id3Parser}
*/
public class Id3ParserTest extends TestCase {
public void testParseTxxxFrames() {
byte[] rawId3 = new byte[] { 73, 68, 51, 4, 0, 0, 0, 0, 0, 41, 84, 88, 88, 88, 0, 0, 0, 31,
0, 0, 3, 0, 109, 100, 105, 97, 108, 111, 103, 95, 86, 73, 78, 68, 73, 67, 79, 49, 53, 50,
55, 54, 54, 52, 95, 115, 116, 97, 114, 116, 0 };
Id3Parser parser = new Id3Parser();
try {
Map<String, Object> metadata = parser.parse(rawId3, rawId3.length);
assertNotNull(metadata);
assertEquals(1, metadata.size());
TxxxMetadata txxx = (TxxxMetadata) metadata.get(TxxxMetadata.TYPE);
assertNotNull(txxx);
assertEquals("", txxx.description);
assertEquals("mdialog_VINDICO1527664_start", txxx.value);
} catch (Exception exception) {
fail(exception.getMessage());
}
}
}
/*
* 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.source;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.SampleSource;
import com.google.android.exoplayer.mp4.Atom;
import com.google.android.exoplayer.upstream.ByteArrayDataSource;
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DataSpec;
import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.MimeTypes;
import com.google.android.exoplayer.util.Util;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.media.MediaExtractor;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import junit.framework.TestCase;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
/**
* Tests for {@link Mp4SampleExtractor}.
*/
@TargetApi(16)
public class Mp4SampleExtractorTest extends TestCase {
/** String of hexadecimal bytes containing the video stsd payload from an AVC video. */
private static final byte[] VIDEO_STSD_PAYLOAD = getByteArray(
"00000000000000010000009961766331000000000000000100000000000000000000000000000000050002"
+ "d00048000000480000000000000001000000000000000000000000000000000000000000000000000000"
+ "00000000000018ffff0000002f617663430164001fffe100186764001facb402802dd808800000030080"
+ "00001e078c195001000468ee3cb000000014627472740000e35c0042a61000216cb8");
private static final byte[] VIDEO_HDLR_PAYLOAD = getByteArray("000000000000000076696465");
private static final byte[] VIDEO_MDHD_PAYLOAD =
getByteArray("0000000000000000cf6c48890000001e00001c8a55c40000");
private static final int TIMESCALE = 30;
private static final int VIDEO_WIDTH = 1280;
private static final int VIDEO_HEIGHT = 720;
/** String of hexadecimal bytes containing the video stsd payload for an mp4v track. */
private static final byte[] VIDEO_STSD_MP4V_PAYLOAD = getByteArray(
"0000000000000001000000A36D703476000000000000000100000000000000000000000000000000014000"
+ "B40048000000480000000000000001000000000000000000000000000000000000000000000000000000"
+ "00000000000018FFFF0000004D6573647300000000033F00000004372011001A400004CF280002F11805"
+ "28000001B001000001B58913000001000000012000C48D8800F50A04169463000001B2476F6F676C6506"
+ "0102");
private static final int VIDEO_MP4V_WIDTH = 320;
private static final int VIDEO_MP4V_HEIGHT = 180;
/** String of hexadecimal bytes containing the audio stsd payload from an AAC track. */
private static final byte[] AUDIO_STSD_PAYLOAD = getByteArray(
"0000000000000001000000596d703461000000000000000100000000000000000001001000000000ac4400"
+ "000000003565736473000000000327000000041f401500023e00024bc000023280051012080000000000"
+ "000000000000000000060102");
private static final byte[] AUDIO_HDLR_PAYLOAD = getByteArray("0000000000000000736f756e");
private static final byte[] AUDIO_MDHD_PAYLOAD =
getByteArray("00000000cf6c4889cf6c488a0000ac4400a3e40055c40000");
/** String of hexadecimal bytes containing an mvhd payload from an AVC/AAC video. */
private static final byte[] MVHD_PAYLOAD = getByteArray(
"00000000cf6c4888cf6c48880000025800023ad40001000001000000000000000000000000010000000000"
+ "000000000000000000000100000000000000000000000000004000000000000000000000000000000000"
+ "000000000000000000000000000003");
/** String of hexadecimal bytes containing a tkhd payload with an unknown duration. */
private static final byte[] TKHD_PAYLOAD =
getByteArray("0000000000000000000000000000000000000000FFFFFFFF");
/** Video frame timestamps in time units. */
private static final int[] SAMPLE_TIMESTAMPS = {0, 2, 3, 5, 6, 7};
/** Video frame sizes in bytes, including a very large sample. */
private static final int[] SAMPLE_SIZES = {100, 20, 20, 44, 100, 1 * 1024 * 1024};
/** Indices of key-frames. */
private static final int[] SYNCHRONIZATION_SAMPLE_INDICES = {0, 4, 5};
/** Indices of video frame chunk offsets. */
private static final int[] CHUNK_OFFSETS = {1000, 2000, 3000, 4000};
/** Numbers of video frames in each chunk. */
private static final int[] SAMPLES_IN_CHUNK = {2, 2, 1, 1};
/** The mdat box must be large enough to avoid reading chunk sample data out of bounds. */
private static final int MDAT_SIZE = 10 * 1024 * 1024;
/** Fake HTTP URI that can't be opened. */
private static final Uri FAKE_URI = Uri.parse("http://");
/** Empty byte array. */
private static final byte[] EMPTY = new byte[0];
public void testParsesValidMp4File() throws Exception {
// Given an extractor with an AVC/AAC file
Mp4ExtractorWrapper extractor =
prepareSampleExtractor(getFakeDataSource(true /* includeStss */, false /* mp4vFormat */));
// The MIME type and metadata are set correctly.
assertEquals(MimeTypes.VIDEO_H264, extractor.mediaFormats[0].mimeType);
assertEquals(MimeTypes.AUDIO_AAC, extractor.mediaFormats[1].mimeType);
assertEquals(VIDEO_WIDTH, extractor.selectedTrackMediaFormat.width);
assertEquals(VIDEO_HEIGHT, extractor.selectedTrackMediaFormat.height);
}
public void testParsesValidMp4vFile() throws Exception {
// Given an extractor with an mp4v file
Mp4ExtractorWrapper extractor =
prepareSampleExtractor(getFakeDataSource(true /* includeStss */, true /* mp4vFormat */));
// The MIME type and metadata are set correctly.
assertEquals(MimeTypes.VIDEO_MP4V, extractor.selectedTrackMediaFormat.mimeType);
assertEquals(VIDEO_MP4V_WIDTH, extractor.selectedTrackMediaFormat.width);
assertEquals(VIDEO_MP4V_HEIGHT, extractor.selectedTrackMediaFormat.height);
}
public void testSampleTimestampsMatch() throws Exception {
// Given an extractor
Mp4ExtractorWrapper extractor =
prepareSampleExtractor(getFakeDataSource(true /* includeStss */, false /* mp4vFormat */));
// The timestamps are set correctly.
SampleHolder sampleHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_NORMAL);
for (int i = 0; i < SAMPLE_TIMESTAMPS.length; i++) {
extractor.readSample(0, sampleHolder);
assertEquals(getVideoTimestampUs(SAMPLE_TIMESTAMPS[i]), sampleHolder.timeUs);
}
assertEquals(SampleSource.END_OF_STREAM, extractor.readSample(0, sampleHolder));
}
public void testSeekToStart() throws Exception {
// When seeking to the start
int timestampTimeUnits = SAMPLE_TIMESTAMPS[0];
long sampleTimestampUs =
getTimestampUsResultingFromSeek(getVideoTimestampUs(timestampTimeUnits));
// The timestamp is at the start.
assertEquals(getVideoTimestampUs(timestampTimeUnits), sampleTimestampUs);
}
public void testSeekToEnd() throws Exception {
// When seeking to the end
int timestampTimeUnits = SAMPLE_TIMESTAMPS[SAMPLE_TIMESTAMPS.length - 1];
long sampleTimestampUs =
getTimestampUsResultingFromSeek(getVideoTimestampUs(timestampTimeUnits));
// The timestamp is at the end.
assertEquals(getVideoTimestampUs(timestampTimeUnits), sampleTimestampUs);
}
public void testSeekToNearStart() throws Exception {
// When seeking to just after the start
int timestampTimeUnits = SAMPLE_TIMESTAMPS[0];
long sampleTimestampUs =
getTimestampUsResultingFromSeek(getVideoTimestampUs(timestampTimeUnits) + 1);
// The timestamp is at the start.
assertEquals(getVideoTimestampUs(timestampTimeUnits), sampleTimestampUs);
}
public void testSeekToBeforeLastSynchronizationSample() throws Exception {
// When seeking to just after the start
long sampleTimestampUs =
getTimestampUsResultingFromSeek(getVideoTimestampUs(SAMPLE_TIMESTAMPS[4]) - 1);
// The timestamp is at the start.
assertEquals(getVideoTimestampUs(SAMPLE_TIMESTAMPS[0]), sampleTimestampUs);
}
public void testAllSamplesAreSynchronizationSamplesWhenStssIsMissing() throws Exception {
// Given an extractor without an stss box
Mp4ExtractorWrapper extractor =
prepareSampleExtractor(getFakeDataSource(false /* includeStss */, false /* mp4vFormat */));
// All samples are synchronization samples.
SampleHolder sampleHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_NORMAL);
int sampleIndex = 0;
while (true) {
int result = extractor.readSample(0, sampleHolder);
if (result == SampleSource.SAMPLE_READ) {
assertTrue((sampleHolder.flags & MediaExtractor.SAMPLE_FLAG_SYNC) != 0);
sampleHolder.clearData();
sampleIndex++;
} else if (result == SampleSource.END_OF_STREAM) {
break;
}
}
assertTrue(sampleIndex == SAMPLE_SIZES.length);
}
public void testReadAllSamplesSucceeds() throws Exception {
// Given an extractor
Mp4ExtractorWrapper extractor =
prepareSampleExtractor(getFakeDataSource(true /* includeStss */, false /* mp4vFormat */));
// The sample sizes are set correctly.
SampleHolder sampleHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_NORMAL);
int sampleIndex = 0;
while (true) {
int result = extractor.readSample(0, sampleHolder);
if (result == SampleSource.SAMPLE_READ) {
assertEquals(SAMPLE_SIZES[sampleIndex], sampleHolder.size);
sampleHolder.clearData();
sampleIndex++;
} else if (result == SampleSource.END_OF_STREAM) {
break;
}
}
assertEquals(SAMPLE_SIZES.length, sampleIndex);
}
/** Returns the sample time read after seeking to {@code timestampTimeUnits}. */
private static long getTimestampUsResultingFromSeek(long timestampTimeUnits) throws Exception {
Mp4ExtractorWrapper extractor =
prepareSampleExtractor(getFakeDataSource(true /* includeStss */, false /* mp4vFormat */));
extractor.seekTo(timestampTimeUnits);
SampleHolder sampleHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_NORMAL);
while (true) {
int result = extractor.readSample(0, sampleHolder);
if (result == SampleSource.SAMPLE_READ) {
return sampleHolder.timeUs;
} else if (result == SampleSource.END_OF_STREAM) {
return -1;
}
}
}
private static Mp4ExtractorWrapper prepareSampleExtractor(DataSource dataSource)
throws Exception {
Mp4ExtractorWrapper extractor = new Mp4ExtractorWrapper(dataSource);
extractor.prepare();
return extractor;
}
/** Returns a video timestamp in microseconds corresponding to {@code timeUnits}. */
private static long getVideoTimestampUs(int timeUnits) {
return Util.scaleLargeTimestamp(timeUnits, C.MICROS_PER_SECOND, TIMESCALE);
}
private static byte[] getStco() {
byte[] result = new byte[4 + 4 + 4 * CHUNK_OFFSETS.length];
ByteBuffer buffer = ByteBuffer.wrap(result);
buffer.putInt(0); // Version (skipped)
buffer.putInt(CHUNK_OFFSETS.length);
for (int chunkOffset : CHUNK_OFFSETS) {
buffer.putInt(chunkOffset);
}
return result;
}
private static byte[] getStsc() {
int samplesPerChunk = -1;
List<Integer> samplesInChunkChangeIndices = new ArrayList<Integer>();
for (int i = 0; i < SAMPLES_IN_CHUNK.length; i++) {
if (SAMPLES_IN_CHUNK[i] != samplesPerChunk) {
samplesInChunkChangeIndices.add(i);
samplesPerChunk = SAMPLES_IN_CHUNK[i];
}
}
byte[] result = new byte[4 + 4 + 3 * 4 * samplesInChunkChangeIndices.size()];
ByteBuffer buffer = ByteBuffer.wrap(result);
buffer.putInt(0); // Version (skipped)
buffer.putInt(samplesInChunkChangeIndices.size());
for (int index : samplesInChunkChangeIndices) {
buffer.putInt(index + 1);
buffer.putInt(SAMPLES_IN_CHUNK[index]);
buffer.putInt(0); // Sample description index (skipped)
}
return result;
}
private static byte[] getStsz() {
byte[] result = new byte[4 + 4 + 4 + 4 * SAMPLE_SIZES.length];
ByteBuffer buffer = ByteBuffer.wrap(result);
buffer.putInt(0); // Version (skipped)
buffer.putInt(0); // No fixed sample size.
buffer.putInt(SAMPLE_SIZES.length);
for (int size : SAMPLE_SIZES) {
buffer.putInt(size);
}
return result;
}
private static byte[] getStss() {
byte[] result = new byte[4 + 4 + 4 * SYNCHRONIZATION_SAMPLE_INDICES.length];
ByteBuffer buffer = ByteBuffer.wrap(result);
buffer.putInt(0); // Version (skipped)
buffer.putInt(SYNCHRONIZATION_SAMPLE_INDICES.length);
for (int synchronizationSampleIndex : SYNCHRONIZATION_SAMPLE_INDICES) {
buffer.putInt(synchronizationSampleIndex + 1);
}
return result;
}
private static byte[] getStts() {
int sampleTimestampDeltaChanges = 0;
int currentSampleTimestampDelta = -1;
for (int i = 1; i < SAMPLE_TIMESTAMPS.length; i++) {
int timestampDelta = SAMPLE_TIMESTAMPS[i] - SAMPLE_TIMESTAMPS[i - 1];
if (timestampDelta != currentSampleTimestampDelta) {
sampleTimestampDeltaChanges++;
currentSampleTimestampDelta = timestampDelta;
}
}
byte[] result = new byte[4 + 4 + 2 * 4 * sampleTimestampDeltaChanges];
ByteBuffer buffer = ByteBuffer.wrap(result);
buffer.putInt(0); // Version (skipped);
buffer.putInt(sampleTimestampDeltaChanges);
int lastTimestampDeltaChangeIndex = 1;
currentSampleTimestampDelta = SAMPLE_TIMESTAMPS[1] - SAMPLE_TIMESTAMPS[0];
for (int i = 2; i < SAMPLE_TIMESTAMPS.length; i++) {
int timestampDelta = SAMPLE_TIMESTAMPS[i] - SAMPLE_TIMESTAMPS[i - 1];
if (timestampDelta != currentSampleTimestampDelta) {
buffer.putInt(i - lastTimestampDeltaChangeIndex);
lastTimestampDeltaChangeIndex = i;
buffer.putInt(currentSampleTimestampDelta);
currentSampleTimestampDelta = timestampDelta;
}
}
// The last sample also has a duration, so the number of entries is the number of samples.
buffer.putInt(SAMPLE_TIMESTAMPS.length - lastTimestampDeltaChangeIndex + 1);
buffer.putInt(currentSampleTimestampDelta);
return result;
}
private static byte[] getMdat() {
// TODO: Put NAL length tags in at each sample position so the sample lengths don't have to
// be multiples of four.
return new byte[MDAT_SIZE];
}
private static final DataSource getFakeDataSource(boolean includeStss, boolean mp4vFormat) {
return new ByteArrayDataSource(includeStss
? getTestMp4File(mp4vFormat) : getTestMp4FileWithoutSynchronizationData(mp4vFormat));
}
/** Gets a valid MP4 file with audio/video tracks and synchronization data. */
private static byte[] getTestMp4File(boolean mp4vFormat) {
return Mp4Atom.serialize(
atom(Atom.TYPE_ftyp, EMPTY),
atom(Atom.TYPE_moov,
atom(Atom.TYPE_mvhd, MVHD_PAYLOAD),
atom(Atom.TYPE_trak,
atom(Atom.TYPE_tkhd, TKHD_PAYLOAD),
atom(Atom.TYPE_mdia,
atom(Atom.TYPE_mdhd, VIDEO_MDHD_PAYLOAD),
atom(Atom.TYPE_hdlr, VIDEO_HDLR_PAYLOAD),
atom(Atom.TYPE_minf,
atom(Atom.TYPE_vmhd, EMPTY),
atom(Atom.TYPE_stbl,
atom(Atom.TYPE_stsd,
mp4vFormat ? VIDEO_STSD_MP4V_PAYLOAD : VIDEO_STSD_PAYLOAD),
atom(Atom.TYPE_stts, getStts()),
atom(Atom.TYPE_stss, getStss()),
atom(Atom.TYPE_stsc, getStsc()),
atom(Atom.TYPE_stsz, getStsz()),
atom(Atom.TYPE_stco, getStco()))))),
atom(Atom.TYPE_trak,
atom(Atom.TYPE_tkhd, TKHD_PAYLOAD),
atom(Atom.TYPE_mdia,
atom(Atom.TYPE_mdhd, AUDIO_MDHD_PAYLOAD),
atom(Atom.TYPE_hdlr, AUDIO_HDLR_PAYLOAD),
atom(Atom.TYPE_minf,
atom(Atom.TYPE_vmhd, EMPTY),
atom(Atom.TYPE_stbl,
atom(Atom.TYPE_stsd, AUDIO_STSD_PAYLOAD),
atom(Atom.TYPE_stts, getStts()),
atom(Atom.TYPE_stss, getStss()),
atom(Atom.TYPE_stsc, getStsc()),
atom(Atom.TYPE_stsz, getStsz()),
atom(Atom.TYPE_stco, getStco())))))),
atom(Atom.TYPE_mdat, getMdat()));
}
/** Gets a valid MP4 file with audio/video tracks and without a synchronization table. */
private static byte[] getTestMp4FileWithoutSynchronizationData(boolean mp4vFormat) {
return Mp4Atom.serialize(
atom(Atom.TYPE_ftyp, EMPTY),
atom(Atom.TYPE_moov,
atom(Atom.TYPE_mvhd, MVHD_PAYLOAD),
atom(Atom.TYPE_trak,
atom(Atom.TYPE_tkhd, TKHD_PAYLOAD),
atom(Atom.TYPE_mdia,
atom(Atom.TYPE_mdhd, VIDEO_MDHD_PAYLOAD),
atom(Atom.TYPE_hdlr, VIDEO_HDLR_PAYLOAD),
atom(Atom.TYPE_minf,
atom(Atom.TYPE_vmhd, EMPTY),
atom(Atom.TYPE_stbl,
atom(Atom.TYPE_stsd,
mp4vFormat ? VIDEO_STSD_MP4V_PAYLOAD : VIDEO_STSD_PAYLOAD),
atom(Atom.TYPE_stts, getStts()),
atom(Atom.TYPE_stsc, getStsc()),
atom(Atom.TYPE_stsz, getStsz()),
atom(Atom.TYPE_stco, getStco()))))),
atom(Atom.TYPE_trak,
atom(Atom.TYPE_tkhd, TKHD_PAYLOAD),
atom(Atom.TYPE_mdia,
atom(Atom.TYPE_mdhd, AUDIO_MDHD_PAYLOAD),
atom(Atom.TYPE_hdlr, AUDIO_HDLR_PAYLOAD),
atom(Atom.TYPE_minf,
atom(Atom.TYPE_vmhd, EMPTY),
atom(Atom.TYPE_stbl,
atom(Atom.TYPE_stsd, AUDIO_STSD_PAYLOAD),
atom(Atom.TYPE_stts, getStts()),
atom(Atom.TYPE_stsc, getStsc()),
atom(Atom.TYPE_stsz, getStsz()),
atom(Atom.TYPE_stco, getStco())))))),
atom(Atom.TYPE_mdat, getMdat()));
}
private static Mp4Atom atom(int type, Mp4Atom... containedMp4Atoms) {
return new Mp4Atom(type, containedMp4Atoms);
}
private static Mp4Atom atom(int type, byte[] payload) {
return new Mp4Atom(type, payload);
}
private static byte[] getByteArray(String hexBytes) {
byte[] result = new byte[hexBytes.length() / 2];
for (int i = 0; i < result.length; i++) {
result[i] = (byte) ((Character.digit(hexBytes.charAt(i * 2), 16) << 4)
+ Character.digit(hexBytes.charAt(i * 2 + 1), 16));
}
return result;
}
/** MP4 atom that can be serialized as a byte array. */
private static final class Mp4Atom {
public static byte[] serialize(Mp4Atom... atoms) {
int size = 0;
for (Mp4Atom atom : atoms) {
size += atom.getSize();
}
ByteBuffer buffer = ByteBuffer.allocate(size);
for (Mp4Atom atom : atoms) {
atom.getData(buffer);
}
return buffer.array();
}
private static final int HEADER_SIZE = 8;
private final int type;
private final Mp4Atom[] containedMp4Atoms;
private final byte[] payload;
private Mp4Atom(int type, Mp4Atom... containedMp4Atoms) {
this.type = type;
this.containedMp4Atoms = containedMp4Atoms;
payload = null;
}
private Mp4Atom(int type, byte[] payload) {
this.type = type;
this.payload = payload;
containedMp4Atoms = null;
}
private int getSize() {
int size = HEADER_SIZE;
if (payload != null) {
size += payload.length;
} else {
for (Mp4Atom atom : containedMp4Atoms) {
size += atom.getSize();
}
}
return size;
}
private void getData(ByteBuffer byteBuffer) {
byteBuffer.putInt(getSize());
byteBuffer.putInt(type);
if (payload != null) {
byteBuffer.put(payload);
} else {
for (Mp4Atom atom : containedMp4Atoms) {
atom.getData(byteBuffer);
}
}
}
}
/**
* Creates a {@link Mp4SampleExtractor} on a separate thread with a looper, so that it can use a
* handler for loading, and provides blocking operations like {@link #seekTo} and
* {@link #readSample}.
*/
private static final class Mp4ExtractorWrapper extends Thread {
private static final int MSG_PREPARE = 0;
private static final int MSG_SEEK_TO = 1;
private static final int MSG_READ_SAMPLE = 2;
private final DataSource dataSource;
// Written by the handler's thread and read by the main thread.
public volatile MediaFormat[] mediaFormats;
public volatile MediaFormat selectedTrackMediaFormat;
private volatile Handler handler;
private volatile int readSampleResult;
private volatile Exception exception;
private volatile CountDownLatch pendingOperationLatch;
public Mp4ExtractorWrapper(DataSource dataSource) {
super("Mp4SampleExtractorTest");
this.dataSource = Assertions.checkNotNull(dataSource);
pendingOperationLatch = new CountDownLatch(1);
start();
}
public void prepare() throws Exception {
// Block until the handler has been created.
pendingOperationLatch.await();
// Block until the extractor has been prepared.
pendingOperationLatch = new CountDownLatch(1);
handler.sendEmptyMessage(MSG_PREPARE);
pendingOperationLatch.await();
if (exception != null) {
throw exception;
}
}
public void seekTo(long timestampUs) {
handler.obtainMessage(MSG_SEEK_TO, timestampUs).sendToTarget();
}
public int readSample(int trackIndex, SampleHolder sampleHolder) throws Exception {
// Block until the extractor has completed readSample.
pendingOperationLatch = new CountDownLatch(1);
handler.obtainMessage(MSG_READ_SAMPLE, trackIndex, 0, sampleHolder).sendToTarget();
pendingOperationLatch.await();
if (exception != null) {
throw exception;
}
return readSampleResult;
}
@SuppressLint("HandlerLeak")
@Override
public void run() {
final Mp4SampleExtractor mp4SampleExtractor =
new Mp4SampleExtractor(dataSource, new DataSpec(FAKE_URI));
Looper.prepare();
handler = new Handler() {
@Override
public void handleMessage(Message message) {
try {
switch (message.what) {
case MSG_PREPARE:
if (!mp4SampleExtractor.prepare()) {
sendEmptyMessage(MSG_PREPARE);
} else {
// Select the video track and get its metadata.
mediaFormats = new MediaFormat[mp4SampleExtractor.getTrackCount()];
for (int track = 0; track < mp4SampleExtractor.getTrackCount(); track++) {
MediaFormat mediaFormat = mp4SampleExtractor.getMediaFormat(track);
mediaFormats[track] = mediaFormat;
if (MimeTypes.isVideo(mediaFormat.mimeType)) {
mp4SampleExtractor.selectTrack(track);
selectedTrackMediaFormat = mediaFormat;
}
}
pendingOperationLatch.countDown();
}
break;
case MSG_SEEK_TO:
long timestampUs = (long) message.obj;
mp4SampleExtractor.seekTo(timestampUs);
break;
case MSG_READ_SAMPLE:
int trackIndex = message.arg1;
SampleHolder sampleHolder = (SampleHolder) message.obj;
sampleHolder.clearData();
readSampleResult = mp4SampleExtractor.readSample(trackIndex, sampleHolder);
if (readSampleResult == SampleSource.NOTHING_READ) {
Message.obtain(message).sendToTarget();
return;
}
pendingOperationLatch.countDown();
break;
}
} catch (Exception e) {
exception = e;
pendingOperationLatch.countDown();
}
}
};
// Unblock waiting for the handler.
pendingOperationLatch.countDown();
Looper.loop();
}
}
}
/*
* 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.testutil;
import java.util.Random;
/**
* Utility methods for tests.
*/
public class Util {
private Util() {}
public static byte[] buildTestData(int length) {
return buildTestData(length, length);
}
public static byte[] buildTestData(int length, int seed) {
Random random = new Random(seed);
byte[] source = new byte[length];
random.nextBytes(source);
return source;
}
}
/*
* 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.text.webvtt;
import com.google.android.exoplayer.C;
import android.test.InstrumentationTestCase;
import java.io.IOException;
import java.io.InputStream;
/**
* Unit test for {@link WebvttParser}.
*/
public class WebvttParserTest extends InstrumentationTestCase {
private static final String TYPICAL_WEBVTT_FILE = "webvtt/typical";
private static final String TYPICAL_WITH_IDS_WEBVTT_FILE = "webvtt/typical_with_identifiers";
private static final String TYPICAL_WITH_TAGS_WEBVTT_FILE = "webvtt/typical_with_tags";
private static final String EMPTY_WEBVTT_FILE = "webvtt/empty";
public void testParseNullWebvttFile() throws IOException {
WebvttParser parser = new WebvttParser();
InputStream inputStream =
getInstrumentation().getContext().getResources().getAssets().open(EMPTY_WEBVTT_FILE);
try {
parser.parse(inputStream, C.UTF8_NAME, 0);
fail("Expected IOException");
} catch (IOException expected) {
// Do nothing.
}
}
public void testParseTypicalWebvttFile() throws IOException {
WebvttParser parser = new WebvttParser();
InputStream inputStream =
getInstrumentation().getContext().getResources().getAssets().open(TYPICAL_WEBVTT_FILE);
WebvttSubtitle subtitle = parser.parse(inputStream, C.UTF8_NAME, 0);
// test start time and event count
long startTimeUs = 5000000;
assertEquals(startTimeUs, subtitle.getStartTime());
assertEquals(4, subtitle.getEventTimeCount());
// test first cue
assertEquals(startTimeUs, subtitle.getEventTime(0));
assertEquals("This is the first subtitle.",
subtitle.getText(subtitle.getEventTime(0)));
assertEquals(startTimeUs + 1234000, subtitle.getEventTime(1));
// test second cue
assertEquals(startTimeUs + 2345000, subtitle.getEventTime(2));
assertEquals("This is the second subtitle.",
subtitle.getText(subtitle.getEventTime(2)));
assertEquals(startTimeUs + 3456000, subtitle.getEventTime(3));
}
public void testParseTypicalWithIdsWebvttFile() throws IOException {
WebvttParser parser = new WebvttParser();
InputStream inputStream =
getInstrumentation().getContext().getResources().getAssets()
.open(TYPICAL_WITH_IDS_WEBVTT_FILE);
WebvttSubtitle subtitle = parser.parse(inputStream, C.UTF8_NAME, 0);
// test start time and event count
long startTimeUs = 5000000;
assertEquals(startTimeUs, subtitle.getStartTime());
assertEquals(4, subtitle.getEventTimeCount());
// test first cue
assertEquals(startTimeUs, subtitle.getEventTime(0));
assertEquals("This is the first subtitle.",
subtitle.getText(subtitle.getEventTime(0)));
assertEquals(startTimeUs + 1234000, subtitle.getEventTime(1));
// test second cue
assertEquals(startTimeUs + 2345000, subtitle.getEventTime(2));
assertEquals("This is the second subtitle.",
subtitle.getText(subtitle.getEventTime(2)));
assertEquals(startTimeUs + 3456000, subtitle.getEventTime(3));
}
public void testParseTypicalWithTagsWebvttFile() throws IOException {
WebvttParser parser = new WebvttParser();
InputStream inputStream =
getInstrumentation().getContext().getResources().getAssets()
.open(TYPICAL_WITH_TAGS_WEBVTT_FILE);
WebvttSubtitle subtitle = parser.parse(inputStream, C.UTF8_NAME, 0);
// test start time and event count
long startTimeUs = 5000000;
assertEquals(startTimeUs, subtitle.getStartTime());
assertEquals(8, subtitle.getEventTimeCount());
// test first cue
assertEquals(startTimeUs, subtitle.getEventTime(0));
assertEquals("This is the first subtitle.",
subtitle.getText(subtitle.getEventTime(0)));
assertEquals(startTimeUs + 1234000, subtitle.getEventTime(1));
// test second cue
assertEquals(startTimeUs + 2345000, subtitle.getEventTime(2));
assertEquals("This is the second subtitle.",
subtitle.getText(subtitle.getEventTime(2)));
assertEquals(startTimeUs + 3456000, subtitle.getEventTime(3));
// test third cue
assertEquals(startTimeUs + 4000000, subtitle.getEventTime(4));
assertEquals("This is the third subtitle.",
subtitle.getText(subtitle.getEventTime(4)));
assertEquals(startTimeUs + 5000000, subtitle.getEventTime(5));
// test fourth cue
assertEquals(startTimeUs + 6000000, subtitle.getEventTime(6));
assertEquals("This is the <fourth> &subtitle.",
subtitle.getText(subtitle.getEventTime(6)));
assertEquals(startTimeUs + 7000000, subtitle.getEventTime(7));
}
}
/*
* 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.text.webvtt;
import junit.framework.TestCase;
/**
* Unit test for {@link WebvttSubtitle}.
*/
public class WebvttSubtitleTest extends TestCase {
private static final String FIRST_SUBTITLE_STRING = "This is the first subtitle.";
private static final String SECOND_SUBTITLE_STRING = "This is the second subtitle.";
private static final String FIRST_AND_SECOND_SUBTITLE_STRING =
FIRST_SUBTITLE_STRING + SECOND_SUBTITLE_STRING;
private WebvttSubtitle emptySubtitle = new WebvttSubtitle(new String[] {}, 0, new long[] {});
private WebvttSubtitle simpleSubtitle = new WebvttSubtitle(
new String[] {FIRST_SUBTITLE_STRING, SECOND_SUBTITLE_STRING}, 0,
new long[] {1000000, 2000000, 3000000, 4000000});
private WebvttSubtitle overlappingSubtitle = new WebvttSubtitle(
new String[] {FIRST_SUBTITLE_STRING, SECOND_SUBTITLE_STRING}, 0,
new long[] {1000000, 3000000, 2000000, 4000000});
private WebvttSubtitle nestedSubtitle = new WebvttSubtitle(
new String[] {FIRST_SUBTITLE_STRING, SECOND_SUBTITLE_STRING}, 0,
new long[] {1000000, 4000000, 2000000, 3000000});
public void testEventCount() {
assertEquals(0, emptySubtitle.getEventTimeCount());
assertEquals(4, simpleSubtitle.getEventTimeCount());
assertEquals(4, overlappingSubtitle.getEventTimeCount());
assertEquals(4, nestedSubtitle.getEventTimeCount());
}
public void testStartTime() {
assertEquals(0, emptySubtitle.getStartTime());
assertEquals(0, simpleSubtitle.getStartTime());
assertEquals(0, overlappingSubtitle.getStartTime());
assertEquals(0, nestedSubtitle.getStartTime());
}
public void testLastEventTime() {
assertEquals(-1, emptySubtitle.getLastEventTime());
assertEquals(4000000, simpleSubtitle.getLastEventTime());
assertEquals(4000000, overlappingSubtitle.getLastEventTime());
assertEquals(4000000, nestedSubtitle.getLastEventTime());
}
public void testSimpleSubtitleEventTimes() {
testSubtitleEventTimesHelper(simpleSubtitle);
}
public void testSimpleSubtitleEventIndices() {
testSubtitleEventIndicesHelper(simpleSubtitle);
}
public void testSimpleSubtitleText() {
// Test before first subtitle
assertNull(simpleSubtitle.getText(0));
assertNull(simpleSubtitle.getText(500000));
assertNull(simpleSubtitle.getText(999999));
// Test first subtitle
assertEquals(FIRST_SUBTITLE_STRING, simpleSubtitle.getText(1000000));
assertEquals(FIRST_SUBTITLE_STRING, simpleSubtitle.getText(1500000));
assertEquals(FIRST_SUBTITLE_STRING, simpleSubtitle.getText(1999999));
// Test after first subtitle, before second subtitle
assertNull(simpleSubtitle.getText(2000000));
assertNull(simpleSubtitle.getText(2500000));
assertNull(simpleSubtitle.getText(2999999));
// Test second subtitle
assertEquals(SECOND_SUBTITLE_STRING, simpleSubtitle.getText(3000000));
assertEquals(SECOND_SUBTITLE_STRING, simpleSubtitle.getText(3500000));
assertEquals(SECOND_SUBTITLE_STRING, simpleSubtitle.getText(3999999));
// Test after second subtitle
assertNull(simpleSubtitle.getText(4000000));
assertNull(simpleSubtitle.getText(4500000));
assertNull(simpleSubtitle.getText(Long.MAX_VALUE));
}
public void testOverlappingSubtitleEventTimes() {
testSubtitleEventTimesHelper(overlappingSubtitle);
}
public void testOverlappingSubtitleEventIndices() {
testSubtitleEventIndicesHelper(overlappingSubtitle);
}
public void testOverlappingSubtitleText() {
// Test before first subtitle
assertNull(overlappingSubtitle.getText(0));
assertNull(overlappingSubtitle.getText(500000));
assertNull(overlappingSubtitle.getText(999999));
// Test first subtitle
assertEquals(FIRST_SUBTITLE_STRING, overlappingSubtitle.getText(1000000));
assertEquals(FIRST_SUBTITLE_STRING, overlappingSubtitle.getText(1500000));
assertEquals(FIRST_SUBTITLE_STRING, overlappingSubtitle.getText(1999999));
// Test after first and second subtitle
assertEquals(FIRST_AND_SECOND_SUBTITLE_STRING, overlappingSubtitle.getText(2000000));
assertEquals(FIRST_AND_SECOND_SUBTITLE_STRING, overlappingSubtitle.getText(2500000));
assertEquals(FIRST_AND_SECOND_SUBTITLE_STRING, overlappingSubtitle.getText(2999999));
// Test second subtitle
assertEquals(SECOND_SUBTITLE_STRING, overlappingSubtitle.getText(3000000));
assertEquals(SECOND_SUBTITLE_STRING, overlappingSubtitle.getText(3500000));
assertEquals(SECOND_SUBTITLE_STRING, overlappingSubtitle.getText(3999999));
// Test after second subtitle
assertNull(overlappingSubtitle.getText(4000000));
assertNull(overlappingSubtitle.getText(4500000));
assertNull(overlappingSubtitle.getText(Long.MAX_VALUE));
}
public void testNestedSubtitleEventTimes() {
testSubtitleEventTimesHelper(nestedSubtitle);
}
public void testNestedSubtitleEventIndices() {
testSubtitleEventIndicesHelper(nestedSubtitle);
}
public void testNestedSubtitleText() {
// Test before first subtitle
assertNull(nestedSubtitle.getText(0));
assertNull(nestedSubtitle.getText(500000));
assertNull(nestedSubtitle.getText(999999));
// Test first subtitle
assertEquals(FIRST_SUBTITLE_STRING, nestedSubtitle.getText(1000000));
assertEquals(FIRST_SUBTITLE_STRING, nestedSubtitle.getText(1500000));
assertEquals(FIRST_SUBTITLE_STRING, nestedSubtitle.getText(1999999));
// Test after first and second subtitle
assertEquals(FIRST_AND_SECOND_SUBTITLE_STRING, nestedSubtitle.getText(2000000));
assertEquals(FIRST_AND_SECOND_SUBTITLE_STRING, nestedSubtitle.getText(2500000));
assertEquals(FIRST_AND_SECOND_SUBTITLE_STRING, nestedSubtitle.getText(2999999));
// Test first subtitle
assertEquals(FIRST_SUBTITLE_STRING, nestedSubtitle.getText(3000000));
assertEquals(FIRST_SUBTITLE_STRING, nestedSubtitle.getText(3500000));
assertEquals(FIRST_SUBTITLE_STRING, nestedSubtitle.getText(3999999));
// Test after second subtitle
assertNull(nestedSubtitle.getText(4000000));
assertNull(nestedSubtitle.getText(4500000));
assertNull(nestedSubtitle.getText(Long.MAX_VALUE));
}
private void testSubtitleEventTimesHelper(WebvttSubtitle subtitle) {
assertEquals(1000000, subtitle.getEventTime(0));
assertEquals(2000000, subtitle.getEventTime(1));
assertEquals(3000000, subtitle.getEventTime(2));
assertEquals(4000000, subtitle.getEventTime(3));
}
private void testSubtitleEventIndicesHelper(WebvttSubtitle subtitle) {
// Test first event
assertEquals(0, subtitle.getNextEventTimeIndex(0));
assertEquals(0, subtitle.getNextEventTimeIndex(500000));
assertEquals(0, subtitle.getNextEventTimeIndex(999999));
// Test second event
assertEquals(1, subtitle.getNextEventTimeIndex(1000000));
assertEquals(1, subtitle.getNextEventTimeIndex(1500000));
assertEquals(1, subtitle.getNextEventTimeIndex(1999999));
// Test third event
assertEquals(2, subtitle.getNextEventTimeIndex(2000000));
assertEquals(2, subtitle.getNextEventTimeIndex(2500000));
assertEquals(2, subtitle.getNextEventTimeIndex(2999999));
// Test fourth event
assertEquals(3, subtitle.getNextEventTimeIndex(3000000));
assertEquals(3, subtitle.getNextEventTimeIndex(3500000));
assertEquals(3, subtitle.getNextEventTimeIndex(3999999));
// Test null event (i.e. look for events after the last event)
assertEquals(-1, subtitle.getNextEventTimeIndex(4000000));
assertEquals(-1, subtitle.getNextEventTimeIndex(4500000));
assertEquals(-1, subtitle.getNextEventTimeIndex(Long.MAX_VALUE));
}
}
/*
* 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);
}
}
/*
* 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.C;
import junit.framework.TestCase;
import java.io.IOException;
/**
* Unit tests for {@link ByteArrayDataSource}.
*/
public class ByteArrayDataSourceTest extends TestCase {
private static final byte[] TEST_DATA = new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
private static final byte[] TEST_DATA_ODD = new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
public void testFullReadSingleBytes() {
readTestData(TEST_DATA, 0, C.LENGTH_UNBOUNDED, 1, 0, 1, false);
}
public void testFullReadAllBytes() {
readTestData(TEST_DATA, 0, C.LENGTH_UNBOUNDED, 100, 0, 100, false);
}
public void testLimitReadSingleBytes() {
// Limit set to the length of the data.
readTestData(TEST_DATA, 0, TEST_DATA.length, 1, 0, 1, false);
// And less.
readTestData(TEST_DATA, 0, 6, 1, 0, 1, false);
}
public void testFullReadTwoBytes() {
// Try with the total data length an exact multiple of the size of each individual read.
readTestData(TEST_DATA, 0, C.LENGTH_UNBOUNDED, 2, 0, 2, false);
// And not.
readTestData(TEST_DATA_ODD, 0, C.LENGTH_UNBOUNDED, 2, 0, 2, false);
}
public void testLimitReadTwoBytes() {
// Try with the limit an exact multiple of the size of each individual read.
readTestData(TEST_DATA, 0, 6, 2, 0, 2, false);
// And not.
readTestData(TEST_DATA, 0, 7, 2, 0, 2, false);
}
public void testReadFromValidOffsets() {
// Read from an offset without bound.
readTestData(TEST_DATA, 1, C.LENGTH_UNBOUNDED, 1, 0, 1, false);
// And with bound.
readTestData(TEST_DATA, 1, 6, 1, 0, 1, false);
// Read from the last possible offset without bound.
readTestData(TEST_DATA, TEST_DATA.length - 1, C.LENGTH_UNBOUNDED, 1, 0, 1, false);
// And with bound.
readTestData(TEST_DATA, TEST_DATA.length - 1, 1, 1, 0, 1, false);
}
public void testReadFromInvalidOffsets() {
// Read from first invalid offset and check failure without bound.
readTestData(TEST_DATA, TEST_DATA.length, C.LENGTH_UNBOUNDED, 1, 0, 1, true);
// And with bound.
readTestData(TEST_DATA, TEST_DATA.length, 1, 1, 0, 1, true);
}
public void testReadWithInvalidLength() {
// Read more data than is available.
readTestData(TEST_DATA, 0, TEST_DATA.length + 1, 1, 0, 1, true);
// And with bound.
readTestData(TEST_DATA, 1, TEST_DATA.length, 1, 0, 1, true);
}
/**
* Tests reading from a {@link ByteArrayDataSource} with various parameters.
*
* @param testData The data that the {@link ByteArrayDataSource} will wrap.
* @param dataOffset The offset from which to read data.
* @param dataLength The total length of data to read.
* @param outputBufferLength The length of the target buffer for each read.
* @param writeOffset The offset into {@code outputBufferLength} for each read.
* @param maxReadLength The maximum length of each read.
* @param expectFailOnOpen Whether it is expected that opening the source will fail.
*/
private void readTestData(byte[] testData, int dataOffset, int dataLength, int outputBufferLength,
int writeOffset, int maxReadLength, boolean expectFailOnOpen) {
int expectedFinalBytesRead =
dataLength == C.LENGTH_UNBOUNDED ? (testData.length - dataOffset) : dataLength;
ByteArrayDataSource dataSource = new ByteArrayDataSource(testData);
boolean opened = false;
try {
// Open the source.
long length = dataSource.open(new DataSpec(null, dataOffset, dataLength, null));
opened = true;
assertFalse(expectFailOnOpen);
// Verify the resolved length is as we expect.
assertEquals(expectedFinalBytesRead, length);
byte[] outputBuffer = new byte[outputBufferLength];
int accumulatedBytesRead = 0;
while (true) {
// Calculate a valid length for the next read, constraining by the specified output buffer
// length, write offset and maximum write length input parameters.
int requestedReadLength = Math.min(maxReadLength, outputBufferLength - writeOffset);
assertTrue(requestedReadLength > 0);
int bytesRead = dataSource.read(outputBuffer, writeOffset, requestedReadLength);
if (bytesRead != -1) {
assertTrue(bytesRead > 0);
assertTrue(bytesRead <= requestedReadLength);
// Check the data read was correct.
for (int i = 0; i < bytesRead; i++) {
assertEquals(testData[dataOffset + accumulatedBytesRead + i],
outputBuffer[writeOffset + i]);
}
// Check that we haven't read more data than we were expecting.
accumulatedBytesRead += bytesRead;
assertTrue(accumulatedBytesRead <= expectedFinalBytesRead);
// If we haven't read all of the bytes the request should have been satisfied in full.
assertTrue(accumulatedBytesRead == expectedFinalBytesRead
|| bytesRead == requestedReadLength);
} else {
// We're done. Check we read the expected number of bytes.
assertEquals(expectedFinalBytesRead, accumulatedBytesRead);
return;
}
}
} catch (IOException e) {
if (expectFailOnOpen && !opened) {
// Expected.
return;
}
// Unexpected failure.
fail();
}
}
}
/*
* 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.testutil.Util;
import junit.framework.TestCase;
import java.io.IOException;
import java.util.Arrays;
/**
* Unit tests for {@link DataSourceStream}.
*/
public class DataSourceStreamTest extends TestCase {
private static final int DATA_LENGTH = 1024;
private static final int BUFFER_LENGTH = 128;
public void testGetLoadedData() throws IOException, InterruptedException {
byte[] testData = Util.buildTestData(DATA_LENGTH);
DataSource dataSource = new ByteArrayDataSource(testData);
DataSpec dataSpec = new DataSpec(null, 0, DATA_LENGTH, null);
DataSourceStream dataSourceStream = new DataSourceStream(dataSource, dataSpec,
new BufferPool(BUFFER_LENGTH));
dataSourceStream.load();
// Assert that the read and load positions are correct.
assertEquals(0, dataSourceStream.getReadPosition());
assertEquals(testData.length, dataSourceStream.getLoadPosition());
int halfTestDataLength = testData.length / 2;
byte[] readData = new byte[testData.length];
int bytesRead = dataSourceStream.read(readData, 0, halfTestDataLength);
// Assert that the read position is updated correctly.
assertEquals(halfTestDataLength, bytesRead);
assertEquals(halfTestDataLength, dataSourceStream.getReadPosition());
bytesRead += dataSourceStream.read(readData, bytesRead, testData.length - bytesRead);
// Assert that the read position was updated correctly.
assertEquals(testData.length, bytesRead);
assertEquals(testData.length, dataSourceStream.getReadPosition());
// Assert that the data read using the two read calls either side of getLoadedData is correct.
assertTrue(Arrays.equals(testData, readData));
}
}
/*
* 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.util;
import junit.framework.TestCase;
import java.util.Arrays;
/**
* Tests for {@link H264Util}.
*/
public class H264UtilTest extends TestCase {
private static final int TEST_PARTIAL_NAL_POSITION = 4;
private static final int TEST_NAL_POSITION = 10;
public void testFindNalUnit() {
byte[] data = buildTestData();
// Should find NAL unit.
int result = H264Util.findNalUnit(data, 0, data.length, null);
assertEquals(TEST_NAL_POSITION, result);
// Should find NAL unit whose prefix ends one byte before the limit.
result = H264Util.findNalUnit(data, 0, TEST_NAL_POSITION + 4, null);
assertEquals(TEST_NAL_POSITION, result);
// Shouldn't find NAL unit whose prefix ends at the limit (since the limit is exclusive).
result = H264Util.findNalUnit(data, 0, TEST_NAL_POSITION + 3, null);
assertEquals(TEST_NAL_POSITION + 3, result);
// Should find NAL unit whose prefix starts at the offset.
result = H264Util.findNalUnit(data, TEST_NAL_POSITION, data.length, null);
assertEquals(TEST_NAL_POSITION, result);
// Shouldn't find NAL unit whose prefix starts one byte past the offset.
result = H264Util.findNalUnit(data, TEST_NAL_POSITION + 1, data.length, null);
assertEquals(data.length, result);
}
public void testFindNalUnitWithPrefix() {
byte[] data = buildTestData();
// First byte of NAL unit in data1, rest in data2.
boolean[] prefixFlags = new boolean[3];
byte[] data1 = Arrays.copyOfRange(data, 0, TEST_NAL_POSITION + 1);
byte[] data2 = Arrays.copyOfRange(data, TEST_NAL_POSITION + 1, data.length);
int result = H264Util.findNalUnit(data1, 0, data1.length, prefixFlags);
assertEquals(data1.length, result);
result = H264Util.findNalUnit(data2, 0, data2.length, prefixFlags);
assertEquals(-1, result);
assertPrefixFlagsCleared(prefixFlags);
// First three bytes of NAL unit in data1, rest in data2.
prefixFlags = new boolean[3];
data1 = Arrays.copyOfRange(data, 0, TEST_NAL_POSITION + 3);
data2 = Arrays.copyOfRange(data, TEST_NAL_POSITION + 3, data.length);
result = H264Util.findNalUnit(data1, 0, data1.length, prefixFlags);
assertEquals(data1.length, result);
result = H264Util.findNalUnit(data2, 0, data2.length, prefixFlags);
assertEquals(-3, result);
assertPrefixFlagsCleared(prefixFlags);
// First byte of NAL unit in data1, second byte in data2, rest in data3.
prefixFlags = new boolean[3];
data1 = Arrays.copyOfRange(data, 0, TEST_NAL_POSITION + 1);
data2 = Arrays.copyOfRange(data, TEST_NAL_POSITION + 1, TEST_NAL_POSITION + 2);
byte[] data3 = Arrays.copyOfRange(data, TEST_NAL_POSITION + 2, data.length);
result = H264Util.findNalUnit(data1, 0, data1.length, prefixFlags);
assertEquals(data1.length, result);
result = H264Util.findNalUnit(data2, 0, data2.length, prefixFlags);
assertEquals(data2.length, result);
result = H264Util.findNalUnit(data3, 0, data3.length, prefixFlags);
assertEquals(-2, result);
assertPrefixFlagsCleared(prefixFlags);
// NAL unit split with one byte in four arrays.
prefixFlags = new boolean[3];
data1 = Arrays.copyOfRange(data, 0, TEST_NAL_POSITION + 1);
data2 = Arrays.copyOfRange(data, TEST_NAL_POSITION + 1, TEST_NAL_POSITION + 2);
data3 = Arrays.copyOfRange(data, TEST_NAL_POSITION + 2, TEST_NAL_POSITION + 3);
byte[] data4 = Arrays.copyOfRange(data, TEST_NAL_POSITION + 2, data.length);
result = H264Util.findNalUnit(data1, 0, data1.length, prefixFlags);
assertEquals(data1.length, result);
result = H264Util.findNalUnit(data2, 0, data2.length, prefixFlags);
assertEquals(data2.length, result);
result = H264Util.findNalUnit(data3, 0, data3.length, prefixFlags);
assertEquals(data3.length, result);
result = H264Util.findNalUnit(data4, 0, data4.length, prefixFlags);
assertEquals(-3, result);
assertPrefixFlagsCleared(prefixFlags);
// NAL unit entirely in data2. data1 ends with partial prefix.
prefixFlags = new boolean[3];
data1 = Arrays.copyOfRange(data, 0, TEST_PARTIAL_NAL_POSITION + 2);
data2 = Arrays.copyOfRange(data, TEST_PARTIAL_NAL_POSITION + 2, data.length);
result = H264Util.findNalUnit(data1, 0, data1.length, prefixFlags);
assertEquals(data1.length, result);
result = H264Util.findNalUnit(data2, 0, data2.length, prefixFlags);
assertEquals(4, result);
assertPrefixFlagsCleared(prefixFlags);
}
private static byte[] buildTestData() {
byte[] data = new byte[20];
for (int i = 0; i < data.length; i++) {
data[i] = (byte) 0xFF;
}
// Insert an incomplete NAL unit start code.
data[TEST_PARTIAL_NAL_POSITION] = 0;
data[TEST_PARTIAL_NAL_POSITION + 1] = 0;
// Insert a complete NAL unit start code.
data[TEST_NAL_POSITION] = 0;
data[TEST_NAL_POSITION + 1] = 0;
data[TEST_NAL_POSITION + 2] = 1;
data[TEST_NAL_POSITION + 3] = 5;
return data;
}
private static void assertPrefixFlagsCleared(boolean[] flags) {
assertEquals(false, flags[0] || flags[1] || flags[2]);
}
}
/*
* 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.util;
import junit.framework.TestCase;
import java.util.Arrays;
/**
* Tests for {@link ParsableByteArray}.
*/
public class ParsableByteArrayTest extends TestCase {
private static final byte[] ARRAY_ELEMENTS =
new byte[] {0x0F, (byte) 0xFF, (byte) 0x42, (byte) 0x0F, 0x00, 0x00, 0x00, 0x00};
private ParsableByteArray parsableByteArray;
@Override
public void setUp() {
parsableByteArray = new ParsableByteArray(ARRAY_ELEMENTS.length);
System.arraycopy(ARRAY_ELEMENTS, 0, parsableByteArray.data, 0, ARRAY_ELEMENTS.length);
}
public void testReadInt() {
// When reading a signed integer
int value = parsableByteArray.readInt();
// Then the read value is equal to the array elements interpreted as an int.
assertEquals((0xFF & ARRAY_ELEMENTS[0]) << 24 | (0xFF & ARRAY_ELEMENTS[1]) << 16
| (0xFF & ARRAY_ELEMENTS[2]) << 8 | (0xFF & ARRAY_ELEMENTS[3]), value);
}
public void testSkipBack() {
// When reading an unsigned integer
long value = parsableByteArray.readUnsignedInt();
// Then skipping back and reading gives the same value.
parsableByteArray.skip(-4);
assertEquals(value, parsableByteArray.readUnsignedInt());
}
public void testReadingMovesPosition() {
// Given an array at the start
assertEquals(0, parsableByteArray.getPosition());
// When reading an integer, the position advances
parsableByteArray.readUnsignedInt();
assertEquals(4, parsableByteArray.getPosition());
}
public void testOutOfBoundsThrows() {
// Given an array at the end
parsableByteArray.readUnsignedLongToLong();
assertEquals(ARRAY_ELEMENTS.length, parsableByteArray.getPosition());
// Then reading more data throws.
try {
parsableByteArray.readUnsignedInt();
fail();
} catch (Exception e) {
// Expected.
}
}
public void testModificationsAffectParsableArray() {
// When modifying the wrapped byte array
byte[] data = parsableByteArray.data;
long readValue = parsableByteArray.readUnsignedInt();
data[0] = (byte) (ARRAY_ELEMENTS[0] + 1);
parsableByteArray.setPosition(0);
// Then the parsed value changes.
assertFalse(parsableByteArray.readUnsignedInt() == readValue);
}
public void testReadingUnsignedLongWithMsbSetThrows() {
// Given an array with the most-significant bit set on the top byte
byte[] data = parsableByteArray.data;
data[0] = (byte) 0x80;
// Then reading an unsigned long throws.
try {
parsableByteArray.readUnsignedLongToLong();
fail();
} catch (Exception e) {
// Expected.
}
}
public void testReadUnsignedFixedPoint1616() {
// When reading the integer part of a 16.16 fixed point value
int value = parsableByteArray.readUnsignedFixedPoint1616();
// Then the read value is equal to the array elements interpreted as a short.
assertEquals((0xFF & ARRAY_ELEMENTS[0]) << 8 | (ARRAY_ELEMENTS[1] & 0xFF), value);
assertEquals(4, parsableByteArray.getPosition());
}
public void testReadingBytesReturnsCopy() {
// When reading all the bytes back
int length = parsableByteArray.limit();
assertEquals(ARRAY_ELEMENTS.length, length);
byte[] copy = new byte[length];
parsableByteArray.readBytes(copy, 0, length);
// Then the array elements are the same.
assertTrue(Arrays.equals(parsableByteArray.data, copy));
}
}
/*
* 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.util;
import junit.framework.TestCase;
/**
* Unit tests for {@link UriUtil}.
*/
public class UriUtilTest extends TestCase {
/**
* Tests normal usage of {@link UriUtil#resolve(String, String)}.
* <p>
* The test cases are taken from RFC-3986 5.4.1.
*/
public void testResolveNormal() {
String base = "http://a/b/c/d;p?q";
assertEquals("g:h", UriUtil.resolve(base, "g:h"));
assertEquals("http://a/b/c/g", UriUtil.resolve(base, "g"));
assertEquals("http://a/b/c/g/", UriUtil.resolve(base, "g/"));
assertEquals("http://a/g", UriUtil.resolve(base, "/g"));
assertEquals("http://g", UriUtil.resolve(base, "//g"));
assertEquals("http://a/b/c/d;p?y", UriUtil.resolve(base, "?y"));
assertEquals("http://a/b/c/g?y", UriUtil.resolve(base, "g?y"));
assertEquals("http://a/b/c/d;p?q#s", UriUtil.resolve(base, "#s"));
assertEquals("http://a/b/c/g#s", UriUtil.resolve(base, "g#s"));
assertEquals("http://a/b/c/g?y#s", UriUtil.resolve(base, "g?y#s"));
assertEquals("http://a/b/c/;x", UriUtil.resolve(base, ";x"));
assertEquals("http://a/b/c/g;x", UriUtil.resolve(base, "g;x"));
assertEquals("http://a/b/c/g;x?y#s", UriUtil.resolve(base, "g;x?y#s"));
assertEquals("http://a/b/c/d;p?q", UriUtil.resolve(base, ""));
assertEquals("http://a/b/c/", UriUtil.resolve(base, "."));
assertEquals("http://a/b/c/", UriUtil.resolve(base, "./"));
assertEquals("http://a/b/", UriUtil.resolve(base, ".."));
assertEquals("http://a/b/", UriUtil.resolve(base, "../"));
assertEquals("http://a/b/g", UriUtil.resolve(base, "../g"));
assertEquals("http://a/", UriUtil.resolve(base, "../.."));
assertEquals("http://a/", UriUtil.resolve(base, "../../"));
assertEquals("http://a/g", UriUtil.resolve(base, "../../g"));
}
/**
* Tests abnormal usage of {@link UriUtil#resolve(String, String)}.
* <p>
* The test cases are taken from RFC-3986 5.4.2.
*/
public void testResolveAbnormal() {
String base = "http://a/b/c/d;p?q";
assertEquals("http://a/g", UriUtil.resolve(base, "../../../g"));
assertEquals("http://a/g", UriUtil.resolve(base, "../../../../g"));
assertEquals("http://a/g", UriUtil.resolve(base, "/./g"));
assertEquals("http://a/g", UriUtil.resolve(base, "/../g"));
assertEquals("http://a/b/c/g.", UriUtil.resolve(base, "g."));
assertEquals("http://a/b/c/.g", UriUtil.resolve(base, ".g"));
assertEquals("http://a/b/c/g..", UriUtil.resolve(base, "g.."));
assertEquals("http://a/b/c/..g", UriUtil.resolve(base, "..g"));
assertEquals("http://a/b/g", UriUtil.resolve(base, "./../g"));
assertEquals("http://a/b/c/g/", UriUtil.resolve(base, "./g/."));
assertEquals("http://a/b/c/g/h", UriUtil.resolve(base, "g/./h"));
assertEquals("http://a/b/c/h", UriUtil.resolve(base, "g/../h"));
assertEquals("http://a/b/c/g;x=1/y", UriUtil.resolve(base, "g;x=1/./y"));
assertEquals("http://a/b/c/y", UriUtil.resolve(base, "g;x=1/../y"));
assertEquals("http://a/b/c/g?y/./x", UriUtil.resolve(base, "g?y/./x"));
assertEquals("http://a/b/c/g?y/../x", UriUtil.resolve(base, "g?y/../x"));
assertEquals("http://a/b/c/g#s/./x", UriUtil.resolve(base, "g#s/./x"));
assertEquals("http://a/b/c/g#s/../x", UriUtil.resolve(base, "g#s/../x"));
assertEquals("http:g", UriUtil.resolve(base, "http:g"));
}
/**
* Tests additional abnormal usage of {@link UriUtil#resolve(String, String)}.
*/
public void testResolveAbnormalAdditional() {
assertEquals("c:e", UriUtil.resolve("http://a/b", "c:d/../e"));
assertEquals("a:c", UriUtil.resolve("a:b", "../c"));
}
}
/*
* 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.util;
import junit.framework.TestCase;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.List;
/**
* Unit tests for {@link Util}.
*/
public class UtilTest extends TestCase {
public void testArrayBinarySearchFloor() {
long[] values = new long[0];
assertEquals(-1, Util.binarySearchFloor(values, 0, false, false));
assertEquals(0, Util.binarySearchFloor(values, 0, false, true));
values = new long[] {1, 3, 5};
assertEquals(-1, Util.binarySearchFloor(values, 0, false, false));
assertEquals(-1, Util.binarySearchFloor(values, 0, true, false));
assertEquals(0, Util.binarySearchFloor(values, 0, false, true));
assertEquals(0, Util.binarySearchFloor(values, 0, true, true));
assertEquals(-1, Util.binarySearchFloor(values, 1, false, false));
assertEquals(0, Util.binarySearchFloor(values, 1, true, false));
assertEquals(0, Util.binarySearchFloor(values, 1, false, true));
assertEquals(0, Util.binarySearchFloor(values, 1, true, true));
assertEquals(1, Util.binarySearchFloor(values, 4, false, false));
assertEquals(1, Util.binarySearchFloor(values, 4, true, false));
assertEquals(1, Util.binarySearchFloor(values, 5, false, false));
assertEquals(2, Util.binarySearchFloor(values, 5, true, false));
assertEquals(2, Util.binarySearchFloor(values, 6, false, false));
assertEquals(2, Util.binarySearchFloor(values, 6, true, false));
}
public void testListBinarySearchFloor() {
List<Integer> values = new ArrayList<Integer>();
assertEquals(-1, Util.binarySearchFloor(values, 0, false, false));
assertEquals(0, Util.binarySearchFloor(values, 0, false, true));
values.add(1);
values.add(3);
values.add(5);
assertEquals(-1, Util.binarySearchFloor(values, 0, false, false));
assertEquals(-1, Util.binarySearchFloor(values, 0, true, false));
assertEquals(0, Util.binarySearchFloor(values, 0, false, true));
assertEquals(0, Util.binarySearchFloor(values, 0, true, true));
assertEquals(-1, Util.binarySearchFloor(values, 1, false, false));
assertEquals(0, Util.binarySearchFloor(values, 1, true, false));
assertEquals(0, Util.binarySearchFloor(values, 1, false, true));
assertEquals(0, Util.binarySearchFloor(values, 1, true, true));
assertEquals(1, Util.binarySearchFloor(values, 4, false, false));
assertEquals(1, Util.binarySearchFloor(values, 4, true, false));
assertEquals(1, Util.binarySearchFloor(values, 5, false, false));
assertEquals(2, Util.binarySearchFloor(values, 5, true, false));
assertEquals(2, Util.binarySearchFloor(values, 6, false, false));
assertEquals(2, Util.binarySearchFloor(values, 6, true, false));
}
public void testArrayBinarySearchCeil() {
long[] values = new long[0];
assertEquals(0, Util.binarySearchCeil(values, 0, false, false));
assertEquals(-1, Util.binarySearchCeil(values, 0, false, true));
values = new long[] {1, 3, 5};
assertEquals(0, Util.binarySearchCeil(values, 0, false, false));
assertEquals(0, Util.binarySearchCeil(values, 0, true, false));
assertEquals(1, Util.binarySearchCeil(values, 1, false, false));
assertEquals(0, Util.binarySearchCeil(values, 1, true, false));
assertEquals(1, Util.binarySearchCeil(values, 2, false, false));
assertEquals(1, Util.binarySearchCeil(values, 2, true, false));
assertEquals(3, Util.binarySearchCeil(values, 5, false, false));
assertEquals(2, Util.binarySearchCeil(values, 5, true, false));
assertEquals(2, Util.binarySearchCeil(values, 5, false, true));
assertEquals(2, Util.binarySearchCeil(values, 5, true, true));
assertEquals(3, Util.binarySearchCeil(values, 6, false, false));
assertEquals(3, Util.binarySearchCeil(values, 6, true, false));
assertEquals(2, Util.binarySearchCeil(values, 6, false, true));
assertEquals(2, Util.binarySearchCeil(values, 6, true, true));
}
public void testListBinarySearchCeil() {
List<Integer> values = new ArrayList<Integer>();
assertEquals(0, Util.binarySearchCeil(values, 0, false, false));
assertEquals(-1, Util.binarySearchCeil(values, 0, false, true));
values.add(1);
values.add(3);
values.add(5);
assertEquals(0, Util.binarySearchCeil(values, 0, false, false));
assertEquals(0, Util.binarySearchCeil(values, 0, true, false));
assertEquals(1, Util.binarySearchCeil(values, 1, false, false));
assertEquals(0, Util.binarySearchCeil(values, 1, true, false));
assertEquals(1, Util.binarySearchCeil(values, 2, false, false));
assertEquals(1, Util.binarySearchCeil(values, 2, true, false));
assertEquals(3, Util.binarySearchCeil(values, 5, false, false));
assertEquals(2, Util.binarySearchCeil(values, 5, true, false));
assertEquals(2, Util.binarySearchCeil(values, 5, false, true));
assertEquals(2, Util.binarySearchCeil(values, 5, true, true));
assertEquals(3, Util.binarySearchCeil(values, 6, false, false));
assertEquals(3, Util.binarySearchCeil(values, 6, true, false));
assertEquals(2, Util.binarySearchCeil(values, 6, false, true));
assertEquals(2, Util.binarySearchCeil(values, 6, true, true));
}
public void testParseXsDuration() {
assertEquals(150279L, Util.parseXsDuration("PT150.279S"));
assertEquals(1500L, Util.parseXsDuration("PT1.500S"));
}
public void testParseXsDateTime() throws ParseException {
assertEquals(1403219262000L, Util.parseXsDateTime("2014-06-19T23:07:42"));
assertEquals(1407322800000L, Util.parseXsDateTime("2014-08-06T11:00:00Z"));
}
}
This file is needed to make sure the libs directory is present.
# This file is automatically generated by Android Tools.
# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
#
# This file must be checked in Version Control Systems.
#
# To customize properties used by the Ant build system edit
# "ant.properties", and override values to adapt the script to your
# project structure.
#
# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home):
#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
# Project target.
target=android-21
This file is needed to make sure the res directory is present.
The file is ignored by the Android toolchain because its name starts with a dot.
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
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.
No preview for this file type
The MIT License
Copyright (c) 2008-2010 Mockito contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
No preview for this file type
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