mirror of
https://github.com/samsonjs/media.git
synced 2026-03-30 10:15:48 +00:00
Merge remote-tracking branch 'upstream/dev' into dev
This commit is contained in:
commit
da7ae2a925
141 changed files with 8712 additions and 1553 deletions
|
|
@ -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'
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() {}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -281,7 +281,7 @@ public final class Ac3PassthroughAudioTrackRenderer extends TrackRenderer {
|
|||
protected void onDisabled() {
|
||||
audioSessionId = AudioTrack.SESSION_ID_NOT_SET;
|
||||
shouldReadInputBuffer = true;
|
||||
audioTrack.reset();
|
||||
audioTrack.release();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
*/
|
||||
package com.google.android.exoplayer;
|
||||
|
||||
import android.media.MediaCodec;
|
||||
import android.media.MediaExtractor;
|
||||
|
||||
/**
|
||||
|
|
@ -43,11 +44,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() {}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -202,7 +202,7 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer {
|
|||
protected void onDisabled() {
|
||||
audioSessionId = AudioTrack.SESSION_ID_NOT_SET;
|
||||
try {
|
||||
audioTrack.reset();
|
||||
audioTrack.release();
|
||||
} finally {
|
||||
super.onDisabled();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
package com.google.android.exoplayer;
|
||||
|
||||
import com.google.android.exoplayer.MediaCodecUtil.DecoderQueryException;
|
||||
import com.google.android.exoplayer.drm.DrmInitData;
|
||||
import com.google.android.exoplayer.drm.DrmSessionManager;
|
||||
import com.google.android.exoplayer.util.Assertions;
|
||||
import com.google.android.exoplayer.util.Util;
|
||||
|
|
@ -25,7 +26,6 @@ import android.media.MediaCodec;
|
|||
import android.media.MediaCodec.CodecException;
|
||||
import android.media.MediaCodec.CryptoException;
|
||||
import android.media.MediaCrypto;
|
||||
import android.media.MediaExtractor;
|
||||
import android.os.Handler;
|
||||
import android.os.SystemClock;
|
||||
|
||||
|
|
@ -33,8 +33,6 @@ import java.io.IOException;
|
|||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* An abstract {@link TrackRenderer} that uses {@link MediaCodec} to decode samples for rendering.
|
||||
|
|
@ -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++) {
|
||||
|
|
|
|||
|
|
@ -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}.
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -40,6 +40,8 @@ public class MediaFormat {
|
|||
public final String mimeType;
|
||||
public final int maxInputSize;
|
||||
|
||||
public final long durationUs;
|
||||
|
||||
public final int width;
|
||||
public final int height;
|
||||
public final float pixelWidthHeightRatio;
|
||||
|
|
@ -49,11 +51,11 @@ public class MediaFormat {
|
|||
|
||||
public final int bitrate;
|
||||
|
||||
public final List<byte[]> initializationData;
|
||||
|
||||
private int maxWidth;
|
||||
private int maxHeight;
|
||||
|
||||
public final List<byte[]> initializationData;
|
||||
|
||||
// Lazy-initialized hashcode.
|
||||
private int hashCode;
|
||||
// Possibly-lazy-initialized framework media format.
|
||||
|
|
@ -66,25 +68,38 @@ public class MediaFormat {
|
|||
|
||||
public static MediaFormat createVideoFormat(String mimeType, int maxInputSize, int width,
|
||||
int height, List<byte[]> initializationData) {
|
||||
return createVideoFormat(mimeType, maxInputSize, width, height, 1, initializationData);
|
||||
return createVideoFormat(
|
||||
mimeType, maxInputSize, C.UNKNOWN_TIME_US, width, height, initializationData);
|
||||
}
|
||||
|
||||
public static MediaFormat createVideoFormat(String mimeType, int maxInputSize, int width,
|
||||
int height, float pixelWidthHeightRatio, List<byte[]> initializationData) {
|
||||
return new MediaFormat(mimeType, maxInputSize, width, height, pixelWidthHeightRatio, NO_VALUE,
|
||||
NO_VALUE, NO_VALUE, initializationData);
|
||||
public static MediaFormat createVideoFormat(String mimeType, int maxInputSize, long durationUs,
|
||||
int width, int height, List<byte[]> initializationData) {
|
||||
return createVideoFormat(
|
||||
mimeType, maxInputSize, durationUs, width, height, 1, initializationData);
|
||||
}
|
||||
|
||||
public static MediaFormat createVideoFormat(String mimeType, int maxInputSize, long durationUs,
|
||||
int width, int height, float pixelWidthHeightRatio, List<byte[]> initializationData) {
|
||||
return new MediaFormat(mimeType, maxInputSize, durationUs, width, height, pixelWidthHeightRatio,
|
||||
NO_VALUE, NO_VALUE, NO_VALUE, initializationData);
|
||||
}
|
||||
|
||||
public static MediaFormat createAudioFormat(String mimeType, int maxInputSize, int channelCount,
|
||||
int sampleRate, List<byte[]> initializationData) {
|
||||
return new MediaFormat(mimeType, maxInputSize, NO_VALUE, NO_VALUE, NO_VALUE, channelCount,
|
||||
sampleRate, NO_VALUE, initializationData);
|
||||
return createAudioFormat(
|
||||
mimeType, maxInputSize, C.UNKNOWN_TIME_US, channelCount, sampleRate, initializationData);
|
||||
}
|
||||
|
||||
public static MediaFormat createAudioFormat(String mimeType, int maxInputSize, int channelCount,
|
||||
int sampleRate, int bitrate, List<byte[]> initializationData) {
|
||||
return new MediaFormat(mimeType, maxInputSize, NO_VALUE, NO_VALUE, NO_VALUE, channelCount,
|
||||
sampleRate, bitrate, initializationData);
|
||||
public static MediaFormat createAudioFormat(String mimeType, int maxInputSize, long durationUs,
|
||||
int channelCount, int sampleRate, List<byte[]> initializationData) {
|
||||
return createAudioFormat(
|
||||
mimeType, maxInputSize, durationUs, channelCount, sampleRate, NO_VALUE, initializationData);
|
||||
}
|
||||
|
||||
public static MediaFormat createAudioFormat(String mimeType, int maxInputSize, long durationUs,
|
||||
int channelCount, int sampleRate, int bitrate, List<byte[]> initializationData) {
|
||||
return new MediaFormat(mimeType, maxInputSize, durationUs, NO_VALUE, NO_VALUE, NO_VALUE,
|
||||
channelCount, sampleRate, bitrate, initializationData);
|
||||
}
|
||||
|
||||
public static MediaFormat createId3Format() {
|
||||
|
|
@ -100,8 +115,8 @@ public class MediaFormat {
|
|||
}
|
||||
|
||||
public static MediaFormat createFormatForMimeType(String mimeType) {
|
||||
return new MediaFormat(mimeType, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE,
|
||||
NO_VALUE, null);
|
||||
return new MediaFormat(mimeType, NO_VALUE, C.UNKNOWN_TIME_US, NO_VALUE, NO_VALUE, NO_VALUE,
|
||||
NO_VALUE, NO_VALUE, NO_VALUE, null);
|
||||
}
|
||||
|
||||
@TargetApi(16)
|
||||
|
|
@ -123,15 +138,18 @@ public class MediaFormat {
|
|||
initializationData.add(data);
|
||||
buffer.flip();
|
||||
}
|
||||
durationUs = format.containsKey(android.media.MediaFormat.KEY_DURATION)
|
||||
? format.getLong(android.media.MediaFormat.KEY_DURATION) : C.UNKNOWN_TIME_US;
|
||||
maxWidth = NO_VALUE;
|
||||
maxHeight = NO_VALUE;
|
||||
}
|
||||
|
||||
private MediaFormat(String mimeType, int maxInputSize, int width, int height,
|
||||
private MediaFormat(String mimeType, int maxInputSize, long durationUs, int width, int height,
|
||||
float pixelWidthHeightRatio, int channelCount, int sampleRate, int bitrate,
|
||||
List<byte[]> initializationData) {
|
||||
this.mimeType = mimeType;
|
||||
this.maxInputSize = maxInputSize;
|
||||
this.durationUs = durationUs;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.pixelWidthHeightRatio = pixelWidthHeightRatio;
|
||||
|
|
@ -169,6 +187,7 @@ public class MediaFormat {
|
|||
result = 31 * result + width;
|
||||
result = 31 * result + height;
|
||||
result = 31 * result + Float.floatToRawIntBits(pixelWidthHeightRatio);
|
||||
result = 31 * result + (int) durationUs;
|
||||
result = 31 * result + maxWidth;
|
||||
result = 31 * result + maxHeight;
|
||||
result = 31 * result + channelCount;
|
||||
|
|
@ -225,7 +244,7 @@ public class MediaFormat {
|
|||
public String toString() {
|
||||
return "MediaFormat(" + mimeType + ", " + maxInputSize + ", " + width + ", " + height + ", "
|
||||
+ pixelWidthHeightRatio + ", " + channelCount + ", " + sampleRate + ", " + bitrate + ", "
|
||||
+ maxWidth + ", " + maxHeight + ")";
|
||||
+ durationUs + ", " + maxWidth + ", " + maxHeight + ")";
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -246,6 +265,9 @@ public class MediaFormat {
|
|||
for (int i = 0; i < initializationData.size(); i++) {
|
||||
format.setByteBuffer("csd-" + i, ByteBuffer.wrap(initializationData.get(i)));
|
||||
}
|
||||
if (durationUs != C.UNKNOWN_TIME_US) {
|
||||
format.setLong(android.media.MediaFormat.KEY_DURATION, durationUs);
|
||||
}
|
||||
maybeSetMaxDimensionsV16(format);
|
||||
frameworkMediaFormat = format;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,8 +15,7 @@
|
|||
*/
|
||||
package com.google.android.exoplayer;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import com.google.android.exoplayer.drm.DrmInitData;
|
||||
|
||||
/**
|
||||
* Holds a {@link MediaFormat} and corresponding drm scheme initialization data.
|
||||
|
|
@ -28,9 +27,8 @@ public final class MediaFormatHolder {
|
|||
*/
|
||||
public MediaFormat format;
|
||||
/**
|
||||
* Initialization data for each of the drm schemes supported by the media, keyed by scheme UUID.
|
||||
* Null if the media is not encrypted.
|
||||
* Initialization data for drm schemes supported by the media. Null if the media is not encrypted.
|
||||
*/
|
||||
public Map<UUID, byte[]> drmInitData;
|
||||
public DrmInitData drmInitData;
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,9 +50,8 @@ public final class SampleHolder {
|
|||
public int size;
|
||||
|
||||
/**
|
||||
* Flags that accompany the sample. A combination of
|
||||
* {@link android.media.MediaExtractor#SAMPLE_FLAG_SYNC} and
|
||||
* {@link android.media.MediaExtractor#SAMPLE_FLAG_ENCRYPTED}
|
||||
* Flags that accompany the sample. A combination of {@link C#SAMPLE_FLAG_SYNC},
|
||||
* {@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.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -44,6 +44,8 @@ import java.nio.ByteBuffer;
|
|||
* <p>Call {@link #reconfigure} when the output format changes.
|
||||
*
|
||||
* <p>Call {@link #reset} to free resources. It is safe to re-{@link #initialize} the instance.
|
||||
*
|
||||
* <p>Call {@link #release} when the instance will no longer be used.
|
||||
*/
|
||||
@TargetApi(16)
|
||||
public final class AudioTrack {
|
||||
|
|
@ -91,6 +93,12 @@ public final class AudioTrack {
|
|||
/** Returned by {@link #getCurrentPositionUs} when the position is not set. */
|
||||
public static final long CURRENT_POSITION_NOT_SET = Long.MIN_VALUE;
|
||||
|
||||
/**
|
||||
* Set to {@code true} to enable a workaround for an issue where an audio effect does not keep its
|
||||
* session active across releasing/initializing a new audio track, on platform API version < 21.
|
||||
*/
|
||||
private static final boolean ENABLE_PRE_V21_AUDIO_SESSION_WORKAROUND = false;
|
||||
|
||||
/** A minimum length for the {@link android.media.AudioTrack} buffer, in microseconds. */
|
||||
private static final long MIN_BUFFER_DURATION_US = 250000;
|
||||
/** A maximum length for the {@link android.media.AudioTrack} buffer, in microseconds. */
|
||||
|
|
@ -132,6 +140,9 @@ public final class AudioTrack {
|
|||
private final ConditionVariable releasingConditionVariable;
|
||||
private final long[] playheadOffsets;
|
||||
|
||||
/** Used to keep the audio session active on pre-V21 builds (see {@link #initialize()}). */
|
||||
private android.media.AudioTrack keepSessionIdAudioTrack;
|
||||
|
||||
private android.media.AudioTrack audioTrack;
|
||||
private AudioTrackUtil audioTrackUtil;
|
||||
private int sampleRate;
|
||||
|
|
@ -267,15 +278,37 @@ public final class AudioTrack {
|
|||
audioTrack = new android.media.AudioTrack(AudioManager.STREAM_MUSIC, sampleRate,
|
||||
channelConfig, encoding, bufferSize, android.media.AudioTrack.MODE_STREAM, sessionId);
|
||||
}
|
||||
|
||||
checkAudioTrackInitialized();
|
||||
|
||||
sessionId = audioTrack.getAudioSessionId();
|
||||
if (ENABLE_PRE_V21_AUDIO_SESSION_WORKAROUND) {
|
||||
if (Util.SDK_INT < 21) {
|
||||
// The workaround creates an audio track with a one byte buffer on the same session, and
|
||||
// does not release it until this object is released, which keeps the session active.
|
||||
if (keepSessionIdAudioTrack != null
|
||||
&& sessionId != keepSessionIdAudioTrack.getAudioSessionId()) {
|
||||
releaseKeepSessionIdAudioTrack();
|
||||
}
|
||||
if (keepSessionIdAudioTrack == null) {
|
||||
int sampleRate = 4000; // Equal to private android.media.AudioTrack.MIN_SAMPLE_RATE.
|
||||
int channelConfig = AudioFormat.CHANNEL_OUT_MONO;
|
||||
int encoding = AudioFormat.ENCODING_PCM_8BIT;
|
||||
int bufferSize = 1; // Use a one byte buffer, as it is not actually used for playback.
|
||||
keepSessionIdAudioTrack = new android.media.AudioTrack(AudioManager.STREAM_MUSIC,
|
||||
sampleRate, channelConfig, encoding, bufferSize, android.media.AudioTrack.MODE_STATIC,
|
||||
sessionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Util.SDK_INT >= 19) {
|
||||
audioTrackUtil = new AudioTrackUtilV19(audioTrack);
|
||||
} else {
|
||||
audioTrackUtil = new AudioTrackUtil(audioTrack);
|
||||
}
|
||||
setVolume(volume);
|
||||
return audioTrack.getAudioSessionId();
|
||||
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -515,9 +548,9 @@ public final class AudioTrack {
|
|||
}
|
||||
|
||||
/**
|
||||
* Releases resources associated with this instance asynchronously. Calling {@link #initialize}
|
||||
* will block until the audio track has been released, so it is safe to initialize immediately
|
||||
* after resetting.
|
||||
* Releases the underlying audio track asynchronously. Calling {@link #initialize} will block
|
||||
* until the audio track has been released, so it is safe to initialize immediately after
|
||||
* resetting. The audio session may remain active until the instance is {@link #release}d.
|
||||
*/
|
||||
public void reset() {
|
||||
if (isInitialized()) {
|
||||
|
|
@ -547,6 +580,29 @@ public final class AudioTrack {
|
|||
}
|
||||
}
|
||||
|
||||
/** Releases all resources associated with this instance. */
|
||||
public void release() {
|
||||
reset();
|
||||
releaseKeepSessionIdAudioTrack();
|
||||
}
|
||||
|
||||
/** Releases {@link #keepSessionIdAudioTrack} asynchronously, if it is non-{@code null}. */
|
||||
private void releaseKeepSessionIdAudioTrack() {
|
||||
if (keepSessionIdAudioTrack == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// AudioTrack.release can take some time, so we call it on a background thread.
|
||||
final android.media.AudioTrack toRelease = keepSessionIdAudioTrack;
|
||||
keepSessionIdAudioTrack = null;
|
||||
new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
toRelease.release();
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
|
||||
/** Returns whether {@link #getCurrentPositionUs} can return the current playback position. */
|
||||
private boolean hasCurrentPositionUs() {
|
||||
return isInitialized() && startMediaTimeUs != START_NOT_SET;
|
||||
|
|
|
|||
|
|
@ -352,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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -18,12 +18,10 @@ package com.google.android.exoplayer.chunk;
|
|||
import com.google.android.exoplayer.MediaFormat;
|
||||
import com.google.android.exoplayer.ParserException;
|
||||
import com.google.android.exoplayer.SampleHolder;
|
||||
import com.google.android.exoplayer.drm.DrmInitData;
|
||||
import com.google.android.exoplayer.upstream.DataSource;
|
||||
import com.google.android.exoplayer.upstream.DataSpec;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* An abstract base class for {@link Chunk}s that contain media samples.
|
||||
*/
|
||||
|
|
@ -129,12 +127,12 @@ public abstract class MediaChunk extends Chunk {
|
|||
public abstract MediaFormat getMediaFormat();
|
||||
|
||||
/**
|
||||
* Returns the pssh information associated with the chunk.
|
||||
* Returns the DRM initialization data associated with the chunk.
|
||||
* <p>
|
||||
* Should only be called after the chunk has been successfully prepared.
|
||||
*
|
||||
* @return The pssh information.
|
||||
* @return The DRM initialization data.
|
||||
*/
|
||||
public abstract Map<UUID, byte[]> getPsshInfo();
|
||||
public abstract DrmInitData getDrmInitData();
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,14 +17,12 @@ package com.google.android.exoplayer.chunk;
|
|||
|
||||
import com.google.android.exoplayer.MediaFormat;
|
||||
import com.google.android.exoplayer.SampleHolder;
|
||||
import com.google.android.exoplayer.drm.DrmInitData;
|
||||
import com.google.android.exoplayer.upstream.DataSource;
|
||||
import com.google.android.exoplayer.upstream.DataSpec;
|
||||
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
|
||||
import com.google.android.exoplayer.util.Assertions;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* A {@link MediaChunk} containing a single sample.
|
||||
*/
|
||||
|
|
@ -132,7 +130,7 @@ public class SingleSampleMediaChunk extends MediaChunk {
|
|||
}
|
||||
|
||||
@Override
|
||||
public Map<UUID, byte[]> getPsshInfo() {
|
||||
public DrmInitData getDrmInitData() {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,15 +15,12 @@
|
|||
*/
|
||||
package com.google.android.exoplayer.chunk.parser;
|
||||
|
||||
import com.google.android.exoplayer.C;
|
||||
import com.google.android.exoplayer.MediaFormat;
|
||||
import com.google.android.exoplayer.ParserException;
|
||||
import com.google.android.exoplayer.SampleHolder;
|
||||
import com.google.android.exoplayer.drm.DrmInitData;
|
||||
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Facilitates extraction of media samples from a container format.
|
||||
*/
|
||||
|
|
@ -43,7 +40,7 @@ public interface Extractor {
|
|||
public static final int RESULT_READ_SAMPLE = 4;
|
||||
/**
|
||||
* Initialization data was read. The parsed data can be read using {@link #getFormat()} and
|
||||
* {@link #getPsshInfo}.
|
||||
* {@link #getDrmInitData()}.
|
||||
*/
|
||||
public static final int RESULT_READ_INIT = 8;
|
||||
/**
|
||||
|
|
@ -80,17 +77,12 @@ public interface Extractor {
|
|||
public MediaFormat getFormat();
|
||||
|
||||
/**
|
||||
* Returns the duration of the stream in microseconds, or {@link C#UNKNOWN_TIME_US} if unknown.
|
||||
*/
|
||||
public long getDurationUs();
|
||||
|
||||
/**
|
||||
* Returns the pssh information parsed from the stream.
|
||||
* Returns DRM initialization data parsed from the stream.
|
||||
*
|
||||
* @return The pssh information. May be null if pssh data has yet to be parsed, or if the stream
|
||||
* does not contain any pssh data.
|
||||
* @return The DRM initialization data. May be null if the initialization data has yet to be
|
||||
* parsed, or if the stream does not contain any DRM initialization data.
|
||||
*/
|
||||
public Map<UUID, byte[]> getPsshInfo();
|
||||
public DrmInitData getDrmInitData();
|
||||
|
||||
/**
|
||||
* Consumes data from a {@link NonBlockingInputStream}.
|
||||
|
|
|
|||
|
|
@ -21,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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -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) {
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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.
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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:
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 + ")");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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[]>();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -57,6 +57,8 @@ package com.google.android.exoplayer.text.eia608;
|
|||
public static final byte CARRIAGE_RETURN = 0x2D;
|
||||
public static final byte ERASE_NON_DISPLAYED_MEMORY = 0x2E;
|
||||
|
||||
public static final byte BACKSPACE = 0x21;
|
||||
|
||||
|
||||
public static final byte MID_ROW_CHAN_1 = 0x11;
|
||||
public static final byte MID_ROW_CHAN_2 = 0x19;
|
||||
|
|
|
|||
|
|
@ -82,6 +82,26 @@ public class Eia608Parser {
|
|||
0xFB // 3F: 251 'û' "Latin small letter U with circumflex"
|
||||
};
|
||||
|
||||
// Extended Spanish/Miscellaneous and French char set.
|
||||
private static final int[] SPECIAL_ES_FR_CHARACTER_SET = new int[] {
|
||||
// Spanish and misc.
|
||||
0xC1, 0xC9, 0xD3, 0xDA, 0xDC, 0xFC, 0x2018, 0xA1,
|
||||
0x2A, 0x27, 0x2014, 0xA9, 0x2120, 0x2022, 0x201C, 0x201D,
|
||||
// French.
|
||||
0xC0, 0xC2, 0xC7, 0xC8, 0xCA, 0xCB, 0xEB, 0xCE,
|
||||
0xCF, 0xEF, 0xD4, 0xD9, 0xF9, 0xDB, 0xAB, 0xBB
|
||||
};
|
||||
|
||||
//Extended Portuguese and German/Danish char set.
|
||||
private static final int[] SPECIAL_PT_DE_CHARACTER_SET = new int[] {
|
||||
// Portuguese.
|
||||
0xC3, 0xE3, 0xCD, 0xCC, 0xEC, 0xD2, 0xF2, 0xD5,
|
||||
0xF5, 0x7B, 0x7D, 0x5C, 0x5E, 0x5F, 0x7C, 0x7E,
|
||||
// German/Danish.
|
||||
0xC4, 0xE4, 0xD6, 0xF6, 0xDF, 0xA5, 0xA4, 0x2502,
|
||||
0xC5, 0xE5, 0xD8, 0xF8, 0x250C, 0x2510, 0x2514, 0x2518
|
||||
};
|
||||
|
||||
private final ParsableBitArray seiBuffer;
|
||||
private final StringBuilder stringBuilder;
|
||||
private final ArrayList<ClosedCaption> captions;
|
||||
|
|
@ -134,31 +154,45 @@ public class Eia608Parser {
|
|||
}
|
||||
|
||||
// Special North American character set.
|
||||
if ((ccData1 == 0x11) && ((ccData2 & 0x70) == 0x30)) {
|
||||
// ccData2 - P|0|1|1|X|X|X|X
|
||||
if ((ccData1 == 0x11 || ccData1 == 0x19)
|
||||
&& ((ccData2 & 0x70) == 0x30)) {
|
||||
stringBuilder.append(getSpecialChar(ccData2));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Extended Spanish/Miscellaneous and French character set.
|
||||
// ccData2 - P|0|1|X|X|X|X|X
|
||||
if ((ccData1 == 0x12 || ccData1 == 0x1A)
|
||||
&& ((ccData2 & 0x60) == 0x20)) {
|
||||
backspace(); // Remove standard equivalent of the special extended char.
|
||||
stringBuilder.append(getExtendedEsFrChar(ccData2));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Extended Portuguese and German/Danish character set.
|
||||
// ccData2 - P|0|1|X|X|X|X|X
|
||||
if ((ccData1 == 0x13 || ccData1 == 0x1B)
|
||||
&& ((ccData2 & 0x60) == 0x20)) {
|
||||
backspace(); // Remove standard equivalent of the special extended char.
|
||||
stringBuilder.append(getExtendedPtDeChar(ccData2));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Control character.
|
||||
if (ccData1 < 0x20) {
|
||||
if (stringBuilder.length() > 0) {
|
||||
captions.add(new ClosedCaptionText(stringBuilder.toString()));
|
||||
stringBuilder.setLength(0);
|
||||
}
|
||||
captions.add(new ClosedCaptionCtrl(ccData1, ccData2));
|
||||
addCtrl(ccData1, ccData2);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Basic North American character set.
|
||||
stringBuilder.append(getChar(ccData1));
|
||||
if (ccData2 != 0) {
|
||||
if (ccData2 >= 0x20) {
|
||||
stringBuilder.append(getChar(ccData2));
|
||||
}
|
||||
}
|
||||
|
||||
if (stringBuilder.length() > 0) {
|
||||
captions.add(new ClosedCaptionText(stringBuilder.toString()));
|
||||
}
|
||||
addBufferedText();
|
||||
|
||||
if (captions.isEmpty()) {
|
||||
return null;
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 + "]";
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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() {}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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
Loading…
Reference in a new issue