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

This commit is contained in:
Sergio Moreno Mozota 2015-04-13 15:56:43 +02:00
commit da7ae2a925
141 changed files with 8712 additions and 1553 deletions

View file

@ -17,9 +17,11 @@
buildscript {
repositories {
mavenCentral()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:1.0.0'
classpath 'com.novoda:bintray-release:0.2.7'
}
}

View file

@ -48,6 +48,7 @@ public class DemoUtil {
public static final int TYPE_SS = 1;
public static final int TYPE_OTHER = 2;
public static final int TYPE_HLS = 3;
public static final int TYPE_MP4 = 4;
private static final CookieManager defaultCookieManager;

View file

@ -163,6 +163,12 @@ public class EventLogger implements DemoPlayer.Listener, DemoPlayer.InfoListener
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) {
Log.e(TAG, "internalError [" + getSessionTimeString() + ", " + type + "]", e);
}

View file

@ -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.RendererBuilder;
import com.google.android.exoplayer.demo.player.HlsRendererBuilder;
import com.google.android.exoplayer.demo.player.Mp4RendererBuilder;
import com.google.android.exoplayer.demo.player.SmoothStreamingRendererBuilder;
import com.google.android.exoplayer.demo.player.UnsupportedDrmException;
import com.google.android.exoplayer.metadata.GeobMetadata;
import com.google.android.exoplayer.metadata.PrivMetadata;
import com.google.android.exoplayer.metadata.TxxxMetadata;
import com.google.android.exoplayer.text.CaptionStyleCompat;
import com.google.android.exoplayer.text.SubtitleView;
@ -213,6 +216,8 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback,
new WidevineTestMediaDrmCallback(contentId), debugTextView, audioCapabilities);
case DemoUtil.TYPE_HLS:
return new HlsRendererBuilder(userAgent, contentUri.toString());
case DemoUtil.TYPE_MP4:
return new Mp4RendererBuilder(contentUri, debugTextView);
default:
return new DefaultRendererBuilder(this, contentUri, debugTextView);
}
@ -446,11 +451,22 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback,
@Override
public void onId3Metadata(Map<String, Object> metadata) {
for (int i = 0; i < metadata.size(); i++) {
if (metadata.containsKey(TxxxMetadata.TYPE)) {
TxxxMetadata txxxMetadata = (TxxxMetadata) metadata.get(TxxxMetadata.TYPE);
Log.i(TAG, String.format("ID3 TimedMetadata: description=%s, value=%s",
txxxMetadata.description, txxxMetadata.value));
for (Map.Entry<String, Object> entry : metadata.entrySet()) {
if (TxxxMetadata.TYPE.equals(entry.getKey())) {
TxxxMetadata txxxMetadata = (TxxxMetadata) entry.getValue();
Log.i(TAG, String.format("ID3 TimedMetadata %s: description=%s, value=%s",
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()));
}
}
}

View file

@ -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"
+ "&expire=19000000000&signature=61611F115EEEC7BADE5536827343FFFE2D83D14F."
+ "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?"
+ "as=fmp4_audio_cenc,fmp4_sd_hd_cenc&sparams=ip,ipbits,expire,as&ip=0.0.0.0&ipbits=0"
+ "&expire=19000000000&signature=88DC53943385CED8CF9F37ADD9E9843E3BF621E6."
@ -123,6 +123,8 @@ import java.util.Locale;
new Sample("Apple AAC media playlist",
"https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/gear0/"
+ "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[] {
@ -133,6 +135,12 @@ import java.util.Locale;
new Sample("Apple AAC 10s", "https://devimages.apple.com.edgekey.net/"
+ "streaming/examples/bipbop_4x3/gear0/fileSequence0.aac",
DemoUtil.TYPE_OTHER),
new Sample("Big Buck Bunny (MP4)",
"http://redirector.c.youtube.com/videoplayback?id=604ed5ce52eda7ee&itag=22&source=youtube"
+ "&sparams=ip,ipbits,expire&ip=0.0.0.0&ipbits=0&expire=19000000000&signature="
+ "2E853B992F6CAB9D28CA3BEBD84A6F26709A8A55.94344B0D8BA83A7417AAD24DACC8C71A9A878ECE"
+ "&key=ik0",
DemoUtil.TYPE_MP4),
};
private Samples() {}

View file

@ -129,6 +129,8 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
void onLoadStarted(int sourceId, String formatId, int trigger, boolean isInitialization,
int mediaStartTimeMs, int mediaEndTimeMs, long length);
void onLoadCompleted(int sourceId, long bytesLoaded);
void onDecoderInitialized(String decoderName, long elapsedRealtimeMs,
long initializationDurationMs);
}
/**
@ -477,6 +479,16 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
}
}
@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) {
if (internalErrorListener != null) {

View file

@ -0,0 +1,69 @@
/*
* 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);
}
}

View file

@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
apply plugin: 'com.android.library'
apply plugin: 'bintray-release'
android {
compileSdkVersion 21
@ -47,3 +48,13 @@ android.libraryVariants.all { variant ->
task.from variant.javaCompile.destinationDir
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'
}

View file

@ -281,7 +281,7 @@ public final class Ac3PassthroughAudioTrackRenderer extends TrackRenderer {
protected void onDisabled() {
audioSessionId = AudioTrack.SESSION_ID_NOT_SET;
shouldReadInputBuffer = true;
audioTrack.reset();
audioTrack.release();
}
@Override

View file

@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer;
import android.media.MediaCodec;
import android.media.MediaExtractor;
/**
@ -43,11 +44,33 @@ public final class C {
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")
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() {}
}

View file

@ -141,14 +141,6 @@ public interface ExoPlayer {
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 {
* {@link ExoPlayer#getPlaybackState()} changes.
*
* @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);
/**
@ -256,7 +249,7 @@ public interface ExoPlayer {
/**
* 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();

View file

@ -202,7 +202,7 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer {
protected void onDisabled() {
audioSessionId = AudioTrack.SESSION_ID_NOT_SET;
try {
audioTrack.reset();
audioTrack.release();
} finally {
super.onDisabled();
}

View file

@ -16,6 +16,7 @@
package com.google.android.exoplayer;
import com.google.android.exoplayer.MediaCodecUtil.DecoderQueryException;
import com.google.android.exoplayer.drm.DrmInitData;
import com.google.android.exoplayer.drm.DrmSessionManager;
import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.Util;
@ -25,7 +26,6 @@ import android.media.MediaCodec;
import android.media.MediaCodec.CodecException;
import android.media.MediaCodec.CryptoException;
import android.media.MediaCrypto;
import android.media.MediaExtractor;
import android.os.Handler;
import android.os.SystemClock;
@ -33,8 +33,6 @@ import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
/**
* An abstract {@link TrackRenderer} that uses {@link MediaCodec} to decode samples for rendering.
@ -61,6 +59,17 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
*/
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 {
*/
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;
private final DrmSessionManager drmSessionManager;
@ -164,7 +190,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
protected final Handler eventHandler;
private MediaFormat format;
private Map<UUID, byte[]> drmInitData;
private DrmInitData drmInitData;
private MediaCodec codec;
private boolean codecIsAdaptive;
private ByteBuffer[] inputBuffers;
@ -175,6 +201,8 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
private boolean openedDrmSession;
private boolean codecReconfigured;
private int codecReconfigurationState;
private int codecReinitializationState;
private boolean codecHasQueuedBuffers;
private int trackIndex;
private int sourceState;
@ -210,6 +238,8 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
formatHolder = new MediaFormatHolder();
decodeOnlyPresentationTimestamps = new ArrayList<Long>();
outputBufferInfo = new MediaCodec.BufferInfo();
codecReconfigurationState = RECONFIGURATION_STATE_NONE;
codecReinitializationState = REINITIALIZATION_STATE_NONE;
}
@Override
@ -281,7 +311,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
throw new ExoPlaybackException("Media requires a DrmSessionManager");
}
if (!openedDrmSession) {
drmSessionManager.open(drmInitData, mimeType);
drmSessionManager.open(drmInitData);
openedDrmSession = true;
}
int drmSessionState = drmSessionManager.getState();
@ -313,9 +343,13 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
String decoderName = decoderInfo.name;
codecIsAdaptive = decoderInfo.adaptive;
try {
long codecInitializingTimestamp = SystemClock.elapsedRealtime();
codec = MediaCodec.createByCodecName(decoderName);
configureCodec(codec, format.getFrameworkMediaFormatV16(), mediaCrypto);
codec.start();
long codecInitializedTimestamp = SystemClock.elapsedRealtime();
notifyDecoderInitialized(decoderName, codecInitializedTimestamp,
codecInitializedTimestamp - codecInitializingTimestamp);
inputBuffers = codec.getInputBuffers();
outputBuffers = codec.getOutputBuffers();
} catch (Exception e) {
@ -370,12 +404,15 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
codecHotswapTimeMs = -1;
inputIndex = -1;
outputIndex = -1;
waitingForKeys = false;
decodeOnlyPresentationTimestamps.clear();
inputBuffers = null;
outputBuffers = null;
codecReconfigured = false;
codecHasQueuedBuffers = false;
codecIsAdaptive = false;
codecReconfigurationState = RECONFIGURATION_STATE_NONE;
codecReinitializationState = REINITIALIZATION_STATE_NONE;
codecCounters.codecReleaseCount++;
try {
codec.stop();
@ -418,7 +455,6 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
sourceState = SOURCE_STATE_NOT_READY;
inputStreamEnded = false;
outputStreamEnded = false;
waitingForKeys = false;
}
@Override
@ -478,11 +514,13 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
inputIndex = -1;
outputIndex = -1;
waitingForFirstSyncFrame = true;
waitingForKeys = false;
decodeOnlyPresentationTimestamps.clear();
// Workaround for framework bugs.
// 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();
codecHasQueuedBuffers = false;
} else {
releaseCodec();
maybeInitCodec();
@ -502,9 +540,13 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
* @throws ExoPlaybackException If an error occurs feeding the input buffer.
*/
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;
}
if (inputIndex < 0) {
inputIndex = codec.dequeueInputBuffer(0);
if (inputIndex < 0) {
@ -514,6 +556,15 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
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;
if (waitingForKeys) {
// We've already read an encrypted sample into sampleHolder, and are waiting for keys.
@ -572,7 +623,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
if (waitingForFirstSyncFrame) {
// TODO: Find out if it's possible to supply samples prior to the first sync
// frame for HE-AAC.
if ((sampleHolder.flags & C.SAMPLE_FLAG_SYNC) == 0) {
if (!sampleHolder.isSyncFrame()) {
sampleHolder.data.clear();
if (codecReconfigurationState == RECONFIGURATION_STATE_QUEUE_PENDING) {
// The buffer we just cleared contained reconfiguration data. We need to re-write this
@ -583,7 +634,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
}
waitingForFirstSyncFrame = false;
}
boolean sampleEncrypted = (sampleHolder.flags & MediaExtractor.SAMPLE_FLAG_ENCRYPTED) != 0;
boolean sampleEncrypted = sampleHolder.isEncrypted();
waitingForKeys = shouldWaitForKeys(sampleEncrypted);
if (waitingForKeys) {
return false;
@ -592,7 +643,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
int bufferSize = sampleHolder.data.position();
int adaptiveReconfigurationBytes = bufferSize - sampleHolder.size;
long presentationTimeUs = sampleHolder.timeUs;
if (sampleHolder.decodeOnly) {
if (sampleHolder.isDecodeOnly()) {
decodeOnlyPresentationTimestamps.add(presentationTimeUs);
}
if (sampleEncrypted) {
@ -603,6 +654,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
codec.queueInputBuffer(inputIndex, 0 , bufferSize, presentationTimeUs, 0);
}
inputIndex = -1;
codecHasQueuedBuffers = true;
codecReconfigurationState = RECONFIGURATION_STATE_NONE;
} catch (CryptoException e) {
notifyCryptoError(e);
@ -656,8 +708,14 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
codecReconfigured = true;
codecReconfigurationState = RECONFIGURATION_STATE_WRITE_PENDING;
} else {
releaseCodec();
maybeInitCodec();
if (codecHasQueuedBuffers) {
// 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 {
}
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;
}
@ -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) {
final int size = decodeOnlyPresentationTimestamps.size();
for (int i = 0; i < size; i++) {

View file

@ -15,6 +15,7 @@
*/
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.Util;
@ -179,6 +180,33 @@ public class MediaCodecUtil {
return capabilities.isFeatureSupported(CodecCapabilities.FEATURE_AdaptivePlayback);
}
/**
* 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 level An AVC profile level from {@link CodecProfileLevel}.

View file

@ -373,6 +373,13 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer {
: holder.format.pixelWidthHeightRatio;
}
/**
* @return True if the first frame has been rendered (playback has not necessarily begun).
*/
protected final boolean haveRenderedFirstFrame() {
return renderedFirstFrame;
}
@Override
protected void onOutputFormatChanged(android.media.MediaFormat format) {
boolean hasCrop = format.containsKey(KEY_CROP_RIGHT) && format.containsKey(KEY_CROP_LEFT)
@ -427,7 +434,6 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer {
if (!renderedFirstFrame) {
renderOutputBufferImmediate(codec, bufferIndex);
renderedFirstFrame = true;
return true;
}
@ -463,14 +469,14 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer {
return false;
}
private void skipOutputBuffer(MediaCodec codec, int bufferIndex) {
protected void skipOutputBuffer(MediaCodec codec, int bufferIndex) {
TraceUtil.beginSection("skipVideoBuffer");
codec.releaseOutputBuffer(bufferIndex, false);
TraceUtil.endSection();
codecCounters.skippedOutputBufferCount++;
}
private void dropOutputBuffer(MediaCodec codec, int bufferIndex) {
protected void dropOutputBuffer(MediaCodec codec, int bufferIndex) {
TraceUtil.beginSection("dropVideoBuffer");
codec.releaseOutputBuffer(bufferIndex, false);
TraceUtil.endSection();
@ -481,22 +487,24 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer {
}
}
private void renderOutputBufferImmediate(MediaCodec codec, int bufferIndex) {
protected void renderOutputBufferImmediate(MediaCodec codec, int bufferIndex) {
maybeNotifyVideoSizeChanged();
TraceUtil.beginSection("renderVideoBufferImmediate");
codec.releaseOutputBuffer(bufferIndex, true);
TraceUtil.endSection();
codecCounters.renderedOutputBufferCount++;
renderedFirstFrame = true;
maybeNotifyDrawnToSurface();
}
@TargetApi(21)
private void renderOutputBufferTimedV21(MediaCodec codec, int bufferIndex, long releaseTimeNs) {
protected void renderOutputBufferTimedV21(MediaCodec codec, int bufferIndex, long releaseTimeNs) {
maybeNotifyVideoSizeChanged();
TraceUtil.beginSection("releaseOutputBufferTimed");
codec.releaseOutputBuffer(bufferIndex, releaseTimeNs);
TraceUtil.endSection();
codecCounters.renderedOutputBufferCount++;
renderedFirstFrame = true;
maybeNotifyDrawnToSurface();
}

View file

@ -40,6 +40,8 @@ public class MediaFormat {
public final String mimeType;
public final int maxInputSize;
public final long durationUs;
public final int width;
public final int height;
public final float pixelWidthHeightRatio;
@ -49,11 +51,11 @@ public class MediaFormat {
public final int bitrate;
public final List<byte[]> initializationData;
private int maxWidth;
private int maxHeight;
public final List<byte[]> initializationData;
// Lazy-initialized hashcode.
private int hashCode;
// Possibly-lazy-initialized framework media format.
@ -66,25 +68,38 @@ public class MediaFormat {
public static MediaFormat createVideoFormat(String mimeType, int maxInputSize, int width,
int height, List<byte[]> initializationData) {
return createVideoFormat(mimeType, maxInputSize, width, height, 1, initializationData);
return createVideoFormat(
mimeType, maxInputSize, C.UNKNOWN_TIME_US, width, height, initializationData);
}
public static MediaFormat createVideoFormat(String mimeType, int maxInputSize, int width,
int height, float pixelWidthHeightRatio, List<byte[]> initializationData) {
return new MediaFormat(mimeType, maxInputSize, width, height, pixelWidthHeightRatio, NO_VALUE,
NO_VALUE, NO_VALUE, initializationData);
public static MediaFormat createVideoFormat(String mimeType, int maxInputSize, long durationUs,
int width, int height, List<byte[]> initializationData) {
return createVideoFormat(
mimeType, maxInputSize, durationUs, width, height, 1, initializationData);
}
public static MediaFormat createVideoFormat(String mimeType, int maxInputSize, long durationUs,
int width, int height, float pixelWidthHeightRatio, List<byte[]> initializationData) {
return new MediaFormat(mimeType, maxInputSize, durationUs, width, height, pixelWidthHeightRatio,
NO_VALUE, NO_VALUE, NO_VALUE, initializationData);
}
public static MediaFormat createAudioFormat(String mimeType, int maxInputSize, int channelCount,
int sampleRate, List<byte[]> initializationData) {
return new MediaFormat(mimeType, maxInputSize, NO_VALUE, NO_VALUE, NO_VALUE, channelCount,
sampleRate, NO_VALUE, initializationData);
return createAudioFormat(
mimeType, maxInputSize, C.UNKNOWN_TIME_US, channelCount, sampleRate, initializationData);
}
public static MediaFormat createAudioFormat(String mimeType, int maxInputSize, int channelCount,
int sampleRate, int bitrate, List<byte[]> initializationData) {
return new MediaFormat(mimeType, maxInputSize, NO_VALUE, NO_VALUE, NO_VALUE, channelCount,
sampleRate, bitrate, initializationData);
public static MediaFormat createAudioFormat(String mimeType, int maxInputSize, long durationUs,
int channelCount, int sampleRate, List<byte[]> initializationData) {
return createAudioFormat(
mimeType, maxInputSize, durationUs, channelCount, sampleRate, NO_VALUE, initializationData);
}
public static MediaFormat createAudioFormat(String mimeType, int maxInputSize, long durationUs,
int channelCount, int sampleRate, int bitrate, List<byte[]> initializationData) {
return new MediaFormat(mimeType, maxInputSize, durationUs, NO_VALUE, NO_VALUE, NO_VALUE,
channelCount, sampleRate, bitrate, initializationData);
}
public static MediaFormat createId3Format() {
@ -100,8 +115,8 @@ public class MediaFormat {
}
public static MediaFormat createFormatForMimeType(String mimeType) {
return new MediaFormat(mimeType, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE,
NO_VALUE, null);
return new MediaFormat(mimeType, NO_VALUE, C.UNKNOWN_TIME_US, NO_VALUE, NO_VALUE, NO_VALUE,
NO_VALUE, NO_VALUE, NO_VALUE, null);
}
@TargetApi(16)
@ -123,15 +138,18 @@ public class MediaFormat {
initializationData.add(data);
buffer.flip();
}
durationUs = format.containsKey(android.media.MediaFormat.KEY_DURATION)
? format.getLong(android.media.MediaFormat.KEY_DURATION) : C.UNKNOWN_TIME_US;
maxWidth = NO_VALUE;
maxHeight = NO_VALUE;
}
private MediaFormat(String mimeType, int maxInputSize, int width, int height,
private MediaFormat(String mimeType, int maxInputSize, long durationUs, int width, int height,
float pixelWidthHeightRatio, int channelCount, int sampleRate, int bitrate,
List<byte[]> initializationData) {
this.mimeType = mimeType;
this.maxInputSize = maxInputSize;
this.durationUs = durationUs;
this.width = width;
this.height = height;
this.pixelWidthHeightRatio = pixelWidthHeightRatio;
@ -169,6 +187,7 @@ public class MediaFormat {
result = 31 * result + width;
result = 31 * result + height;
result = 31 * result + Float.floatToRawIntBits(pixelWidthHeightRatio);
result = 31 * result + (int) durationUs;
result = 31 * result + maxWidth;
result = 31 * result + maxHeight;
result = 31 * result + channelCount;
@ -225,7 +244,7 @@ public class MediaFormat {
public String toString() {
return "MediaFormat(" + mimeType + ", " + maxInputSize + ", " + width + ", " + height + ", "
+ pixelWidthHeightRatio + ", " + channelCount + ", " + sampleRate + ", " + bitrate + ", "
+ maxWidth + ", " + maxHeight + ")";
+ durationUs + ", " + maxWidth + ", " + maxHeight + ")";
}
/**
@ -246,6 +265,9 @@ public class MediaFormat {
for (int i = 0; i < initializationData.size(); i++) {
format.setByteBuffer("csd-" + i, ByteBuffer.wrap(initializationData.get(i)));
}
if (durationUs != C.UNKNOWN_TIME_US) {
format.setLong(android.media.MediaFormat.KEY_DURATION, durationUs);
}
maybeSetMaxDimensionsV16(format);
frameworkMediaFormat = format;
}

View file

@ -15,8 +15,7 @@
*/
package com.google.android.exoplayer;
import java.util.Map;
import java.util.UUID;
import com.google.android.exoplayer.drm.DrmInitData;
/**
* Holds a {@link MediaFormat} and corresponding drm scheme initialization data.
@ -28,9 +27,8 @@ public final class MediaFormatHolder {
*/
public MediaFormat format;
/**
* Initialization data for each of the drm schemes supported by the media, keyed by scheme UUID.
* Null if the media is not encrypted.
* Initialization data for drm schemes supported by the media. Null if the media is not encrypted.
*/
public Map<UUID, byte[]> drmInitData;
public DrmInitData drmInitData;
}

View file

@ -50,9 +50,8 @@ public final class SampleHolder {
public int size;
/**
* Flags that accompany the sample. A combination of
* {@link android.media.MediaExtractor#SAMPLE_FLAG_SYNC} and
* {@link android.media.MediaExtractor#SAMPLE_FLAG_ENCRYPTED}
* Flags that accompany the sample. A combination of {@link C#SAMPLE_FLAG_SYNC},
* {@link C#SAMPLE_FLAG_ENCRYPTED} and {@link C#SAMPLE_FLAG_DECODE_ONLY}.
*/
public int flags;
@ -61,11 +60,6 @@ public final class SampleHolder {
*/
public long timeUs;
/**
* If true then the sample should be decoded, but should not be presented.
*/
public boolean decodeOnly;
private final int bufferReplacementMode;
/**
@ -96,6 +90,27 @@ public final class SampleHolder {
return false;
}
/**
* 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.
*/

View file

@ -44,6 +44,8 @@ import java.nio.ByteBuffer;
* <p>Call {@link #reconfigure} when the output format changes.
*
* <p>Call {@link #reset} to free resources. It is safe to re-{@link #initialize} the instance.
*
* <p>Call {@link #release} when the instance will no longer be used.
*/
@TargetApi(16)
public final class AudioTrack {
@ -91,6 +93,12 @@ public final class AudioTrack {
/** Returned by {@link #getCurrentPositionUs} when the position is not set. */
public static final long CURRENT_POSITION_NOT_SET = Long.MIN_VALUE;
/**
* Set to {@code true} to enable a workaround for an issue where an audio effect does not keep its
* session active across releasing/initializing a new audio track, on platform API version < 21.
*/
private static final boolean ENABLE_PRE_V21_AUDIO_SESSION_WORKAROUND = false;
/** A minimum length for the {@link android.media.AudioTrack} buffer, in microseconds. */
private static final long MIN_BUFFER_DURATION_US = 250000;
/** A maximum length for the {@link android.media.AudioTrack} buffer, in microseconds. */
@ -132,6 +140,9 @@ public final class AudioTrack {
private final ConditionVariable releasingConditionVariable;
private final long[] playheadOffsets;
/** Used to keep the audio session active on pre-V21 builds (see {@link #initialize()}). */
private android.media.AudioTrack keepSessionIdAudioTrack;
private android.media.AudioTrack audioTrack;
private AudioTrackUtil audioTrackUtil;
private int sampleRate;
@ -267,15 +278,37 @@ public final class AudioTrack {
audioTrack = new android.media.AudioTrack(AudioManager.STREAM_MUSIC, sampleRate,
channelConfig, encoding, bufferSize, android.media.AudioTrack.MODE_STREAM, sessionId);
}
checkAudioTrackInitialized();
sessionId = audioTrack.getAudioSessionId();
if (ENABLE_PRE_V21_AUDIO_SESSION_WORKAROUND) {
if (Util.SDK_INT < 21) {
// The workaround creates an audio track with a one byte buffer on the same session, and
// does not release it until this object is released, which keeps the session active.
if (keepSessionIdAudioTrack != null
&& sessionId != keepSessionIdAudioTrack.getAudioSessionId()) {
releaseKeepSessionIdAudioTrack();
}
if (keepSessionIdAudioTrack == null) {
int sampleRate = 4000; // Equal to private android.media.AudioTrack.MIN_SAMPLE_RATE.
int channelConfig = AudioFormat.CHANNEL_OUT_MONO;
int encoding = AudioFormat.ENCODING_PCM_8BIT;
int bufferSize = 1; // Use a one byte buffer, as it is not actually used for playback.
keepSessionIdAudioTrack = new android.media.AudioTrack(AudioManager.STREAM_MUSIC,
sampleRate, channelConfig, encoding, bufferSize, android.media.AudioTrack.MODE_STATIC,
sessionId);
}
}
}
if (Util.SDK_INT >= 19) {
audioTrackUtil = new AudioTrackUtilV19(audioTrack);
} else {
audioTrackUtil = new AudioTrackUtil(audioTrack);
}
setVolume(volume);
return audioTrack.getAudioSessionId();
return sessionId;
}
/**
@ -515,9 +548,9 @@ public final class AudioTrack {
}
/**
* Releases resources associated with this instance asynchronously. Calling {@link #initialize}
* will block until the audio track has been released, so it is safe to initialize immediately
* after resetting.
* Releases the underlying audio track asynchronously. Calling {@link #initialize} will block
* until the audio track has been released, so it is safe to initialize immediately after
* resetting. The audio session may remain active until the instance is {@link #release}d.
*/
public void reset() {
if (isInitialized()) {
@ -547,6 +580,29 @@ public final class AudioTrack {
}
}
/** Releases all resources associated with this instance. */
public void release() {
reset();
releaseKeepSessionIdAudioTrack();
}
/** Releases {@link #keepSessionIdAudioTrack} asynchronously, if it is non-{@code null}. */
private void releaseKeepSessionIdAudioTrack() {
if (keepSessionIdAudioTrack == null) {
return;
}
// AudioTrack.release can take some time, so we call it on a background thread.
final android.media.AudioTrack toRelease = keepSessionIdAudioTrack;
keepSessionIdAudioTrack = null;
new Thread() {
@Override
public void run() {
toRelease.release();
}
}.start();
}
/** Returns whether {@link #getCurrentPositionUs} can return the current playback position. */
private boolean hasCurrentPositionUs() {
return isInitialized() && startMediaTimeUs != START_NOT_SET;

View file

@ -352,13 +352,14 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback {
if (mediaFormat != null && !mediaFormat.equals(downstreamMediaFormat, true)) {
chunkSource.getMaxVideoDimensions(mediaFormat);
formatHolder.format = mediaFormat;
formatHolder.drmInitData = mediaChunk.getPsshInfo();
formatHolder.drmInitData = mediaChunk.getDrmInitData();
downstreamMediaFormat = mediaFormat;
return FORMAT_READ;
}
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);
return SAMPLE_READ;
} else {

View file

@ -19,14 +19,12 @@ import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.chunk.parser.Extractor;
import com.google.android.exoplayer.drm.DrmInitData;
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DataSpec;
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
import com.google.android.exoplayer.util.Assertions;
import java.util.Map;
import java.util.UUID;
/**
* A {@link MediaChunk} extracted from a container.
*/
@ -38,7 +36,7 @@ public final class ContainerMediaChunk extends MediaChunk {
private boolean prepared;
private MediaFormat mediaFormat;
private Map<UUID, byte[]> psshInfo;
private DrmInitData drmInitData;
/**
* @deprecated Use the other constructor, passing null as {@code psshInfo}.
@ -60,8 +58,9 @@ public final class ContainerMediaChunk extends MediaChunk {
* @param endTimeUs The end time of the media contained by the chunk, in microseconds.
* @param nextChunkIndex The index of the next chunk, or -1 if this is the last chunk.
* @param extractor The extractor that will be used to extract the samples.
* @param psshInfo Pssh data. May be null if pssh data is present within the stream, meaning it
* can be obtained directly from {@code extractor}, or if no pssh data is required.
* @param drmInitData DRM initialization data. May be null if DRM initialization data is present
* within the stream, meaning it can be obtained directly from {@code extractor}, or if no
* DRM initialization data is required.
* @param maybeSelfContained Set to true if this chunk might be self contained, meaning it might
* contain a moov atom defining the media format of the chunk. This parameter can always be
* safely set to true. Setting to false where the chunk is known to not be self contained may
@ -70,12 +69,12 @@ public final class ContainerMediaChunk extends MediaChunk {
*/
public ContainerMediaChunk(DataSource dataSource, DataSpec dataSpec, Format format,
int trigger, long startTimeUs, long endTimeUs, int nextChunkIndex, Extractor extractor,
Map<UUID, byte[]> psshInfo, boolean maybeSelfContained, long sampleOffsetUs) {
DrmInitData drmInitData, boolean maybeSelfContained, long sampleOffsetUs) {
super(dataSource, dataSpec, format, trigger, startTimeUs, endTimeUs, nextChunkIndex);
this.extractor = extractor;
this.maybeSelfContained = maybeSelfContained;
this.sampleOffsetUs = sampleOffsetUs;
this.psshInfo = psshInfo;
this.drmInitData = drmInitData;
}
@Override
@ -111,9 +110,9 @@ public final class ContainerMediaChunk extends MediaChunk {
}
if (prepared) {
mediaFormat = extractor.getFormat();
Map<UUID, byte[]> extractorPsshInfo = extractor.getPsshInfo();
if (extractorPsshInfo != null) {
psshInfo = extractorPsshInfo;
DrmInitData extractorDrmInitData = extractor.getDrmInitData();
if (extractorDrmInitData != null) {
drmInitData = extractorDrmInitData;
}
}
}
@ -145,8 +144,8 @@ public final class ContainerMediaChunk extends MediaChunk {
}
@Override
public Map<UUID, byte[]> getPsshInfo() {
return psshInfo;
public DrmInitData getDrmInitData() {
return drmInitData;
}
}

View file

@ -84,14 +84,6 @@ public class Format {
*/
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 mimeType The format mime type.
@ -144,7 +136,6 @@ public class Format {
this.bitrate = bitrate;
this.language = language;
this.codecs = codecs;
this.bandwidth = bitrate / 8;
}
@Override

View file

@ -18,12 +18,10 @@ package com.google.android.exoplayer.chunk;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.drm.DrmInitData;
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DataSpec;
import java.util.Map;
import java.util.UUID;
/**
* An abstract base class for {@link Chunk}s that contain media samples.
*/
@ -129,12 +127,12 @@ public abstract class MediaChunk extends Chunk {
public abstract MediaFormat getMediaFormat();
/**
* Returns the pssh information associated with the chunk.
* Returns the DRM initialization data associated with the chunk.
* <p>
* Should only be called after the chunk has been successfully prepared.
*
* @return The pssh information.
* @return The DRM initialization data.
*/
public abstract Map<UUID, byte[]> getPsshInfo();
public abstract DrmInitData getDrmInitData();
}

View file

@ -17,14 +17,12 @@ package com.google.android.exoplayer.chunk;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.drm.DrmInitData;
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DataSpec;
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
import com.google.android.exoplayer.util.Assertions;
import java.util.Map;
import java.util.UUID;
/**
* A {@link MediaChunk} containing a single sample.
*/
@ -132,7 +130,7 @@ public class SingleSampleMediaChunk extends MediaChunk {
}
@Override
public Map<UUID, byte[]> getPsshInfo() {
public DrmInitData getDrmInitData() {
return null;
}

View file

@ -15,15 +15,12 @@
*/
package com.google.android.exoplayer.chunk.parser;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.drm.DrmInitData;
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
import java.util.Map;
import java.util.UUID;
/**
* Facilitates extraction of media samples from a container format.
*/
@ -43,7 +40,7 @@ public interface Extractor {
public static final int RESULT_READ_SAMPLE = 4;
/**
* Initialization data was read. The parsed data can be read using {@link #getFormat()} and
* {@link #getPsshInfo}.
* {@link #getDrmInitData()}.
*/
public static final int RESULT_READ_INIT = 8;
/**
@ -80,17 +77,12 @@ public interface Extractor {
public MediaFormat getFormat();
/**
* Returns the duration of the stream in microseconds, or {@link C#UNKNOWN_TIME_US} if unknown.
*/
public long getDurationUs();
/**
* Returns the pssh information parsed from the stream.
* Returns DRM initialization data parsed from the stream.
*
* @return The pssh information. May be null if pssh data has yet to be parsed, or if the stream
* does not contain any pssh data.
* @return The DRM initialization data. May be null if the initialization data has yet to be
* parsed, or if the stream does not contain any DRM initialization data.
*/
public Map<UUID, byte[]> getPsshInfo();
public DrmInitData getDrmInitData();
/**
* Consumes data from a {@link NonBlockingInputStream}.

View file

@ -21,27 +21,23 @@ import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.chunk.parser.Extractor;
import com.google.android.exoplayer.chunk.parser.SegmentIndex;
import com.google.android.exoplayer.drm.DrmInitData;
import com.google.android.exoplayer.mp4.Atom;
import com.google.android.exoplayer.mp4.Atom.ContainerAtom;
import com.google.android.exoplayer.mp4.Atom.LeafAtom;
import com.google.android.exoplayer.mp4.CommonMp4AtomParsers;
import com.google.android.exoplayer.mp4.Mp4Util;
import com.google.android.exoplayer.mp4.Track;
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
import com.google.android.exoplayer.util.H264Util;
import com.google.android.exoplayer.util.MimeTypes;
import com.google.android.exoplayer.util.ParsableByteArray;
import com.google.android.exoplayer.util.Util;
import android.annotation.SuppressLint;
import android.media.MediaCodec;
import android.media.MediaExtractor;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Stack;
import java.util.UUID;
@ -145,7 +141,7 @@ public final class FragmentedMp4Extractor implements Extractor {
private int lastSyncSampleIndex;
// Data parsed from moov and sidx atoms
private final HashMap<UUID, byte[]> psshData;
private DrmInitData.Mapped drmInitData;
private SegmentIndex segmentIndex;
private Track track;
private DefaultSampleValues extendsDefaults;
@ -161,11 +157,10 @@ public final class FragmentedMp4Extractor implements Extractor {
public FragmentedMp4Extractor(int workaroundFlags) {
this.workaroundFlags = workaroundFlags;
parserState = STATE_READING_ATOM_HEADER;
atomHeader = new ParsableByteArray(Mp4Util.ATOM_HEADER_SIZE);
atomHeader = new ParsableByteArray(Atom.ATOM_HEADER_SIZE);
extendedTypeScratch = new byte[16];
containerAtoms = new Stack<ContainerAtom>();
fragmentRun = new TrackFragment();
psshData = new HashMap<UUID, byte[]>();
}
/**
@ -179,8 +174,8 @@ public final class FragmentedMp4Extractor implements Extractor {
}
@Override
public Map<UUID, byte[]> getPsshInfo() {
return psshData.isEmpty() ? null : psshData;
public DrmInitData getDrmInitData() {
return drmInitData;
}
@Override
@ -198,11 +193,6 @@ public final class FragmentedMp4Extractor implements Extractor {
return track == null ? null : track.mediaFormat;
}
@Override
public long getDurationUs() {
return track == null ? C.UNKNOWN_TIME_US : track.durationUs;
}
@Override
public int read(NonBlockingInputStream inputStream, SampleHolder out)
throws ParserException {
@ -269,14 +259,14 @@ public final class FragmentedMp4Extractor implements Extractor {
}
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);
if (bytesRead == -1) {
return RESULT_END_OF_STREAM;
}
rootAtomBytesRead += bytesRead;
atomBytesRead += bytesRead;
if (atomBytesRead != Mp4Util.ATOM_HEADER_SIZE) {
if (atomBytesRead != Atom.ATOM_HEADER_SIZE) {
return RESULT_NEED_MORE_DATA;
}
@ -298,10 +288,10 @@ public final class FragmentedMp4Extractor implements Extractor {
if (CONTAINER_TYPES.contains(atomTypeInteger)) {
enterState(STATE_READING_ATOM_HEADER);
containerAtoms.add(new ContainerAtom(atomType,
rootAtomBytesRead + atomSize - Mp4Util.ATOM_HEADER_SIZE));
rootAtomBytesRead + atomSize - Atom.ATOM_HEADER_SIZE));
} else {
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);
}
} else {
@ -370,12 +360,15 @@ public final class FragmentedMp4Extractor implements Extractor {
LeafAtom child = moovChildren.get(i);
if (child.type == Atom.TYPE_pssh) {
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());
int dataSize = psshAtom.readInt();
byte[] data = new byte[dataSize];
psshAtom.readBytes(data, 0, dataSize);
psshData.put(uuid, data);
if (drmInitData == null) {
drmInitData = new DrmInitData.Mapped(MimeTypes.VIDEO_MP4);
}
drmInitData.put(uuid, data);
}
}
ContainerAtom mvex = moov.getContainerAtomOfType(Atom.TYPE_mvex);
@ -406,7 +399,7 @@ public final class FragmentedMp4Extractor implements Extractor {
* Parses a trex atom (defined in 14496-12).
*/
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 defaultSampleDuration = trex.readUnsignedIntToInt();
int defaultSampleSize = trex.readUnsignedIntToInt();
@ -460,9 +453,9 @@ public final class FragmentedMp4Extractor implements Extractor {
private static void parseSaiz(TrackEncryptionBox encryptionBox, ParsableByteArray saiz,
TrackFragment out) {
int vectorSize = encryptionBox.initializationVectorSize;
saiz.setPosition(Mp4Util.ATOM_HEADER_SIZE);
saiz.setPosition(Atom.ATOM_HEADER_SIZE);
int fullAtom = saiz.readInt();
int flags = Mp4Util.parseFullAtomFlags(fullAtom);
int flags = Atom.parseFullAtomFlags(fullAtom);
if ((flags & 0x01) == 1) {
saiz.skip(8);
}
@ -497,9 +490,9 @@ public final class FragmentedMp4Extractor implements Extractor {
*/
private static DefaultSampleValues parseTfhd(DefaultSampleValues extendsDefaults,
ParsableByteArray tfhd) {
tfhd.setPosition(Mp4Util.ATOM_HEADER_SIZE);
tfhd.setPosition(Atom.ATOM_HEADER_SIZE);
int fullAtom = tfhd.readInt();
int flags = Mp4Util.parseFullAtomFlags(fullAtom);
int flags = Atom.parseFullAtomFlags(fullAtom);
tfhd.skip(4); // trackId
if ((flags & 0x01 /* base_data_offset_present */) != 0) {
@ -526,9 +519,9 @@ public final class FragmentedMp4Extractor implements Extractor {
* media, expressed in the media's timescale.
*/
private static long parseTfdt(ParsableByteArray tfdt) {
tfdt.setPosition(Mp4Util.ATOM_HEADER_SIZE);
tfdt.setPosition(Atom.ATOM_HEADER_SIZE);
int fullAtom = tfdt.readInt();
int version = Mp4Util.parseFullAtomVersion(fullAtom);
int version = Atom.parseFullAtomVersion(fullAtom);
return version == 1 ? tfdt.readUnsignedLongToLong() : tfdt.readUnsignedInt();
}
@ -543,9 +536,9 @@ public final class FragmentedMp4Extractor implements Extractor {
*/
private static void parseTrun(Track track, DefaultSampleValues defaultSampleValues,
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 flags = Mp4Util.parseFullAtomFlags(fullAtom);
int flags = Atom.parseFullAtomFlags(fullAtom);
int sampleCount = trun.readUnsignedIntToInt();
if ((flags & 0x01 /* data_offset_present */) != 0) {
@ -603,7 +596,7 @@ public final class FragmentedMp4Extractor implements Extractor {
private static void parseUuid(ParsableByteArray uuid, TrackFragment out,
byte[] extendedTypeScratch) {
uuid.setPosition(Mp4Util.ATOM_HEADER_SIZE);
uuid.setPosition(Atom.ATOM_HEADER_SIZE);
uuid.readBytes(extendedTypeScratch, 0, 16);
// Currently this parser only supports Microsoft's PIFF SampleEncryptionBox.
@ -622,9 +615,9 @@ public final class FragmentedMp4Extractor implements Extractor {
}
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 flags = Mp4Util.parseFullAtomFlags(fullAtom);
int flags = Atom.parseFullAtomFlags(fullAtom);
if ((flags & 0x01 /* override_track_encryption_box_parameters */) != 0) {
// TODO: Implement this.
@ -646,9 +639,9 @@ public final class FragmentedMp4Extractor implements Extractor {
* Parses a sidx atom (defined in 14496-12).
*/
private static SegmentIndex parseSidx(ParsableByteArray atom) {
atom.setPosition(Mp4Util.ATOM_HEADER_SIZE);
atom.setPosition(Atom.ATOM_HEADER_SIZE);
int fullAtom = atom.readInt();
int version = Mp4Util.parseFullAtomVersion(fullAtom);
int version = Atom.parseFullAtomVersion(fullAtom);
atom.skip(4);
long timescale = atom.readUnsignedInt();
@ -788,7 +781,7 @@ public final class FragmentedMp4Extractor implements Extractor {
if (track.type == Track.TYPE_VIDEO) {
// The mp4 file contains length-prefixed NAL units, but the decoder wants start code
// delimited content.
Mp4Util.replaceLengthPrefixesWithAvcStartCodes(outputData, sampleSize);
H264Util.replaceLengthPrefixesWithAvcStartCodes(outputData, sampleSize);
}
out.size = sampleSize;
}
@ -798,12 +791,14 @@ public final class FragmentedMp4Extractor implements Extractor {
return RESULT_READ_SAMPLE;
}
@SuppressLint("InlinedApi")
private void readSampleEncryptionData(ParsableByteArray sampleEncryptionData, SampleHolder out) {
TrackEncryptionBox encryptionBox =
track.sampleDescriptionEncryptionBoxes[fragmentRun.sampleDescriptionIndex];
if (!encryptionBox.isEncrypted) {
return;
}
byte[] keyId = encryptionBox.keyId;
boolean isEncrypted = encryptionBox.isEncrypted;
int vectorSize = encryptionBox.initializationVectorSize;
boolean subsampleEncryption = fragmentRun.sampleHasSubsampleEncryptionTable[sampleIndex];
@ -831,11 +826,10 @@ public final class FragmentedMp4Extractor implements Extractor {
clearDataSizes[0] = 0;
encryptedDataSizes[0] = fragmentRun.sampleSizeTable[sampleIndex];
}
out.cryptoInfo.set(subsampleCount, clearDataSizes, encryptedDataSizes, keyId, vector,
isEncrypted ? MediaCodec.CRYPTO_MODE_AES_CTR : MediaCodec.CRYPTO_MODE_UNENCRYPTED);
if (isEncrypted) {
out.flags |= MediaExtractor.SAMPLE_FLAG_ENCRYPTED;
}
C.CRYPTO_MODE_AES_CTR);
out.flags |= C.SAMPLE_FLAG_ENCRYPTED;
}
}

View file

@ -21,6 +21,7 @@ import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.chunk.parser.Extractor;
import com.google.android.exoplayer.chunk.parser.SegmentIndex;
import com.google.android.exoplayer.drm.DrmInitData;
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
import com.google.android.exoplayer.util.LongArray;
import com.google.android.exoplayer.util.MimeTypes;
@ -28,8 +29,6 @@ import com.google.android.exoplayer.util.MimeTypes;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Map;
import java.util.UUID;
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
* 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>.
* 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 {
@ -47,6 +48,7 @@ public final class WebmExtractor implements Extractor {
private static final String CODEC_ID_OPUS = "A_OPUS";
private static final int VORBIS_MAX_INPUT_SIZE = 8192;
private static final int OPUS_MAX_INPUT_SIZE = 5760;
private static final int BLOCK_COUNTER_SIZE = 16;
private static final int UNKNOWN = -1;
// Element IDs
@ -80,23 +82,31 @@ public final class WebmExtractor implements Extractor {
private static final int ID_CHANNELS = 0x9F;
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_CUE_POINT = 0xBB;
private static final int ID_CUE_TIME = 0xB3;
private static final int ID_CUE_TRACK_POSITIONS = 0xB7;
private static final int ID_CUE_CLUSTER_POSITION = 0xF1;
// SimpleBlock Lacing Values
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
| RESULT_READ_SAMPLE | RESULT_NEED_SAMPLE_HOLDER;
private final EbmlReader reader;
private final byte[] simpleBlockTimecodeAndFlags = new byte[3];
private DrmInitData.Universal drmInitData;
private SampleHolder sampleHolder;
private int readResults;
@ -104,7 +114,7 @@ public final class WebmExtractor implements Extractor {
private long segmentStartOffsetBytes = UNKNOWN;
private long segmentEndOffsetBytes = UNKNOWN;
private long timecodeScale = 1000000L;
private long durationUs = UNKNOWN;
private long durationUs = C.UNKNOWN_TIME_US;
private int pixelWidth = UNKNOWN;
private int pixelHeight = UNKNOWN;
private int channelCount = UNKNOWN;
@ -113,7 +123,9 @@ public final class WebmExtractor implements Extractor {
private String codecId;
private long codecDelayNs;
private long seekPreRollNs;
private boolean seenAudioTrack;
private boolean isAudioTrack;
private boolean hasContentEncryption;
private byte[] encryptionKeyId;
private long cuesSizeBytes = UNKNOWN;
private long clusterTimecodeUs = UNKNOWN;
private long simpleBlockTimecodeUs = UNKNOWN;
@ -182,14 +194,8 @@ public final class WebmExtractor implements Extractor {
}
@Override
public long getDurationUs() {
return durationUs == UNKNOWN ? C.UNKNOWN_TIME_US : durationUs;
}
@Override
public Map<UUID, byte[]> getPsshInfo() {
// TODO: Parse pssh data from Webm streams.
return null;
public DrmInitData getDrmInitData() {
return drmInitData;
}
/* package */ int getElementType(int id) {
@ -202,6 +208,10 @@ public final class WebmExtractor implements Extractor {
case ID_TRACK_ENTRY:
case ID_AUDIO:
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_CUE_POINT:
case ID_CUE_TRACK_POSITIONS:
@ -216,12 +226,18 @@ public final class WebmExtractor implements Extractor {
case ID_CODEC_DELAY:
case ID_SEEK_PRE_ROLL:
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_CLUSTER_POSITION:
return EbmlReader.TYPE_UNSIGNED_INT;
case ID_DOC_TYPE:
case ID_CODEC_ID:
return EbmlReader.TYPE_STRING;
case ID_CONTENT_ENCRYPTION_KEY_ID:
case ID_SIMPLE_BLOCK:
case ID_BLOCK:
case ID_CODEC_PRIVATE:
@ -250,6 +266,12 @@ public final class WebmExtractor implements Extractor {
cueTimesUs = new LongArray();
cueClusterPositions = new LongArray();
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:
// pass
}
@ -261,17 +283,24 @@ public final class WebmExtractor implements Extractor {
case ID_CUES:
buildCues();
return false;
case ID_VIDEO:
buildVideoFormat();
case ID_CONTENT_ENCODING:
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;
case ID_AUDIO:
seenAudioTrack = true;
isAudioTrack = true;
return true;
case ID_TRACK_ENTRY:
if (seenAudioTrack) {
// Audio format has to be built here since codec private may not be available at the end
// of ID_AUDIO.
if (isAudioTrack) {
buildAudioFormat();
} else {
buildVideoFormat();
}
return true;
default:
@ -311,6 +340,37 @@ public final class WebmExtractor implements Extractor {
case ID_CHANNELS:
channelCount = (int) value;
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:
cueTimesUs.add(scaleTimecodeToUs(value));
break;
@ -402,22 +462,49 @@ public final class WebmExtractor implements Extractor {
}
boolean invisible = (simpleBlockTimecodeAndFlags[2] & 0x08) == 0x08;
int lacing = (simpleBlockTimecodeAndFlags[2] & 0x06) >> 1;
if (lacing != LACING_NONE) {
throw new ParserException("Lacing mode " + lacing + " not supported");
}
long elementEndOffsetBytes = elementOffsetBytes + headerSizeBytes + contentsSizeBytes;
simpleBlockTimecodeUs = clusterTimecodeUs + timecodeUs;
sampleHolder.flags = (keyframe ? C.SAMPLE_FLAG_SYNC : 0)
| (invisible ? C.SAMPLE_FLAG_DECODE_ONLY : 0);
sampleHolder.timeUs = clusterTimecodeUs + timecodeUs;
sampleHolder.size = (int) (elementEndOffsetBytes - reader.getBytesRead());
// Validate lacing and set info into sample holder.
switch (lacing) {
case LACING_NONE:
long elementEndOffsetBytes = elementOffsetBytes + headerSizeBytes + contentsSizeBytes;
simpleBlockTimecodeUs = clusterTimecodeUs + timecodeUs;
sampleHolder.flags = keyframe ? C.SAMPLE_FLAG_SYNC : 0;
sampleHolder.decodeOnly = invisible;
sampleHolder.timeUs = clusterTimecodeUs + timecodeUs;
sampleHolder.size = (int) (elementEndOffsetBytes - reader.getBytesRead());
break;
case LACING_EBML:
case LACING_FIXED:
case LACING_XIPH:
default:
throw new ParserException("Lacing mode " + lacing + " not supported");
if (hasContentEncryption) {
byte[] signalByte = new byte[1];
reader.readBytes(inputStream, signalByte, 1);
sampleHolder.size -= 1;
// First bit of the signalByte (extension bit) must be 0.
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) {
@ -437,6 +524,10 @@ public final class WebmExtractor implements Extractor {
codecPrivate = new byte[contentsSizeBytes];
reader.readBytes(inputStream, codecPrivate, contentsSizeBytes);
break;
case ID_CONTENT_ENCRYPTION_KEY_ID:
encryptionKeyId = new byte[contentsSizeBytes];
reader.readBytes(inputStream, encryptionKeyId, contentsSizeBytes);
break;
default:
// pass
}
@ -463,8 +554,8 @@ public final class WebmExtractor implements Extractor {
private void buildVideoFormat() throws ParserException {
if (pixelWidth != UNKNOWN && pixelHeight != UNKNOWN
&& (format == null || format.width != pixelWidth || format.height != pixelHeight)) {
format = MediaFormat.createVideoFormat(
MimeTypes.VIDEO_VP9, MediaFormat.NO_VALUE, pixelWidth, pixelHeight, null);
format = MediaFormat.createVideoFormat(MimeTypes.VIDEO_VP9, MediaFormat.NO_VALUE, durationUs,
pixelWidth, pixelHeight, null);
readResults |= RESULT_READ_INIT;
} else if (format == null) {
throw new ParserException("Unable to build format");
@ -485,17 +576,15 @@ public final class WebmExtractor implements Extractor {
&& (format == null || format.channelCount != channelCount
|| format.sampleRate != sampleRate)) {
if (CODEC_ID_VORBIS.equals(codecId)) {
format = MediaFormat.createAudioFormat(
MimeTypes.AUDIO_VORBIS, VORBIS_MAX_INPUT_SIZE,
channelCount, sampleRate, parseVorbisCodecPrivate());
format = MediaFormat.createAudioFormat(MimeTypes.AUDIO_VORBIS, VORBIS_MAX_INPUT_SIZE,
durationUs, channelCount, sampleRate, parseVorbisCodecPrivate());
} else if (CODEC_ID_OPUS.equals(codecId)) {
ArrayList<byte[]> opusInitializationData = new ArrayList<byte[]>(3);
opusInitializationData.add(codecPrivate);
opusInitializationData.add(ByteBuffer.allocate(Long.SIZE).putLong(codecDelayNs).array());
opusInitializationData.add(ByteBuffer.allocate(Long.SIZE).putLong(seekPreRollNs).array());
format = MediaFormat.createAudioFormat(
MimeTypes.AUDIO_OPUS, OPUS_MAX_INPUT_SIZE, channelCount, sampleRate,
opusInitializationData);
format = MediaFormat.createAudioFormat(MimeTypes.AUDIO_OPUS, OPUS_MAX_INPUT_SIZE,
durationUs, channelCount, sampleRate, opusInitializationData);
}
readResults |= RESULT_READ_INIT;
} else if (format == null) {
@ -512,7 +601,7 @@ public final class WebmExtractor implements Extractor {
private void buildCues() throws ParserException {
if (segmentStartOffsetBytes == 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");
} else if (cuesSizeBytes == UNKNOWN) {
throw new ParserException("Cues size unknown");

View file

@ -39,6 +39,7 @@ import com.google.android.exoplayer.dash.mpd.MediaPresentationDescription;
import com.google.android.exoplayer.dash.mpd.Period;
import com.google.android.exoplayer.dash.mpd.RangedUri;
import com.google.android.exoplayer.dash.mpd.Representation;
import com.google.android.exoplayer.drm.DrmInitData;
import com.google.android.exoplayer.text.webvtt.WebvttParser;
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DataSpec;
@ -54,8 +55,6 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
/**
* An {@link ChunkSource} for DASH streams.
@ -96,7 +95,7 @@ public class DashChunkSource implements ChunkSource {
private final ManifestFetcher<MediaPresentationDescription> manifestFetcher;
private final int adaptationSetIndex;
private final int[] representationIndices;
private final Map<UUID, byte[]> psshInfo;
private final DrmInitData drmInitData;
private MediaPresentationDescription currentManifest;
private boolean finishedCurrentManifest;
@ -190,7 +189,7 @@ public class DashChunkSource implements ChunkSource {
this.evaluation = new Evaluation();
this.headerBuilder = new StringBuilder();
psshInfo = getPsshInfo(currentManifest, adaptationSetIndex);
drmInitData = getDrmInitData(currentManifest, adaptationSetIndex);
Representation[] representations = getFilteredRepresentations(currentManifest,
adaptationSetIndex, representationIndices);
long periodDurationUs = (representations[0].periodDurationMs == TrackRenderer.UNKNOWN_TIME_US)
@ -407,7 +406,7 @@ public class DashChunkSource implements ChunkSource {
// Do nothing.
}
private boolean mimeTypeIsWebm(String mimeType) {
private static boolean mimeTypeIsWebm(String mimeType) {
return mimeType.startsWith(MimeTypes.VIDEO_WEBM) || mimeType.startsWith(MimeTypes.AUDIO_WEBM);
}
@ -475,8 +474,8 @@ public class DashChunkSource implements ChunkSource {
startTimeUs, endTimeUs, nextAbsoluteSegmentNum, null, representationHolder.vttHeader);
} else {
return new ContainerMediaChunk(dataSource, dataSpec, representation.format, trigger,
startTimeUs, endTimeUs, nextAbsoluteSegmentNum, representationHolder.extractor, psshInfo,
false, presentationTimeOffsetUs);
startTimeUs, endTimeUs, nextAbsoluteSegmentNum, representationHolder.extractor,
drmInitData, false, presentationTimeOffsetUs);
}
}
@ -529,19 +528,24 @@ public class DashChunkSource implements ChunkSource {
}
}
private static Map<UUID, byte[]> getPsshInfo(MediaPresentationDescription manifest,
private static DrmInitData getDrmInitData(MediaPresentationDescription manifest,
int adaptationSetIndex) {
AdaptationSet adaptationSet = manifest.periods.get(0).adaptationSets.get(adaptationSetIndex);
String drmInitMimeType = mimeTypeIsWebm(adaptationSet.representations.get(0).format.mimeType)
? MimeTypes.VIDEO_WEBM : MimeTypes.VIDEO_MP4;
if (adaptationSet.contentProtections.isEmpty()) {
return null;
} else {
Map<UUID, byte[]> psshInfo = new HashMap<UUID, byte[]>();
DrmInitData.Mapped drmInitData = null;
for (ContentProtection contentProtection : adaptationSet.contentProtections) {
if (contentProtection.uuid != null && contentProtection.data != null) {
psshInfo.put(contentProtection.uuid, contentProtection.data);
if (drmInitData == null) {
drmInitData = new DrmInitData.Mapped(drmInitMimeType);
}
drmInitData.put(contentProtection.uuid, contentProtection.data);
}
}
return psshInfo.isEmpty() ? null : psshInfo;
return drmInitData;
}
}
@ -581,7 +585,7 @@ public class DashChunkSource implements ChunkSource {
}
if ((result & Extractor.RESULT_READ_INDEX) != 0) {
representationHolders.get(format.id).segmentIndex =
new DashWrappingSegmentIndex(extractor.getIndex(), uri, indexAnchor);
new DashWrappingSegmentIndex(extractor.getIndex(), uri.toString(), indexAnchor);
}
}

View file

@ -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.util.Util;
import android.net.Uri;
/**
* An implementation of {@link DashSegmentIndex} that wraps a {@link SegmentIndex} parsed from a
* media stream.
@ -28,16 +26,16 @@ import android.net.Uri;
public class DashWrappingSegmentIndex implements DashSegmentIndex {
private final SegmentIndex segmentIndex;
private final Uri uri;
private final String uri;
private final long indexAnchor;
/**
* @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
* 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.uri = uri;
this.indexAnchor = indexAnchor;

View file

@ -15,6 +15,10 @@
*/
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;
/**
@ -43,9 +47,38 @@ public class ContentProtection {
* @param data Protection scheme specific initialization data. May be null.
*/
public ContentProtection(String schemeUriId, UUID uuid, byte[] data) {
this.schemeUriId = schemeUriId;
this.schemeUriId = Assertions.checkNotNull(schemeUriId);
this.uuid = uuid;
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;
}
}

View file

@ -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.util.Assertions;
import com.google.android.exoplayer.util.MimeTypes;
import com.google.android.exoplayer.util.UriUtil;
import com.google.android.exoplayer.util.Util;
import android.net.Uri;
import android.text.TextUtils;
import org.xml.sax.helpers.DefaultHandler;
@ -38,6 +38,8 @@ import java.io.IOException;
import java.io.InputStream;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
/**
@ -83,7 +85,7 @@ public class MediaPresentationDescriptionParser extends DefaultHandler
throw new ParserException(
"inputStream does not contain a valid media presentation description");
}
return parseMediaPresentationDescription(xpp, Util.parseBaseUri(connectionUrl));
return parseMediaPresentationDescription(xpp, connectionUrl);
} catch (XmlPullParserException e) {
throw new ParserException(e);
} catch (ParseException e) {
@ -92,7 +94,7 @@ public class MediaPresentationDescriptionParser extends DefaultHandler
}
protected MediaPresentationDescription parseMediaPresentationDescription(XmlPullParser xpp,
Uri baseUrl) throws XmlPullParserException, IOException, ParseException {
String baseUrl) throws XmlPullParserException, IOException, ParseException {
long availabilityStartTime = parseDateTime(xpp, "availabilityStartTime", -1);
long durationMs = parseDuration(xpp, "mediaPresentationDuration", -1);
long minBufferTimeMs = parseDuration(xpp, "minBufferTime", -1);
@ -137,7 +139,7 @@ public class MediaPresentationDescriptionParser extends DefaultHandler
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 {
String id = xpp.getAttributeValue(null, "id");
long startMs = parseDuration(xpp, "start", 0);
@ -170,7 +172,7 @@ public class MediaPresentationDescriptionParser extends DefaultHandler
// 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 {
String mimeType = xpp.getAttributeValue(null, "mimeType");
@ -178,24 +180,22 @@ public class MediaPresentationDescriptionParser extends DefaultHandler
int contentType = parseAdaptationSetTypeFromMimeType(mimeType);
int id = -1;
List<ContentProtection> contentProtections = null;
ContentProtectionsBuilder contentProtectionsBuilder = new ContentProtectionsBuilder();
List<Representation> representations = new ArrayList<Representation>();
do {
xpp.next();
if (isStartTag(xpp, "BaseURL")) {
baseUrl = parseBaseUrl(xpp, baseUrl);
} else if (isStartTag(xpp, "ContentProtection")) {
if (contentProtections == null) {
contentProtections = new ArrayList<ContentProtection>();
}
contentProtections.add(parseContentProtection(xpp));
contentProtectionsBuilder.addAdaptationSetProtection(parseContentProtection(xpp));
} else if (isStartTag(xpp, "ContentComponent")) {
id = Integer.parseInt(xpp.getAttributeValue(null, "id"));
contentType = checkAdaptationSetTypeConsistency(contentType,
parseAdaptationSetType(xpp.getAttributeValue(null, "contentType")));
} else if (isStartTag(xpp, "Representation")) {
Representation representation = parseRepresentation(xpp, baseUrl, periodStartMs,
periodDurationMs, mimeType, language, segmentBase);
periodDurationMs, mimeType, language, segmentBase, contentProtectionsBuilder);
contentProtectionsBuilder.endRepresentation();
contentType = checkAdaptationSetTypeConsistency(contentType,
parseAdaptationSetTypeFromMimeType(representation.format.mimeType));
representations.add(representation);
@ -211,7 +211,7 @@ public class MediaPresentationDescriptionParser extends DefaultHandler
}
} while (!isEndTag(xpp, "AdaptationSet"));
return buildAdaptationSet(id, contentType, representations, contentProtections);
return buildAdaptationSet(id, contentType, representations, contentProtectionsBuilder.build());
}
protected AdaptationSet buildAdaptationSet(int id, int contentType,
@ -287,8 +287,9 @@ public class MediaPresentationDescriptionParser extends DefaultHandler
// Representation parsing.
protected Representation parseRepresentation(XmlPullParser xpp, Uri baseUrl, long periodStartMs,
long periodDurationMs, String mimeType, String language, SegmentBase segmentBase)
protected Representation parseRepresentation(XmlPullParser xpp, String baseUrl,
long periodStartMs, long periodDurationMs, String mimeType, String language,
SegmentBase segmentBase, ContentProtectionsBuilder contentProtectionsBuilder)
throws XmlPullParserException, IOException {
String id = xpp.getAttributeValue(null, "id");
int bandwidth = parseInt(xpp, "bandwidth");
@ -312,6 +313,8 @@ public class MediaPresentationDescriptionParser extends DefaultHandler
} else if (isStartTag(xpp, "SegmentTemplate")) {
segmentBase = parseSegmentTemplate(xpp, baseUrl, (SegmentTemplate) segmentBase,
periodDurationMs);
} else if (isStartTag(xpp, "ContentProtection")) {
contentProtectionsBuilder.addRepresentationProtection(parseContentProtection(xpp));
}
} while (!isEndTag(xpp, "Representation"));
@ -335,7 +338,7 @@ public class MediaPresentationDescriptionParser extends DefaultHandler
// SegmentBase, SegmentList and SegmentTemplate parsing.
protected SingleSegmentBase parseSegmentBase(XmlPullParser xpp, Uri baseUrl,
protected SingleSegmentBase parseSegmentBase(XmlPullParser xpp, String baseUrl,
SingleSegmentBase parent) throws XmlPullParserException, IOException {
long timescale = parseLong(xpp, "timescale", parent != null ? parent.timescale : 1);
@ -364,12 +367,12 @@ public class MediaPresentationDescriptionParser extends DefaultHandler
}
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,
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 timescale = parseLong(xpp, "timescale", parent != null ? parent.timescale : 1);
@ -413,7 +416,7 @@ public class MediaPresentationDescriptionParser extends DefaultHandler
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 {
long timescale = parseLong(xpp, "timescale", parent != null ? parent.timescale : 1);
@ -450,7 +453,7 @@ public class MediaPresentationDescriptionParser extends DefaultHandler
protected SegmentTemplate buildSegmentTemplate(RangedUri initialization, long timescale,
long presentationTimeOffset, long periodDurationMs, int startNumber, long duration,
List<SegmentTimelineElement> timeline, UrlTemplate initializationTemplate,
UrlTemplate mediaTemplate, Uri baseUrl) {
UrlTemplate mediaTemplate, String baseUrl) {
return new SegmentTemplate(initialization, timescale, presentationTimeOffset, periodDurationMs,
startNumber, duration, timeline, initializationTemplate, mediaTemplate, baseUrl);
}
@ -487,15 +490,15 @@ public class MediaPresentationDescriptionParser extends DefaultHandler
return defaultValue;
}
protected RangedUri parseInitialization(XmlPullParser xpp, Uri baseUrl) {
protected RangedUri parseInitialization(XmlPullParser xpp, String baseUrl) {
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");
}
protected RangedUri parseRangedUrl(XmlPullParser xpp, Uri baseUrl, String urlAttribute,
protected RangedUri parseRangedUrl(XmlPullParser xpp, String baseUrl, String urlAttribute,
String rangeAttribute) {
String urlText = xpp.getAttributeValue(null, urlAttribute);
long rangeStart = 0;
@ -509,7 +512,7 @@ public class MediaPresentationDescriptionParser extends DefaultHandler
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) {
return new RangedUri(baseUrl, urlText, rangeStart, rangeLength);
}
@ -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 {
xpp.next();
String newBaseUrlText = xpp.getText();
Uri newBaseUri = Uri.parse(newBaseUrlText);
if (!newBaseUri.isAbsolute()) {
newBaseUri = Uri.withAppendedPath(parentBaseUrl, newBaseUrlText);
}
return newBaseUri;
return UriUtil.resolve(parentBaseUrl, xpp.getText());
}
protected static int parseInt(XmlPullParser xpp, String name) {
@ -582,4 +580,120 @@ public class MediaPresentationDescriptionParser extends DefaultHandler
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);
}
}
}

View file

@ -16,7 +16,7 @@
package com.google.android.exoplayer.dash.mpd;
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;
@ -35,31 +35,28 @@ public final class RangedUri {
*/
public final long length;
// The {@link Uri} is stored internally in two parts, {@link #baseUri} and {@link uriString}.
// This helps optimize memory usage in the same way that DASH manifests allow many URLs to be
// expressed concisely in the form of a single BaseURL and many relative paths. Note that this
// optimization relies on the same {@code Uri} being passed as the {@link #baseUri} to many
// The URI is stored internally in two parts: reference URI and a base URI to use when
// resolving it. This helps optimize memory usage in the same way that DASH manifests allow many
// URLs to be expressed concisely in the form of a single BaseURL and many relative paths. Note
// that this optimization relies on the same object being passed as the base URI to many
// instances of this class.
private final Uri baseUri;
private final String stringUri;
private final String baseUri;
private final String referenceUri;
private int hashCode;
/**
* 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 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 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) {
Assertions.checkArgument(baseUri != null || stringUri != null);
public RangedUri(String baseUri, String referenceUri, long start, long length) {
Assertions.checkArgument(baseUri != null || referenceUri != null);
this.baseUri = baseUri;
this.stringUri = stringUri;
this.referenceUri = referenceUri;
this.start = start;
this.length = length;
}
@ -70,7 +67,16 @@ public final class RangedUri {
* @return The {@link Uri} represented by the instance.
*/
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 {
* @return The merged {@link RangedUri} if the merge was successful. Null otherwise.
*/
public RangedUri attemptMerge(RangedUri other) {
if (other == null || !getUri().equals(other.getUri())) {
if (other == null || !getUriString().equals(other.getUriString())) {
return null;
} 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);
} 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);
} else {
return null;
@ -104,7 +110,7 @@ public final class RangedUri {
int result = 17;
result = 31 * result + (int) start;
result = 31 * result + (int) length;
result = 31 * result + getUri().hashCode();
result = 31 * result + getUriString().hashCode();
hashCode = result;
}
return hashCode;
@ -121,7 +127,7 @@ public final class RangedUri {
RangedUri other = (RangedUri) obj;
return this.start == other.start
&& this.length == other.length
&& getUri().equals(other.getUri());
&& getUriString().equals(other.getUriString());
}
}

View file

@ -147,7 +147,7 @@ public abstract class Representation {
public static class SingleSegmentRepresentation extends Representation {
/**
* The {@link Uri} of the single segment.
* The uri of the single segment.
*/
public final Uri uri;
@ -174,7 +174,7 @@ public abstract class Representation {
* @param contentLength The content length, or -1 if unknown.
*/
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) {
RangedUri rangedUri = new RangedUri(uri, null, initializationStart,
initializationEnd - initializationStart + 1);
@ -197,13 +197,13 @@ public abstract class Representation {
public SingleSegmentRepresentation(long periodStartMs, long periodDurationMs, String contentId,
long revisionId, Format format, SingleSegmentBase segmentBase, long contentLength) {
super(periodStartMs, periodDurationMs, contentId, revisionId, format, segmentBase);
this.uri = segmentBase.uri;
this.uri = Uri.parse(segmentBase.uri);
this.indexUri = segmentBase.getIndex();
this.contentLength = contentLength;
// 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.
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

View file

@ -19,8 +19,6 @@ import com.google.android.exoplayer.C;
import com.google.android.exoplayer.dash.DashSegmentIndex;
import com.google.android.exoplayer.util.Util;
import android.net.Uri;
import java.util.List;
/**
@ -73,7 +71,7 @@ public abstract class SegmentBase {
/**
* The uri of the segment.
*/
public final Uri uri;
public final String uri;
/* package */ final long indexStart;
/* package */ final long indexLength;
@ -89,7 +87,7 @@ public abstract class SegmentBase {
* @param indexLength The length of the index data in bytes.
*/
public SingleSegmentBase(RangedUri initialization, long timescale, long presentationTimeOffset,
Uri uri, long indexStart, long indexLength) {
String uri, long indexStart, long indexLength) {
super(initialization, timescale, presentationTimeOffset);
this.uri = uri;
this.indexStart = indexStart;
@ -99,7 +97,7 @@ public abstract class SegmentBase {
/**
* @param uri The uri of the segment.
*/
public SingleSegmentBase(Uri uri) {
public SingleSegmentBase(String uri) {
this(null, 1, 0, uri, 0, -1);
}
@ -289,7 +287,7 @@ public abstract class SegmentBase {
/* package */ final UrlTemplate initializationTemplate;
/* package */ final UrlTemplate mediaTemplate;
private final Uri baseUrl;
private final String baseUrl;
/**
* @param initialization A {@link RangedUri} corresponding to initialization data, if such data
@ -315,7 +313,7 @@ public abstract class SegmentBase {
public SegmentTemplate(RangedUri initialization, long timescale, long presentationTimeOffset,
long periodDurationMs, int startNumber, long duration,
List<SegmentTimelineElement> segmentTimeline, UrlTemplate initializationTemplate,
UrlTemplate mediaTemplate, Uri baseUrl) {
UrlTemplate mediaTemplate, String baseUrl) {
super(initialization, timescale, presentationTimeOffset, periodDurationMs, startNumber,
duration, segmentTimeline);
this.initializationTemplate = initializationTemplate;

View file

@ -32,6 +32,7 @@ import java.io.InputStreamReader;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Locale;
import java.util.TimeZone;
import java.util.concurrent.CancellationException;
/**
@ -173,6 +174,7 @@ public class UtcTimingElementResolver implements Loader.Callback {
try {
// 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);
format.setTimeZone(TimeZone.getTimeZone("UTC"));
return format.parse(firstLine).getTime();
} catch (ParseException e) {
throw new ParserException(e);

View file

@ -0,0 +1,103 @@
/*
* 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;
}
}
}

View file

@ -18,9 +18,6 @@ package com.google.android.exoplayer.drm;
import android.annotation.TargetApi;
import android.media.MediaCrypto;
import java.util.Map;
import java.util.UUID;
/**
* Manages a DRM session.
*/
@ -36,7 +33,7 @@ public interface DrmSessionManager {
*/
public static final int STATE_CLOSED = 1;
/**
* The session is being opened (i.e. {@link #open(Map, String)} has been called, but the session
* The session is being opened (i.e. {@link #open(DrmInitData)} has been called, but the session
* is not yet open).
*/
public static final int STATE_OPENING = 2;
@ -52,11 +49,9 @@ public interface DrmSessionManager {
/**
* Opens the session, possibly asynchronously.
*
* @param drmInitData Initialization data for the drm schemes supported by the media, keyed by
* scheme UUID.
* @param mimeType The mimeType of the media.
* @param drmInitData DRM initialization data.
*/
void open(Map<UUID, byte[]> drmInitData, String mimeType);
void open(DrmInitData drmInitData);
/**
* Closes the session.

View file

@ -0,0 +1,22 @@
/*
* 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 {
}

View file

@ -31,7 +31,6 @@ import android.os.Looper;
import android.os.Message;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
@ -81,15 +80,6 @@ public class StreamingDrmSessionManager implements DrmSessionManager {
private byte[] schemePsshData;
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 playbackLooper The looper associated with the media playback thread. Should usually be
@ -168,7 +158,7 @@ public class StreamingDrmSessionManager implements DrmSessionManager {
}
@Override
public void open(Map<UUID, byte[]> psshData, String mimeType) {
public void open(DrmInitData drmInitData) {
if (++openCount != 1) {
return;
}
@ -178,8 +168,8 @@ public class StreamingDrmSessionManager implements DrmSessionManager {
postRequestHandler = new PostRequestHandler(requestHandlerThread.getLooper());
}
if (this.schemePsshData == null) {
this.mimeType = mimeType;
schemePsshData = psshData.get(uuid);
mimeType = drmInitData.mimeType;
schemePsshData = drmInitData.get(uuid);
if (schemePsshData == null) {
onError(new IllegalStateException("Media does not support uuid: " + uuid));
return;
@ -332,7 +322,7 @@ public class StreamingDrmSessionManager implements DrmSessionManager {
return;
case MediaDrm.EVENT_KEY_EXPIRED:
state = STATE_OPENED;
postKeyRequest();
onError(new KeysExpiredException());
return;
case MediaDrm.EVENT_PROVISION_REQUIRED:
state = STATE_OPENED;

View file

@ -0,0 +1,114 @@
/*
* 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;
}
}

View file

@ -13,20 +13,21 @@
* See the License for the specific language governing permissions and
* 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.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;
/**
* Wraps a {@link RollingSampleBuffer}, adding higher level functionality such as enforcing that
* the first sample returned from the queue is a keyframe, allowing splicing to another queue, and
* so on.
* A {@link TrackOutput} that buffers extracted samples in a queue, and allows for consumption from
* that queue.
*/
/* package */ abstract class SampleQueue {
public final class DefaultTrackOutput implements TrackOutput {
private final RollingSampleBuffer rollingBuffer;
private final SampleHolder sampleInfoHolder;
@ -36,14 +37,11 @@ import com.google.android.exoplayer.util.ParsableByteArray;
private long lastReadTimeUs;
private long spliceOutTimeUs;
// Accessed only by the loading thread.
private boolean writingSample;
// Accessed by both the loading and consuming threads.
private volatile MediaFormat mediaFormat;
private volatile long largestParsedTimestampUs;
private volatile MediaFormat format;
protected SampleQueue(BufferPool bufferPool) {
public DefaultTrackOutput(BufferPool bufferPool) {
rollingBuffer = new RollingSampleBuffer(bufferPool);
sampleInfoHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_DISABLED);
needKeyframe = true;
@ -52,24 +50,60 @@ import com.google.android.exoplayer.util.ParsableByteArray;
largestParsedTimestampUs = Long.MIN_VALUE;
}
public void release() {
rollingBuffer.release();
// Called by the consuming thread, but only when there is no loading thread.
/**
* 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.
/**
* Returns the current absolute read index.
*/
public int getReadIndex() {
return rollingBuffer.getReadIndex();
}
/**
* True if the output has received a format. False otherwise.
*/
public boolean hasFormat() {
return format != null;
}
/**
* 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;
}
public boolean hasMediaFormat() {
return mediaFormat != null;
}
public MediaFormat getMediaFormat() {
return mediaFormat;
}
/**
* True if at least one sample can be read from the queue. False otherwise.
*/
public boolean isEmpty() {
return !advanceToEligibleSample();
}
@ -115,7 +149,7 @@ import com.google.android.exoplayer.util.ParsableByteArray;
* @param nextQueue The queue being spliced to.
* @return Whether the splice was configured successfully.
*/
public boolean configureSpliceTo(SampleQueue nextQueue) {
public boolean configureSpliceTo(DefaultTrackOutput nextQueue) {
if (spliceOutTimeUs != Long.MIN_VALUE) {
// We've already configured the splice.
return true;
@ -128,8 +162,7 @@ import com.google.android.exoplayer.util.ParsableByteArray;
}
RollingSampleBuffer nextRollingBuffer = nextQueue.rollingBuffer;
while (nextRollingBuffer.peekSample(sampleInfoHolder)
&& (sampleInfoHolder.timeUs < firstPossibleSpliceTime
|| (sampleInfoHolder.flags & C.SAMPLE_FLAG_SYNC) == 0)) {
&& (sampleInfoHolder.timeUs < firstPossibleSpliceTime || !sampleInfoHolder.isSyncFrame())) {
// Discard samples from the next queue for as long as they are before the earliest possible
// splice time, or not keyframes.
nextRollingBuffer.skipSample();
@ -152,7 +185,7 @@ import com.google.android.exoplayer.util.ParsableByteArray;
private boolean advanceToEligibleSample() {
boolean haveNext = rollingBuffer.peekSample(sampleInfoHolder);
if (needKeyframe) {
while (haveNext && (sampleInfoHolder.flags & C.SAMPLE_FLAG_SYNC) == 0) {
while (haveNext && !sampleInfoHolder.isSyncFrame()) {
rollingBuffer.skipSample();
haveNext = rollingBuffer.peekSample(sampleInfoHolder);
}
@ -168,35 +201,27 @@ import com.google.android.exoplayer.util.ParsableByteArray;
// Called by the loading thread.
protected boolean writingSample() {
return writingSample;
public int sampleData(DataSource dataSource, int length) throws IOException {
return rollingBuffer.appendData(dataSource, length);
}
protected void setMediaFormat(MediaFormat mediaFormat) {
this.mediaFormat = mediaFormat;
// TrackOutput implementation. Called by the loading thread.
@Override
public void format(MediaFormat format) {
this.format = format;
}
protected void startSample(long sampleTimeUs) {
startSample(sampleTimeUs, 0);
}
protected void startSample(long sampleTimeUs, int offset) {
writingSample = true;
largestParsedTimestampUs = Math.max(largestParsedTimestampUs, sampleTimeUs);
rollingBuffer.startSample(sampleTimeUs, offset);
}
protected void appendData(ParsableByteArray buffer, int length) {
@Override
public void sampleData(ParsableByteArray buffer, int length) {
rollingBuffer.appendData(buffer, length);
}
protected void commitSample(boolean isKeyframe) {
commitSample(isKeyframe, 0);
}
protected void commitSample(boolean isKeyframe, int offset) {
rollingBuffer.commitSample(isKeyframe, offset);
writingSample = false;
@Override
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,
size, encryptionKey);
}
}

View file

@ -0,0 +1,58 @@
/*
* 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;
}

View file

@ -0,0 +1,109 @@
/*
* 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();
}

View file

@ -0,0 +1,39 @@
/*
* 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();
}

View file

@ -0,0 +1,518 @@
/*
* 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;
}
}

View file

@ -0,0 +1,58 @@
/*
* 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);
}

View file

@ -0,0 +1,71 @@
/*
* 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.ts;
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.util.ParsableByteArray;
import java.io.IOException;
/**
* Facilitates the extraction of AAC samples from elementary audio files formatted as AAC with ADTS
* headers.
*/
public class AdtsExtractor implements Extractor {
private static final int MAX_PACKET_SIZE = 200;
private final long firstSampleTimestamp;
private final ParsableByteArray packetBuffer;
// Accessed only by the loading thread.
private AdtsReader adtsReader;
private boolean firstPacket;
public AdtsExtractor(long firstSampleTimestamp) {
this.firstSampleTimestamp = firstSampleTimestamp;
packetBuffer = new ParsableByteArray(MAX_PACKET_SIZE);
firstPacket = true;
}
@Override
public void init(ExtractorOutput output) {
adtsReader = new AdtsReader(output.track(0));
output.endTracks();
}
@Override
public int read(ExtractorInput input)
throws IOException, InterruptedException {
int bytesRead = input.read(packetBuffer.data, 0, MAX_PACKET_SIZE);
if (bytesRead == -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.setLimit(bytesRead);
// TODO: Make it possible for adtsReader to consume the dataSource directly, so that it becomes
// unnecessary to copy the data through packetBuffer.
adtsReader.consume(packetBuffer, firstSampleTimestamp, firstPacket);
firstPacket = false;
return RESULT_CONTINUE;
}
}

View file

@ -13,11 +13,11 @@
* See the License for the specific language governing permissions and
* 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.upstream.BufferPool;
import com.google.android.exoplayer.extractor.TrackOutput;
import com.google.android.exoplayer.util.CodecSpecificDataUtil;
import com.google.android.exoplayer.util.MimeTypes;
import com.google.android.exoplayer.util.ParsableBitArray;
@ -48,15 +48,16 @@ import java.util.Collections;
private boolean lastByteWasFF;
private boolean hasCrc;
// Parsed from the header.
// Used when parsing the header.
private boolean hasOutputFormat;
private long frameDurationUs;
private int sampleSize;
// Used when reading the samples.
private long timeUs;
public AdtsReader(BufferPool bufferPool) {
super(bufferPool);
public AdtsReader(TrackOutput output) {
super(output);
adtsScratch = new ParsableBitArray(new byte[HEADER_SIZE + CRC_SIZE]);
state = STATE_FINDING_SYNC;
}
@ -78,17 +79,16 @@ import java.util.Collections;
int targetLength = hasCrc ? HEADER_SIZE + CRC_SIZE : HEADER_SIZE;
if (continueRead(data, adtsScratch.getData(), targetLength)) {
parseHeader();
startSample(timeUs);
bytesRead = 0;
state = STATE_READING_SAMPLE;
}
break;
case STATE_READING_SAMPLE:
int bytesToRead = Math.min(data.bytesLeft(), sampleSize - bytesRead);
appendData(data, bytesToRead);
output.sampleData(data, bytesToRead);
bytesRead += bytesToRead;
if (bytesRead == sampleSize) {
commitSample(true);
output.sampleMetadata(timeUs, C.SAMPLE_FLAG_SYNC, sampleSize, 0, null);
timeUs += frameDurationUs;
bytesRead = 0;
state = STATE_FINDING_SYNC;
@ -152,7 +152,7 @@ import java.util.Collections;
private void parseHeader() {
adtsScratch.setPosition(0);
if (!hasMediaFormat()) {
if (!hasOutputFormat) {
int audioObjectType = adtsScratch.readBits(2) + 1;
int sampleRateIndex = adtsScratch.readBits(4);
adtsScratch.skipBits(1);
@ -167,7 +167,8 @@ import java.util.Collections;
MediaFormat.NO_VALUE, audioParams.second, audioParams.first,
Collections.singletonList(audioSpecificConfig));
frameDurationUs = (C.MICROS_PER_SECOND * 1024L) / mediaFormat.sampleRate;
setMediaFormat(mediaFormat);
output.format(mediaFormat);
hasOutputFormat = true;
} else {
adtsScratch.skipBits(10);
}

View file

@ -13,18 +13,23 @@
* See the License for the specific language governing permissions and
* 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;
/**
* Extracts individual samples from an elementary media stream, preserving original order.
*/
/* package */ abstract class ElementaryStreamReader extends SampleQueue {
/* package */ abstract class ElementaryStreamReader {
protected ElementaryStreamReader(BufferPool bufferPool) {
super(bufferPool);
protected final TrackOutput output;
/**
* @param output A {@link TrackOutput} to which samples should be written.
*/
protected ElementaryStreamReader(TrackOutput output) {
this.output = output;
}
/**

View file

@ -13,16 +13,19 @@
* See the License for the specific language governing permissions and
* 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.mp4.Mp4Util;
import com.google.android.exoplayer.upstream.BufferPool;
import com.google.android.exoplayer.extractor.TrackOutput;
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.ParsableBitArray;
import com.google.android.exoplayer.util.ParsableByteArray;
import android.util.Log;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@ -32,29 +35,59 @@ import java.util.List;
*/
/* 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_SEI = 6;
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_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 boolean[] prefixFlags;
private final NalUnitTargetBuffer sps;
private final NalUnitTargetBuffer pps;
private final NalUnitTargetBuffer sei;
private final ParsableByteArray seiWrapper;
private boolean hasOutputFormat;
private int scratchEscapeCount;
private int[] scratchEscapePositions;
private boolean isKeyframe;
public H264Reader(BufferPool bufferPool, SeiReader seiReader) {
super(bufferPool);
private boolean writingSample;
private boolean isKeyframe;
private long samplePosition;
private long sampleTimeUs;
private long totalBytesWritten;
public H264Reader(TrackOutput output, SeiReader seiReader) {
super(output);
this.seiReader = seiReader;
prefixFlags = new boolean[3];
sps = new NalUnitTargetBuffer(NAL_UNIT_TYPE_SPS, 128);
pps = new NalUnitTargetBuffer(NAL_UNIT_TYPE_PPS, 128);
sei = new NalUnitTargetBuffer(NAL_UNIT_TYPE_SEI, 128);
seiWrapper = new ParsableByteArray();
scratchEscapePositions = new int[10];
}
@ -66,11 +99,12 @@ import java.util.List;
byte[] dataArray = data.data;
// 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
while (offset < limit) {
int nextNalUnitOffset = Mp4Util.findNalUnit(dataArray, offset, limit, prefixFlags);
int nextNalUnitOffset = H264Util.findNalUnit(dataArray, offset, limit, prefixFlags);
if (nextNalUnitOffset < limit) {
// We've seen the start of a NAL unit.
@ -81,16 +115,21 @@ import java.util.List;
feedNalUnitTargetBuffersData(dataArray, offset, nextNalUnitOffset);
}
int nalUnitType = Mp4Util.getNalUnitType(dataArray, nextNalUnitOffset);
int nalUnitOffsetInData = nextNalUnitOffset - limit;
int nalUnitType = H264Util.getNalUnitType(dataArray, nextNalUnitOffset);
int bytesWrittenPastNalUnit = limit - nextNalUnitOffset;
if (nalUnitType == NAL_UNIT_TYPE_AUD) {
if (writingSample()) {
if (isKeyframe && !hasMediaFormat() && sps.isCompleted() && pps.isCompleted()) {
if (writingSample) {
if (isKeyframe && !hasOutputFormat && sps.isCompleted() && pps.isCompleted()) {
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;
} else if (nalUnitType == NAL_UNIT_TYPE_IDR) {
isKeyframe = true;
@ -117,7 +156,7 @@ import java.util.List;
}
private void feedNalUnitTargetBuffersStart(int nalUnitType) {
if (!hasMediaFormat()) {
if (!hasOutputFormat) {
sps.startNalUnit(nalUnitType);
pps.startNalUnit(nalUnitType);
}
@ -125,7 +164,7 @@ import java.util.List;
}
private void feedNalUnitTargetBuffersData(byte[] dataArray, int offset, int limit) {
if (!hasMediaFormat()) {
if (!hasOutputFormat) {
sps.appendToNalUnit(dataArray, offset, limit);
pps.appendToNalUnit(dataArray, offset, limit);
}
@ -137,7 +176,8 @@ import java.util.List;
pps.endNalUnit(discardPadding);
if (sei.endNalUnit(discardPadding)) {
int unescapedLength = unescapeStream(sei.nalData, sei.nalLength);
seiReader.read(sei.nalData, 0, unescapedLength, pesTimeUs);
seiWrapper.reset(sei.nalData, unescapedLength);
seiReader.consume(seiWrapper, pesTimeUs, true);
}
}
@ -228,9 +268,29 @@ import java.util.List;
frameHeight -= (frameCropTopOffset + frameCropBottomOffset) * cropUnitY;
}
// Set the format.
setMediaFormat(MediaFormat.createVideoFormat(MimeTypes.VIDEO_H264, MediaFormat.NO_VALUE,
frameWidth, frameHeight, initializationData));
float pixelWidthHeightRatio = 1;
boolean vuiParametersPresentFlag = bitArray.readBit();
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) {

View file

@ -13,10 +13,11 @@
* See the License for the specific language governing permissions and
* 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.upstream.BufferPool;
import com.google.android.exoplayer.extractor.TrackOutput;
import com.google.android.exoplayer.util.ParsableByteArray;
/**
@ -24,24 +25,32 @@ import com.google.android.exoplayer.util.ParsableByteArray;
*/
/* package */ class Id3Reader extends ElementaryStreamReader {
public Id3Reader(BufferPool bufferPool) {
super(bufferPool);
setMediaFormat(MediaFormat.createId3Format());
private boolean writingSample;
private long sampleTimeUs;
private int sampleSize;
public Id3Reader(TrackOutput output) {
super(output);
output.format(MediaFormat.createId3Format());
}
@Override
public void consume(ParsableByteArray data, long pesTimeUs, boolean startOfPacket) {
if (startOfPacket) {
startSample(pesTimeUs);
writingSample = true;
sampleTimeUs = pesTimeUs;
sampleSize = 0;
}
if (writingSample()) {
appendData(data, data.bytesLeft());
if (writingSample) {
sampleSize += data.bytesLeft();
output.sampleData(data, data.bytesLeft());
}
}
@Override
public void packetFinished() {
commitSample(true);
output.sampleMetadata(sampleTimeUs, C.SAMPLE_FLAG_SYNC, sampleSize, 0, null);
writingSample = false;
}
}

View file

@ -13,11 +13,12 @@
* See the License for the specific language governing permissions and
* 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.extractor.TrackOutput;
import com.google.android.exoplayer.text.eia608.Eia608Parser;
import com.google.android.exoplayer.upstream.BufferPool;
import com.google.android.exoplayer.util.ParsableByteArray;
/**
@ -26,20 +27,17 @@ import com.google.android.exoplayer.util.ParsableByteArray;
* TODO: Technically, we shouldn't allow a sample to be read from the queue until we're sure that
* a sample with an earlier timestamp won't be added to it.
*/
/* package */ class SeiReader extends SampleQueue {
/* package */ class SeiReader extends ElementaryStreamReader {
private final ParsableByteArray seiBuffer;
public SeiReader(BufferPool bufferPool) {
super(bufferPool);
setMediaFormat(MediaFormat.createEia608Format());
seiBuffer = new ParsableByteArray();
public SeiReader(TrackOutput output) {
super(output);
output.format(MediaFormat.createEia608Format());
}
public void read(byte[] data, int position, int limit, long pesTimeUs) {
seiBuffer.reset(data, limit);
@Override
public void consume(ParsableByteArray seiBuffer, long pesTimeUs, boolean startOfPacket) {
// Skip the NAL prefix and type.
seiBuffer.setPosition(position + 4);
seiBuffer.skip(4);
int b;
while (seiBuffer.bytesLeft() > 1 /* last byte will be rbsp_trailing_bits */) {
@ -57,13 +55,17 @@ import com.google.android.exoplayer.util.ParsableByteArray;
} while (b == 0xFF);
// Process the payload. We only support EIA-608 payloads currently.
if (Eia608Parser.isSeiMessageEia608(payloadType, payloadSize, seiBuffer)) {
startSample(pesTimeUs);
appendData(seiBuffer, payloadSize);
commitSample(true);
output.sampleData(seiBuffer, payloadSize);
output.sampleMetadata(pesTimeUs, C.SAMPLE_FLAG_SYNC, payloadSize, 0, null);
} else {
seiBuffer.skip(payloadSize);
}
}
}
@Override
public void packetFinished() {
// Do nothing.
}
}

View file

@ -13,14 +13,12 @@
* See the License for the specific language governing permissions and
* 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.SampleHolder;
import com.google.android.exoplayer.upstream.BufferPool;
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.extractor.Extractor;
import com.google.android.exoplayer.extractor.ExtractorInput;
import com.google.android.exoplayer.extractor.ExtractorOutput;
import com.google.android.exoplayer.util.ParsableBitArray;
import com.google.android.exoplayer.util.ParsableByteArray;
@ -32,7 +30,7 @@ import java.io.IOException;
/**
* Facilitates the extraction of data from the MPEG-2 TS container format.
*/
public final class TsExtractor extends HlsExtractor {
public final class TsExtractor implements Extractor {
private static final String TAG = "TsExtractor";
@ -50,119 +48,43 @@ public final class TsExtractor extends HlsExtractor {
private static final long MAX_PTS = 0x1FFFFFFFFL;
private final ParsableByteArray tsPacketBuffer;
private final SparseArray<SampleQueue> sampleQueues; // Indexed by streamType
private final SparseArray<ElementaryStreamReader> streamReaders; // Indexed by streamType
private final SparseArray<TsPayloadReader> tsPayloadReaders; // Indexed by pid
private final BufferPool bufferPool;
private final long firstSampleTimestamp;
private final ParsableBitArray tsScratch;
// Accessed only by the loading thread.
private int tsPacketBytesRead;
private ExtractorOutput output;
private long timestampOffsetUs;
private long lastPts;
// Accessed by both the loading and consuming threads.
private volatile boolean prepared;
public TsExtractor(boolean shouldSpliceIn, long firstSampleTimestamp, BufferPool bufferPool) {
super(shouldSpliceIn);
public TsExtractor(long firstSampleTimestamp) {
this.firstSampleTimestamp = firstSampleTimestamp;
this.bufferPool = bufferPool;
tsScratch = new ParsableBitArray(new byte[3]);
tsPacketBuffer = new ParsableByteArray(TS_PACKET_SIZE);
sampleQueues = new SparseArray<SampleQueue>();
streamReaders = new SparseArray<ElementaryStreamReader>();
tsPayloadReaders = new SparseArray<TsPayloadReader>();
tsPayloadReaders.put(TS_PAT_PID, new PatReader());
lastPts = Long.MIN_VALUE;
}
@Override
public int getTrackCount() {
Assertions.checkState(prepared);
return sampleQueues.size();
public void init(ExtractorOutput output) {
this.output = output;
}
@Override
public MediaFormat getFormat(int track) {
Assertions.checkState(prepared);
return sampleQueues.valueAt(track).getMediaFormat();
}
@Override
public boolean isPrepared() {
return prepared;
}
@Override
public void release() {
for (int i = 0; i < sampleQueues.size(); i++) {
sampleQueues.valueAt(i).release();
}
}
@Override
public long getLargestSampleTimestamp() {
long largestParsedTimestampUs = Long.MIN_VALUE;
for (int i = 0; i < sampleQueues.size(); i++) {
largestParsedTimestampUs = Math.max(largestParsedTimestampUs,
sampleQueues.valueAt(i).getLargestParsedTimestampUs());
}
return largestParsedTimestampUs;
}
@Override
public boolean getSample(int track, SampleHolder holder) {
Assertions.checkState(prepared);
return sampleQueues.valueAt(track).getSample(holder);
}
@Override
public void discardUntil(int track, long timeUs) {
Assertions.checkState(prepared);
sampleQueues.valueAt(track).discardUntil(timeUs);
}
@Override
public boolean hasSamples(int track) {
Assertions.checkState(prepared);
return !sampleQueues.valueAt(track).isEmpty();
}
private boolean checkPrepared() {
int pesPayloadReaderCount = sampleQueues.size();
if (pesPayloadReaderCount == 0) {
return false;
}
for (int i = 0; i < pesPayloadReaderCount; i++) {
if (!sampleQueues.valueAt(i).hasMediaFormat()) {
return false;
}
}
return true;
}
@Override
public int read(DataSource dataSource) throws IOException {
int bytesRead = dataSource.read(tsPacketBuffer.data, tsPacketBytesRead,
TS_PACKET_SIZE - tsPacketBytesRead);
if (bytesRead == -1) {
return -1;
public int read(ExtractorInput input)
throws IOException, InterruptedException {
if (!input.readFully(tsPacketBuffer.data, 0, TS_PACKET_SIZE, true)) {
return RESULT_END_OF_INPUT;
}
tsPacketBytesRead += bytesRead;
if (tsPacketBytesRead < TS_PACKET_SIZE) {
// We haven't read the whole packet yet.
return bytesRead;
}
// Reset before reading the packet.
tsPacketBytesRead = 0;
tsPacketBuffer.setPosition(0);
tsPacketBuffer.setLimit(TS_PACKET_SIZE);
int syncByte = tsPacketBuffer.readUnsignedByte();
if (syncByte != TS_SYNC_BYTE) {
return bytesRead;
return RESULT_CONTINUE;
}
tsPacketBuffer.readBytes(tsScratch, 3);
@ -185,20 +107,11 @@ public final class TsExtractor extends HlsExtractor {
if (payloadExists) {
TsPayloadReader payloadReader = tsPayloadReaders.get(pid);
if (payloadReader != null) {
payloadReader.consume(tsPacketBuffer, payloadUnitStartIndicator);
payloadReader.consume(tsPacketBuffer, payloadUnitStartIndicator, output);
}
}
if (!prepared) {
prepared = checkPrepared();
}
return bytesRead;
}
@Override
protected SampleQueue getSampleQueue(int track) {
return sampleQueues.valueAt(track);
return RESULT_CONTINUE;
}
/**
@ -233,7 +146,8 @@ public final class TsExtractor extends HlsExtractor {
*/
private abstract static class TsPayloadReader {
public abstract void consume(ParsableByteArray data, boolean payloadUnitStartIndicator);
public abstract void consume(ParsableByteArray data, boolean payloadUnitStartIndicator,
ExtractorOutput output);
}
@ -249,7 +163,8 @@ public final class TsExtractor extends HlsExtractor {
}
@Override
public void consume(ParsableByteArray data, boolean payloadUnitStartIndicator) {
public void consume(ParsableByteArray data, boolean payloadUnitStartIndicator,
ExtractorOutput output) {
// Skip pointer.
if (payloadUnitStartIndicator) {
int pointerField = data.readUnsignedByte();
@ -288,7 +203,8 @@ public final class TsExtractor extends HlsExtractor {
}
@Override
public void consume(ParsableByteArray data, boolean payloadUnitStartIndicator) {
public void consume(ParsableByteArray data, boolean payloadUnitStartIndicator,
ExtractorOutput output) {
// Skip pointer.
if (payloadUnitStartIndicator) {
int pointerField = data.readUnsignedByte();
@ -325,7 +241,7 @@ public final class TsExtractor extends HlsExtractor {
data.skip(esInfoLength);
entriesSize -= esInfoLength + 5;
if (sampleQueues.get(streamType) != null) {
if (streamReaders.get(streamType) != null) {
continue;
}
@ -336,25 +252,26 @@ public final class TsExtractor extends HlsExtractor {
pesPayloadReader = new MpaReader(bufferPool);
break;
case TS_STREAM_TYPE_AAC:
pesPayloadReader = new AdtsReader(bufferPool);
pesPayloadReader = new AdtsReader(output.track(TS_STREAM_TYPE_AAC));
break;
case TS_STREAM_TYPE_H264:
SeiReader seiReader = new SeiReader(bufferPool);
sampleQueues.put(TS_STREAM_TYPE_EIA608, seiReader);
pesPayloadReader = new H264Reader(bufferPool, seiReader);
SeiReader seiReader = new SeiReader(output.track(TS_STREAM_TYPE_EIA608));
streamReaders.put(TS_STREAM_TYPE_EIA608, seiReader);
pesPayloadReader = new H264Reader(output.track(TS_STREAM_TYPE_H264),
seiReader);
break;
case TS_STREAM_TYPE_ID3:
pesPayloadReader = new Id3Reader(bufferPool);
pesPayloadReader = new Id3Reader(output.track(TS_STREAM_TYPE_ID3));
break;
}
if (pesPayloadReader != null) {
sampleQueues.put(streamType, pesPayloadReader);
streamReaders.put(streamType, pesPayloadReader);
tsPayloadReaders.put(elementaryPid, new PesReader(pesPayloadReader));
}
}
// Skip CRC_32.
output.endTracks();
}
}
@ -393,7 +310,8 @@ public final class TsExtractor extends HlsExtractor {
}
@Override
public void consume(ParsableByteArray data, boolean payloadUnitStartIndicator) {
public void consume(ParsableByteArray data, boolean payloadUnitStartIndicator,
ExtractorOutput output) {
if (payloadUnitStartIndicator) {
switch (state) {
case STATE_FINDING_HEADER:

View file

@ -17,9 +17,9 @@ package com.google.android.exoplayer.hls;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.hls.parser.AdtsExtractor;
import com.google.android.exoplayer.hls.parser.HlsExtractor;
import com.google.android.exoplayer.hls.parser.TsExtractor;
import com.google.android.exoplayer.extractor.Extractor;
import com.google.android.exoplayer.extractor.ts.AdtsExtractor;
import com.google.android.exoplayer.extractor.ts.TsExtractor;
import com.google.android.exoplayer.upstream.Aes128DataSource;
import com.google.android.exoplayer.upstream.BandwidthMeter;
import com.google.android.exoplayer.upstream.BufferPool;
@ -27,6 +27,7 @@ import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DataSpec;
import com.google.android.exoplayer.upstream.HttpDataSource.InvalidResponseCodeException;
import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.UriUtil;
import com.google.android.exoplayer.util.Util;
import android.net.Uri;
@ -107,10 +108,9 @@ public class HlsChunkSource {
public static final long DEFAULT_MAX_BUFFER_TO_SWITCH_DOWN_MS = 20000;
/**
* The default maximum time a media playlist is blacklisted without
* rechecking if it is alive again (because an encoder reset, for example)
* The default time for which a media playlist should be blacklisted.
*/
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 AAC_FILE_EXTENSION = ".aac";
@ -122,7 +122,7 @@ public class HlsChunkSource {
private final Variant[] enabledVariants;
private final BandwidthMeter bandwidthMeter;
private final int adaptiveMode;
private final Uri baseUri;
private final String baseUri;
private final int maxWidth;
private final int maxHeight;
private final int targetBufferSize;
@ -132,8 +132,7 @@ public class HlsChunkSource {
/* package */ byte[] scratchSpace;
/* package */ final HlsMediaPlaylist[] mediaPlaylists;
/* package */ final boolean[] mediaPlaylistBlacklistFlags;
/* package */ final long[] mediaPlaylistBlacklistedTimeMs;
/* package */ final long[] mediaPlaylistBlacklistTimesMs;
/* package */ final long[] lastMediaPlaylistLoadTimesMs;
/* package */ boolean live;
/* package */ long durationUs;
@ -188,16 +187,14 @@ public class HlsChunkSource {
if (playlist.type == HlsPlaylist.TYPE_MEDIA) {
enabledVariants = new Variant[] {new Variant(0, playlistUrl, 0, null, -1, -1)};
mediaPlaylists = new HlsMediaPlaylist[1];
mediaPlaylistBlacklistFlags = new boolean[1];
mediaPlaylistBlacklistedTimeMs = new long[1];
mediaPlaylistBlacklistTimesMs = new long[1];
lastMediaPlaylistLoadTimesMs = new long[1];
setMediaPlaylist(0, (HlsMediaPlaylist) playlist);
} else {
Assertions.checkState(playlist.type == HlsPlaylist.TYPE_MASTER);
enabledVariants = filterVariants((HlsMasterPlaylist) playlist, variantIndices);
mediaPlaylists = new HlsMediaPlaylist[enabledVariants.length];
mediaPlaylistBlacklistFlags = new boolean[enabledVariants.length];
mediaPlaylistBlacklistedTimeMs = new long[enabledVariants.length];
mediaPlaylistBlacklistTimesMs = new long[enabledVariants.length];
lastMediaPlaylistLoadTimesMs = new long[enabledVariants.length];
}
@ -305,11 +302,11 @@ public class HlsChunkSource {
}
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.
if (HlsMediaPlaylist.ENCRYPTION_METHOD_AES_128.equals(segment.encryptionMethod)) {
Uri keyUri = Util.getMergedUri(mediaPlaylist.baseUri, segment.encryptionKeyUri);
if (segment.isEncrypted) {
Uri keyUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, segment.encryptionKeyUri);
if (!keyUri.equals(encryptionKeyUri)) {
// Encryption is specified and the key has changed.
HlsChunk toReturn = newEncryptionKeyChunk(keyUri, segment.encryptionIV);
@ -344,16 +341,17 @@ public class HlsChunkSource {
boolean isLastChunk = !mediaPlaylist.live && chunkIndex == mediaPlaylist.segments.size() - 1;
// Configure the extractor that will read the chunk.
HlsExtractor extractor;
HlsExtractorWrapper extractorWrapper;
if (previousTsChunk == null || segment.discontinuity || switchingVariant || liveDiscontinuity) {
extractor = chunkUri.getLastPathSegment().endsWith(AAC_FILE_EXTENSION)
? new AdtsExtractor(switchingVariantSpliced, startTimeUs, bufferPool)
: new TsExtractor(switchingVariantSpliced, startTimeUs, bufferPool);
Extractor extractor = chunkUri.getLastPathSegment().endsWith(AAC_FILE_EXTENSION)
? new AdtsExtractor(startTimeUs)
: new TsExtractor(startTimeUs);
extractorWrapper = new HlsExtractorWrapper(bufferPool, extractor, switchingVariantSpliced);
} else {
extractor = previousTsChunk.extractor;
extractorWrapper = previousTsChunk.extractor;
}
return new TsChunk(dataSource, dataSpec, extractor, enabledVariants[variantIndex].index,
return new TsChunk(dataSource, dataSpec, extractorWrapper, enabledVariants[variantIndex].index,
startTimeUs, endTimeUs, chunkMediaSequence, isLastChunk);
}
@ -370,8 +368,7 @@ public class HlsChunkSource {
int responseCode = responseCodeException.responseCode;
if (responseCode == 404 || responseCode == 410) {
MediaPlaylistChunk playlistChunk = (MediaPlaylistChunk) chunk;
mediaPlaylistBlacklistFlags[playlistChunk.variantIndex] = true;
mediaPlaylistBlacklistedTimeMs[playlistChunk.variantIndex] = SystemClock.elapsedRealtime();
mediaPlaylistBlacklistTimesMs[playlistChunk.variantIndex] = SystemClock.elapsedRealtime();
if (!allPlaylistsBlacklisted()) {
// We've handled the 404/410 by blacklisting the playlist.
Log.w(TAG, "Blacklisted playlist (" + responseCode + "): "
@ -381,8 +378,7 @@ public class HlsChunkSource {
// This was the last non-blacklisted playlist. Don't blacklist it.
Log.w(TAG, "Final playlist not blacklisted (" + responseCode + "): "
+ playlistChunk.dataSpec.uri);
mediaPlaylistBlacklistFlags[playlistChunk.variantIndex] = false;
mediaPlaylistBlacklistedTimeMs[playlistChunk.variantIndex] = 0;
mediaPlaylistBlacklistTimesMs[playlistChunk.variantIndex] = 0;
return false;
}
}
@ -392,19 +388,27 @@ public class HlsChunkSource {
private int getNextVariantIndex(TsChunk previousTsChunk, long playbackPositionUs) {
clearStaleBlacklistedPlaylists();
if (previousTsChunk == null) {
// Don't consider switching if we don't have a previous chunk.
return variantIndex;
}
long bitrateEstimate = bandwidthMeter.getBitrateEstimate();
if (bitrateEstimate == BandwidthMeter.NO_ESTIMATE) {
// Don't consider switching if we don't have a bandwidth estimate.
return variantIndex;
}
int idealVariantIndex = getVariantIndexForBandwdith(
(int) (bandwidthMeter.getBitrateEstimate() * BANDWIDTH_FRACTION));
(int) (bitrateEstimate * BANDWIDTH_FRACTION));
if (idealVariantIndex == variantIndex) {
// We're already using the ideal variant.
return variantIndex;
}
// We're not using the ideal variant for the available bandwidth, but only switch if the
// conditions are appropriate.
long bufferedPositionUs = previousTsChunk == null ? playbackPositionUs
: adaptiveMode == ADAPTIVE_MODE_SPLICE ? previousTsChunk.startTimeUs
long bufferedPositionUs = adaptiveMode == ADAPTIVE_MODE_SPLICE ? previousTsChunk.startTimeUs
: previousTsChunk.endTimeUs;
long bufferedUs = bufferedPositionUs - playbackPositionUs;
if (mediaPlaylistBlacklistFlags[variantIndex]
if (mediaPlaylistBlacklistTimesMs[variantIndex] != 0
|| (idealVariantIndex > variantIndex && bufferedUs < maxBufferDurationToSwitchDownUs)
|| (idealVariantIndex < variantIndex && bufferedUs > minBufferDurationToSwitchUpUs)) {
// Switch variant.
@ -417,7 +421,7 @@ public class HlsChunkSource {
private int getVariantIndexForBandwdith(int bandwidth) {
int lowestQualityEnabledVariant = 0;
for (int i = 0; i < enabledVariants.length; i++) {
if (!mediaPlaylistBlacklistFlags[i]) {
if (mediaPlaylistBlacklistTimesMs[i] == 0) {
if (enabledVariants[i].bandwidth <= bandwidth) {
return i;
}
@ -443,14 +447,15 @@ public class HlsChunkSource {
}
private MediaPlaylistChunk newMediaPlaylistChunk(int variantIndex) {
Uri mediaPlaylistUri = Util.getMergedUri(baseUri, enabledVariants[variantIndex].url);
DataSpec dataSpec = new DataSpec(mediaPlaylistUri, 0, C.LENGTH_UNBOUNDED, null);
Uri mediaPlaylistUri = UriUtil.resolveToUri(baseUri, enabledVariants[variantIndex].url);
DataSpec dataSpec = new DataSpec(mediaPlaylistUri, 0, C.LENGTH_UNBOUNDED, null,
DataSpec.FLAG_ALLOW_GZIP);
return new MediaPlaylistChunk(variantIndex, upstreamDataSource, dataSpec,
mediaPlaylistUri.toString());
}
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);
}
@ -468,7 +473,7 @@ public class HlsChunkSource {
System.arraycopy(ivData, offset, ivDataWithPadding, ivDataWithPadding.length - ivData.length
+ offset, ivData.length - offset);
encryptedDataSource = new Aes128DataSource(secretKey, ivDataWithPadding, upstreamDataSource);
encryptedDataSource = new Aes128DataSource(upstreamDataSource, secretKey, ivDataWithPadding);
encryptionKeyUri = keyUri;
encryptedDataSourceIv = iv;
encryptedDataSourceSecretKey = secretKey;
@ -545,8 +550,8 @@ public class HlsChunkSource {
}
private boolean allPlaylistsBlacklisted() {
for (int i = 0; i < mediaPlaylistBlacklistFlags.length; i++) {
if (!mediaPlaylistBlacklistFlags[i]) {
for (int i = 0; i < mediaPlaylistBlacklistTimesMs.length; i++) {
if (mediaPlaylistBlacklistTimesMs[i] == 0) {
return false;
}
}
@ -555,11 +560,10 @@ public class HlsChunkSource {
private void clearStaleBlacklistedPlaylists() {
long currentTime = SystemClock.elapsedRealtime();
for (int i = 0; i < mediaPlaylistBlacklistFlags.length; i++) {
if (mediaPlaylistBlacklistFlags[i] &&
currentTime - mediaPlaylistBlacklistedTimeMs[i] > DEFAULT_MAX_TIME_MEDIA_PLAYLIST_BLACKLISTED_MS) {
mediaPlaylistBlacklistFlags[i] = false;
mediaPlaylistBlacklistedTimeMs[i] = 0;
for (int i = 0; i < mediaPlaylistBlacklistTimesMs.length; i++) {
if (mediaPlaylistBlacklistTimesMs[i] != 0
&& currentTime - mediaPlaylistBlacklistTimesMs[i] > DEFAULT_PLAYLIST_BLACKLIST_MS) {
mediaPlaylistBlacklistTimesMs[i] = 0;
}
}
}

View file

@ -13,27 +13,44 @@
* See the License for the specific language governing permissions and
* 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.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;
/**
* 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 abstract class HlsExtractor {
public final class HlsExtractorWrapper implements ExtractorOutput {
private final BufferPool bufferPool;
private final Extractor extractor;
private final SparseArray<DefaultTrackOutput> sampleQueues;
private final boolean shouldSpliceIn;
private volatile boolean tracksBuilt;
// Accessed only by the consuming thread.
private boolean prepared;
private boolean spliceConfigured;
public HlsExtractor(boolean shouldSpliceIn) {
public HlsExtractorWrapper(BufferPool bufferPool, Extractor extractor, boolean shouldSpliceIn) {
this.bufferPool = bufferPool;
this.extractor = extractor;
this.shouldSpliceIn = shouldSpliceIn;
sampleQueues = new SparseArray<DefaultTrackOutput>();
extractor.init(this);
}
/**
@ -50,7 +67,7 @@ public abstract class HlsExtractor {
*
* @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()) {
// 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.
@ -59,7 +76,9 @@ public abstract class HlsExtractor {
boolean spliceConfigured = true;
int trackCount = getTrackCount();
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;
return;
@ -72,7 +91,9 @@ public abstract class HlsExtractor {
*
* @return The number of available tracks.
*/
public abstract int getTrackCount();
public int getTrackCount() {
return sampleQueues.size();
}
/**
* Gets the format of the specified track.
@ -82,28 +103,49 @@ public abstract class HlsExtractor {
* @param track The track index.
* @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.
*
* @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.
* <p>
* This method should not be called whilst {@link #read(DataSource)} is also being invoked.
* Clears queues for all tracks, returning all allocations to the buffer pool.
*/
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.
*
* @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.
@ -112,7 +154,10 @@ public abstract class HlsExtractor {
* @param holder A {@link SampleHolder} into which the sample should be read.
* @return True if a sample was read. False otherwise.
*/
public abstract boolean getSample(int track, SampleHolder holder);
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.
@ -120,7 +165,10 @@ public abstract class HlsExtractor {
* @param track The track from which samples should be discarded.
* @param timeUs The time up to which samples should be discarded, in microseconds.
*/
public abstract void discardUntil(int track, long timeUs);
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
@ -129,23 +177,36 @@ public abstract class HlsExtractor {
* @return True if samples are available for reading from {@link #getSample(int, SampleHolder)}
* for the specified track. False otherwise.
*/
public abstract boolean hasSamples(int track);
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.
* @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;
}
/**
* Gets the {@link SampleQueue} for the specified track.
*
* @param track The track index.
* @return The corresponding sample queue.
*/
protected abstract SampleQueue getSampleQueue(int track);
// ExtractorOutput implementation.
@Override
public TrackOutput track(int id) {
DefaultTrackOutput sampleQueue = new DefaultTrackOutput(bufferPool);
sampleQueues.put(id, sampleQueue);
return sampleQueue;
}
@Override
public void endTracks() {
this.tracksBuilt = true;
}
}

View file

@ -15,8 +15,6 @@
*/
package com.google.android.exoplayer.hls;
import android.net.Uri;
import java.util.List;
/**
@ -25,10 +23,12 @@ import java.util.List;
public final class HlsMasterPlaylist extends HlsPlaylist {
public final List<Variant> variants;
public final List<Subtitle> subtitles;
public HlsMasterPlaylist(Uri baseUri, List<Variant> variants) {
public HlsMasterPlaylist(String baseUri, List<Variant> variants, List<Subtitle> subtitles) {
super(baseUri, HlsPlaylist.TYPE_MASTER);
this.variants = variants;
this.subtitles = subtitles;
}
}

View file

@ -17,8 +17,6 @@ package com.google.android.exoplayer.hls;
import com.google.android.exoplayer.C;
import android.net.Uri;
import java.util.List;
/**
@ -30,24 +28,25 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
* Media segment reference.
*/
public static final class Segment implements Comparable<Long> {
public final boolean discontinuity;
public final double durationSecs;
public final String url;
public final long startTimeUs;
public final String encryptionMethod;
public final boolean isEncrypted;
public final String encryptionKeyUri;
public final String encryptionIV;
public final int byterangeOffset;
public final int byterangeLength;
public Segment(String uri, double durationSecs, boolean discontinuity, long startTimeUs,
String encryptionMethod, String encryptionKeyUri, String encryptionIV,
int byterangeOffset, int byterangeLength) {
boolean isEncrypted, String encryptionKeyUri, String encryptionIV, int byterangeOffset,
int byterangeLength) {
this.url = uri;
this.durationSecs = durationSecs;
this.discontinuity = discontinuity;
this.startTimeUs = startTimeUs;
this.encryptionMethod = encryptionMethod;
this.isEncrypted = isEncrypted;
this.encryptionKeyUri = encryptionKeyUri;
this.encryptionIV = encryptionIV;
this.byterangeOffset = byterangeOffset;
@ -70,7 +69,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
public final boolean live;
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) {
super(baseUri, HlsPlaylist.TYPE_MEDIA);
this.mediaSequence = mediaSequence;

View file

@ -23,7 +23,10 @@ import java.util.regex.Pattern;
/**
* 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() {}
@ -36,14 +39,6 @@ import java.util.regex.Pattern;
throw new ParserException(String.format("Couldn't match %s tag in %s", tag, line));
}
public static String parseOptionalStringAttr(String line, Pattern pattern) {
Matcher matcher = pattern.matcher(line);
if (matcher.find() && matcher.groupCount() == 1) {
return matcher.group(1);
}
return null;
}
public static int parseIntAttr(String line, Pattern pattern, String tag)
throws ParserException {
return Integer.parseInt(parseStringAttr(line, pattern, tag));
@ -54,4 +49,24 @@ import java.util.regex.Pattern;
return Double.parseDouble(parseStringAttr(line, pattern, tag));
}
public static String parseOptionalStringAttr(String line, Pattern pattern) {
Matcher matcher = pattern.matcher(line);
if (matcher.find() && matcher.groupCount() == 1) {
return matcher.group(1);
}
return null;
}
public static boolean parseOptionalBooleanAttr(String line, Pattern pattern) {
Matcher matcher = pattern.matcher(line);
if (matcher.find() && matcher.groupCount() == 1) {
return BOOLEAN_YES.equals(matcher.group(1));
}
return false;
}
public static Pattern compileBooleanAttrPattern(String attrName) {
return Pattern.compile(attrName + "=(" + BOOLEAN_YES + "|" + BOOLEAN_NO + ")");
}
}

View file

@ -15,7 +15,6 @@
*/
package com.google.android.exoplayer.hls;
import android.net.Uri;
/**
* Represents an HLS playlist.
@ -25,10 +24,10 @@ public abstract class HlsPlaylist {
public final static int TYPE_MASTER = 0;
public final static int TYPE_MEDIA = 1;
public final Uri baseUri;
public final String baseUri;
public final int type;
protected HlsPlaylist(Uri baseUri, int type) {
protected HlsPlaylist(String baseUri, int type) {
this.baseUri = baseUri;
this.type = type;
}

View file

@ -19,9 +19,6 @@ import com.google.android.exoplayer.C;
import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.hls.HlsMediaPlaylist.Segment;
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.IOException;
@ -40,12 +37,8 @@ import java.util.regex.Pattern;
public final class HlsPlaylistParser implements NetworkLoadable.Parser<HlsPlaylist> {
private static final String VERSION_TAG = "#EXT-X-VERSION";
private static final String STREAM_INF_TAG = "#EXT-X-STREAM-INF";
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 MEDIA_TAG = "#EXT-X-MEDIA";
private static final String DISCONTINUITY_TAG = "#EXT-X-DISCONTINUITY";
private static final String MEDIA_DURATION_TAG = "#EXTINF";
private static final String MEDIA_SEQUENCE_TAG = "#EXT-X-MEDIA-SEQUENCE";
@ -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 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 URI_ATTR = "URI";
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 =
Pattern.compile(BANDWIDTH_ATTR + "=(\\d+)\\b");
private static final Pattern CODECS_ATTR_REGEX =
Pattern.compile(CODECS_ATTR + "=\"(.+?)\"");
private static final Pattern RESOLUTION_ATTR_REGEX =
Pattern.compile(RESOLUTION_ATTR + "=(\\d+x\\d+)");
private static final Pattern MEDIA_DURATION_REGEX =
Pattern.compile(MEDIA_DURATION_TAG + ":([\\d.]+),");
private static final Pattern MEDIA_SEQUENCE_REGEX =
@ -77,16 +85,26 @@ public final class HlsPlaylistParser implements NetworkLoadable.Parser<HlsPlayli
Pattern.compile(BYTERANGE_TAG + ":(\\d+(?:@\\d+)?)\\b");
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 =
Pattern.compile(URI_ATTR + "=\"(.+)\"");
private static final Pattern IV_ATTR_REGEX =
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
public HlsPlaylist parse(String connectionUrl, InputStream inputStream)
throws IOException, ParserException {
Uri baseUri = Util.parseBaseUri(connectionUrl);
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
Queue<String> extraLines = new LinkedList<String>();
String line;
@ -97,7 +115,7 @@ public final class HlsPlaylistParser implements NetworkLoadable.Parser<HlsPlayli
// Do nothing.
} else if (line.startsWith(STREAM_INF_TAG)) {
extraLines.add(line);
return parseMasterPlaylist(new LineIterator(extraLines, reader), baseUri);
return parseMasterPlaylist(new LineIterator(extraLines, reader), connectionUrl);
} else if (line.startsWith(TARGET_DURATION_TAG)
|| line.startsWith(MEDIA_SEQUENCE_TAG)
|| line.startsWith(MEDIA_DURATION_TAG)
@ -106,11 +124,9 @@ public final class HlsPlaylistParser implements NetworkLoadable.Parser<HlsPlayli
|| line.equals(DISCONTINUITY_TAG)
|| line.equals(ENDLIST_TAG)) {
extraLines.add(line);
return parseMediaPlaylist(new LineIterator(extraLines, reader), baseUri);
} else if (line.startsWith(VERSION_TAG)) {
return parseMediaPlaylist(new LineIterator(extraLines, reader), connectionUrl);
} else {
extraLines.add(line);
} else if (!line.startsWith("#")) {
throw new ParserException("Missing a tag before URL.");
}
}
} finally {
@ -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.");
}
private static HlsMasterPlaylist parseMasterPlaylist(LineIterator iterator, Uri baseUri)
private static HlsMasterPlaylist parseMasterPlaylist(LineIterator iterator, String baseUri)
throws IOException {
List<Variant> variants = new ArrayList<Variant>();
ArrayList<Variant> variants = new ArrayList<Variant>();
ArrayList<Subtitle> subtitles = new ArrayList<Subtitle>();
int bandwidth = 0;
String[] codecs = null;
int width = -1;
int height = -1;
int variantIndex = 0;
boolean expectingStreamInfUrl = false;
String line;
while (iterator.hasNext()) {
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);
String codecsString = HlsParserUtil.parseOptionalStringAttr(line, CODECS_ATTR_REGEX);
if (codecsString != null) {
@ -149,18 +180,21 @@ public final class HlsPlaylistParser implements NetworkLoadable.Parser<HlsPlayli
width = -1;
height = -1;
}
} else if (!line.startsWith("#")) {
expectingStreamInfUrl = true;
} else if (!line.startsWith("#") && expectingStreamInfUrl) {
variants.add(new Variant(variantIndex++, line, bandwidth, codecs, width, height));
bandwidth = 0;
codecs = null;
width = -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 {
int mediaSequence = 0;
int targetDurationSecs = 0;
@ -171,14 +205,14 @@ public final class HlsPlaylistParser implements NetworkLoadable.Parser<HlsPlayli
double segmentDurationSecs = 0.0;
boolean segmentDiscontinuity = false;
long segmentStartTimeUs = 0;
String segmentEncryptionMethod = null;
String segmentEncryptionKeyUri = null;
String segmentEncryptionIV = null;
int segmentByterangeOffset = 0;
int segmentByterangeLength = C.LENGTH_UNBOUNDED;
int segmentMediaSequence = 0;
boolean isEncrypted = false;
String encryptionKeyUri = null;
String encryptionIV = null;
String line;
while (iterator.hasNext()) {
line = iterator.next();
@ -194,18 +228,14 @@ public final class HlsPlaylistParser implements NetworkLoadable.Parser<HlsPlayli
segmentDurationSecs = HlsParserUtil.parseDoubleAttr(line, MEDIA_DURATION_REGEX,
MEDIA_DURATION_TAG);
} else if (line.startsWith(KEY_TAG)) {
segmentEncryptionMethod = HlsParserUtil.parseStringAttr(line, METHOD_ATTR_REGEX,
METHOD_ATTR);
if (segmentEncryptionMethod.equals(HlsMediaPlaylist.ENCRYPTION_METHOD_NONE)) {
segmentEncryptionKeyUri = null;
segmentEncryptionIV = null;
String method = HlsParserUtil.parseStringAttr(line, METHOD_ATTR_REGEX, METHOD_ATTR);
isEncrypted = METHOD_AES128.equals(method);
if (isEncrypted) {
encryptionKeyUri = HlsParserUtil.parseStringAttr(line, URI_ATTR_REGEX, URI_ATTR);
encryptionIV = HlsParserUtil.parseOptionalStringAttr(line, IV_ATTR_REGEX);
} else {
segmentEncryptionKeyUri = HlsParserUtil.parseStringAttr(line, URI_ATTR_REGEX,
URI_ATTR);
segmentEncryptionIV = HlsParserUtil.parseOptionalStringAttr(line, IV_ATTR_REGEX);
if (segmentEncryptionIV == null) {
segmentEncryptionIV = Integer.toHexString(segmentMediaSequence);
}
encryptionKeyUri = null;
encryptionIV = null;
}
} else if (line.startsWith(BYTERANGE_TAG)) {
String byteRange = HlsParserUtil.parseStringAttr(line, BYTERANGE_REGEX, BYTERANGE_TAG);
@ -217,13 +247,21 @@ public final class HlsPlaylistParser implements NetworkLoadable.Parser<HlsPlayli
} else if (line.equals(DISCONTINUITY_TAG)) {
segmentDiscontinuity = true;
} else if (!line.startsWith("#")) {
String segmentEncryptionIV;
if (!isEncrypted) {
segmentEncryptionIV = null;
} else if (encryptionIV != null) {
segmentEncryptionIV = encryptionIV;
} else {
segmentEncryptionIV = Integer.toHexString(segmentMediaSequence);
}
segmentMediaSequence++;
if (segmentByterangeLength == C.LENGTH_UNBOUNDED) {
segmentByterangeOffset = 0;
}
segments.add(new Segment(line, segmentDurationSecs, segmentDiscontinuity,
segmentStartTimeUs, segmentEncryptionMethod, segmentEncryptionKeyUri,
segmentEncryptionIV, segmentByterangeOffset, segmentByterangeLength));
segmentStartTimeUs, isEncrypted, encryptionKeyUri, segmentEncryptionIV,
segmentByterangeOffset, segmentByterangeLength));
segmentStartTimeUs += (long) (segmentDurationSecs * C.MICROS_PER_SECOND);
segmentDiscontinuity = false;
segmentDurationSecs = 0.0;

View file

@ -15,13 +15,13 @@
*/
package com.google.android.exoplayer.hls;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.MediaFormatHolder;
import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.SampleSource;
import com.google.android.exoplayer.TrackInfo;
import com.google.android.exoplayer.TrackRenderer;
import com.google.android.exoplayer.hls.parser.HlsExtractor;
import com.google.android.exoplayer.upstream.Loader;
import com.google.android.exoplayer.upstream.Loader.Loadable;
import com.google.android.exoplayer.util.Assertions;
@ -44,7 +44,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
private static final int NO_RESET_PENDING = -1;
private final HlsChunkSource chunkSource;
private final LinkedList<HlsExtractor> extractors;
private final LinkedList<HlsExtractorWrapper> extractors;
private final boolean frameAccurateSeeking;
private final int minLoadableRetryCount;
@ -83,7 +83,8 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
this.frameAccurateSeeking = frameAccurateSeeking;
this.remainingReleaseCount = downstreamRendererCount;
this.minLoadableRetryCount = minLoadableRetryCount;
extractors = new LinkedList<HlsExtractor>();
this.pendingResetPositionUs = NO_RESET_PENDING;
extractors = new LinkedList<HlsExtractorWrapper>();
}
@Override
@ -96,7 +97,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
}
continueBufferingInternal();
if (!extractors.isEmpty()) {
HlsExtractor extractor = extractors.getFirst();
HlsExtractorWrapper extractor = extractors.getFirst();
if (extractor.isPrepared()) {
trackCount = extractor.getTrackCount();
trackEnabledStates = new boolean[trackCount];
@ -190,12 +191,16 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
return DISCONTINUITY_READ;
}
if (onlyReadDiscontinuity || isPendingReset() || extractors.isEmpty()) {
if (onlyReadDiscontinuity) {
return NOTHING_READ;
}
if (isPendingReset()) {
maybeThrowLoadableException();
return NOTHING_READ;
}
HlsExtractor extractor = getCurrentExtractor();
HlsExtractorWrapper extractor = getCurrentExtractor();
if (extractors.size() > 1) {
// If there's more than one extractor, attempt to configure a seamless splice from the
// current one to the next one.
@ -223,7 +228,8 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
}
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;
}
@ -240,10 +246,11 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
Assertions.checkState(prepared);
Assertions.checkState(enabledTrackCount > 0);
lastSeekPositionUs = positionUs;
if (pendingResetPositionUs == positionUs || downstreamPositionUs == positionUs) {
downstreamPositionUs = positionUs;
if ((isPendingReset() ? pendingResetPositionUs : downstreamPositionUs) == positionUs) {
return;
}
// TODO: Optimize the seek for the case where the position is already buffered.
downstreamPositionUs = positionUs;
for (int i = 0; i < pendingDiscontinuities.length; i++) {
pendingDiscontinuities[i] = true;
@ -260,9 +267,9 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
} else if (loadingFinished) {
return TrackRenderer.END_OF_TRACK_US;
} else {
long largestSampleTimestamp = extractors.getLast().getLargestSampleTimestamp();
return largestSampleTimestamp == Long.MIN_VALUE ? downstreamPositionUs
: largestSampleTimestamp;
long largestParsedTimestampUs = extractors.getLast().getLargestParsedTimestampUs();
return largestParsedTimestampUs == Long.MIN_VALUE ? downstreamPositionUs
: largestParsedTimestampUs;
}
}
@ -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.
*/
private HlsExtractor getCurrentExtractor() {
HlsExtractor extractor = extractors.getFirst();
private HlsExtractorWrapper getCurrentExtractor() {
HlsExtractorWrapper extractor = extractors.getFirst();
while (extractors.size() > 1 && !haveSamplesForEnabledTracks(extractor)) {
// We're finished reading from the extractor for all tracks, and so can discard it.
extractors.removeFirst().release();
extractors.removeFirst().clear();
extractor = extractors.getFirst();
}
return extractor;
}
private void discardSamplesForDisabledTracks(HlsExtractor extractor, long timeUs) {
private void discardSamplesForDisabledTracks(HlsExtractorWrapper extractor, long timeUs) {
if (!extractor.isPrepared()) {
return;
}
@ -349,7 +356,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
}
}
private boolean haveSamplesForEnabledTracks(HlsExtractor extractor) {
private boolean haveSamplesForEnabledTracks(HlsExtractorWrapper extractor) {
if (!extractor.isPrepared()) {
return false;
}
@ -381,7 +388,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
private void clearState() {
for (int i = 0; i < extractors.size(); i++) {
extractors.get(i).release();
extractors.get(i).clear();
}
extractors.clear();
clearCurrentLoadable();

View file

@ -0,0 +1,37 @@
/*
* 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;
}
}

View file

@ -15,7 +15,9 @@
*/
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.DataSpec;
@ -26,8 +28,6 @@ import java.io.IOException;
*/
public final class TsChunk extends HlsChunk {
private static final byte[] SCRATCH_SPACE = new byte[4096];
/**
* The index of the variant in the master playlist.
*/
@ -51,7 +51,7 @@ public final class TsChunk extends HlsChunk {
/**
* The extractor into which this chunk is being consumed.
*/
public final HlsExtractor extractor;
public final HlsExtractorWrapper extractor;
private int loadPosition;
private volatile boolean loadFinished;
@ -67,7 +67,7 @@ public final class TsChunk extends HlsChunk {
* @param chunkIndex The index of the chunk.
* @param isLastChunk True if this is the last chunk in the media. False otherwise.
*/
public TsChunk(DataSource dataSource, DataSpec dataSpec, HlsExtractor extractor,
public TsChunk(DataSource dataSource, DataSpec dataSpec, HlsExtractorWrapper extractor,
int variantIndex, long startTimeUs, long endTimeUs, int chunkIndex, boolean isLastChunk) {
super(dataSource, dataSpec);
this.extractor = extractor;
@ -102,30 +102,23 @@ public final class TsChunk extends HlsChunk {
@Override
public void load() throws IOException, InterruptedException {
ExtractorInput input;
try {
dataSource.open(dataSpec);
int bytesRead = 0;
int bytesSkipped = 0;
input = new DefaultExtractorInput(dataSource, 0, dataSource.open(dataSpec));
// If we previously fed part of this chunk to the extractor, skip it this time.
// TODO: Ideally we'd construct a dataSpec that only loads the remainder of the data here,
// rather than loading the whole chunk again and then skipping data we previously loaded. To
// do this is straightforward for non-encrypted content, but more complicated for content
// encrypted with AES, for which we'll need to modify the way that decryption is performed.
while (bytesRead != -1 && !loadCanceled && bytesSkipped < loadPosition) {
int skipLength = Math.min(loadPosition - bytesSkipped, SCRATCH_SPACE.length);
bytesRead = dataSource.read(SCRATCH_SPACE, 0, skipLength);
if (bytesRead != -1) {
bytesSkipped += bytesRead;
input.skipFully(loadPosition);
try {
int result = Extractor.RESULT_CONTINUE;
while (result == Extractor.RESULT_CONTINUE && !loadCanceled) {
result = extractor.read(input);
}
} finally {
loadPosition = (int) input.getPosition();
}
// Feed the remaining data into the extractor.
while (bytesRead != -1 && !loadCanceled) {
bytesRead = extractor.read(dataSource);
if (bytesRead != -1) {
loadPosition += bytesRead;
}
}
loadFinished = !loadCanceled;
} finally {
dataSource.close();
}

View file

@ -1,126 +0,0 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.hls.parser;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.upstream.BufferPool;
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.ParsableByteArray;
import java.io.IOException;
/**
* Facilitates the extraction of AAC samples from elementary audio files formatted as AAC with ADTS
* headers.
*/
public class AdtsExtractor extends HlsExtractor {
private static final int MAX_PACKET_SIZE = 200;
private final long firstSampleTimestamp;
private final ParsableByteArray packetBuffer;
private final AdtsReader adtsReader;
// Accessed only by the loading thread.
private boolean firstPacket;
// Accessed by both the loading and consuming threads.
private volatile boolean prepared;
public AdtsExtractor(boolean shouldSpliceIn, long firstSampleTimestamp, BufferPool bufferPool) {
super(shouldSpliceIn);
this.firstSampleTimestamp = firstSampleTimestamp;
packetBuffer = new ParsableByteArray(MAX_PACKET_SIZE);
adtsReader = new AdtsReader(bufferPool);
firstPacket = true;
}
@Override
public int getTrackCount() {
Assertions.checkState(prepared);
return 1;
}
@Override
public MediaFormat getFormat(int track) {
Assertions.checkState(prepared);
return adtsReader.getMediaFormat();
}
@Override
public boolean isPrepared() {
return prepared;
}
@Override
public void release() {
adtsReader.release();
}
@Override
public long getLargestSampleTimestamp() {
return adtsReader.getLargestParsedTimestampUs();
}
@Override
public boolean getSample(int track, SampleHolder holder) {
Assertions.checkState(prepared);
Assertions.checkState(track == 0);
return adtsReader.getSample(holder);
}
@Override
public void discardUntil(int track, long timeUs) {
Assertions.checkState(prepared);
Assertions.checkState(track == 0);
adtsReader.discardUntil(timeUs);
}
@Override
public boolean hasSamples(int track) {
Assertions.checkState(prepared);
Assertions.checkState(track == 0);
return !adtsReader.isEmpty();
}
@Override
public int read(DataSource dataSource) throws IOException {
int bytesRead = dataSource.read(packetBuffer.data, 0, MAX_PACKET_SIZE);
if (bytesRead == -1) {
return -1;
}
packetBuffer.setPosition(0);
packetBuffer.setLimit(bytesRead);
// TODO: Make it possible for adtsReader to consume the dataSource directly, so that it becomes
// unnecessary to copy the data through packetBuffer.
adtsReader.consume(packetBuffer, firstSampleTimestamp, firstPacket);
firstPacket = false;
if (!prepared) {
prepared = adtsReader.hasMediaFormat();
}
return bytesRead;
}
@Override
protected SampleQueue getSampleQueue(int track) {
Assertions.checkState(track == 0);
return adtsReader;
}
}

View file

@ -1,303 +0,0 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.hls.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;
}
}
}
}
}

View file

@ -0,0 +1,38 @@
/*
* 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;
}
}

View file

@ -29,6 +29,11 @@ import java.util.Map;
*/
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
public boolean canParse(String mimeType) {
return mimeType.equals(MimeTypes.APPLICATION_ID3);
@ -60,13 +65,48 @@ public class Id3Parser implements MetadataParser<Map<String, Object>> {
byte[] frame = new byte[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);
int valueStartIndex = indexOfNot(frame, firstZeroIndex, (byte) 0);
int valueEndIndex = indexOf(frame, valueStartIndex, (byte) 0);
int valueStartIndex = firstZeroIndex + delimiterLength(encoding);
int valueEndIndex = indexOfEOS(frame, valueStartIndex, encoding);
String value = new String(frame, valueStartIndex, valueEndIndex - valueStartIndex,
charset);
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 {
String type = String.format("%c%c%c%c", frameId0, frameId1, frameId2, frameId3);
byte[] frame = new byte[frameSize];
@ -89,15 +129,30 @@ public class Id3Parser implements MetadataParser<Map<String, Object>> {
return data.length;
}
private static int indexOfNot(byte[] data, int fromIndex, byte key) {
for (int i = fromIndex; i < data.length; i++) {
if (data[i] != key) {
return i;
}
private static int indexOfEOS(byte[] data, int fromIndex, int encodingByte) {
int terminationPos = indexOf(data, fromIndex, (byte) 0);
// 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;
}
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.
*
@ -142,13 +197,13 @@ public class Id3Parser implements MetadataParser<Map<String, Object>> {
*/
private static String getCharsetName(int encodingByte) {
switch (encodingByte) {
case 0:
case ID3_TEXT_ENCODING_ISO_8859_1:
return "ISO-8859-1";
case 1:
case ID3_TEXT_ENCODING_UTF_16:
return "UTF-16";
case 2:
case ID3_TEXT_ENCODING_UTF_16BE:
return "UTF-16BE";
case 3:
case ID3_TEXT_ENCODING_UTF_8:
return "UTF-8";
default:
return "ISO-8859-1";

View file

@ -0,0 +1,34 @@
/*
* 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;
}
}

View file

@ -24,6 +24,19 @@ import java.util.List;
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_avc3 = getAtomTypeInteger("avc3");
public static final int TYPE_esds = getAtomTypeInteger("esds");
@ -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) {
return "" + (char) (type >> 24)
+ (char) ((type >> 16) & 0xFF)

View file

@ -20,6 +20,7 @@ import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.chunk.parser.mp4.TrackEncryptionBox;
import com.google.android.exoplayer.util.Assertions;
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.ParsableByteArray;
import com.google.android.exoplayer.util.Util;
@ -67,7 +68,7 @@ public final class CommonMp4AtomParsers {
long mediaTimescale = parseMdhd(mdia.getLeafAtomOfType(Atom.TYPE_mdhd).data);
Pair<MediaFormat, TrackEncryptionBox[]> sampleDescriptions =
parseStsd(stbl.getLeafAtomOfType(Atom.TYPE_stsd).data);
parseStsd(stbl.getLeafAtomOfType(Atom.TYPE_stsd).data, durationUs);
return new Track(id, trackType, mediaTimescale, durationUs, sampleDescriptions.first,
sampleDescriptions.second);
}
@ -102,7 +103,7 @@ public final class CommonMp4AtomParsers {
ParsableByteArray ctts = cttsAtom != null ? cttsAtom.data : null;
// Skip full atom.
stsz.setPosition(Mp4Util.FULL_ATOM_HEADER_SIZE);
stsz.setPosition(Atom.FULL_ATOM_HEADER_SIZE);
int fixedSampleSize = stsz.readUnsignedIntToInt();
int sampleCount = stsz.readUnsignedIntToInt();
@ -112,10 +113,10 @@ public final class CommonMp4AtomParsers {
int[] flags = new int[sampleCount];
// Prepare to read chunk offsets.
chunkOffsets.setPosition(Mp4Util.FULL_ATOM_HEADER_SIZE);
chunkOffsets.setPosition(Atom.FULL_ATOM_HEADER_SIZE);
int chunkCount = chunkOffsets.readUnsignedIntToInt();
stsc.setPosition(Mp4Util.FULL_ATOM_HEADER_SIZE);
stsc.setPosition(Atom.FULL_ATOM_HEADER_SIZE);
int remainingSamplesPerChunkChanges = stsc.readUnsignedIntToInt() - 1;
Assertions.checkState(stsc.readInt() == 1, "stsc first chunk must be 1");
int samplesPerChunk = stsc.readUnsignedIntToInt();
@ -130,7 +131,7 @@ public final class CommonMp4AtomParsers {
int remainingSamplesInChunk = samplesPerChunk;
// 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 remainingSamplesAtTimestampDelta = stts.readUnsignedIntToInt();
int timestampDeltaInTimeUnits = stts.readUnsignedIntToInt();
@ -141,8 +142,8 @@ public final class CommonMp4AtomParsers {
int remainingTimestampOffsetChanges = 0;
int timestampOffset = 0;
if (ctts != null) {
ctts.setPosition(Mp4Util.ATOM_HEADER_SIZE);
cttsHasSignedOffsets = Mp4Util.parseFullAtomVersion(ctts.readInt()) == 1;
ctts.setPosition(Atom.ATOM_HEADER_SIZE);
cttsHasSignedOffsets = Atom.parseFullAtomVersion(ctts.readInt()) == 1;
remainingTimestampOffsetChanges = ctts.readUnsignedIntToInt() - 1;
remainingSamplesAtTimestampOffset = ctts.readUnsignedIntToInt();
timestampOffset = cttsHasSignedOffsets ? ctts.readInt() : ctts.readUnsignedIntToInt();
@ -151,7 +152,7 @@ public final class CommonMp4AtomParsers {
int nextSynchronizationSampleIndex = -1;
int remainingSynchronizationSamples = 0;
if (stss != null) {
stss.setPosition(Mp4Util.FULL_ATOM_HEADER_SIZE);
stss.setPosition(Atom.FULL_ATOM_HEADER_SIZE);
remainingSynchronizationSamples = stss.readUnsignedIntToInt();
nextSynchronizationSampleIndex = stss.readUnsignedIntToInt() - 1;
}
@ -249,10 +250,10 @@ public final class CommonMp4AtomParsers {
* @return Timescale for the movie.
*/
private static long parseMvhd(ParsableByteArray mvhd) {
mvhd.setPosition(Mp4Util.ATOM_HEADER_SIZE);
mvhd.setPosition(Atom.ATOM_HEADER_SIZE);
int fullAtom = mvhd.readInt();
int version = Mp4Util.parseFullAtomVersion(fullAtom);
int version = Atom.parseFullAtomVersion(fullAtom);
mvhd.skip(version == 0 ? 8 : 16);
@ -266,9 +267,9 @@ public final class CommonMp4AtomParsers {
* the movie header box). The duration is set to -1 if the duration is unspecified.
*/
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 version = Mp4Util.parseFullAtomVersion(fullAtom);
int version = Atom.parseFullAtomVersion(fullAtom);
tkhd.skip(version == 0 ? 8 : 16);
@ -302,7 +303,7 @@ public final class CommonMp4AtomParsers {
* @return The track type.
*/
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();
}
@ -313,16 +314,17 @@ public final class CommonMp4AtomParsers {
* @return The media timescale, defined as the number of time units that pass in one second.
*/
private static long parseMdhd(ParsableByteArray mdhd) {
mdhd.setPosition(Mp4Util.ATOM_HEADER_SIZE);
mdhd.setPosition(Atom.ATOM_HEADER_SIZE);
int fullAtom = mdhd.readInt();
int version = Mp4Util.parseFullAtomVersion(fullAtom);
int version = Atom.parseFullAtomVersion(fullAtom);
mdhd.skip(version == 0 ? 8 : 16);
return mdhd.readUnsignedInt();
}
private static Pair<MediaFormat, TrackEncryptionBox[]> parseStsd(ParsableByteArray stsd) {
stsd.setPosition(Mp4Util.FULL_ATOM_HEADER_SIZE);
private static Pair<MediaFormat, TrackEncryptionBox[]> parseStsd(
ParsableByteArray stsd, long durationUs) {
stsd.setPosition(Atom.FULL_ATOM_HEADER_SIZE);
int numberOfEntries = stsd.readInt();
MediaFormat mediaFormat = null;
TrackEncryptionBox[] trackEncryptionBoxes = new TrackEncryptionBox[numberOfEntries];
@ -334,19 +336,19 @@ public final class CommonMp4AtomParsers {
if (childAtomType == Atom.TYPE_avc1 || childAtomType == Atom.TYPE_avc3
|| childAtomType == Atom.TYPE_encv) {
Pair<MediaFormat, TrackEncryptionBox> avc =
parseAvcFromParent(stsd, childStartPosition, childAtomSize);
parseAvcFromParent(stsd, childStartPosition, childAtomSize, durationUs);
mediaFormat = avc.first;
trackEncryptionBoxes[i] = avc.second;
} else if (childAtomType == Atom.TYPE_mp4a || childAtomType == Atom.TYPE_enca
|| childAtomType == Atom.TYPE_ac_3) {
Pair<MediaFormat, TrackEncryptionBox> audioSampleEntry =
parseAudioSampleEntry(stsd, childAtomType, childStartPosition, childAtomSize);
Pair<MediaFormat, TrackEncryptionBox> audioSampleEntry = parseAudioSampleEntry(stsd,
childAtomType, childStartPosition, childAtomSize, durationUs);
mediaFormat = audioSampleEntry.first;
trackEncryptionBoxes[i] = audioSampleEntry.second;
} else if (childAtomType == Atom.TYPE_TTML) {
mediaFormat = MediaFormat.createTtmlFormat();
} else if (childAtomType == Atom.TYPE_mp4v) {
mediaFormat = parseMp4vFromParent(stsd, childStartPosition, childAtomSize);
mediaFormat = parseMp4vFromParent(stsd, childStartPosition, childAtomSize, durationUs);
}
stsd.setPosition(childStartPosition + childAtomSize);
}
@ -355,8 +357,8 @@ public final class CommonMp4AtomParsers {
/** Returns the media format for an avc1 box. */
private static Pair<MediaFormat, TrackEncryptionBox> parseAvcFromParent(ParsableByteArray parent,
int position, int size) {
parent.setPosition(position + Mp4Util.ATOM_HEADER_SIZE);
int position, int size, long durationUs) {
parent.setPosition(position + Atom.ATOM_HEADER_SIZE);
parent.skip(24);
int width = parent.readUnsignedShort();
@ -388,12 +390,12 @@ public final class CommonMp4AtomParsers {
}
MediaFormat format = MediaFormat.createVideoFormat(MimeTypes.VIDEO_H264, MediaFormat.NO_VALUE,
width, height, pixelWidthHeightRatio, initializationData);
durationUs, width, height, pixelWidthHeightRatio, initializationData);
return Pair.create(format, trackEncryptionBox);
}
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)
int nalUnitLength = (parent.readUnsignedByte() & 0x3) + 1;
if (nalUnitLength != 4) {
@ -406,18 +408,18 @@ public final class CommonMp4AtomParsers {
// expose the AVC profile and level somewhere useful; Most likely in MediaFormat.
int numSequenceParameterSets = parent.readUnsignedByte() & 0x1F;
for (int j = 0; j < numSequenceParameterSets; j++) {
initializationData.add(Mp4Util.parseChildNalUnit(parent));
initializationData.add(H264Util.parseChildNalUnit(parent));
}
int numPictureParameterSets = parent.readUnsignedByte();
for (int j = 0; j < numPictureParameterSets; j++) {
initializationData.add(Mp4Util.parseChildNalUnit(parent));
initializationData.add(H264Util.parseChildNalUnit(parent));
}
return initializationData;
}
private static TrackEncryptionBox parseSinfFromParent(ParsableByteArray parent, int position,
int size) {
int childPosition = position + Mp4Util.ATOM_HEADER_SIZE;
int childPosition = position + Atom.ATOM_HEADER_SIZE;
TrackEncryptionBox trackEncryptionBox = null;
while (childPosition - position < size) {
@ -440,7 +442,7 @@ public final class CommonMp4AtomParsers {
}
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 vSpacing = parent.readUnsignedIntToInt();
return (float) hSpacing / vSpacing;
@ -448,7 +450,7 @@ public final class CommonMp4AtomParsers {
private static TrackEncryptionBox parseSchiFromParent(ParsableByteArray parent, int position,
int size) {
int childPosition = position + Mp4Util.ATOM_HEADER_SIZE;
int childPosition = position + Atom.ATOM_HEADER_SIZE;
while (childPosition - position < size) {
parent.setPosition(childPosition);
int childAtomSize = parent.readInt();
@ -468,9 +470,9 @@ public final class CommonMp4AtomParsers {
}
/** Returns the media format for an mp4v box. */
private static MediaFormat parseMp4vFromParent(ParsableByteArray parent,
int position, int size) {
parent.setPosition(position + Mp4Util.ATOM_HEADER_SIZE);
private static MediaFormat parseMp4vFromParent(ParsableByteArray parent, int position, int size,
long durationUs) {
parent.setPosition(position + Atom.ATOM_HEADER_SIZE);
parent.skip(24);
int width = parent.readUnsignedShort();
@ -492,12 +494,12 @@ public final class CommonMp4AtomParsers {
}
return MediaFormat.createVideoFormat(
MimeTypes.VIDEO_MP4V, MediaFormat.NO_VALUE, width, height, initializationData);
MimeTypes.VIDEO_MP4V, MediaFormat.NO_VALUE, durationUs, width, height, initializationData);
}
private static Pair<MediaFormat, TrackEncryptionBox> parseAudioSampleEntry(
ParsableByteArray parent, int atomType, int position, int size) {
parent.setPosition(position + Mp4Util.ATOM_HEADER_SIZE);
ParsableByteArray parent, int atomType, int position, int size, long durationUs) {
parent.setPosition(position + Atom.ATOM_HEADER_SIZE);
parent.skip(16);
int channelCount = parent.readUnsignedShort();
int sampleSize = parent.readUnsignedShort();
@ -555,14 +557,14 @@ public final class CommonMp4AtomParsers {
}
MediaFormat format = MediaFormat.createAudioFormat(
mimeType, sampleSize, channelCount, sampleRate, bitrate,
mimeType, sampleSize, durationUs, channelCount, sampleRate, bitrate,
initializationData == null ? null : Collections.singletonList(initializationData));
return Pair.create(format, trackEncryptionBox);
}
/** Returns codec-specific initialization data contained in an esds box. */
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)
parent.skip(1); // ES_Descriptor tag
int varIntByte = parent.readUnsignedByte();
@ -606,7 +608,7 @@ public final class CommonMp4AtomParsers {
private static Ac3Format parseAc3SpecificBoxFromParent(ParsableByteArray parent, int position) {
// 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)
int fscod = (parent.readUnsignedByte() & 0xC0) >> 6;
@ -644,7 +646,7 @@ public final class CommonMp4AtomParsers {
private static int parseEc3SpecificBoxFromParent(ParsableByteArray parent, int position) {
// 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.
return 0;
}

View file

@ -22,6 +22,9 @@ import com.google.android.exoplayer.util.Util;
/** Sample table for a track in an MP4 file. */
public final class Mp4TrackSampleTable {
/** Sample index when no sample is available. */
public static final int NO_SAMPLE = -1;
/** Sample offsets in bytes. */
public final long[] offsets;
/** Sample sizes in bytes. */
@ -53,7 +56,7 @@ public final class Mp4TrackSampleTable {
* timestamp, if one is available.
*
* @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) {
int startIndex = Util.binarySearchFloor(timestampsUs, timeUs, true, false);
@ -63,7 +66,7 @@ public final class Mp4TrackSampleTable {
}
}
return Mp4Util.NO_SAMPLE;
return NO_SAMPLE;
}
/**
@ -71,7 +74,7 @@ public final class Mp4TrackSampleTable {
* if one is available.
*
* @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) {
int startIndex = Util.binarySearchCeil(timestampsUs, timeUs, true, false);
@ -81,7 +84,7 @@ public final class Mp4TrackSampleTable {
}
}
return Mp4Util.NO_SAMPLE;
return NO_SAMPLE;
}
}

View file

@ -30,6 +30,7 @@ import com.google.android.exoplayer.chunk.MediaChunk;
import com.google.android.exoplayer.chunk.parser.Extractor;
import com.google.android.exoplayer.chunk.parser.mp4.FragmentedMp4Extractor;
import com.google.android.exoplayer.chunk.parser.mp4.TrackEncryptionBox;
import com.google.android.exoplayer.drm.DrmInitData;
import com.google.android.exoplayer.mp4.Track;
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.ProtectionElement;
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.StreamElement;
@ -38,6 +39,7 @@ import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DataSpec;
import com.google.android.exoplayer.util.CodecSpecificDataUtil;
import com.google.android.exoplayer.util.ManifestFetcher;
import com.google.android.exoplayer.util.MimeTypes;
import android.net.Uri;
import android.os.SystemClock;
@ -48,8 +50,6 @@ import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.UUID;
/**
* An {@link ChunkSource} for SmoothStreaming.
@ -71,7 +71,7 @@ public class SmoothStreamingChunkSource implements ChunkSource {
private final int maxHeight;
private final SparseArray<FragmentedMp4Extractor> extractors;
private final Map<UUID, byte[]> psshInfo;
private final DrmInitData drmInitData;
private final SmoothStreamingFormat[] formats;
private SmoothStreamingManifest currentManifest;
@ -143,9 +143,11 @@ public class SmoothStreamingChunkSource implements ChunkSource {
byte[] keyId = getKeyId(protectionElement.data);
trackEncryptionBoxes = new TrackEncryptionBox[1];
trackEncryptionBoxes[0] = new TrackEncryptionBox(true, INITIALIZATION_VECTOR_SIZE, keyId);
psshInfo = Collections.singletonMap(protectionElement.uuid, protectionElement.data);
DrmInitData.Mapped drmInitData = new DrmInitData.Mapped(MimeTypes.VIDEO_MP4);
drmInitData.put(protectionElement.uuid, protectionElement.data);
this.drmInitData = drmInitData;
} else {
psshInfo = null;
drmInitData = null;
}
int trackCount = trackIndices != null ? trackIndices.length : streamElement.tracks.length;
@ -299,7 +301,7 @@ public class SmoothStreamingChunkSource implements ChunkSource {
Uri uri = streamElement.buildRequestUri(selectedFormat.trackIndex, chunkIndex);
Chunk mediaChunk = newMediaChunk(selectedFormat, uri, null,
extractors.get(Integer.parseInt(selectedFormat.id)), psshInfo, dataSource,
extractors.get(Integer.parseInt(selectedFormat.id)), drmInitData, dataSource,
currentAbsoluteChunkIndex, isLastChunk, chunkStartTimeUs, nextChunkStartTimeUs, 0);
out.chunk = mediaChunk;
}
@ -365,7 +367,7 @@ public class SmoothStreamingChunkSource implements ChunkSource {
}
private static MediaChunk newMediaChunk(Format formatInfo, Uri uri, String cacheKey,
Extractor extractor, Map<UUID, byte[]> psshInfo, DataSource dataSource, int chunkIndex,
Extractor extractor, DrmInitData drmInitData, DataSource dataSource, int chunkIndex,
boolean isLast, long chunkStartTimeUs, long nextChunkStartTimeUs, int trigger) {
int nextChunkIndex = isLast ? -1 : chunkIndex + 1;
long nextStartTimeUs = isLast ? -1 : nextChunkStartTimeUs;
@ -374,7 +376,7 @@ public class SmoothStreamingChunkSource implements ChunkSource {
// In SmoothStreaming each chunk contains sample timestamps relative to the start of the chunk.
// To convert them the absolute timestamps, we need to set sampleOffsetUs to -chunkStartTimeUs.
return new ContainerMediaChunk(dataSource, dataSpec, formatInfo, trigger, chunkStartTimeUs,
nextStartTimeUs, nextChunkIndex, extractor, psshInfo, false, -chunkStartTimeUs);
nextStartTimeUs, nextChunkIndex, extractor, drmInitData, false, -chunkStartTimeUs);
}
private static byte[] getKeyId(byte[] initData) {

View file

@ -17,6 +17,7 @@ package com.google.android.exoplayer.smoothstreaming;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.UriUtil;
import com.google.android.exoplayer.util.Util;
import android.net.Uri;
@ -197,14 +198,14 @@ public class SmoothStreamingManifest {
public final TrackElement[] tracks;
public final int chunkCount;
private final Uri baseUri;
private final String baseUri;
private final String chunkTemplate;
private final List<Long> chunkStartTimes;
private final long[] chunkStartTimesUs;
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,
int displayWidth, int displayHeight, String language, TrackElement[] tracks,
List<Long> chunkStartTimes, long lastChunkDuration) {
@ -274,7 +275,7 @@ public class SmoothStreamingManifest {
String chunkUrl = chunkTemplate
.replace(URL_PLACEHOLDER_BITRATE, Integer.toString(tracks[track].bitrate))
.replace(URL_PLACEHOLDER_START_TIME, chunkStartTimes.get(chunkIndex).toString());
return Util.getMergedUri(baseUri, chunkUrl);
return UriUtil.resolveToUri(baseUri, chunkUrl);
}
}

View file

@ -23,9 +23,7 @@ import com.google.android.exoplayer.upstream.NetworkLoadable;
import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.CodecSpecificDataUtil;
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.Pair;
@ -65,8 +63,8 @@ public class SmoothStreamingManifestParser implements
try {
XmlPullParser xmlParser = xmlParserFactory.newPullParser();
xmlParser.setInput(inputStream, null);
SmoothStreamMediaParser smoothStreamMediaParser = new SmoothStreamMediaParser(null,
Util.parseBaseUri(connectionUrl));
SmoothStreamMediaParser smoothStreamMediaParser =
new SmoothStreamMediaParser(null, connectionUrl);
return (SmoothStreamingManifest) smoothStreamMediaParser.parse(xmlParser);
} catch (XmlPullParserException e) {
throw new ParserException(e);
@ -89,13 +87,13 @@ public class SmoothStreamingManifestParser implements
*/
private static abstract class ElementParser {
private final Uri baseUri;
private final String baseUri;
private final String tag;
private final ElementParser parent;
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.baseUri = baseUri;
this.tag = tag;
@ -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)) {
return new TrackElementParser(parent, baseUri);
} else if (ProtectionElementParser.TAG.equals(name)) {
@ -342,7 +340,7 @@ public class SmoothStreamingManifestParser implements
private ProtectionElement protectionElement;
private List<StreamElement> streamElements;
public SmoothStreamMediaParser(ElementParser parent, Uri baseUri) {
public SmoothStreamMediaParser(ElementParser parent, String baseUri) {
super(parent, baseUri, TAG);
lookAheadCount = -1;
protectionElement = null;
@ -392,7 +390,7 @@ public class SmoothStreamingManifestParser implements
private UUID uuid;
private byte[] initData;
public ProtectionElementParser(ElementParser parent, Uri baseUri) {
public ProtectionElementParser(ElementParser parent, String baseUri) {
super(parent, baseUri, TAG);
}
@ -455,7 +453,7 @@ public class SmoothStreamingManifestParser implements
private static final String KEY_FRAGMENT_START_TIME = "t";
private static final String KEY_FRAGMENT_REPEAT_COUNT = "r";
private final Uri baseUri;
private final String baseUri;
private final List<TrackElement> tracks;
private int type;
@ -473,7 +471,7 @@ public class SmoothStreamingManifestParser implements
private long lastChunkDuration;
public StreamElementParser(ElementParser parent, Uri baseUri) {
public StreamElementParser(ElementParser parent, String baseUri) {
super(parent, baseUri, TAG);
this.baseUri = baseUri;
tracks = new LinkedList<TrackElement>();
@ -615,7 +613,7 @@ public class SmoothStreamingManifestParser implements
private int nalUnitLengthField;
private String content;
public TrackElementParser(ElementParser parent, Uri baseUri) {
public TrackElementParser(ElementParser parent, String baseUri) {
super(parent, baseUri, TAG);
this.csd = new LinkedList<byte[]>();
}

View file

@ -16,6 +16,7 @@
package com.google.android.exoplayer.source;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.MediaFormatHolder;
import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.SampleSource;
@ -62,9 +63,14 @@ public final class DefaultSampleSource implements SampleSource {
if (sampleExtractor.prepare()) {
prepared = true;
trackInfos = sampleExtractor.getTrackInfos();
trackStates = new int[trackInfos.length];
pendingDiscontinuities = new boolean[trackInfos.length];
int trackCount = sampleExtractor.getTrackCount();
trackStates = new int[trackCount];
pendingDiscontinuities = new boolean[trackCount];
trackInfos = new TrackInfo[trackCount];
for (int track = 0; track < trackCount; track++) {
MediaFormat mediaFormat = sampleExtractor.getMediaFormat(track);
trackInfos[track] = new TrackInfo(mediaFormat.mimeType, mediaFormat.durationUs);
}
}
return prepared;
@ -119,7 +125,8 @@ public final class DefaultSampleSource implements SampleSource {
return NOTHING_READ;
}
if (trackStates[track] != TRACK_STATE_FORMAT_SENT) {
sampleExtractor.getTrackMediaFormat(track, formatHolder);
formatHolder.format = sampleExtractor.getMediaFormat(track);
formatHolder.drmInitData = sampleExtractor.getDrmInitData(track);
trackStates[track] = TRACK_STATE_FORMAT_SENT;
return FORMAT_READ;
}

View file

@ -15,14 +15,13 @@
*/
package com.google.android.exoplayer.source;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.MediaFormatHolder;
import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.SampleSource;
import com.google.android.exoplayer.TrackInfo;
import com.google.android.exoplayer.TrackRenderer;
import com.google.android.exoplayer.drm.DrmInitData;
import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.MimeTypes;
import com.google.android.exoplayer.util.Util;
import android.annotation.TargetApi;
@ -53,8 +52,6 @@ public final class FrameworkSampleExtractor implements SampleExtractor {
private final MediaExtractor mediaExtractor;
private TrackInfo[] trackInfos;
/**
* Instantiates a new sample extractor reading from the specified {@code uri}.
*
@ -106,24 +103,9 @@ public final class FrameworkSampleExtractor implements SampleExtractor {
mediaExtractor.setDataSource(fileDescriptor, fileDescriptorOffset, fileDescriptorLength);
}
int trackCount = mediaExtractor.getTrackCount();
trackInfos = new TrackInfo[trackCount];
for (int i = 0; i < trackCount; i++) {
android.media.MediaFormat format = mediaExtractor.getTrackFormat(i);
long durationUs = format.containsKey(android.media.MediaFormat.KEY_DURATION)
? format.getLong(android.media.MediaFormat.KEY_DURATION) : C.UNKNOWN_TIME_US;
String mime = format.getString(android.media.MediaFormat.KEY_MIME);
trackInfos[i] = new TrackInfo(mime, durationUs);
}
return true;
}
@Override
public TrackInfo[] getTrackInfos() {
return trackInfos;
}
@Override
public void selectTrack(int index) {
mediaExtractor.selectTrack(index);
@ -151,10 +133,18 @@ public final class FrameworkSampleExtractor implements SampleExtractor {
}
@Override
public void getTrackMediaFormat(int track, MediaFormatHolder mediaFormatHolder) {
mediaFormatHolder.format =
MediaFormat.createFromFrameworkMediaFormatV16(mediaExtractor.getTrackFormat(track));
mediaFormatHolder.drmInitData = Util.SDK_INT >= 18 ? getPsshInfoV18() : null;
public int getTrackCount() {
return mediaExtractor.getTrackCount();
}
@Override
public MediaFormat getMediaFormat(int track) {
return MediaFormat.createFromFrameworkMediaFormatV16(mediaExtractor.getTrackFormat(track));
}
@Override
public DrmInitData getDrmInitData(int track) {
return Util.SDK_INT >= 18 ? getDrmInitDataV18() : null;
}
@Override
@ -173,7 +163,7 @@ public final class FrameworkSampleExtractor implements SampleExtractor {
}
sampleHolder.timeUs = mediaExtractor.getSampleTime();
sampleHolder.flags = mediaExtractor.getSampleFlags();
if ((sampleHolder.flags & MediaExtractor.SAMPLE_FLAG_ENCRYPTED) != 0) {
if (sampleHolder.isEncrypted()) {
sampleHolder.cryptoInfo.setFromExtractorV16(mediaExtractor);
}
@ -188,9 +178,15 @@ public final class FrameworkSampleExtractor implements SampleExtractor {
}
@TargetApi(18)
private Map<UUID, byte[]> getPsshInfoV18() {
private DrmInitData getDrmInitDataV18() {
// MediaExtractor only supports psshInfo for MP4, so it's ok to hard code the mimeType here.
Map<UUID, byte[]> psshInfo = mediaExtractor.getPsshInfo();
return (psshInfo == null || psshInfo.isEmpty()) ? null : psshInfo;
if (psshInfo == null || psshInfo.isEmpty()) {
return null;
}
DrmInitData.Mapped drmInitData = new DrmInitData.Mapped(MimeTypes.VIDEO_MP4);
drmInitData.putAll(psshInfo);
return drmInitData;
}
}

View file

@ -0,0 +1,743 @@
/*
* 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;
}
}
}

View file

@ -16,11 +16,10 @@
package com.google.android.exoplayer.source;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.MediaFormatHolder;
import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.SampleSource;
import com.google.android.exoplayer.TrackInfo;
import com.google.android.exoplayer.TrackRenderer;
import com.google.android.exoplayer.drm.DrmInitData;
import java.io.IOException;
@ -28,7 +27,7 @@ import java.io.IOException;
* Extractor for reading track metadata and samples stored in tracks.
*
* <p>Call {@link #prepare} until it returns {@code true}, then access track metadata via
* {@link #getTrackInfos} and {@link #getTrackMediaFormat}.
* {@link #getMediaFormat}.
*
* <p>Pass indices of tracks to read from to {@link #selectTrack}. A track can later be deselected
* by calling {@link #deselectTrack}. It is safe to select/deselect tracks after reading sample
@ -46,9 +45,6 @@ public interface SampleExtractor {
*/
boolean prepare() throws IOException;
/** Returns track information about all tracks that can be selected. */
TrackInfo[] getTrackInfos();
/** Selects the track at {@code index} for reading sample data. */
void selectTrack(int index);
@ -75,8 +71,14 @@ public interface SampleExtractor {
*/
void seekTo(long positionUs);
/** Stores the {@link MediaFormat} of {@code track}. */
void getTrackMediaFormat(int track, MediaFormatHolder mediaFormatHolder);
/** Returns the number of tracks, if {@link #prepare} has returned {@code true}. */
int getTrackCount();
/** Returns the {@link MediaFormat} of {@code track}. */
MediaFormat getMediaFormat(int track);
/** Returns the DRM initialization data for {@code track}. */
DrmInitData getDrmInitData(int track);
/**
* Reads the next sample in the track at index {@code track} into {@code sampleHolder}, returning

View file

@ -179,7 +179,7 @@ public class TextTrackRenderer extends TrackRenderer implements Callback {
SampleHolder sampleHolder = parserHelper.getSampleHolder();
sampleHolder.clearData();
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();
textRendererNeedsUpdate = false;
} else if (result == SampleSource.END_OF_STREAM) {

View file

@ -57,6 +57,8 @@ package com.google.android.exoplayer.text.eia608;
public static final byte CARRIAGE_RETURN = 0x2D;
public static final byte ERASE_NON_DISPLAYED_MEMORY = 0x2E;
public static final byte BACKSPACE = 0x21;
public static final byte MID_ROW_CHAN_1 = 0x11;
public static final byte MID_ROW_CHAN_2 = 0x19;

View file

@ -82,6 +82,26 @@ public class Eia608Parser {
0xFB // 3F: 251 'û' "Latin small letter U with circumflex"
};
// Extended Spanish/Miscellaneous and French char set.
private static final int[] SPECIAL_ES_FR_CHARACTER_SET = new int[] {
// Spanish and misc.
0xC1, 0xC9, 0xD3, 0xDA, 0xDC, 0xFC, 0x2018, 0xA1,
0x2A, 0x27, 0x2014, 0xA9, 0x2120, 0x2022, 0x201C, 0x201D,
// French.
0xC0, 0xC2, 0xC7, 0xC8, 0xCA, 0xCB, 0xEB, 0xCE,
0xCF, 0xEF, 0xD4, 0xD9, 0xF9, 0xDB, 0xAB, 0xBB
};
//Extended Portuguese and German/Danish char set.
private static final int[] SPECIAL_PT_DE_CHARACTER_SET = new int[] {
// Portuguese.
0xC3, 0xE3, 0xCD, 0xCC, 0xEC, 0xD2, 0xF2, 0xD5,
0xF5, 0x7B, 0x7D, 0x5C, 0x5E, 0x5F, 0x7C, 0x7E,
// German/Danish.
0xC4, 0xE4, 0xD6, 0xF6, 0xDF, 0xA5, 0xA4, 0x2502,
0xC5, 0xE5, 0xD8, 0xF8, 0x250C, 0x2510, 0x2514, 0x2518
};
private final ParsableBitArray seiBuffer;
private final StringBuilder stringBuilder;
private final ArrayList<ClosedCaption> captions;
@ -134,31 +154,45 @@ public class Eia608Parser {
}
// Special North American character set.
if ((ccData1 == 0x11) && ((ccData2 & 0x70) == 0x30)) {
// ccData2 - P|0|1|1|X|X|X|X
if ((ccData1 == 0x11 || ccData1 == 0x19)
&& ((ccData2 & 0x70) == 0x30)) {
stringBuilder.append(getSpecialChar(ccData2));
continue;
}
// Extended Spanish/Miscellaneous and French character set.
// ccData2 - P|0|1|X|X|X|X|X
if ((ccData1 == 0x12 || ccData1 == 0x1A)
&& ((ccData2 & 0x60) == 0x20)) {
backspace(); // Remove standard equivalent of the special extended char.
stringBuilder.append(getExtendedEsFrChar(ccData2));
continue;
}
// Extended Portuguese and German/Danish character set.
// ccData2 - P|0|1|X|X|X|X|X
if ((ccData1 == 0x13 || ccData1 == 0x1B)
&& ((ccData2 & 0x60) == 0x20)) {
backspace(); // Remove standard equivalent of the special extended char.
stringBuilder.append(getExtendedPtDeChar(ccData2));
continue;
}
// Control character.
if (ccData1 < 0x20) {
if (stringBuilder.length() > 0) {
captions.add(new ClosedCaptionText(stringBuilder.toString()));
stringBuilder.setLength(0);
}
captions.add(new ClosedCaptionCtrl(ccData1, ccData2));
addCtrl(ccData1, ccData2);
continue;
}
// Basic North American character set.
stringBuilder.append(getChar(ccData1));
if (ccData2 != 0) {
if (ccData2 >= 0x20) {
stringBuilder.append(getChar(ccData2));
}
}
if (stringBuilder.length() > 0) {
captions.add(new ClosedCaptionText(stringBuilder.toString()));
}
addBufferedText();
if (captions.isEmpty()) {
return null;
@ -166,7 +200,7 @@ public class Eia608Parser {
ClosedCaption[] captionArray = new ClosedCaption[captions.size()];
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) {
@ -179,6 +213,32 @@ public class Eia608Parser {
return (char) SPECIAL_CHARACTER_SET[index];
}
private static char getExtendedEsFrChar(byte ccData) {
int index = ccData & 0x1F;
return (char) SPECIAL_ES_FR_CHARACTER_SET[index];
}
private static char getExtendedPtDeChar(byte ccData) {
int index = ccData & 0x1F;
return (char) SPECIAL_PT_DE_CHARACTER_SET[index];
}
private void addBufferedText() {
if (stringBuilder.length() > 0) {
captions.add(new ClosedCaptionText(stringBuilder.toString()));
stringBuilder.setLength(0);
}
}
private void addCtrl(byte ccData1, byte ccData2) {
addBufferedText();
captions.add(new ClosedCaptionCtrl(ccData1, ccData2));
}
private void backspace() {
addCtrl((byte) 0x14, ClosedCaptionCtrl.BACKSPACE);
}
/**
* Inspects an sei message to determine whether it contains EIA-608.
* <p>

View file

@ -317,6 +317,11 @@ public class Eia608TrackRenderer extends TrackRenderer implements Callback {
case ClosedCaptionCtrl.CARRIAGE_RETURN:
maybeAppendNewline();
return;
case ClosedCaptionCtrl.BACKSPACE:
if (captionStringBuilder.length() > 0) {
captionStringBuilder.setLength(captionStringBuilder.length() - 1);
}
return;
}
}

View file

@ -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
* with a 128-bit key and PKCS7 padding.
*
*/
public class Aes128DataSource implements DataSource {
private final DataSource upstream;
private final byte[] secretKey;
private final byte[] iv;
private final byte[] encryptionKey;
private final byte[] encryptionIv;
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.secretKey = secretKey;
this.iv = iv;
this.encryptionKey = encryptionKey;
this.encryptionIv = encryptionIv;
}
@Override
@ -61,8 +65,8 @@ public class Aes128DataSource implements DataSource {
throw new RuntimeException(e);
}
Key cipherKey = new SecretKeySpec(secretKey, "AES");
AlgorithmParameterSpec cipherIV = new IvParameterSpec(iv);
Key cipherKey = new SecretKeySpec(encryptionKey, "AES");
AlgorithmParameterSpec cipherIV = new IvParameterSpec(encryptionIv);
try {
cipher.init(Cipher.DECRYPT_MODE, cipherKey, cipherIV);

View file

@ -0,0 +1,150 @@
/*
* 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;
}
}

View file

@ -55,13 +55,16 @@ public interface DataSource {
/**
* Reads up to {@code length} bytes of data and stores them into {@code buffer}, starting at
* index {@code offset}. This method blocks until at least one byte of data can be read, the end
* of the opened range is detected, or an exception is thrown.
* index {@code offset}.
* <p>
* This method blocks until at least one byte of data can be read, the end of the opened range is
* detected, or an exception is thrown.
*
* @param buffer The buffer into which the read data should be stored.
* @param offset The start offset into {@code buffer} at which data should be written.
* @param readLength The maximum number of bytes to read.
* @return The actual number of bytes read, or -1 if the end of the opened range is reached.
* @return The number of bytes read, or {@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.
*/
public int read(byte[] buffer, int offset, int readLength) throws IOException;

View file

@ -47,6 +47,10 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream
private final Allocator allocator;
private final ReadHead readHead;
/** Whether {@link #allocation}'s capacity is fixed. If true, the allocation is not resized. */
private final boolean isAllocationFixedSize;
private final int allocationSize;
private Allocation allocation;
private volatile boolean loadCanceled;
@ -58,6 +62,9 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream
private int writeFragmentRemainingLength;
/**
* Constructs an instance whose allocation grows to contain all of the data specified by the
* {@code dataSpec}.
*
* @param dataSource The source from which the data should be loaded.
* @param dataSpec Defines the data to be loaded. {@code dataSpec.length} must not exceed
* {@link Integer#MAX_VALUE}. If {@code dataSpec.length == C.LENGTH_UNBOUNDED} then
@ -72,12 +79,48 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream
this.allocator = allocator;
resolvedLength = C.LENGTH_UNBOUNDED;
readHead = new ReadHead();
isAllocationFixedSize = false;
allocationSize = 0;
}
/**
* Constructs an instance whose allocation is of a fixed size, which may be smaller than the data
* specified by the {@code dataSpec}.
* <p>
* The allocation size determines how far ahead loading can proceed relative to the current
* reading position.
*
* @param dataSource The source form which the data should be loaded.
* @param dataSpec Defines the data to be loaded.
* @param allocator Used to obtain an {@link Allocation} for holding the data.
* @param allocationSize The minimum size for a fixed-size allocation that will hold the data
* loaded from {@code dataSource}.
*/
public DataSourceStream(
DataSource dataSource, DataSpec dataSpec, Allocator allocator, int allocationSize) {
Assertions.checkState(dataSpec.length <= Integer.MAX_VALUE);
this.dataSource = dataSource;
this.dataSpec = dataSpec;
this.allocator = allocator;
this.allocationSize = allocationSize;
resolvedLength = C.LENGTH_UNBOUNDED;
readHead = new ReadHead();
isAllocationFixedSize = true;
}
/**
* Resets the read position to the start of the data.
*
* @throws UnsupportedOperationException Thrown if the allocation size is fixed.
*/
public void resetReadPosition() {
if (isAllocationFixedSize) {
throw new UnsupportedOperationException(
"The read position cannot be reset when using a fixed allocation");
}
readHead.reset();
}
@ -176,7 +219,12 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream
byte[][] buffers = allocation.getBuffers();
while (bytesRead < bytesToRead) {
if (readHead.fragmentRemaining == 0) {
readHead.fragmentIndex++;
if (readHead.fragmentIndex == buffers.length - 1) {
Assertions.checkState(isAllocationFixedSize);
readHead.fragmentIndex = 0;
} else {
readHead.fragmentIndex++;
}
readHead.fragmentOffset = allocation.getFragmentOffset(readHead.fragmentIndex);
readHead.fragmentRemaining = allocation.getFragmentLength(readHead.fragmentIndex);
}
@ -194,6 +242,13 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream
readHead.fragmentRemaining -= bufferReadLength;
}
if (isAllocationFixedSize) {
synchronized (readHead) {
// Notify load() of the updated position so it can resume.
readHead.notify();
}
}
return bytesRead;
}
@ -210,6 +265,7 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream
}
@Override
@SuppressWarnings("NonAtomicVolatileUpdate")
public void load() throws IOException, InterruptedException {
if (loadCanceled || isLoadFinished()) {
// The load was canceled, or is already complete.
@ -221,7 +277,7 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream
if (loadPosition == 0 && resolvedLength == C.LENGTH_UNBOUNDED) {
loadDataSpec = dataSpec;
long resolvedLength = dataSource.open(loadDataSpec);
if (resolvedLength > Integer.MAX_VALUE) {
if (!isAllocationFixedSize && resolvedLength > Integer.MAX_VALUE) {
throw new DataSourceStreamLoadException(
new UnexpectedLengthException(dataSpec.length, resolvedLength));
}
@ -230,14 +286,18 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream
long remainingLength = resolvedLength != C.LENGTH_UNBOUNDED
? resolvedLength - loadPosition : C.LENGTH_UNBOUNDED;
loadDataSpec = new DataSpec(dataSpec.uri, dataSpec.position + loadPosition,
remainingLength, dataSpec.key);
remainingLength, dataSpec.key, dataSpec.flags);
dataSource.open(loadDataSpec);
}
if (allocation == null) {
int initialAllocationSize = resolvedLength != C.LENGTH_UNBOUNDED
? (int) resolvedLength : CHUNKED_ALLOCATION_INCREMENT;
allocation = allocator.allocate(initialAllocationSize);
if (isAllocationFixedSize) {
allocation = allocator.allocate(allocationSize);
} else {
int initialAllocationSize = resolvedLength != C.LENGTH_UNBOUNDED
? (int) resolvedLength : CHUNKED_ALLOCATION_INCREMENT;
allocation = allocator.allocate(initialAllocationSize);
}
}
int allocationCapacity = allocation.capacity();
@ -253,18 +313,25 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream
if (Thread.interrupted()) {
throw new InterruptedException();
}
read = dataSource.read(buffers[writeFragmentIndex], writeFragmentOffset,
writeFragmentRemainingLength);
int bytesToWrite = getBytesToWrite();
read = dataSource.read(buffers[writeFragmentIndex], writeFragmentOffset, bytesToWrite);
if (read > 0) {
loadPosition += read;
writeFragmentOffset += read;
writeFragmentRemainingLength -= read;
if (writeFragmentRemainingLength == 0 && maybeMoreToLoad()) {
writeFragmentIndex++;
if (loadPosition == allocationCapacity) {
allocation.ensureCapacity(allocationCapacity + CHUNKED_ALLOCATION_INCREMENT);
allocationCapacity = allocation.capacity();
buffers = allocation.getBuffers();
if (writeFragmentIndex == buffers.length) {
if (isAllocationFixedSize) {
// Wrap back to the first fragment.
writeFragmentIndex = 0;
} else {
// Grow the allocation.
allocation.ensureCapacity(allocationCapacity + CHUNKED_ALLOCATION_INCREMENT);
allocationCapacity = allocation.capacity();
buffers = allocation.getBuffers();
}
}
writeFragmentOffset = allocation.getFragmentOffset(writeFragmentIndex);
writeFragmentRemainingLength = allocation.getFragmentLength(writeFragmentIndex);
@ -281,6 +348,25 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream
}
}
/**
* Returns the number of bytes that can be written to the current fragment, blocking until the
* reader has consumed data if the allocation has a fixed size and is full.
*/
private int getBytesToWrite() throws InterruptedException {
if (!isAllocationFixedSize) {
return writeFragmentRemainingLength;
}
synchronized (readHead) {
while (loadPosition == readHead.position + allocation.capacity()) {
readHead.wait();
}
}
return Math.min(writeFragmentRemainingLength,
allocation.capacity() - (int) (loadPosition - readHead.position));
}
private boolean maybeMoreToLoad() {
return resolvedLength == C.LENGTH_UNBOUNDED || loadPosition < resolvedLength;
}

View file

@ -25,22 +25,32 @@ import android.net.Uri;
*/
public final class DataSpec {
/**
* 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 static final int FLAG_ALLOW_GZIP = 1;
/**
* Identifies the source from which data should be read.
*/
public final Uri uri;
/**
* True if the data at {@link #uri} is the full stream. False otherwise. An example where this
* may be false is if {@link #uri} defines the location of a cached part of the stream.
*/
public final boolean uriIsFullStream;
/**
* The absolute position of the data in the full stream.
*/
public final long absoluteStreamPosition;
/**
* The position of the data when read from {@link #uri}. Always equal to
* {@link #absoluteStreamPosition} if {@link #uriIsFullStream}.
* The position of the data when read from {@link #uri}.
* <p>
* Always equal to {@link #absoluteStreamPosition} unless the {@link #uri} defines the location
* of a subset of the underyling data.
*/
public final long position;
/**
@ -52,6 +62,10 @@ public final class DataSpec {
* {@link DataSpec} is not intended to be used in conjunction with a cache.
*/
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.
@ -59,11 +73,21 @@ public final class DataSpec {
* @param uri {@link #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 absoluteStreamPosition {@link #absoluteStreamPosition}, equal to {@link #position}.
@ -71,50 +95,50 @@ public final class DataSpec {
* @param key {@link #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 absoluteStreamPosition {@link #absoluteStreamPosition}.
* @param absoluteStreamPosition {@link #absoluteStreamPosition}, equal to {@link #position}.
* @param length {@link #length}.
* @param key {@link #key}.
* @param position {@link #position}.
* @param flags {@link #flags}.
*/
public DataSpec(Uri uri, long absoluteStreamPosition, long length, String key, long position) {
this(uri, absoluteStreamPosition, length, key, position, false);
public DataSpec(Uri uri, long absoluteStreamPosition, long length, String key, int flags) {
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 absoluteStreamPosition {@link #absoluteStreamPosition}.
* @param position {@link #position}.
* @param length {@link #length}.
* @param key {@link #key}.
* @param position {@link #position}.
* @param uriIsFullStream {@link #uriIsFullStream}.
* @param flags {@link #flags}.
*/
public DataSpec(Uri uri, long absoluteStreamPosition, long length, String key, long position,
boolean uriIsFullStream) {
public DataSpec(Uri uri, long absoluteStreamPosition, long position, long length, String key,
int flags) {
Assertions.checkArgument(absoluteStreamPosition >= 0);
Assertions.checkArgument(position >= 0);
Assertions.checkArgument(length > 0 || length == C.LENGTH_UNBOUNDED);
Assertions.checkArgument(absoluteStreamPosition == position || !uriIsFullStream);
this.uri = uri;
this.uriIsFullStream = uriIsFullStream;
this.absoluteStreamPosition = absoluteStreamPosition;
this.position = position;
this.length = length;
this.key = key;
this.flags = flags;
}
@Override
public String toString() {
return "DataSpec[" + uri + ", " + uriIsFullStream + ", " + absoluteStreamPosition + ", " +
position + ", " + length + ", " + key + "]";
return "DataSpec[" + uri + ", " + ", " + absoluteStreamPosition + ", " +
position + ", " + length + ", " + key + ", " + flags + "]";
}
}

View file

@ -18,17 +18,21 @@ package com.google.android.exoplayer.upstream;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.Predicate;
import com.google.android.exoplayer.util.Util;
import android.text.TextUtils;
import android.util.Log;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -43,6 +47,7 @@ public class DefaultHttpDataSource implements HttpDataSource {
private static final String TAG = "HttpDataSource";
private static final Pattern CONTENT_RANGE_HEADER =
Pattern.compile("^bytes (\\d+)-(\\d+)/(\\d+)$");
private static final AtomicReference<byte[]> skipBufferReference = new AtomicReference<byte[]>();
private final int connectTimeoutMillis;
private final int readTimeoutMillis;
@ -56,7 +61,10 @@ public class DefaultHttpDataSource implements HttpDataSource {
private InputStream inputStream;
private boolean opened;
private long dataLength;
private long bytesToSkip;
private long bytesToRead;
private long bytesSkipped;
private long bytesRead;
/**
@ -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
public long open(DataSpec dataSpec) throws HttpDataSourceException {
this.dataSpec = dataSpec;
this.bytesRead = 0;
this.bytesSkipped = 0;
try {
connection = makeConnection(dataSpec);
} catch (IOException e) {
@ -156,14 +152,16 @@ public class DefaultHttpDataSource implements HttpDataSource {
dataSpec);
}
// Check for a valid response code.
int responseCode;
try {
responseCode = connection.getResponseCode();
} catch (IOException e) {
closeConnection();
throw new HttpDataSourceException("Unable to connect to " + dataSpec.uri.toString(), e,
dataSpec);
}
// Check for a valid response code.
if (responseCode < 200 || responseCode > 299) {
Map<String, List<String>> headers = connection.getHeaderFields();
closeConnection();
@ -177,16 +175,23 @@ public class DefaultHttpDataSource implements HttpDataSource {
throw new InvalidContentTypeException(contentType, dataSpec);
}
long contentLength = getContentLength(connection);
dataLength = dataSpec.length == C.LENGTH_UNBOUNDED ? contentLength : dataSpec.length;
// If we requested a range starting from a non-zero position and received a 200 rather than a
// 206, then the server does not support partial requests. We'll need to manually skip to the
// requested position.
bytesToSkip = responseCode == 200 && dataSpec.position != 0 ? dataSpec.position : 0;
if (dataSpec.length != C.LENGTH_UNBOUNDED && contentLength != C.LENGTH_UNBOUNDED
&& contentLength != dataSpec.length) {
// The DataSpec specified a length and we resolved a length from the response headers, but
// the two lengths do not match.
closeConnection();
throw new HttpDataSourceException(
new UnexpectedLengthException(dataSpec.length, contentLength), dataSpec);
// Determine the length of the data to be read, after skipping.
if ((dataSpec.flags & DataSpec.FLAG_ALLOW_GZIP) == 0) {
long contentLength = getContentLength(connection);
bytesToRead = dataSpec.length != C.LENGTH_UNBOUNDED ? dataSpec.length
: 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 {
@ -201,37 +206,24 @@ public class DefaultHttpDataSource implements HttpDataSource {
listener.onTransferStart();
}
return dataLength;
return bytesToRead;
}
@Override
public int read(byte[] buffer, int offset, int readLength) throws HttpDataSourceException {
int read = 0;
try {
read = inputStream.read(buffer, offset, readLength);
skipInternal();
return readInternal(buffer, offset, readLength);
} catch (IOException e) {
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
public void close() throws HttpDataSourceException {
try {
if (inputStream != null) {
Util.maybeTerminateInputStream(connection, bytesRemaining());
try {
inputStream.close();
} catch (IOException e) {
@ -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.
*
@ -266,6 +251,16 @@ public class DefaultHttpDataSource implements HttpDataSource {
return connection;
}
/**
* 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
* {@link #open(DataSpec)}.
@ -285,7 +280,7 @@ public class DefaultHttpDataSource implements HttpDataSource {
* @return The remaining length, or {@link C#LENGTH_UNBOUNDED}.
*/
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 {
@ -301,6 +296,9 @@ public class DefaultHttpDataSource implements HttpDataSource {
}
setRangeHeader(connection, dataSpec);
connection.setRequestProperty("User-Agent", userAgent);
if ((dataSpec.flags & DataSpec.FLAG_ALLOW_GZIP) == 0) {
connection.setRequestProperty("Accept-Encoding", "identity");
}
connection.connect();
return connection;
}
@ -355,4 +353,87 @@ public class DefaultHttpDataSource implements HttpDataSource {
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;
}
}
}

View file

@ -17,6 +17,7 @@ package com.google.android.exoplayer.upstream;
import com.google.android.exoplayer.C;
import java.io.EOFException;
import java.io.IOException;
import java.io.RandomAccessFile;
@ -65,6 +66,9 @@ public final class FileDataSource implements DataSource {
file.seek(dataSpec.position);
bytesRemaining = dataSpec.length == C.LENGTH_UNBOUNDED ? file.length() - dataSpec.position
: dataSpec.length;
if (bytesRemaining < 0) {
throw new EOFException();
}
} catch (IOException e) {
throw new FileDataSourceException(e);
}

View file

@ -127,6 +127,21 @@ public final class Loader {
startLoading(myLooper, loadable, callback);
}
/**
* Invokes {@link #startLoading(Looper, Loadable, Callback)}, using the {@link Looper}
* associated with the calling thread. Loading is delayed by {@code delayMs}.
*
* @param loadable The {@link Loadable} to load.
* @param callback A callback to invoke when the load ends.
* @param delayMs Number of milliseconds to wait before calling {@link Loadable#load()}.
* @throws IllegalStateException If the calling thread does not have an associated {@link Looper}.
*/
public void startLoading(Loadable loadable, Callback callback, int delayMs) {
Looper myLooper = Looper.myLooper();
Assertions.checkState(myLooper != null);
startLoading(myLooper, loadable, callback, delayMs);
}
/**
* Start loading a {@link Loadable}.
* <p>
@ -138,9 +153,24 @@ public final class Loader {
* @param callback A callback to invoke when the load ends.
*/
public void startLoading(Looper looper, Loadable loadable, Callback callback) {
startLoading(looper, loadable, callback, 0);
}
/**
* Start loading a {@link Loadable} after {@code delayMs} has elapsed.
* <p>
* A {@link Loader} instance can only load one {@link Loadable} at a time, and so this method
* must not be called when another load is in progress.
*
* @param looper The looper of the thread on which the callback should be invoked.
* @param loadable The {@link Loadable} to load.
* @param callback A callback to invoke when the load ends.
* @param delayMs Number of milliseconds to wait before calling {@link Loadable#load()}.
*/
public void startLoading(Looper looper, Loadable loadable, Callback callback, int delayMs) {
Assertions.checkState(!loading);
loading = true;
currentTask = new LoadTask(looper, loadable, callback);
currentTask = new LoadTask(looper, loadable, callback, delayMs);
downloadExecutorService.submit(currentTask);
}
@ -182,13 +212,15 @@ public final class Loader {
private final Loadable loadable;
private final Loader.Callback callback;
private final int delayMs;
private volatile Thread executorThread;
public LoadTask(Looper looper, Loadable loadable, Loader.Callback callback) {
public LoadTask(Looper looper, Loadable loadable, Loader.Callback callback, int delayMs) {
super(looper);
this.loadable = loadable;
this.callback = callback;
this.delayMs = delayMs;
}
public void quit() {
@ -202,6 +234,9 @@ public final class Loader {
public void run() {
try {
executorThread = Thread.currentThread();
if (delayMs > 0) {
Thread.sleep(delayMs);
}
if (!loadable.isLoadCanceled()) {
loadable.load();
}

View file

@ -63,7 +63,7 @@ public final class NetworkLoadable<T> implements Loadable {
public NetworkLoadable(String url, HttpDataSource httpDataSource, Parser<T> parser) {
this.httpDataSource = httpDataSource;
this.parser = parser;
dataSpec = new DataSpec(Uri.parse(url));
dataSpec = new DataSpec(Uri.parse(url), DataSpec.FLAG_ALLOW_GZIP);
}
/**

View file

@ -42,8 +42,8 @@ public final class TeeDataSource implements DataSource {
long dataLength = upstream.open(dataSpec);
if (dataSpec.length == C.LENGTH_UNBOUNDED && dataLength != C.LENGTH_UNBOUNDED) {
// Reconstruct dataSpec in order to provide the resolved length to the sink.
dataSpec = new DataSpec(dataSpec.uri, dataSpec.absoluteStreamPosition, dataLength,
dataSpec.key, dataSpec.position, dataSpec.uriIsFullStream);
dataSpec = new DataSpec(dataSpec.uri, dataSpec.absoluteStreamPosition, dataSpec.position,
dataLength, dataSpec.key, dataSpec.flags);
}
dataSink.open(dataSpec);
return dataLength;
@ -61,8 +61,11 @@ public final class TeeDataSource implements DataSource {
@Override
public void close() throws IOException {
upstream.close();
dataSink.close();
try {
upstream.close();
} finally {
dataSink.close();
}
}
}

View file

@ -20,6 +20,7 @@ import java.io.IOException;
/**
* Thrown when the length of some data does not match an expected length.
*/
@Deprecated
public final class UnexpectedLengthException extends IOException {
/**

View file

@ -19,6 +19,7 @@ import com.google.android.exoplayer.C;
import com.google.android.exoplayer.upstream.DataSink;
import com.google.android.exoplayer.upstream.DataSpec;
import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.Util;
import java.io.File;
import java.io.FileNotFoundException;
@ -115,11 +116,23 @@ public class CacheDataSink implements DataSink {
}
private void closeCurrentOutputStream() throws IOException {
if (outputStream != null) {
if (outputStream == null) {
return;
}
boolean success = false;
try {
outputStream.flush();
outputStream.close();
outputStream.getFD().sync();
success = true;
} finally {
Util.closeQuietly(outputStream);
if (success) {
cache.commitFile(file);
} else {
file.delete();
}
outputStream = null;
cache.commitFile(file);
file = null;
}
}

View file

@ -22,7 +22,6 @@ import com.google.android.exoplayer.upstream.DataSpec;
import com.google.android.exoplayer.upstream.FileDataSource;
import com.google.android.exoplayer.upstream.TeeDataSource;
import com.google.android.exoplayer.upstream.cache.CacheDataSink.CacheDataSinkException;
import com.google.android.exoplayer.util.Assertions;
import android.net.Uri;
import android.util.Log;
@ -64,6 +63,7 @@ public final class CacheDataSource implements DataSource {
private DataSource currentDataSource;
private Uri uri;
private int flags;
private String key;
private long readPosition;
private long bytesRemaining;
@ -125,9 +125,9 @@ public final class CacheDataSource implements DataSource {
@Override
public long open(DataSpec dataSpec) throws IOException {
Assertions.checkState(dataSpec.uriIsFullStream);
try {
uri = dataSpec.uri;
flags = dataSpec.flags;
key = dataSpec.key;
readPosition = dataSpec.position;
bytesRemaining = dataSpec.length;
@ -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
// from upstream.
currentDataSource = upstreamDataSource;
dataSpec = new DataSpec(uri, readPosition, bytesRemaining, key);
dataSpec = new DataSpec(uri, readPosition, bytesRemaining, key, flags);
} else if (span.isCached) {
// Data is cached, read from cache.
Uri fileUri = Uri.fromFile(span.file);
long filePosition = readPosition - span.position;
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;
} else {
// Data is not cached, and data is not locked, read from upstream with cache backing.
lockedSpan = span;
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
: upstreamDataSource;
}

View file

@ -13,67 +13,17 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.mp4;
import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.CodecSpecificDataUtil;
import com.google.android.exoplayer.util.ParsableByteArray;
package com.google.android.exoplayer.util;
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 {
/** 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;
public final class H264Util {
/** 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};
/** 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;
}
public static final byte[] NAL_START_CODE = new byte[] {0, 0, 0, 1};
/**
* Replaces length prefixes of NAL units in {@code buffer} with start code prefixes, within the
@ -92,7 +42,9 @@ public final class Mp4Util {
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) {
int length = atom.readUnsignedShort();
int offset = atom.getPosition();
@ -101,43 +53,39 @@ public final class Mp4Util {
}
/**
* Finds the first NAL unit in {@code data}.
* <p>
* For a NAL unit to be found, its first four bytes must be contained within the part of the
* array being searched.
* Gets the type of the NAL unit in {@code data} that starts at {@code offset}.
*
* @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 type The type of the NAL unit to search for, or -1 for any NAL unit.
* @return The offset of the NAL unit, or {@code endOffset} if a NAL unit was not found.
* @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 findNalUnit(byte[] data, int startOffset, int endOffset, int type) {
return findNalUnit(data, startOffset, endOffset, type, null);
public static int getNalUnitType(byte[] data, int offset) {
return data[offset + 3] & 0x1F;
}
/**
* Like {@link #findNalUnit(byte[], int, int, int)}, but supports finding of NAL units across
* array boundaries.
* Finds the first NAL unit in {@code data}.
* <p>
* To use this method, pass the same {@code prefixFlags} parameter to successive calls where the
* data passed represents a contiguous stream. The state maintained in this parameter allows the
* detection of NAL units where the NAL unit prefix spans array boundaries.
* If {@code prefixFlags} is null then the first four bytes of a NAL unit must be entirely
* contained within the part of the array being searched in order for it to be found.
* <p>
* Note that when using {@code prefixFlags} 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.
* When {@code prefixFlags} is non-null, this method supports finding NAL units whose first four
* bytes span {@code data} arrays passed to successive calls. To use this feature, pass the same
* {@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 startOffset The offset (inclusive) in the data to start 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
* required to detect NAL units where the NAL unit prefix spans array boundaries. The array
* must be at least 3 elements long.
* @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) {
int length = endOffset - startOffset;
@ -147,15 +95,14 @@ public final class Mp4Util {
}
if (prefixFlags != null) {
if (prefixFlags[0] && matchesType(data, startOffset, type)) {
if (prefixFlags[0]) {
clearPrefixFlags(prefixFlags);
return startOffset - 3;
} else if (length > 1 && prefixFlags[1] && data[startOffset] == 1
&& matchesType(data, startOffset + 1, type)) {
} else if (length > 1 && prefixFlags[1] && data[startOffset] == 1) {
clearPrefixFlags(prefixFlags);
return startOffset - 2;
} else if (length > 2 && prefixFlags[2] && data[startOffset] == 0
&& data[startOffset + 1] == 1 && matchesType(data, startOffset + 2, type)) {
&& data[startOffset + 1] == 1) {
clearPrefixFlags(prefixFlags);
return startOffset - 1;
}
@ -169,8 +116,7 @@ public final class Mp4Util {
if ((data[i] & 0xFE) != 0) {
// There isn't a NAL prefix here, or at the next two positions. Do nothing and let the
// loop advance the index by three.
} else if (data[i - 2] == 0 && data[i - 1] == 0 && data[i] == 1
&& matchesType(data, i + 1, type)) {
} else if (data[i - 2] == 0 && data[i - 1] == 0 && data[i] == 1) {
if (prefixFlags != null) {
clearPrefixFlags(prefixFlags);
}
@ -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.
* @throws IllegalArgumentException If the top bit of the input data is set.
*/
public static int findNalUnit(byte[] data, int startOffset, int endOffset) {
return findNalUnit(data, startOffset, endOffset, null);
private 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;
}
/**
* 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.
* @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) {
return data[offset + 3] & 0x1F;
}
/**
* 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.
*/
@ -247,11 +173,4 @@ public final class Mp4Util {
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;
}
}

View file

@ -37,6 +37,10 @@ public class MimeTypes {
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_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_OPUS = BASE_TYPE_AUDIO + "/opus";
@ -45,6 +49,7 @@ public class MimeTypes {
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_TTML = BASE_TYPE_APPLICATION + "/ttml+xml";
public static final String APPLICATION_M3U8 = BASE_TYPE_APPLICATION + "/x-mpegURL";
private MimeTypes() {}

View file

@ -37,6 +37,12 @@ public final class ParsableByteArray {
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.
*
@ -171,6 +177,13 @@ public final class ParsableByteArray {
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. */
public long readUnsignedInt() {
long result = shiftIntoLong(data, position, 4);
@ -180,9 +193,11 @@ public final class ParsableByteArray {
/** Reads the next four bytes as a signed value. */
public int readInt() {
int result = shiftIntoInt(data, position, 4);
position += 4;
return result;
// shiftIntoInt inlined as performance optimization.
return (data[position++] & 0xFF) << 24
| (data[position++] & 0xFF) << 16
| (data[position++] & 0xFF) << 8
| data[position++] & 0xFF;
}
/** Reads the next eight bytes as a signed value. */
@ -221,8 +236,11 @@ public final class ParsableByteArray {
* @throws IllegalArgumentException Thrown if the top bit of the input data is set.
*/
public int readUnsignedIntToInt() {
int result = shiftIntoInt(data, position, 4);
position += 4;
// shiftIntoInt inlined as performance optimization.
final int result = (data[position++] & 0xFF) << 24
| (data[position++] & 0xFF) << 16
| (data[position++] & 0xFF) << 8
| data[position++] & 0xFF;
if (result < 0) {
throw new IllegalArgumentException("Top bit not zero: " + result);
}

View file

@ -0,0 +1,258 @@
/*
* 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;
}
}

View file

@ -15,13 +15,17 @@
*/
package com.google.android.exoplayer.util;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.upstream.DataSource;
import android.net.Uri;
import android.text.TextUtils;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.net.HttpURLConnection;
import java.net.URL;
import java.text.ParseException;
import java.util.Arrays;
@ -58,6 +62,8 @@ public final class Util {
Pattern.compile("^(-)?P(([0-9]*)Y)?(([0-9]*)M)?(([0-9]*)D)?"
+ "(T(([0-9]*)H)?(([0-9]*)M)?(([0-9.]*)S)?)?$");
private static final long MAX_BYTES_TO_DRAIN = 2048;
private Util() {}
/**
@ -124,6 +130,19 @@ public final class Util {
}
}
/**
* Closes an {@link OutputStream}, suppressing any {@link IOException} that may occur.
*
* @param outputStream The {@link OutputStream} to close.
*/
public static void closeQuietly(OutputStream outputStream) {
try {
outputStream.close();
} catch (IOException e) {
// Ignore.
}
}
/**
* Converts text to lower case using {@link Locale#US}.
*
@ -134,54 +153,6 @@ public final class Util {
return text == null ? null : text.toLowerCase(Locale.US);
}
/**
* 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.
* <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 stringUri A relative or absolute uri in string form.
* @return The merged uri.
*/
public static Uri getMergedUri(Uri baseUri, String stringUri) {
if (stringUri == null) {
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);
}
/**
* Returns the index of the largest value in an array that is less than (or optionally equal to)
* a specified key.
@ -445,4 +416,48 @@ public final class Util {
return intArray;
}
/**
* On platform API levels 19 and 20, okhttp's implementation of {@link InputStream#close} can
* block for a long time if the stream has a lot of data remaining. Call this method before
* closing the input stream to make a best effort to cause the input stream to encounter an
* unexpected end of input, working around this issue. On other platform API levels, the method
* does nothing.
*
* @param connection The connection whose {@link InputStream} should be terminated.
* @param bytesRemaining The number of bytes remaining to be read from the input stream if its
* length is known. {@link C#LENGTH_UNBOUNDED} otherwise.
*/
public static void maybeTerminateInputStream(HttpURLConnection connection, long bytesRemaining) {
if (SDK_INT != 19 && SDK_INT != 20) {
return;
}
try {
InputStream inputStream = connection.getInputStream();
if (bytesRemaining == C.LENGTH_UNBOUNDED) {
// If the input stream has already ended, do nothing. The socket may be re-used.
if (inputStream.read() == -1) {
return;
}
} else if (bytesRemaining <= MAX_BYTES_TO_DRAIN) {
// There isn't much data left. Prefer to allow it to drain, which may allow the socket to be
// re-used.
return;
}
String className = inputStream.getClass().getName();
if (className.equals("com.android.okhttp.internal.http.HttpTransport$ChunkedInputStream")
|| className.equals(
"com.android.okhttp.internal.http.HttpTransport$FixedLengthInputStream")) {
Class<?> superclass = inputStream.getClass().getSuperclass();
Method unexpectedEndOfInput = superclass.getDeclaredMethod("unexpectedEndOfInput");
unexpectedEndOfInput.setAccessible(true);
unexpectedEndOfInput.invoke(inputStream);
}
} catch (IOException e) {
// The connection didn't ever have an input stream, or it was closed already.
} catch (Exception e) {
// Something went wrong. The device probably isn't using okhttp.
}
}
}

Some files were not shown because too many files have changed in this diff Show more