Commit da7ae2a9 by Sergio Moreno Mozota

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

parents 9208c655 6c5af232
Showing with 1849 additions and 701 deletions
...@@ -17,9 +17,11 @@ ...@@ -17,9 +17,11 @@
buildscript { buildscript {
repositories { repositories {
mavenCentral() mavenCentral()
jcenter()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:1.0.0' classpath 'com.android.tools.build:gradle:1.0.0'
classpath 'com.novoda:bintray-release:0.2.7'
} }
} }
......
...@@ -48,6 +48,7 @@ public class DemoUtil { ...@@ -48,6 +48,7 @@ public class DemoUtil {
public static final int TYPE_SS = 1; public static final int TYPE_SS = 1;
public static final int TYPE_OTHER = 2; public static final int TYPE_OTHER = 2;
public static final int TYPE_HLS = 3; public static final int TYPE_HLS = 3;
public static final int TYPE_MP4 = 4;
private static final CookieManager defaultCookieManager; private static final CookieManager defaultCookieManager;
......
...@@ -163,6 +163,12 @@ public class EventLogger implements DemoPlayer.Listener, DemoPlayer.InfoListener ...@@ -163,6 +163,12 @@ public class EventLogger implements DemoPlayer.Listener, DemoPlayer.InfoListener
printInternalError("cryptoError", e); printInternalError("cryptoError", e);
} }
@Override
public void onDecoderInitialized(String decoderName, long elapsedRealtimeMs,
long initializationDurationMs) {
Log.d(TAG, "decoderInitialized [" + getSessionTimeString() + "]");
}
private void printInternalError(String type, Exception e) { private void printInternalError(String type, Exception e) {
Log.e(TAG, "internalError [" + getSessionTimeString() + ", " + type + "]", e); Log.e(TAG, "internalError [" + getSessionTimeString() + ", " + type + "]", e);
} }
......
...@@ -24,8 +24,11 @@ import com.google.android.exoplayer.demo.player.DefaultRendererBuilder; ...@@ -24,8 +24,11 @@ import com.google.android.exoplayer.demo.player.DefaultRendererBuilder;
import com.google.android.exoplayer.demo.player.DemoPlayer; import com.google.android.exoplayer.demo.player.DemoPlayer;
import com.google.android.exoplayer.demo.player.DemoPlayer.RendererBuilder; import com.google.android.exoplayer.demo.player.DemoPlayer.RendererBuilder;
import com.google.android.exoplayer.demo.player.HlsRendererBuilder; import com.google.android.exoplayer.demo.player.HlsRendererBuilder;
import com.google.android.exoplayer.demo.player.Mp4RendererBuilder;
import com.google.android.exoplayer.demo.player.SmoothStreamingRendererBuilder; import com.google.android.exoplayer.demo.player.SmoothStreamingRendererBuilder;
import com.google.android.exoplayer.demo.player.UnsupportedDrmException; import com.google.android.exoplayer.demo.player.UnsupportedDrmException;
import com.google.android.exoplayer.metadata.GeobMetadata;
import com.google.android.exoplayer.metadata.PrivMetadata;
import com.google.android.exoplayer.metadata.TxxxMetadata; import com.google.android.exoplayer.metadata.TxxxMetadata;
import com.google.android.exoplayer.text.CaptionStyleCompat; import com.google.android.exoplayer.text.CaptionStyleCompat;
import com.google.android.exoplayer.text.SubtitleView; import com.google.android.exoplayer.text.SubtitleView;
...@@ -213,6 +216,8 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback, ...@@ -213,6 +216,8 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback,
new WidevineTestMediaDrmCallback(contentId), debugTextView, audioCapabilities); new WidevineTestMediaDrmCallback(contentId), debugTextView, audioCapabilities);
case DemoUtil.TYPE_HLS: case DemoUtil.TYPE_HLS:
return new HlsRendererBuilder(userAgent, contentUri.toString()); return new HlsRendererBuilder(userAgent, contentUri.toString());
case DemoUtil.TYPE_MP4:
return new Mp4RendererBuilder(contentUri, debugTextView);
default: default:
return new DefaultRendererBuilder(this, contentUri, debugTextView); return new DefaultRendererBuilder(this, contentUri, debugTextView);
} }
...@@ -446,11 +451,22 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback, ...@@ -446,11 +451,22 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback,
@Override @Override
public void onId3Metadata(Map<String, Object> metadata) { public void onId3Metadata(Map<String, Object> metadata) {
for (int i = 0; i < metadata.size(); i++) { for (Map.Entry<String, Object> entry : metadata.entrySet()) {
if (metadata.containsKey(TxxxMetadata.TYPE)) { if (TxxxMetadata.TYPE.equals(entry.getKey())) {
TxxxMetadata txxxMetadata = (TxxxMetadata) metadata.get(TxxxMetadata.TYPE); TxxxMetadata txxxMetadata = (TxxxMetadata) entry.getValue();
Log.i(TAG, String.format("ID3 TimedMetadata: description=%s, value=%s", Log.i(TAG, String.format("ID3 TimedMetadata %s: description=%s, value=%s",
txxxMetadata.description, txxxMetadata.value)); TxxxMetadata.TYPE, txxxMetadata.description, txxxMetadata.value));
} else if (PrivMetadata.TYPE.equals(entry.getKey())) {
PrivMetadata privMetadata = (PrivMetadata) entry.getValue();
Log.i(TAG, String.format("ID3 TimedMetadata %s: owner=%s",
PrivMetadata.TYPE, privMetadata.owner));
} else if (GeobMetadata.TYPE.equals(entry.getKey())) {
GeobMetadata geobMetadata = (GeobMetadata) entry.getValue();
Log.i(TAG, String.format("ID3 TimedMetadata %s: mimeType=%s, filename=%s, description=%s",
GeobMetadata.TYPE, geobMetadata.mimeType, geobMetadata.filename,
geobMetadata.description));
} else {
Log.i(TAG, String.format("ID3 TimedMetadata %s", entry.getKey()));
} }
} }
} }
......
...@@ -103,7 +103,7 @@ import java.util.Locale; ...@@ -103,7 +103,7 @@ import java.util.Locale;
+ "as=fmp4_audio_cenc,fmp4_sd_hd_cenc&sparams=ip,ipbits,expire,as&ip=0.0.0.0&ipbits=0" + "as=fmp4_audio_cenc,fmp4_sd_hd_cenc&sparams=ip,ipbits,expire,as&ip=0.0.0.0&ipbits=0"
+ "&expire=19000000000&signature=61611F115EEEC7BADE5536827343FFFE2D83D14F." + "&expire=19000000000&signature=61611F115EEEC7BADE5536827343FFFE2D83D14F."
+ "2FDF4BFA502FB5865C5C86401314BDDEA4799BD0&key=ik0", DemoUtil.TYPE_DASH), + "2FDF4BFA502FB5865C5C86401314BDDEA4799BD0&key=ik0", DemoUtil.TYPE_DASH),
new Sample("WV: 30s license duration", "f9a34cab7b05881a", new Sample("WV: 30s license duration (fails at ~30s)", "f9a34cab7b05881a",
"http://www.youtube.com/api/manifest/dash/id/f9a34cab7b05881a/source/youtube?" "http://www.youtube.com/api/manifest/dash/id/f9a34cab7b05881a/source/youtube?"
+ "as=fmp4_audio_cenc,fmp4_sd_hd_cenc&sparams=ip,ipbits,expire,as&ip=0.0.0.0&ipbits=0" + "as=fmp4_audio_cenc,fmp4_sd_hd_cenc&sparams=ip,ipbits,expire,as&ip=0.0.0.0&ipbits=0"
+ "&expire=19000000000&signature=88DC53943385CED8CF9F37ADD9E9843E3BF621E6." + "&expire=19000000000&signature=88DC53943385CED8CF9F37ADD9E9843E3BF621E6."
...@@ -123,6 +123,8 @@ import java.util.Locale; ...@@ -123,6 +123,8 @@ import java.util.Locale;
new Sample("Apple AAC media playlist", new Sample("Apple AAC media playlist",
"https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/gear0/" "https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/gear0/"
+ "prog_index.m3u8", DemoUtil.TYPE_HLS), + "prog_index.m3u8", DemoUtil.TYPE_HLS),
new Sample("Apple ID3 metadata", "http://devimages.apple.com/samplecode/adDemo/ad.m3u8",
DemoUtil.TYPE_HLS),
}; };
public static final Sample[] MISC = new Sample[] { public static final Sample[] MISC = new Sample[] {
...@@ -133,6 +135,12 @@ import java.util.Locale; ...@@ -133,6 +135,12 @@ import java.util.Locale;
new Sample("Apple AAC 10s", "https://devimages.apple.com.edgekey.net/" new Sample("Apple AAC 10s", "https://devimages.apple.com.edgekey.net/"
+ "streaming/examples/bipbop_4x3/gear0/fileSequence0.aac", + "streaming/examples/bipbop_4x3/gear0/fileSequence0.aac",
DemoUtil.TYPE_OTHER), DemoUtil.TYPE_OTHER),
new Sample("Big Buck Bunny (MP4)",
"http://redirector.c.youtube.com/videoplayback?id=604ed5ce52eda7ee&itag=22&source=youtube"
+ "&sparams=ip,ipbits,expire&ip=0.0.0.0&ipbits=0&expire=19000000000&signature="
+ "2E853B992F6CAB9D28CA3BEBD84A6F26709A8A55.94344B0D8BA83A7417AAD24DACC8C71A9A878ECE"
+ "&key=ik0",
DemoUtil.TYPE_MP4),
}; };
private Samples() {} private Samples() {}
......
...@@ -129,6 +129,8 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi ...@@ -129,6 +129,8 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
void onLoadStarted(int sourceId, String formatId, int trigger, boolean isInitialization, void onLoadStarted(int sourceId, String formatId, int trigger, boolean isInitialization,
int mediaStartTimeMs, int mediaEndTimeMs, long length); int mediaStartTimeMs, int mediaEndTimeMs, long length);
void onLoadCompleted(int sourceId, long bytesLoaded); void onLoadCompleted(int sourceId, long bytesLoaded);
void onDecoderInitialized(String decoderName, long elapsedRealtimeMs,
long initializationDurationMs);
} }
/** /**
...@@ -478,6 +480,16 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi ...@@ -478,6 +480,16 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
} }
@Override @Override
public void onDecoderInitialized(
String decoderName,
long elapsedRealtimeMs,
long initializationDurationMs) {
if (infoListener != null) {
infoListener.onDecoderInitialized(decoderName, elapsedRealtimeMs, initializationDurationMs);
}
}
@Override
public void onUpstreamError(int sourceId, IOException e) { public void onUpstreamError(int sourceId, IOException e) {
if (internalErrorListener != null) { if (internalErrorListener != null) {
internalErrorListener.onUpstreamError(sourceId, e); internalErrorListener.onUpstreamError(sourceId, e);
......
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.demo.player;
import com.google.android.exoplayer.MediaCodecAudioTrackRenderer;
import com.google.android.exoplayer.MediaCodecVideoTrackRenderer;
import com.google.android.exoplayer.TrackRenderer;
import com.google.android.exoplayer.demo.player.DemoPlayer.RendererBuilder;
import com.google.android.exoplayer.demo.player.DemoPlayer.RendererBuilderCallback;
import com.google.android.exoplayer.source.DefaultSampleSource;
import com.google.android.exoplayer.source.Mp4SampleExtractor;
import com.google.android.exoplayer.upstream.DataSpec;
import com.google.android.exoplayer.upstream.UriDataSource;
import android.media.MediaCodec;
import android.net.Uri;
import android.widget.TextView;
/**
* A {@link RendererBuilder} for streams that can be read using {@link Mp4SampleExtractor}.
*/
public class Mp4RendererBuilder implements RendererBuilder {
private final Uri uri;
private final TextView debugTextView;
public Mp4RendererBuilder(Uri uri, TextView debugTextView) {
this.uri = uri;
this.debugTextView = debugTextView;
}
@Override
public void buildRenderers(DemoPlayer player, RendererBuilderCallback callback) {
// Build the video and audio renderers.
DefaultSampleSource sampleSource = new DefaultSampleSource(
new Mp4SampleExtractor(new UriDataSource("exoplayer", null), new DataSpec(uri)), 2);
MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(sampleSource,
null, true, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 5000, null, player.getMainHandler(),
player, 50);
MediaCodecAudioTrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer(sampleSource,
null, true, player.getMainHandler(), player);
// Build the debug renderer.
TrackRenderer debugRenderer = debugTextView != null
? new DebugTrackRenderer(debugTextView, videoRenderer)
: null;
// Invoke the callback.
TrackRenderer[] renderers = new TrackRenderer[DemoPlayer.RENDERER_COUNT];
renderers[DemoPlayer.TYPE_VIDEO] = videoRenderer;
renderers[DemoPlayer.TYPE_AUDIO] = audioRenderer;
renderers[DemoPlayer.TYPE_DEBUG] = debugRenderer;
callback.onRenderers(null, null, renderers);
}
}
...@@ -12,6 +12,7 @@ ...@@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
apply plugin: 'com.android.library' apply plugin: 'com.android.library'
apply plugin: 'bintray-release'
android { android {
compileSdkVersion 21 compileSdkVersion 21
...@@ -47,3 +48,13 @@ android.libraryVariants.all { variant -> ...@@ -47,3 +48,13 @@ android.libraryVariants.all { variant ->
task.from variant.javaCompile.destinationDir task.from variant.javaCompile.destinationDir
artifacts.add('archives', task); artifacts.add('archives', task);
} }
publish {
repoName = 'exoplayer'
userOrg = 'google'
groupId = 'com.google.android.exoplayer'
artifactId = 'exoplayer'
version = 'r1.2.3'
description = 'The ExoPlayer library.'
website = 'https://github.com/google/ExoPlayer'
}
...@@ -281,7 +281,7 @@ public final class Ac3PassthroughAudioTrackRenderer extends TrackRenderer { ...@@ -281,7 +281,7 @@ public final class Ac3PassthroughAudioTrackRenderer extends TrackRenderer {
protected void onDisabled() { protected void onDisabled() {
audioSessionId = AudioTrack.SESSION_ID_NOT_SET; audioSessionId = AudioTrack.SESSION_ID_NOT_SET;
shouldReadInputBuffer = true; shouldReadInputBuffer = true;
audioTrack.reset(); audioTrack.release();
} }
@Override @Override
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer; package com.google.android.exoplayer;
import android.media.MediaCodec;
import android.media.MediaExtractor; import android.media.MediaExtractor;
/** /**
...@@ -43,11 +44,33 @@ public final class C { ...@@ -43,11 +44,33 @@ public final class C {
public static final String UTF8_NAME = "UTF-8"; public static final String UTF8_NAME = "UTF-8";
/** /**
* Sample flag that indicates the sample is a synchronization sample. * @see MediaCodec#CRYPTO_MODE_AES_CTR
*/
@SuppressWarnings("InlinedApi")
public static final int CRYPTO_MODE_AES_CTR = MediaCodec.CRYPTO_MODE_AES_CTR;
/**
* @see MediaExtractor#SAMPLE_FLAG_SYNC
*/ */
@SuppressWarnings("InlinedApi") @SuppressWarnings("InlinedApi")
public static final int SAMPLE_FLAG_SYNC = MediaExtractor.SAMPLE_FLAG_SYNC; public static final int SAMPLE_FLAG_SYNC = MediaExtractor.SAMPLE_FLAG_SYNC;
/**
* @see MediaExtractor#SAMPLE_FLAG_ENCRYPTED
*/
@SuppressWarnings("InlinedApi")
public static final int SAMPLE_FLAG_ENCRYPTED = MediaExtractor.SAMPLE_FLAG_ENCRYPTED;
/**
* Indicates that a sample should be decoded but not rendered.
*/
public static final int SAMPLE_FLAG_DECODE_ONLY = 0x8000000;
/**
* A return value for methods where the end of an input was encountered.
*/
public static final int RESULT_END_OF_INPUT = -1;
private C() {} private C() {}
} }
...@@ -141,14 +141,6 @@ public interface ExoPlayer { ...@@ -141,14 +141,6 @@ public interface ExoPlayer {
return new ExoPlayerImpl(rendererCount, DEFAULT_MIN_BUFFER_MS, DEFAULT_MIN_REBUFFER_MS); return new ExoPlayerImpl(rendererCount, DEFAULT_MIN_BUFFER_MS, DEFAULT_MIN_REBUFFER_MS);
} }
/**
* @deprecated Please use {@link #newInstance(int, int, int)}.
*/
@Deprecated
public static ExoPlayer newInstance(int rendererCount, int minRebufferMs) {
return new ExoPlayerImpl(rendererCount, DEFAULT_MIN_BUFFER_MS, minRebufferMs);
}
} }
/** /**
...@@ -160,7 +152,8 @@ public interface ExoPlayer { ...@@ -160,7 +152,8 @@ public interface ExoPlayer {
* {@link ExoPlayer#getPlaybackState()} changes. * {@link ExoPlayer#getPlaybackState()} changes.
* *
* @param playWhenReady Whether playback will proceed when ready. * @param playWhenReady Whether playback will proceed when ready.
* @param playbackState One of the {@code STATE} constants defined in this class. * @param playbackState One of the {@code STATE} constants defined in the {@link ExoPlayer}
* interface.
*/ */
void onPlayerStateChanged(boolean playWhenReady, int playbackState); void onPlayerStateChanged(boolean playWhenReady, int playbackState);
/** /**
...@@ -256,7 +249,7 @@ public interface ExoPlayer { ...@@ -256,7 +249,7 @@ public interface ExoPlayer {
/** /**
* Returns the current state of the player. * Returns the current state of the player.
* *
* @return One of the {@code STATE} constants defined in this class. * @return One of the {@code STATE} constants defined in this interface.
*/ */
public int getPlaybackState(); public int getPlaybackState();
......
...@@ -202,7 +202,7 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer { ...@@ -202,7 +202,7 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer {
protected void onDisabled() { protected void onDisabled() {
audioSessionId = AudioTrack.SESSION_ID_NOT_SET; audioSessionId = AudioTrack.SESSION_ID_NOT_SET;
try { try {
audioTrack.reset(); audioTrack.release();
} finally { } finally {
super.onDisabled(); super.onDisabled();
} }
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer; package com.google.android.exoplayer;
import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.MimeTypes; import com.google.android.exoplayer.util.MimeTypes;
import com.google.android.exoplayer.util.Util; import com.google.android.exoplayer.util.Util;
...@@ -180,6 +181,33 @@ public class MediaCodecUtil { ...@@ -180,6 +181,33 @@ public class MediaCodecUtil {
} }
/** /**
* Tests whether the device advertises it can decode video of a given type at a specified
* width, height, and frame rate.
* <p>
* Must not be called if the device SDK version is less than 21.
*
* @param mimeType The mime type.
* @param secure Whether the decoder is required to support secure decryption. Always pass false
* unless secure decryption really is required.
* @param width Width in pixels.
* @param height Height in pixels.
* @param frameRate Frame rate in frames per second.
* @return Whether the decoder advertises support of the given size and frame rate.
*/
@TargetApi(21)
public static boolean isSizeAndRateSupportedV21(String mimeType, boolean secure,
int width, int height, double frameRate) throws DecoderQueryException {
Assertions.checkState(Util.SDK_INT >= 21);
Pair<String, CodecCapabilities> info = getMediaCodecInfo(mimeType, secure);
if (info == null) {
return false;
}
MediaCodecInfo.VideoCapabilities videoCapabilities = info.second.getVideoCapabilities();
return videoCapabilities != null
&& videoCapabilities.areSizeAndRateSupported(width, height, frameRate);
}
/**
* @param profile An AVC profile constant from {@link CodecProfileLevel}. * @param profile An AVC profile constant from {@link CodecProfileLevel}.
* @param level An AVC profile level from {@link CodecProfileLevel}. * @param level An AVC profile level from {@link CodecProfileLevel}.
* @return Whether the specified profile is supported at the specified level. * @return Whether the specified profile is supported at the specified level.
......
...@@ -373,6 +373,13 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer { ...@@ -373,6 +373,13 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer {
: holder.format.pixelWidthHeightRatio; : holder.format.pixelWidthHeightRatio;
} }
/**
* @return True if the first frame has been rendered (playback has not necessarily begun).
*/
protected final boolean haveRenderedFirstFrame() {
return renderedFirstFrame;
}
@Override @Override
protected void onOutputFormatChanged(android.media.MediaFormat format) { protected void onOutputFormatChanged(android.media.MediaFormat format) {
boolean hasCrop = format.containsKey(KEY_CROP_RIGHT) && format.containsKey(KEY_CROP_LEFT) boolean hasCrop = format.containsKey(KEY_CROP_RIGHT) && format.containsKey(KEY_CROP_LEFT)
...@@ -427,7 +434,6 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer { ...@@ -427,7 +434,6 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer {
if (!renderedFirstFrame) { if (!renderedFirstFrame) {
renderOutputBufferImmediate(codec, bufferIndex); renderOutputBufferImmediate(codec, bufferIndex);
renderedFirstFrame = true;
return true; return true;
} }
...@@ -463,14 +469,14 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer { ...@@ -463,14 +469,14 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer {
return false; return false;
} }
private void skipOutputBuffer(MediaCodec codec, int bufferIndex) { protected void skipOutputBuffer(MediaCodec codec, int bufferIndex) {
TraceUtil.beginSection("skipVideoBuffer"); TraceUtil.beginSection("skipVideoBuffer");
codec.releaseOutputBuffer(bufferIndex, false); codec.releaseOutputBuffer(bufferIndex, false);
TraceUtil.endSection(); TraceUtil.endSection();
codecCounters.skippedOutputBufferCount++; codecCounters.skippedOutputBufferCount++;
} }
private void dropOutputBuffer(MediaCodec codec, int bufferIndex) { protected void dropOutputBuffer(MediaCodec codec, int bufferIndex) {
TraceUtil.beginSection("dropVideoBuffer"); TraceUtil.beginSection("dropVideoBuffer");
codec.releaseOutputBuffer(bufferIndex, false); codec.releaseOutputBuffer(bufferIndex, false);
TraceUtil.endSection(); TraceUtil.endSection();
...@@ -481,22 +487,24 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer { ...@@ -481,22 +487,24 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer {
} }
} }
private void renderOutputBufferImmediate(MediaCodec codec, int bufferIndex) { protected void renderOutputBufferImmediate(MediaCodec codec, int bufferIndex) {
maybeNotifyVideoSizeChanged(); maybeNotifyVideoSizeChanged();
TraceUtil.beginSection("renderVideoBufferImmediate"); TraceUtil.beginSection("renderVideoBufferImmediate");
codec.releaseOutputBuffer(bufferIndex, true); codec.releaseOutputBuffer(bufferIndex, true);
TraceUtil.endSection(); TraceUtil.endSection();
codecCounters.renderedOutputBufferCount++; codecCounters.renderedOutputBufferCount++;
renderedFirstFrame = true;
maybeNotifyDrawnToSurface(); maybeNotifyDrawnToSurface();
} }
@TargetApi(21) @TargetApi(21)
private void renderOutputBufferTimedV21(MediaCodec codec, int bufferIndex, long releaseTimeNs) { protected void renderOutputBufferTimedV21(MediaCodec codec, int bufferIndex, long releaseTimeNs) {
maybeNotifyVideoSizeChanged(); maybeNotifyVideoSizeChanged();
TraceUtil.beginSection("releaseOutputBufferTimed"); TraceUtil.beginSection("releaseOutputBufferTimed");
codec.releaseOutputBuffer(bufferIndex, releaseTimeNs); codec.releaseOutputBuffer(bufferIndex, releaseTimeNs);
TraceUtil.endSection(); TraceUtil.endSection();
codecCounters.renderedOutputBufferCount++; codecCounters.renderedOutputBufferCount++;
renderedFirstFrame = true;
maybeNotifyDrawnToSurface(); maybeNotifyDrawnToSurface();
} }
......
...@@ -40,6 +40,8 @@ public class MediaFormat { ...@@ -40,6 +40,8 @@ public class MediaFormat {
public final String mimeType; public final String mimeType;
public final int maxInputSize; public final int maxInputSize;
public final long durationUs;
public final int width; public final int width;
public final int height; public final int height;
public final float pixelWidthHeightRatio; public final float pixelWidthHeightRatio;
...@@ -49,11 +51,11 @@ public class MediaFormat { ...@@ -49,11 +51,11 @@ public class MediaFormat {
public final int bitrate; public final int bitrate;
public final List<byte[]> initializationData;
private int maxWidth; private int maxWidth;
private int maxHeight; private int maxHeight;
public final List<byte[]> initializationData;
// Lazy-initialized hashcode. // Lazy-initialized hashcode.
private int hashCode; private int hashCode;
// Possibly-lazy-initialized framework media format. // Possibly-lazy-initialized framework media format.
...@@ -66,25 +68,38 @@ public class MediaFormat { ...@@ -66,25 +68,38 @@ public class MediaFormat {
public static MediaFormat createVideoFormat(String mimeType, int maxInputSize, int width, public static MediaFormat createVideoFormat(String mimeType, int maxInputSize, int width,
int height, List<byte[]> initializationData) { int height, List<byte[]> initializationData) {
return createVideoFormat(mimeType, maxInputSize, width, height, 1, initializationData); return createVideoFormat(
mimeType, maxInputSize, C.UNKNOWN_TIME_US, width, height, initializationData);
} }
public static MediaFormat createVideoFormat(String mimeType, int maxInputSize, int width, public static MediaFormat createVideoFormat(String mimeType, int maxInputSize, long durationUs,
int height, float pixelWidthHeightRatio, List<byte[]> initializationData) { int width, int height, List<byte[]> initializationData) {
return new MediaFormat(mimeType, maxInputSize, width, height, pixelWidthHeightRatio, NO_VALUE, return createVideoFormat(
NO_VALUE, NO_VALUE, initializationData); mimeType, maxInputSize, durationUs, width, height, 1, initializationData);
}
public static MediaFormat createVideoFormat(String mimeType, int maxInputSize, long durationUs,
int width, int height, float pixelWidthHeightRatio, List<byte[]> initializationData) {
return new MediaFormat(mimeType, maxInputSize, durationUs, width, height, pixelWidthHeightRatio,
NO_VALUE, NO_VALUE, NO_VALUE, initializationData);
} }
public static MediaFormat createAudioFormat(String mimeType, int maxInputSize, int channelCount, public static MediaFormat createAudioFormat(String mimeType, int maxInputSize, int channelCount,
int sampleRate, List<byte[]> initializationData) { int sampleRate, List<byte[]> initializationData) {
return new MediaFormat(mimeType, maxInputSize, NO_VALUE, NO_VALUE, NO_VALUE, channelCount, return createAudioFormat(
sampleRate, NO_VALUE, initializationData); mimeType, maxInputSize, C.UNKNOWN_TIME_US, channelCount, sampleRate, initializationData);
} }
public static MediaFormat createAudioFormat(String mimeType, int maxInputSize, int channelCount, public static MediaFormat createAudioFormat(String mimeType, int maxInputSize, long durationUs,
int sampleRate, int bitrate, List<byte[]> initializationData) { int channelCount, int sampleRate, List<byte[]> initializationData) {
return new MediaFormat(mimeType, maxInputSize, NO_VALUE, NO_VALUE, NO_VALUE, channelCount, return createAudioFormat(
sampleRate, bitrate, initializationData); mimeType, maxInputSize, durationUs, channelCount, sampleRate, NO_VALUE, initializationData);
}
public static MediaFormat createAudioFormat(String mimeType, int maxInputSize, long durationUs,
int channelCount, int sampleRate, int bitrate, List<byte[]> initializationData) {
return new MediaFormat(mimeType, maxInputSize, durationUs, NO_VALUE, NO_VALUE, NO_VALUE,
channelCount, sampleRate, bitrate, initializationData);
} }
public static MediaFormat createId3Format() { public static MediaFormat createId3Format() {
...@@ -100,8 +115,8 @@ public class MediaFormat { ...@@ -100,8 +115,8 @@ public class MediaFormat {
} }
public static MediaFormat createFormatForMimeType(String mimeType) { public static MediaFormat createFormatForMimeType(String mimeType) {
return new MediaFormat(mimeType, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, return new MediaFormat(mimeType, NO_VALUE, C.UNKNOWN_TIME_US, NO_VALUE, NO_VALUE, NO_VALUE,
NO_VALUE, null); NO_VALUE, NO_VALUE, NO_VALUE, null);
} }
@TargetApi(16) @TargetApi(16)
...@@ -123,15 +138,18 @@ public class MediaFormat { ...@@ -123,15 +138,18 @@ public class MediaFormat {
initializationData.add(data); initializationData.add(data);
buffer.flip(); buffer.flip();
} }
durationUs = format.containsKey(android.media.MediaFormat.KEY_DURATION)
? format.getLong(android.media.MediaFormat.KEY_DURATION) : C.UNKNOWN_TIME_US;
maxWidth = NO_VALUE; maxWidth = NO_VALUE;
maxHeight = NO_VALUE; maxHeight = NO_VALUE;
} }
private MediaFormat(String mimeType, int maxInputSize, int width, int height, private MediaFormat(String mimeType, int maxInputSize, long durationUs, int width, int height,
float pixelWidthHeightRatio, int channelCount, int sampleRate, int bitrate, float pixelWidthHeightRatio, int channelCount, int sampleRate, int bitrate,
List<byte[]> initializationData) { List<byte[]> initializationData) {
this.mimeType = mimeType; this.mimeType = mimeType;
this.maxInputSize = maxInputSize; this.maxInputSize = maxInputSize;
this.durationUs = durationUs;
this.width = width; this.width = width;
this.height = height; this.height = height;
this.pixelWidthHeightRatio = pixelWidthHeightRatio; this.pixelWidthHeightRatio = pixelWidthHeightRatio;
...@@ -169,6 +187,7 @@ public class MediaFormat { ...@@ -169,6 +187,7 @@ public class MediaFormat {
result = 31 * result + width; result = 31 * result + width;
result = 31 * result + height; result = 31 * result + height;
result = 31 * result + Float.floatToRawIntBits(pixelWidthHeightRatio); result = 31 * result + Float.floatToRawIntBits(pixelWidthHeightRatio);
result = 31 * result + (int) durationUs;
result = 31 * result + maxWidth; result = 31 * result + maxWidth;
result = 31 * result + maxHeight; result = 31 * result + maxHeight;
result = 31 * result + channelCount; result = 31 * result + channelCount;
...@@ -225,7 +244,7 @@ public class MediaFormat { ...@@ -225,7 +244,7 @@ public class MediaFormat {
public String toString() { public String toString() {
return "MediaFormat(" + mimeType + ", " + maxInputSize + ", " + width + ", " + height + ", " return "MediaFormat(" + mimeType + ", " + maxInputSize + ", " + width + ", " + height + ", "
+ pixelWidthHeightRatio + ", " + channelCount + ", " + sampleRate + ", " + bitrate + ", " + pixelWidthHeightRatio + ", " + channelCount + ", " + sampleRate + ", " + bitrate + ", "
+ maxWidth + ", " + maxHeight + ")"; + durationUs + ", " + maxWidth + ", " + maxHeight + ")";
} }
/** /**
...@@ -246,6 +265,9 @@ public class MediaFormat { ...@@ -246,6 +265,9 @@ public class MediaFormat {
for (int i = 0; i < initializationData.size(); i++) { for (int i = 0; i < initializationData.size(); i++) {
format.setByteBuffer("csd-" + i, ByteBuffer.wrap(initializationData.get(i))); format.setByteBuffer("csd-" + i, ByteBuffer.wrap(initializationData.get(i)));
} }
if (durationUs != C.UNKNOWN_TIME_US) {
format.setLong(android.media.MediaFormat.KEY_DURATION, durationUs);
}
maybeSetMaxDimensionsV16(format); maybeSetMaxDimensionsV16(format);
frameworkMediaFormat = format; frameworkMediaFormat = format;
} }
......
...@@ -15,8 +15,7 @@ ...@@ -15,8 +15,7 @@
*/ */
package com.google.android.exoplayer; package com.google.android.exoplayer;
import java.util.Map; import com.google.android.exoplayer.drm.DrmInitData;
import java.util.UUID;
/** /**
* Holds a {@link MediaFormat} and corresponding drm scheme initialization data. * Holds a {@link MediaFormat} and corresponding drm scheme initialization data.
...@@ -28,9 +27,8 @@ public final class MediaFormatHolder { ...@@ -28,9 +27,8 @@ public final class MediaFormatHolder {
*/ */
public MediaFormat format; public MediaFormat format;
/** /**
* Initialization data for each of the drm schemes supported by the media, keyed by scheme UUID. * Initialization data for drm schemes supported by the media. Null if the media is not encrypted.
* Null if the media is not encrypted.
*/ */
public Map<UUID, byte[]> drmInitData; public DrmInitData drmInitData;
} }
...@@ -50,9 +50,8 @@ public final class SampleHolder { ...@@ -50,9 +50,8 @@ public final class SampleHolder {
public int size; public int size;
/** /**
* Flags that accompany the sample. A combination of * Flags that accompany the sample. A combination of {@link C#SAMPLE_FLAG_SYNC},
* {@link android.media.MediaExtractor#SAMPLE_FLAG_SYNC} and * {@link C#SAMPLE_FLAG_ENCRYPTED} and {@link C#SAMPLE_FLAG_DECODE_ONLY}.
* {@link android.media.MediaExtractor#SAMPLE_FLAG_ENCRYPTED}
*/ */
public int flags; public int flags;
...@@ -61,11 +60,6 @@ public final class SampleHolder { ...@@ -61,11 +60,6 @@ public final class SampleHolder {
*/ */
public long timeUs; public long timeUs;
/**
* If true then the sample should be decoded, but should not be presented.
*/
public boolean decodeOnly;
private final int bufferReplacementMode; private final int bufferReplacementMode;
/** /**
...@@ -97,6 +91,27 @@ public final class SampleHolder { ...@@ -97,6 +91,27 @@ public final class SampleHolder {
} }
/** /**
* Returns whether {@link #flags} has {@link C#SAMPLE_FLAG_ENCRYPTED} set.
*/
public boolean isEncrypted() {
return (flags & C.SAMPLE_FLAG_ENCRYPTED) != 0;
}
/**
* Returns whether {@link #flags} has {@link C#SAMPLE_FLAG_DECODE_ONLY} set.
*/
public boolean isDecodeOnly() {
return (flags & C.SAMPLE_FLAG_DECODE_ONLY) != 0;
}
/**
* Returns whether {@link #flags} has {@link C#SAMPLE_FLAG_SYNC} set.
*/
public boolean isSyncFrame() {
return (flags & C.SAMPLE_FLAG_SYNC) != 0;
}
/**
* Clears {@link #data}. Does nothing if {@link #data} is null. * Clears {@link #data}. Does nothing if {@link #data} is null.
*/ */
public void clearData() { public void clearData() {
......
...@@ -44,6 +44,8 @@ import java.nio.ByteBuffer; ...@@ -44,6 +44,8 @@ import java.nio.ByteBuffer;
* <p>Call {@link #reconfigure} when the output format changes. * <p>Call {@link #reconfigure} when the output format changes.
* *
* <p>Call {@link #reset} to free resources. It is safe to re-{@link #initialize} the instance. * <p>Call {@link #reset} to free resources. It is safe to re-{@link #initialize} the instance.
*
* <p>Call {@link #release} when the instance will no longer be used.
*/ */
@TargetApi(16) @TargetApi(16)
public final class AudioTrack { public final class AudioTrack {
...@@ -91,6 +93,12 @@ public final class AudioTrack { ...@@ -91,6 +93,12 @@ public final class AudioTrack {
/** Returned by {@link #getCurrentPositionUs} when the position is not set. */ /** Returned by {@link #getCurrentPositionUs} when the position is not set. */
public static final long CURRENT_POSITION_NOT_SET = Long.MIN_VALUE; public static final long CURRENT_POSITION_NOT_SET = Long.MIN_VALUE;
/**
* Set to {@code true} to enable a workaround for an issue where an audio effect does not keep its
* session active across releasing/initializing a new audio track, on platform API version < 21.
*/
private static final boolean ENABLE_PRE_V21_AUDIO_SESSION_WORKAROUND = false;
/** A minimum length for the {@link android.media.AudioTrack} buffer, in microseconds. */ /** A minimum length for the {@link android.media.AudioTrack} buffer, in microseconds. */
private static final long MIN_BUFFER_DURATION_US = 250000; private static final long MIN_BUFFER_DURATION_US = 250000;
/** A maximum length for the {@link android.media.AudioTrack} buffer, in microseconds. */ /** A maximum length for the {@link android.media.AudioTrack} buffer, in microseconds. */
...@@ -132,6 +140,9 @@ public final class AudioTrack { ...@@ -132,6 +140,9 @@ public final class AudioTrack {
private final ConditionVariable releasingConditionVariable; private final ConditionVariable releasingConditionVariable;
private final long[] playheadOffsets; private final long[] playheadOffsets;
/** Used to keep the audio session active on pre-V21 builds (see {@link #initialize()}). */
private android.media.AudioTrack keepSessionIdAudioTrack;
private android.media.AudioTrack audioTrack; private android.media.AudioTrack audioTrack;
private AudioTrackUtil audioTrackUtil; private AudioTrackUtil audioTrackUtil;
private int sampleRate; private int sampleRate;
...@@ -267,15 +278,37 @@ public final class AudioTrack { ...@@ -267,15 +278,37 @@ public final class AudioTrack {
audioTrack = new android.media.AudioTrack(AudioManager.STREAM_MUSIC, sampleRate, audioTrack = new android.media.AudioTrack(AudioManager.STREAM_MUSIC, sampleRate,
channelConfig, encoding, bufferSize, android.media.AudioTrack.MODE_STREAM, sessionId); channelConfig, encoding, bufferSize, android.media.AudioTrack.MODE_STREAM, sessionId);
} }
checkAudioTrackInitialized(); checkAudioTrackInitialized();
sessionId = audioTrack.getAudioSessionId();
if (ENABLE_PRE_V21_AUDIO_SESSION_WORKAROUND) {
if (Util.SDK_INT < 21) {
// The workaround creates an audio track with a one byte buffer on the same session, and
// does not release it until this object is released, which keeps the session active.
if (keepSessionIdAudioTrack != null
&& sessionId != keepSessionIdAudioTrack.getAudioSessionId()) {
releaseKeepSessionIdAudioTrack();
}
if (keepSessionIdAudioTrack == null) {
int sampleRate = 4000; // Equal to private android.media.AudioTrack.MIN_SAMPLE_RATE.
int channelConfig = AudioFormat.CHANNEL_OUT_MONO;
int encoding = AudioFormat.ENCODING_PCM_8BIT;
int bufferSize = 1; // Use a one byte buffer, as it is not actually used for playback.
keepSessionIdAudioTrack = new android.media.AudioTrack(AudioManager.STREAM_MUSIC,
sampleRate, channelConfig, encoding, bufferSize, android.media.AudioTrack.MODE_STATIC,
sessionId);
}
}
}
if (Util.SDK_INT >= 19) { if (Util.SDK_INT >= 19) {
audioTrackUtil = new AudioTrackUtilV19(audioTrack); audioTrackUtil = new AudioTrackUtilV19(audioTrack);
} else { } else {
audioTrackUtil = new AudioTrackUtil(audioTrack); audioTrackUtil = new AudioTrackUtil(audioTrack);
} }
setVolume(volume); setVolume(volume);
return audioTrack.getAudioSessionId();
return sessionId;
} }
/** /**
...@@ -515,9 +548,9 @@ public final class AudioTrack { ...@@ -515,9 +548,9 @@ public final class AudioTrack {
} }
/** /**
* Releases resources associated with this instance asynchronously. Calling {@link #initialize} * Releases the underlying audio track asynchronously. Calling {@link #initialize} will block
* will block until the audio track has been released, so it is safe to initialize immediately * until the audio track has been released, so it is safe to initialize immediately after
* after resetting. * resetting. The audio session may remain active until the instance is {@link #release}d.
*/ */
public void reset() { public void reset() {
if (isInitialized()) { if (isInitialized()) {
...@@ -547,6 +580,29 @@ public final class AudioTrack { ...@@ -547,6 +580,29 @@ public final class AudioTrack {
} }
} }
/** Releases all resources associated with this instance. */
public void release() {
reset();
releaseKeepSessionIdAudioTrack();
}
/** Releases {@link #keepSessionIdAudioTrack} asynchronously, if it is non-{@code null}. */
private void releaseKeepSessionIdAudioTrack() {
if (keepSessionIdAudioTrack == null) {
return;
}
// AudioTrack.release can take some time, so we call it on a background thread.
final android.media.AudioTrack toRelease = keepSessionIdAudioTrack;
keepSessionIdAudioTrack = null;
new Thread() {
@Override
public void run() {
toRelease.release();
}
}.start();
}
/** Returns whether {@link #getCurrentPositionUs} can return the current playback position. */ /** Returns whether {@link #getCurrentPositionUs} can return the current playback position. */
private boolean hasCurrentPositionUs() { private boolean hasCurrentPositionUs() {
return isInitialized() && startMediaTimeUs != START_NOT_SET; return isInitialized() && startMediaTimeUs != START_NOT_SET;
......
...@@ -352,13 +352,14 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback { ...@@ -352,13 +352,14 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback {
if (mediaFormat != null && !mediaFormat.equals(downstreamMediaFormat, true)) { if (mediaFormat != null && !mediaFormat.equals(downstreamMediaFormat, true)) {
chunkSource.getMaxVideoDimensions(mediaFormat); chunkSource.getMaxVideoDimensions(mediaFormat);
formatHolder.format = mediaFormat; formatHolder.format = mediaFormat;
formatHolder.drmInitData = mediaChunk.getPsshInfo(); formatHolder.drmInitData = mediaChunk.getDrmInitData();
downstreamMediaFormat = mediaFormat; downstreamMediaFormat = mediaFormat;
return FORMAT_READ; return FORMAT_READ;
} }
if (mediaChunk.read(sampleHolder)) { if (mediaChunk.read(sampleHolder)) {
sampleHolder.decodeOnly = frameAccurateSeeking && sampleHolder.timeUs < lastSeekPositionUs; boolean decodeOnly = frameAccurateSeeking && sampleHolder.timeUs < lastSeekPositionUs;
sampleHolder.flags |= decodeOnly ? C.SAMPLE_FLAG_DECODE_ONLY : 0;
onSampleRead(mediaChunk, sampleHolder); onSampleRead(mediaChunk, sampleHolder);
return SAMPLE_READ; return SAMPLE_READ;
} else { } else {
......
...@@ -19,14 +19,12 @@ import com.google.android.exoplayer.MediaFormat; ...@@ -19,14 +19,12 @@ import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.ParserException; import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.SampleHolder; import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.chunk.parser.Extractor; import com.google.android.exoplayer.chunk.parser.Extractor;
import com.google.android.exoplayer.drm.DrmInitData;
import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DataSpec; import com.google.android.exoplayer.upstream.DataSpec;
import com.google.android.exoplayer.upstream.NonBlockingInputStream; import com.google.android.exoplayer.upstream.NonBlockingInputStream;
import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.Assertions;
import java.util.Map;
import java.util.UUID;
/** /**
* A {@link MediaChunk} extracted from a container. * A {@link MediaChunk} extracted from a container.
*/ */
...@@ -38,7 +36,7 @@ public final class ContainerMediaChunk extends MediaChunk { ...@@ -38,7 +36,7 @@ public final class ContainerMediaChunk extends MediaChunk {
private boolean prepared; private boolean prepared;
private MediaFormat mediaFormat; private MediaFormat mediaFormat;
private Map<UUID, byte[]> psshInfo; private DrmInitData drmInitData;
/** /**
* @deprecated Use the other constructor, passing null as {@code psshInfo}. * @deprecated Use the other constructor, passing null as {@code psshInfo}.
...@@ -60,8 +58,9 @@ public final class ContainerMediaChunk extends MediaChunk { ...@@ -60,8 +58,9 @@ public final class ContainerMediaChunk extends MediaChunk {
* @param endTimeUs The end time of the media contained by the chunk, in microseconds. * @param endTimeUs The end time of the media contained by the chunk, in microseconds.
* @param nextChunkIndex The index of the next chunk, or -1 if this is the last chunk. * @param nextChunkIndex The index of the next chunk, or -1 if this is the last chunk.
* @param extractor The extractor that will be used to extract the samples. * @param extractor The extractor that will be used to extract the samples.
* @param psshInfo Pssh data. May be null if pssh data is present within the stream, meaning it * @param drmInitData DRM initialization data. May be null if DRM initialization data is present
* can be obtained directly from {@code extractor}, or if no pssh data is required. * within the stream, meaning it can be obtained directly from {@code extractor}, or if no
* DRM initialization data is required.
* @param maybeSelfContained Set to true if this chunk might be self contained, meaning it might * @param maybeSelfContained Set to true if this chunk might be self contained, meaning it might
* contain a moov atom defining the media format of the chunk. This parameter can always be * contain a moov atom defining the media format of the chunk. This parameter can always be
* safely set to true. Setting to false where the chunk is known to not be self contained may * safely set to true. Setting to false where the chunk is known to not be self contained may
...@@ -70,12 +69,12 @@ public final class ContainerMediaChunk extends MediaChunk { ...@@ -70,12 +69,12 @@ public final class ContainerMediaChunk extends MediaChunk {
*/ */
public ContainerMediaChunk(DataSource dataSource, DataSpec dataSpec, Format format, public ContainerMediaChunk(DataSource dataSource, DataSpec dataSpec, Format format,
int trigger, long startTimeUs, long endTimeUs, int nextChunkIndex, Extractor extractor, int trigger, long startTimeUs, long endTimeUs, int nextChunkIndex, Extractor extractor,
Map<UUID, byte[]> psshInfo, boolean maybeSelfContained, long sampleOffsetUs) { DrmInitData drmInitData, boolean maybeSelfContained, long sampleOffsetUs) {
super(dataSource, dataSpec, format, trigger, startTimeUs, endTimeUs, nextChunkIndex); super(dataSource, dataSpec, format, trigger, startTimeUs, endTimeUs, nextChunkIndex);
this.extractor = extractor; this.extractor = extractor;
this.maybeSelfContained = maybeSelfContained; this.maybeSelfContained = maybeSelfContained;
this.sampleOffsetUs = sampleOffsetUs; this.sampleOffsetUs = sampleOffsetUs;
this.psshInfo = psshInfo; this.drmInitData = drmInitData;
} }
@Override @Override
...@@ -111,9 +110,9 @@ public final class ContainerMediaChunk extends MediaChunk { ...@@ -111,9 +110,9 @@ public final class ContainerMediaChunk extends MediaChunk {
} }
if (prepared) { if (prepared) {
mediaFormat = extractor.getFormat(); mediaFormat = extractor.getFormat();
Map<UUID, byte[]> extractorPsshInfo = extractor.getPsshInfo(); DrmInitData extractorDrmInitData = extractor.getDrmInitData();
if (extractorPsshInfo != null) { if (extractorDrmInitData != null) {
psshInfo = extractorPsshInfo; drmInitData = extractorDrmInitData;
} }
} }
} }
...@@ -145,8 +144,8 @@ public final class ContainerMediaChunk extends MediaChunk { ...@@ -145,8 +144,8 @@ public final class ContainerMediaChunk extends MediaChunk {
} }
@Override @Override
public Map<UUID, byte[]> getPsshInfo() { public DrmInitData getDrmInitData() {
return psshInfo; return drmInitData;
} }
} }
...@@ -85,14 +85,6 @@ public class Format { ...@@ -85,14 +85,6 @@ public class Format {
public final String language; public final String language;
/** /**
* The average bandwidth in bytes per second.
*
* @deprecated Use {@link #bitrate}. However note that the units of measurement are different.
*/
@Deprecated
public final int bandwidth;
/**
* @param id The format identifier. * @param id The format identifier.
* @param mimeType The format mime type. * @param mimeType The format mime type.
* @param width The width of the video in pixels, or -1 for non-video formats. * @param width The width of the video in pixels, or -1 for non-video formats.
...@@ -144,7 +136,6 @@ public class Format { ...@@ -144,7 +136,6 @@ public class Format {
this.bitrate = bitrate; this.bitrate = bitrate;
this.language = language; this.language = language;
this.codecs = codecs; this.codecs = codecs;
this.bandwidth = bitrate / 8;
} }
@Override @Override
......
...@@ -18,12 +18,10 @@ package com.google.android.exoplayer.chunk; ...@@ -18,12 +18,10 @@ package com.google.android.exoplayer.chunk;
import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.ParserException; import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.SampleHolder; import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.drm.DrmInitData;
import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DataSpec; import com.google.android.exoplayer.upstream.DataSpec;
import java.util.Map;
import java.util.UUID;
/** /**
* An abstract base class for {@link Chunk}s that contain media samples. * An abstract base class for {@link Chunk}s that contain media samples.
*/ */
...@@ -129,12 +127,12 @@ public abstract class MediaChunk extends Chunk { ...@@ -129,12 +127,12 @@ public abstract class MediaChunk extends Chunk {
public abstract MediaFormat getMediaFormat(); public abstract MediaFormat getMediaFormat();
/** /**
* Returns the pssh information associated with the chunk. * Returns the DRM initialization data associated with the chunk.
* <p> * <p>
* Should only be called after the chunk has been successfully prepared. * Should only be called after the chunk has been successfully prepared.
* *
* @return The pssh information. * @return The DRM initialization data.
*/ */
public abstract Map<UUID, byte[]> getPsshInfo(); public abstract DrmInitData getDrmInitData();
} }
...@@ -17,14 +17,12 @@ package com.google.android.exoplayer.chunk; ...@@ -17,14 +17,12 @@ package com.google.android.exoplayer.chunk;
import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.SampleHolder; import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.drm.DrmInitData;
import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DataSpec; import com.google.android.exoplayer.upstream.DataSpec;
import com.google.android.exoplayer.upstream.NonBlockingInputStream; import com.google.android.exoplayer.upstream.NonBlockingInputStream;
import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.Assertions;
import java.util.Map;
import java.util.UUID;
/** /**
* A {@link MediaChunk} containing a single sample. * A {@link MediaChunk} containing a single sample.
*/ */
...@@ -132,7 +130,7 @@ public class SingleSampleMediaChunk extends MediaChunk { ...@@ -132,7 +130,7 @@ public class SingleSampleMediaChunk extends MediaChunk {
} }
@Override @Override
public Map<UUID, byte[]> getPsshInfo() { public DrmInitData getDrmInitData() {
return null; return null;
} }
......
...@@ -15,15 +15,12 @@ ...@@ -15,15 +15,12 @@
*/ */
package com.google.android.exoplayer.chunk.parser; package com.google.android.exoplayer.chunk.parser;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.ParserException; import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.SampleHolder; import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.drm.DrmInitData;
import com.google.android.exoplayer.upstream.NonBlockingInputStream; import com.google.android.exoplayer.upstream.NonBlockingInputStream;
import java.util.Map;
import java.util.UUID;
/** /**
* Facilitates extraction of media samples from a container format. * Facilitates extraction of media samples from a container format.
*/ */
...@@ -43,7 +40,7 @@ public interface Extractor { ...@@ -43,7 +40,7 @@ public interface Extractor {
public static final int RESULT_READ_SAMPLE = 4; public static final int RESULT_READ_SAMPLE = 4;
/** /**
* Initialization data was read. The parsed data can be read using {@link #getFormat()} and * Initialization data was read. The parsed data can be read using {@link #getFormat()} and
* {@link #getPsshInfo}. * {@link #getDrmInitData()}.
*/ */
public static final int RESULT_READ_INIT = 8; public static final int RESULT_READ_INIT = 8;
/** /**
...@@ -80,17 +77,12 @@ public interface Extractor { ...@@ -80,17 +77,12 @@ public interface Extractor {
public MediaFormat getFormat(); public MediaFormat getFormat();
/** /**
* Returns the duration of the stream in microseconds, or {@link C#UNKNOWN_TIME_US} if unknown. * Returns DRM initialization data parsed from the stream.
*/
public long getDurationUs();
/**
* Returns the pssh information parsed from the stream.
* *
* @return The pssh information. May be null if pssh data has yet to be parsed, or if the stream * @return The DRM initialization data. May be null if the initialization data has yet to be
* does not contain any pssh data. * parsed, or if the stream does not contain any DRM initialization data.
*/ */
public Map<UUID, byte[]> getPsshInfo(); public DrmInitData getDrmInitData();
/** /**
* Consumes data from a {@link NonBlockingInputStream}. * Consumes data from a {@link NonBlockingInputStream}.
......
...@@ -39,6 +39,7 @@ import com.google.android.exoplayer.dash.mpd.MediaPresentationDescription; ...@@ -39,6 +39,7 @@ import com.google.android.exoplayer.dash.mpd.MediaPresentationDescription;
import com.google.android.exoplayer.dash.mpd.Period; import com.google.android.exoplayer.dash.mpd.Period;
import com.google.android.exoplayer.dash.mpd.RangedUri; import com.google.android.exoplayer.dash.mpd.RangedUri;
import com.google.android.exoplayer.dash.mpd.Representation; import com.google.android.exoplayer.dash.mpd.Representation;
import com.google.android.exoplayer.drm.DrmInitData;
import com.google.android.exoplayer.text.webvtt.WebvttParser; import com.google.android.exoplayer.text.webvtt.WebvttParser;
import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DataSpec; import com.google.android.exoplayer.upstream.DataSpec;
...@@ -54,8 +55,6 @@ import java.util.Arrays; ...@@ -54,8 +55,6 @@ import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.UUID;
/** /**
* An {@link ChunkSource} for DASH streams. * An {@link ChunkSource} for DASH streams.
...@@ -96,7 +95,7 @@ public class DashChunkSource implements ChunkSource { ...@@ -96,7 +95,7 @@ public class DashChunkSource implements ChunkSource {
private final ManifestFetcher<MediaPresentationDescription> manifestFetcher; private final ManifestFetcher<MediaPresentationDescription> manifestFetcher;
private final int adaptationSetIndex; private final int adaptationSetIndex;
private final int[] representationIndices; private final int[] representationIndices;
private final Map<UUID, byte[]> psshInfo; private final DrmInitData drmInitData;
private MediaPresentationDescription currentManifest; private MediaPresentationDescription currentManifest;
private boolean finishedCurrentManifest; private boolean finishedCurrentManifest;
...@@ -190,7 +189,7 @@ public class DashChunkSource implements ChunkSource { ...@@ -190,7 +189,7 @@ public class DashChunkSource implements ChunkSource {
this.evaluation = new Evaluation(); this.evaluation = new Evaluation();
this.headerBuilder = new StringBuilder(); this.headerBuilder = new StringBuilder();
psshInfo = getPsshInfo(currentManifest, adaptationSetIndex); drmInitData = getDrmInitData(currentManifest, adaptationSetIndex);
Representation[] representations = getFilteredRepresentations(currentManifest, Representation[] representations = getFilteredRepresentations(currentManifest,
adaptationSetIndex, representationIndices); adaptationSetIndex, representationIndices);
long periodDurationUs = (representations[0].periodDurationMs == TrackRenderer.UNKNOWN_TIME_US) long periodDurationUs = (representations[0].periodDurationMs == TrackRenderer.UNKNOWN_TIME_US)
...@@ -407,7 +406,7 @@ public class DashChunkSource implements ChunkSource { ...@@ -407,7 +406,7 @@ public class DashChunkSource implements ChunkSource {
// Do nothing. // Do nothing.
} }
private boolean mimeTypeIsWebm(String mimeType) { private static boolean mimeTypeIsWebm(String mimeType) {
return mimeType.startsWith(MimeTypes.VIDEO_WEBM) || mimeType.startsWith(MimeTypes.AUDIO_WEBM); return mimeType.startsWith(MimeTypes.VIDEO_WEBM) || mimeType.startsWith(MimeTypes.AUDIO_WEBM);
} }
...@@ -475,8 +474,8 @@ public class DashChunkSource implements ChunkSource { ...@@ -475,8 +474,8 @@ public class DashChunkSource implements ChunkSource {
startTimeUs, endTimeUs, nextAbsoluteSegmentNum, null, representationHolder.vttHeader); startTimeUs, endTimeUs, nextAbsoluteSegmentNum, null, representationHolder.vttHeader);
} else { } else {
return new ContainerMediaChunk(dataSource, dataSpec, representation.format, trigger, return new ContainerMediaChunk(dataSource, dataSpec, representation.format, trigger,
startTimeUs, endTimeUs, nextAbsoluteSegmentNum, representationHolder.extractor, psshInfo, startTimeUs, endTimeUs, nextAbsoluteSegmentNum, representationHolder.extractor,
false, presentationTimeOffsetUs); drmInitData, false, presentationTimeOffsetUs);
} }
} }
...@@ -529,19 +528,24 @@ public class DashChunkSource implements ChunkSource { ...@@ -529,19 +528,24 @@ public class DashChunkSource implements ChunkSource {
} }
} }
private static Map<UUID, byte[]> getPsshInfo(MediaPresentationDescription manifest, private static DrmInitData getDrmInitData(MediaPresentationDescription manifest,
int adaptationSetIndex) { int adaptationSetIndex) {
AdaptationSet adaptationSet = manifest.periods.get(0).adaptationSets.get(adaptationSetIndex); AdaptationSet adaptationSet = manifest.periods.get(0).adaptationSets.get(adaptationSetIndex);
String drmInitMimeType = mimeTypeIsWebm(adaptationSet.representations.get(0).format.mimeType)
? MimeTypes.VIDEO_WEBM : MimeTypes.VIDEO_MP4;
if (adaptationSet.contentProtections.isEmpty()) { if (adaptationSet.contentProtections.isEmpty()) {
return null; return null;
} else { } else {
Map<UUID, byte[]> psshInfo = new HashMap<UUID, byte[]>(); DrmInitData.Mapped drmInitData = null;
for (ContentProtection contentProtection : adaptationSet.contentProtections) { for (ContentProtection contentProtection : adaptationSet.contentProtections) {
if (contentProtection.uuid != null && contentProtection.data != null) { if (contentProtection.uuid != null && contentProtection.data != null) {
psshInfo.put(contentProtection.uuid, contentProtection.data); if (drmInitData == null) {
drmInitData = new DrmInitData.Mapped(drmInitMimeType);
} }
drmInitData.put(contentProtection.uuid, contentProtection.data);
} }
return psshInfo.isEmpty() ? null : psshInfo; }
return drmInitData;
} }
} }
...@@ -581,7 +585,7 @@ public class DashChunkSource implements ChunkSource { ...@@ -581,7 +585,7 @@ public class DashChunkSource implements ChunkSource {
} }
if ((result & Extractor.RESULT_READ_INDEX) != 0) { if ((result & Extractor.RESULT_READ_INDEX) != 0) {
representationHolders.get(format.id).segmentIndex = representationHolders.get(format.id).segmentIndex =
new DashWrappingSegmentIndex(extractor.getIndex(), uri, indexAnchor); new DashWrappingSegmentIndex(extractor.getIndex(), uri.toString(), indexAnchor);
} }
} }
......
...@@ -19,8 +19,6 @@ import com.google.android.exoplayer.chunk.parser.SegmentIndex; ...@@ -19,8 +19,6 @@ import com.google.android.exoplayer.chunk.parser.SegmentIndex;
import com.google.android.exoplayer.dash.mpd.RangedUri; import com.google.android.exoplayer.dash.mpd.RangedUri;
import com.google.android.exoplayer.util.Util; import com.google.android.exoplayer.util.Util;
import android.net.Uri;
/** /**
* An implementation of {@link DashSegmentIndex} that wraps a {@link SegmentIndex} parsed from a * An implementation of {@link DashSegmentIndex} that wraps a {@link SegmentIndex} parsed from a
* media stream. * media stream.
...@@ -28,16 +26,16 @@ import android.net.Uri; ...@@ -28,16 +26,16 @@ import android.net.Uri;
public class DashWrappingSegmentIndex implements DashSegmentIndex { public class DashWrappingSegmentIndex implements DashSegmentIndex {
private final SegmentIndex segmentIndex; private final SegmentIndex segmentIndex;
private final Uri uri; private final String uri;
private final long indexAnchor; private final long indexAnchor;
/** /**
* @param segmentIndex The {@link SegmentIndex} to wrap. * @param segmentIndex The {@link SegmentIndex} to wrap.
* @param uri The {@link Uri} where the data is located. * @param uri The URI where the data is located.
* @param indexAnchor The index anchor point. This value is added to the byte offsets specified * @param indexAnchor The index anchor point. This value is added to the byte offsets specified
* in the wrapped {@link SegmentIndex}. * in the wrapped {@link SegmentIndex}.
*/ */
public DashWrappingSegmentIndex(SegmentIndex segmentIndex, Uri uri, long indexAnchor) { public DashWrappingSegmentIndex(SegmentIndex segmentIndex, String uri, long indexAnchor) {
this.segmentIndex = segmentIndex; this.segmentIndex = segmentIndex;
this.uri = uri; this.uri = uri;
this.indexAnchor = indexAnchor; this.indexAnchor = indexAnchor;
......
...@@ -15,6 +15,10 @@ ...@@ -15,6 +15,10 @@
*/ */
package com.google.android.exoplayer.dash.mpd; package com.google.android.exoplayer.dash.mpd;
import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.Util;
import java.util.Arrays;
import java.util.UUID; import java.util.UUID;
/** /**
...@@ -43,9 +47,38 @@ public class ContentProtection { ...@@ -43,9 +47,38 @@ public class ContentProtection {
* @param data Protection scheme specific initialization data. May be null. * @param data Protection scheme specific initialization data. May be null.
*/ */
public ContentProtection(String schemeUriId, UUID uuid, byte[] data) { public ContentProtection(String schemeUriId, UUID uuid, byte[] data) {
this.schemeUriId = schemeUriId; this.schemeUriId = Assertions.checkNotNull(schemeUriId);
this.uuid = uuid; this.uuid = uuid;
this.data = data; this.data = data;
} }
@Override
public boolean equals(Object obj) {
if (!(obj instanceof ContentProtection)) {
return false;
}
if (obj == this) {
return true;
}
ContentProtection other = (ContentProtection) obj;
return schemeUriId.equals(other.schemeUriId)
&& Util.areEqual(uuid, other.uuid)
&& Arrays.equals(data, other.data);
}
@Override
public int hashCode() {
int hashCode = 1;
hashCode = hashCode * 37 + schemeUriId.hashCode();
if (uuid != null) {
hashCode = hashCode * 37 + uuid.hashCode();
}
if (data != null) {
hashCode = hashCode * 37 + Arrays.hashCode(data);
}
return hashCode;
}
} }
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
package com.google.android.exoplayer.dash.mpd; package com.google.android.exoplayer.dash.mpd;
import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.Util; import com.google.android.exoplayer.util.UriUtil;
import android.net.Uri; import android.net.Uri;
...@@ -35,31 +35,28 @@ public final class RangedUri { ...@@ -35,31 +35,28 @@ public final class RangedUri {
*/ */
public final long length; public final long length;
// The {@link Uri} is stored internally in two parts, {@link #baseUri} and {@link uriString}. // The URI is stored internally in two parts: reference URI and a base URI to use when
// This helps optimize memory usage in the same way that DASH manifests allow many URLs to be // resolving it. This helps optimize memory usage in the same way that DASH manifests allow many
// expressed concisely in the form of a single BaseURL and many relative paths. Note that this // URLs to be expressed concisely in the form of a single BaseURL and many relative paths. Note
// optimization relies on the same {@code Uri} being passed as the {@link #baseUri} to many // that this optimization relies on the same object being passed as the base URI to many
// instances of this class. // instances of this class.
private final Uri baseUri; private final String baseUri;
private final String stringUri; private final String referenceUri;
private int hashCode; private int hashCode;
/** /**
* Constructs an ranged uri. * Constructs an ranged uri.
* <p>
* See {@link Util#getMergedUri(Uri, String)} for a description of how {@code baseUri} and
* {@code stringUri} are merged.
* *
* @param baseUri A uri that can form the base of the uri defined by the instance. * @param baseUri A uri that can form the base of the uri defined by the instance.
* @param stringUri A relative or absolute uri in string form. * @param referenceUri A reference uri that should be resolved with respect to {@code baseUri}.
* @param start The (zero based) index of the first byte of the range. * @param start The (zero based) index of the first byte of the range.
* @param length The length of the range, or -1 to indicate that the range is unbounded. * @param length The length of the range, or -1 to indicate that the range is unbounded.
*/ */
public RangedUri(Uri baseUri, String stringUri, long start, long length) { public RangedUri(String baseUri, String referenceUri, long start, long length) {
Assertions.checkArgument(baseUri != null || stringUri != null); Assertions.checkArgument(baseUri != null || referenceUri != null);
this.baseUri = baseUri; this.baseUri = baseUri;
this.stringUri = stringUri; this.referenceUri = referenceUri;
this.start = start; this.start = start;
this.length = length; this.length = length;
} }
...@@ -70,7 +67,16 @@ public final class RangedUri { ...@@ -70,7 +67,16 @@ public final class RangedUri {
* @return The {@link Uri} represented by the instance. * @return The {@link Uri} represented by the instance.
*/ */
public Uri getUri() { public Uri getUri() {
return Util.getMergedUri(baseUri, stringUri); return UriUtil.resolveToUri(baseUri, referenceUri);
}
/**
* Returns the uri represented by the instance as a string.
*
* @return The uri represented by the instance.
*/
public String getUriString() {
return UriUtil.resolve(baseUri, referenceUri);
} }
/** /**
...@@ -85,13 +91,13 @@ public final class RangedUri { ...@@ -85,13 +91,13 @@ public final class RangedUri {
* @return The merged {@link RangedUri} if the merge was successful. Null otherwise. * @return The merged {@link RangedUri} if the merge was successful. Null otherwise.
*/ */
public RangedUri attemptMerge(RangedUri other) { public RangedUri attemptMerge(RangedUri other) {
if (other == null || !getUri().equals(other.getUri())) { if (other == null || !getUriString().equals(other.getUriString())) {
return null; return null;
} else if (length != -1 && start + length == other.start) { } else if (length != -1 && start + length == other.start) {
return new RangedUri(baseUri, stringUri, start, return new RangedUri(baseUri, referenceUri, start,
other.length == -1 ? -1 : length + other.length); other.length == -1 ? -1 : length + other.length);
} else if (other.length != -1 && other.start + other.length == start) { } else if (other.length != -1 && other.start + other.length == start) {
return new RangedUri(baseUri, stringUri, other.start, return new RangedUri(baseUri, referenceUri, other.start,
length == -1 ? -1 : other.length + length); length == -1 ? -1 : other.length + length);
} else { } else {
return null; return null;
...@@ -104,7 +110,7 @@ public final class RangedUri { ...@@ -104,7 +110,7 @@ public final class RangedUri {
int result = 17; int result = 17;
result = 31 * result + (int) start; result = 31 * result + (int) start;
result = 31 * result + (int) length; result = 31 * result + (int) length;
result = 31 * result + getUri().hashCode(); result = 31 * result + getUriString().hashCode();
hashCode = result; hashCode = result;
} }
return hashCode; return hashCode;
...@@ -121,7 +127,7 @@ public final class RangedUri { ...@@ -121,7 +127,7 @@ public final class RangedUri {
RangedUri other = (RangedUri) obj; RangedUri other = (RangedUri) obj;
return this.start == other.start return this.start == other.start
&& this.length == other.length && this.length == other.length
&& getUri().equals(other.getUri()); && getUriString().equals(other.getUriString());
} }
} }
...@@ -147,7 +147,7 @@ public abstract class Representation { ...@@ -147,7 +147,7 @@ public abstract class Representation {
public static class SingleSegmentRepresentation extends Representation { public static class SingleSegmentRepresentation extends Representation {
/** /**
* The {@link Uri} of the single segment. * The uri of the single segment.
*/ */
public final Uri uri; public final Uri uri;
...@@ -174,7 +174,7 @@ public abstract class Representation { ...@@ -174,7 +174,7 @@ public abstract class Representation {
* @param contentLength The content length, or -1 if unknown. * @param contentLength The content length, or -1 if unknown.
*/ */
public static SingleSegmentRepresentation newInstance(long periodStartMs, long periodDurationMs, public static SingleSegmentRepresentation newInstance(long periodStartMs, long periodDurationMs,
String contentId, long revisionId, Format format, Uri uri, long initializationStart, String contentId, long revisionId, Format format, String uri, long initializationStart,
long initializationEnd, long indexStart, long indexEnd, long contentLength) { long initializationEnd, long indexStart, long indexEnd, long contentLength) {
RangedUri rangedUri = new RangedUri(uri, null, initializationStart, RangedUri rangedUri = new RangedUri(uri, null, initializationStart,
initializationEnd - initializationStart + 1); initializationEnd - initializationStart + 1);
...@@ -197,13 +197,13 @@ public abstract class Representation { ...@@ -197,13 +197,13 @@ public abstract class Representation {
public SingleSegmentRepresentation(long periodStartMs, long periodDurationMs, String contentId, public SingleSegmentRepresentation(long periodStartMs, long periodDurationMs, String contentId,
long revisionId, Format format, SingleSegmentBase segmentBase, long contentLength) { long revisionId, Format format, SingleSegmentBase segmentBase, long contentLength) {
super(periodStartMs, periodDurationMs, contentId, revisionId, format, segmentBase); super(periodStartMs, periodDurationMs, contentId, revisionId, format, segmentBase);
this.uri = segmentBase.uri; this.uri = Uri.parse(segmentBase.uri);
this.indexUri = segmentBase.getIndex(); this.indexUri = segmentBase.getIndex();
this.contentLength = contentLength; this.contentLength = contentLength;
// If we have an index uri then the index is defined externally, and we shouldn't return one // If we have an index uri then the index is defined externally, and we shouldn't return one
// directly. If we don't, then we can't do better than an index defining a single segment. // directly. If we don't, then we can't do better than an index defining a single segment.
segmentIndex = indexUri != null ? null : new DashSingleSegmentIndex(periodStartMs * 1000, segmentIndex = indexUri != null ? null : new DashSingleSegmentIndex(periodStartMs * 1000,
periodDurationMs * 1000, new RangedUri(uri, null, 0, -1)); periodDurationMs * 1000, new RangedUri(segmentBase.uri, null, 0, -1));
} }
@Override @Override
......
...@@ -19,8 +19,6 @@ import com.google.android.exoplayer.C; ...@@ -19,8 +19,6 @@ import com.google.android.exoplayer.C;
import com.google.android.exoplayer.dash.DashSegmentIndex; import com.google.android.exoplayer.dash.DashSegmentIndex;
import com.google.android.exoplayer.util.Util; import com.google.android.exoplayer.util.Util;
import android.net.Uri;
import java.util.List; import java.util.List;
/** /**
...@@ -73,7 +71,7 @@ public abstract class SegmentBase { ...@@ -73,7 +71,7 @@ public abstract class SegmentBase {
/** /**
* The uri of the segment. * The uri of the segment.
*/ */
public final Uri uri; public final String uri;
/* package */ final long indexStart; /* package */ final long indexStart;
/* package */ final long indexLength; /* package */ final long indexLength;
...@@ -89,7 +87,7 @@ public abstract class SegmentBase { ...@@ -89,7 +87,7 @@ public abstract class SegmentBase {
* @param indexLength The length of the index data in bytes. * @param indexLength The length of the index data in bytes.
*/ */
public SingleSegmentBase(RangedUri initialization, long timescale, long presentationTimeOffset, public SingleSegmentBase(RangedUri initialization, long timescale, long presentationTimeOffset,
Uri uri, long indexStart, long indexLength) { String uri, long indexStart, long indexLength) {
super(initialization, timescale, presentationTimeOffset); super(initialization, timescale, presentationTimeOffset);
this.uri = uri; this.uri = uri;
this.indexStart = indexStart; this.indexStart = indexStart;
...@@ -99,7 +97,7 @@ public abstract class SegmentBase { ...@@ -99,7 +97,7 @@ public abstract class SegmentBase {
/** /**
* @param uri The uri of the segment. * @param uri The uri of the segment.
*/ */
public SingleSegmentBase(Uri uri) { public SingleSegmentBase(String uri) {
this(null, 1, 0, uri, 0, -1); this(null, 1, 0, uri, 0, -1);
} }
...@@ -289,7 +287,7 @@ public abstract class SegmentBase { ...@@ -289,7 +287,7 @@ public abstract class SegmentBase {
/* package */ final UrlTemplate initializationTemplate; /* package */ final UrlTemplate initializationTemplate;
/* package */ final UrlTemplate mediaTemplate; /* package */ final UrlTemplate mediaTemplate;
private final Uri baseUrl; private final String baseUrl;
/** /**
* @param initialization A {@link RangedUri} corresponding to initialization data, if such data * @param initialization A {@link RangedUri} corresponding to initialization data, if such data
...@@ -315,7 +313,7 @@ public abstract class SegmentBase { ...@@ -315,7 +313,7 @@ public abstract class SegmentBase {
public SegmentTemplate(RangedUri initialization, long timescale, long presentationTimeOffset, public SegmentTemplate(RangedUri initialization, long timescale, long presentationTimeOffset,
long periodDurationMs, int startNumber, long duration, long periodDurationMs, int startNumber, long duration,
List<SegmentTimelineElement> segmentTimeline, UrlTemplate initializationTemplate, List<SegmentTimelineElement> segmentTimeline, UrlTemplate initializationTemplate,
UrlTemplate mediaTemplate, Uri baseUrl) { UrlTemplate mediaTemplate, String baseUrl) {
super(initialization, timescale, presentationTimeOffset, periodDurationMs, startNumber, super(initialization, timescale, presentationTimeOffset, periodDurationMs, startNumber,
duration, segmentTimeline); duration, segmentTimeline);
this.initializationTemplate = initializationTemplate; this.initializationTemplate = initializationTemplate;
......
...@@ -32,6 +32,7 @@ import java.io.InputStreamReader; ...@@ -32,6 +32,7 @@ import java.io.InputStreamReader;
import java.text.ParseException; import java.text.ParseException;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.Locale; import java.util.Locale;
import java.util.TimeZone;
import java.util.concurrent.CancellationException; import java.util.concurrent.CancellationException;
/** /**
...@@ -173,6 +174,7 @@ public class UtcTimingElementResolver implements Loader.Callback { ...@@ -173,6 +174,7 @@ public class UtcTimingElementResolver implements Loader.Callback {
try { try {
// TODO: It may be necessary to handle timestamp offsets from UTC. // TODO: It may be necessary to handle timestamp offsets from UTC.
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US); SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US);
format.setTimeZone(TimeZone.getTimeZone("UTC"));
return format.parse(firstLine).getTime(); return format.parse(firstLine).getTime();
} catch (ParseException e) { } catch (ParseException e) {
throw new ParserException(e); throw new ParserException(e);
......
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.drm;
import android.media.MediaDrm;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* Encapsulates initialization data required by a {@link MediaDrm} instance.
*/
public abstract class DrmInitData {
/**
* The container mime type.
*/
public final String mimeType;
public DrmInitData(String mimeType) {
this.mimeType = mimeType;
}
/**
* Retrieves initialization data for a given DRM scheme, specified by its UUID.
*
* @param schemeUuid The DRM scheme's UUID.
* @return The initialization data for the scheme, or null if the scheme is not supported.
*/
public abstract byte[] get(UUID schemeUuid);
/**
* A {@link DrmInitData} implementation that maps UUID onto scheme specific data.
*/
public static final class Mapped extends DrmInitData {
private final Map<UUID, byte[]> schemeData;
public Mapped(String mimeType) {
super(mimeType);
schemeData = new HashMap<UUID, byte[]>();
}
@Override
public byte[] get(UUID schemeUuid) {
return schemeData.get(schemeUuid);
}
/**
* Inserts scheme specific initialization data.
*
* @param schemeUuid The scheme UUID.
* @param data The corresponding initialization data.
*/
public void put(UUID schemeUuid, byte[] data) {
schemeData.put(schemeUuid, data);
}
/**
* Inserts scheme specific initialization data.
*
* @param data A mapping from scheme UUID to initialization data.
*/
public void putAll(Map<UUID, byte[]> data) {
schemeData.putAll(data);
}
}
/**
* A {@link DrmInitData} implementation that returns the same initialization data for all schemes.
*/
public static final class Universal extends DrmInitData {
private byte[] data;
public Universal(String mimeType, byte[] data) {
super(mimeType);
this.data = data;
}
@Override
public byte[] get(UUID schemeUuid) {
return data;
}
}
}
...@@ -18,9 +18,6 @@ package com.google.android.exoplayer.drm; ...@@ -18,9 +18,6 @@ package com.google.android.exoplayer.drm;
import android.annotation.TargetApi; import android.annotation.TargetApi;
import android.media.MediaCrypto; import android.media.MediaCrypto;
import java.util.Map;
import java.util.UUID;
/** /**
* Manages a DRM session. * Manages a DRM session.
*/ */
...@@ -36,7 +33,7 @@ public interface DrmSessionManager { ...@@ -36,7 +33,7 @@ public interface DrmSessionManager {
*/ */
public static final int STATE_CLOSED = 1; public static final int STATE_CLOSED = 1;
/** /**
* The session is being opened (i.e. {@link #open(Map, String)} has been called, but the session * The session is being opened (i.e. {@link #open(DrmInitData)} has been called, but the session
* is not yet open). * is not yet open).
*/ */
public static final int STATE_OPENING = 2; public static final int STATE_OPENING = 2;
...@@ -52,11 +49,9 @@ public interface DrmSessionManager { ...@@ -52,11 +49,9 @@ public interface DrmSessionManager {
/** /**
* Opens the session, possibly asynchronously. * Opens the session, possibly asynchronously.
* *
* @param drmInitData Initialization data for the drm schemes supported by the media, keyed by * @param drmInitData DRM initialization data.
* scheme UUID.
* @param mimeType The mimeType of the media.
*/ */
void open(Map<UUID, byte[]> drmInitData, String mimeType); void open(DrmInitData drmInitData);
/** /**
* Closes the session. * Closes the session.
......
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.drm;
/**
* Thrown when the drm keys loaded into an open session expire.
*/
public final class KeysExpiredException extends Exception {
}
...@@ -31,7 +31,6 @@ import android.os.Looper; ...@@ -31,7 +31,6 @@ import android.os.Looper;
import android.os.Message; import android.os.Message;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map;
import java.util.UUID; import java.util.UUID;
/** /**
...@@ -82,15 +81,6 @@ public class StreamingDrmSessionManager implements DrmSessionManager { ...@@ -82,15 +81,6 @@ public class StreamingDrmSessionManager implements DrmSessionManager {
private byte[] sessionId; private byte[] sessionId;
/** /**
* @deprecated Use the other constructor, passing null as {@code optionalKeyRequestParameters}.
*/
@Deprecated
public StreamingDrmSessionManager(UUID uuid, Looper playbackLooper, MediaDrmCallback callback,
Handler eventHandler, EventListener eventListener) throws UnsupportedSchemeException {
this(uuid, playbackLooper, callback, null, eventHandler, eventListener);
}
/**
* @param uuid The UUID of the drm scheme. * @param uuid The UUID of the drm scheme.
* @param playbackLooper The looper associated with the media playback thread. Should usually be * @param playbackLooper The looper associated with the media playback thread. Should usually be
* obtained using {@link com.google.android.exoplayer.ExoPlayer#getPlaybackLooper()}. * obtained using {@link com.google.android.exoplayer.ExoPlayer#getPlaybackLooper()}.
...@@ -168,7 +158,7 @@ public class StreamingDrmSessionManager implements DrmSessionManager { ...@@ -168,7 +158,7 @@ public class StreamingDrmSessionManager implements DrmSessionManager {
} }
@Override @Override
public void open(Map<UUID, byte[]> psshData, String mimeType) { public void open(DrmInitData drmInitData) {
if (++openCount != 1) { if (++openCount != 1) {
return; return;
} }
...@@ -178,8 +168,8 @@ public class StreamingDrmSessionManager implements DrmSessionManager { ...@@ -178,8 +168,8 @@ public class StreamingDrmSessionManager implements DrmSessionManager {
postRequestHandler = new PostRequestHandler(requestHandlerThread.getLooper()); postRequestHandler = new PostRequestHandler(requestHandlerThread.getLooper());
} }
if (this.schemePsshData == null) { if (this.schemePsshData == null) {
this.mimeType = mimeType; mimeType = drmInitData.mimeType;
schemePsshData = psshData.get(uuid); schemePsshData = drmInitData.get(uuid);
if (schemePsshData == null) { if (schemePsshData == null) {
onError(new IllegalStateException("Media does not support uuid: " + uuid)); onError(new IllegalStateException("Media does not support uuid: " + uuid));
return; return;
...@@ -332,7 +322,7 @@ public class StreamingDrmSessionManager implements DrmSessionManager { ...@@ -332,7 +322,7 @@ public class StreamingDrmSessionManager implements DrmSessionManager {
return; return;
case MediaDrm.EVENT_KEY_EXPIRED: case MediaDrm.EVENT_KEY_EXPIRED:
state = STATE_OPENED; state = STATE_OPENED;
postKeyRequest(); onError(new KeysExpiredException());
return; return;
case MediaDrm.EVENT_PROVISION_REQUIRED: case MediaDrm.EVENT_PROVISION_REQUIRED:
state = STATE_OPENED; state = STATE_OPENED;
......
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.extractor;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.upstream.DataSource;
import java.io.EOFException;
import java.io.IOException;
/**
* An {@link ExtractorInput} that wraps a {@link DataSource}.
*/
public final class DefaultExtractorInput implements ExtractorInput {
private static final byte[] SCRATCH_SPACE = new byte[4096];
private final DataSource dataSource;
private long position;
private long length;
/**
* @param dataSource The wrapped {@link DataSource}.
* @param position The initial position in the stream.
* @param length The length of the stream, or {@link C#LENGTH_UNBOUNDED} if it is unknown.
*/
public DefaultExtractorInput(DataSource dataSource, long position, long length) {
this.dataSource = dataSource;
this.position = position;
this.length = length;
}
@Override
public int read(byte[] target, int offset, int length) throws IOException, InterruptedException {
if (Thread.interrupted()) {
throw new InterruptedException();
}
int bytesRead = dataSource.read(target, offset, length);
if (bytesRead == C.RESULT_END_OF_INPUT) {
return C.RESULT_END_OF_INPUT;
}
position += bytesRead;
return bytesRead;
}
@Override
public boolean readFully(byte[] target, int offset, int length, boolean allowEndOfInput)
throws IOException, InterruptedException {
int remaining = length;
while (remaining > 0) {
if (Thread.interrupted()) {
throw new InterruptedException();
}
int bytesRead = dataSource.read(target, offset, remaining);
if (bytesRead == C.RESULT_END_OF_INPUT) {
if (allowEndOfInput && remaining == length) {
return false;
}
throw new EOFException();
}
offset += bytesRead;
remaining -= bytesRead;
}
position += length;
return true;
}
@Override
public void readFully(byte[] target, int offset, int length)
throws IOException, InterruptedException {
readFully(target, offset, length, false);
}
@Override
public void skipFully(int length) throws IOException, InterruptedException {
int remaining = length;
while (remaining > 0) {
if (Thread.interrupted()) {
throw new InterruptedException();
}
int bytesRead = dataSource.read(SCRATCH_SPACE, 0, Math.min(SCRATCH_SPACE.length, remaining));
if (bytesRead == C.RESULT_END_OF_INPUT) {
throw new EOFException();
}
remaining -= bytesRead;
}
position += length;
}
@Override
public long getPosition() {
return position;
}
@Override
public long getLength() {
return length;
}
}
...@@ -13,20 +13,21 @@ ...@@ -13,20 +13,21 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package com.google.android.exoplayer.hls.parser; package com.google.android.exoplayer.extractor;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.SampleHolder; import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.upstream.BufferPool; import com.google.android.exoplayer.upstream.BufferPool;
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.util.ParsableByteArray; import com.google.android.exoplayer.util.ParsableByteArray;
import java.io.IOException;
/** /**
* Wraps a {@link RollingSampleBuffer}, adding higher level functionality such as enforcing that * A {@link TrackOutput} that buffers extracted samples in a queue, and allows for consumption from
* the first sample returned from the queue is a keyframe, allowing splicing to another queue, and * that queue.
* so on.
*/ */
/* package */ abstract class SampleQueue { public final class DefaultTrackOutput implements TrackOutput {
private final RollingSampleBuffer rollingBuffer; private final RollingSampleBuffer rollingBuffer;
private final SampleHolder sampleInfoHolder; private final SampleHolder sampleInfoHolder;
...@@ -36,14 +37,11 @@ import com.google.android.exoplayer.util.ParsableByteArray; ...@@ -36,14 +37,11 @@ import com.google.android.exoplayer.util.ParsableByteArray;
private long lastReadTimeUs; private long lastReadTimeUs;
private long spliceOutTimeUs; private long spliceOutTimeUs;
// Accessed only by the loading thread.
private boolean writingSample;
// Accessed by both the loading and consuming threads. // Accessed by both the loading and consuming threads.
private volatile MediaFormat mediaFormat;
private volatile long largestParsedTimestampUs; private volatile long largestParsedTimestampUs;
private volatile MediaFormat format;
protected SampleQueue(BufferPool bufferPool) { public DefaultTrackOutput(BufferPool bufferPool) {
rollingBuffer = new RollingSampleBuffer(bufferPool); rollingBuffer = new RollingSampleBuffer(bufferPool);
sampleInfoHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_DISABLED); sampleInfoHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_DISABLED);
needKeyframe = true; needKeyframe = true;
...@@ -52,24 +50,60 @@ import com.google.android.exoplayer.util.ParsableByteArray; ...@@ -52,24 +50,60 @@ import com.google.android.exoplayer.util.ParsableByteArray;
largestParsedTimestampUs = Long.MIN_VALUE; largestParsedTimestampUs = Long.MIN_VALUE;
} }
public void release() { // Called by the consuming thread, but only when there is no loading thread.
rollingBuffer.release();
/**
* Clears the queue, returning all allocations to the allocator.
*/
public void clear() {
rollingBuffer.clear();
needKeyframe = true;
lastReadTimeUs = Long.MIN_VALUE;
spliceOutTimeUs = Long.MIN_VALUE;
largestParsedTimestampUs = Long.MIN_VALUE;
}
/**
* Returns the current absolute write index.
*/
public int getWriteIndex() {
return rollingBuffer.getWriteIndex();
} }
// Called by the consuming thread. // Called by the consuming thread.
public long getLargestParsedTimestampUs() { /**
return largestParsedTimestampUs; * Returns the current absolute read index.
*/
public int getReadIndex() {
return rollingBuffer.getReadIndex();
} }
public boolean hasMediaFormat() { /**
return mediaFormat != null; * True if the output has received a format. False otherwise.
*/
public boolean hasFormat() {
return format != null;
} }
public MediaFormat getMediaFormat() { /**
return mediaFormat; * The format most recently received by the output, or null if a format has yet to be received.
*/
public MediaFormat getFormat() {
return format;
} }
/**
* The largest timestamp of any sample received by the output, or {@link Long#MIN_VALUE} if a
* sample has yet to be received.
*/
public long getLargestParsedTimestampUs() {
return largestParsedTimestampUs;
}
/**
* True if at least one sample can be read from the queue. False otherwise.
*/
public boolean isEmpty() { public boolean isEmpty() {
return !advanceToEligibleSample(); return !advanceToEligibleSample();
} }
...@@ -115,7 +149,7 @@ import com.google.android.exoplayer.util.ParsableByteArray; ...@@ -115,7 +149,7 @@ import com.google.android.exoplayer.util.ParsableByteArray;
* @param nextQueue The queue being spliced to. * @param nextQueue The queue being spliced to.
* @return Whether the splice was configured successfully. * @return Whether the splice was configured successfully.
*/ */
public boolean configureSpliceTo(SampleQueue nextQueue) { public boolean configureSpliceTo(DefaultTrackOutput nextQueue) {
if (spliceOutTimeUs != Long.MIN_VALUE) { if (spliceOutTimeUs != Long.MIN_VALUE) {
// We've already configured the splice. // We've already configured the splice.
return true; return true;
...@@ -128,8 +162,7 @@ import com.google.android.exoplayer.util.ParsableByteArray; ...@@ -128,8 +162,7 @@ import com.google.android.exoplayer.util.ParsableByteArray;
} }
RollingSampleBuffer nextRollingBuffer = nextQueue.rollingBuffer; RollingSampleBuffer nextRollingBuffer = nextQueue.rollingBuffer;
while (nextRollingBuffer.peekSample(sampleInfoHolder) while (nextRollingBuffer.peekSample(sampleInfoHolder)
&& (sampleInfoHolder.timeUs < firstPossibleSpliceTime && (sampleInfoHolder.timeUs < firstPossibleSpliceTime || !sampleInfoHolder.isSyncFrame())) {
|| (sampleInfoHolder.flags & C.SAMPLE_FLAG_SYNC) == 0)) {
// Discard samples from the next queue for as long as they are before the earliest possible // Discard samples from the next queue for as long as they are before the earliest possible
// splice time, or not keyframes. // splice time, or not keyframes.
nextRollingBuffer.skipSample(); nextRollingBuffer.skipSample();
...@@ -152,7 +185,7 @@ import com.google.android.exoplayer.util.ParsableByteArray; ...@@ -152,7 +185,7 @@ import com.google.android.exoplayer.util.ParsableByteArray;
private boolean advanceToEligibleSample() { private boolean advanceToEligibleSample() {
boolean haveNext = rollingBuffer.peekSample(sampleInfoHolder); boolean haveNext = rollingBuffer.peekSample(sampleInfoHolder);
if (needKeyframe) { if (needKeyframe) {
while (haveNext && (sampleInfoHolder.flags & C.SAMPLE_FLAG_SYNC) == 0) { while (haveNext && !sampleInfoHolder.isSyncFrame()) {
rollingBuffer.skipSample(); rollingBuffer.skipSample();
haveNext = rollingBuffer.peekSample(sampleInfoHolder); haveNext = rollingBuffer.peekSample(sampleInfoHolder);
} }
...@@ -168,35 +201,27 @@ import com.google.android.exoplayer.util.ParsableByteArray; ...@@ -168,35 +201,27 @@ import com.google.android.exoplayer.util.ParsableByteArray;
// Called by the loading thread. // Called by the loading thread.
protected boolean writingSample() { public int sampleData(DataSource dataSource, int length) throws IOException {
return writingSample; return rollingBuffer.appendData(dataSource, length);
}
protected void setMediaFormat(MediaFormat mediaFormat) {
this.mediaFormat = mediaFormat;
} }
protected void startSample(long sampleTimeUs) { // TrackOutput implementation. Called by the loading thread.
startSample(sampleTimeUs, 0);
}
protected void startSample(long sampleTimeUs, int offset) { @Override
writingSample = true; public void format(MediaFormat format) {
largestParsedTimestampUs = Math.max(largestParsedTimestampUs, sampleTimeUs); this.format = format;
rollingBuffer.startSample(sampleTimeUs, offset);
} }
protected void appendData(ParsableByteArray buffer, int length) { @Override
public void sampleData(ParsableByteArray buffer, int length) {
rollingBuffer.appendData(buffer, length); rollingBuffer.appendData(buffer, length);
} }
protected void commitSample(boolean isKeyframe) { @Override
commitSample(isKeyframe, 0); public void sampleMetadata(long timeUs, int flags, int size, int offset, byte[] encryptionKey) {
} largestParsedTimestampUs = Math.max(largestParsedTimestampUs, timeUs);
rollingBuffer.commitSample(timeUs, flags, rollingBuffer.getWritePosition() - size - offset,
protected void commitSample(boolean isKeyframe, int offset) { size, encryptionKey);
rollingBuffer.commitSample(isKeyframe, offset);
writingSample = false;
} }
} }
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.extractor;
import com.google.android.exoplayer.C;
import java.io.IOException;
/**
* Facilitates extraction of data from a container format.
*/
public interface Extractor {
/**
* Returned by {@link #read(ExtractorInput)} if the {@link ExtractorInput} passed to the next
* {@link #read(ExtractorInput)} is required to provide data continuing from the position in the
* stream reached by the returning call.
*/
public static final int RESULT_CONTINUE = 0;
/**
* Returned by {@link #read(ExtractorInput)} if the end of the {@link ExtractorInput} was reached.
* Equal to {@link C#RESULT_END_OF_INPUT}.
*/
public static final int RESULT_END_OF_INPUT = C.RESULT_END_OF_INPUT;
/**
* Initializes the extractor with an {@link ExtractorOutput}.
*
* @param output An {@link ExtractorOutput} to receive extracted data.
*/
void init(ExtractorOutput output);
/**
* Extracts data read from a provided {@link ExtractorInput}.
* <p>
* Each read will extract at most one sample from the stream before returning.
*
* @param input The {@link ExtractorInput} from which data should be read.
* @return One of the {@code RESULT_} values defined in this interface.
* @throws IOException If an error occurred reading from the input.
* @throws InterruptedException If the thread was interrupted.
*/
int read(ExtractorInput input) throws IOException, InterruptedException;
}
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.extractor;
import com.google.android.exoplayer.C;
import java.io.EOFException;
import java.io.IOException;
/**
* Provides data to be consumed by an {@link Extractor}.
*/
public interface ExtractorInput {
/**
* Reads up to {@code length} bytes from the input.
* <p>
* This method blocks until at least one byte of data can be read, the end of the input is
* detected, or an exception is thrown.
*
* @param target A target array into which data should be written.
* @param offset The offset into the target array at which to write.
* @param length The maximum number of bytes to read from the input.
* @return The number of bytes read, or {@link C#RESULT_END_OF_INPUT} if the input has ended.
* @throws IOException If an error occurs reading from the input.
* @throws InterruptedException If the thread has been interrupted.
*/
int read(byte[] target, int offset, int length) throws IOException, InterruptedException;
/**
* Like {@link #read(byte[], int, int)}, but reads the requested {@code length} in full.
* <p>
* If the end of the input is found having read no data, then behavior is dependent on
* {@code allowEndOfInput}. If {@code allowEndOfInput == true} then {@code false} is returned.
* Otherwise an {@link EOFException} is thrown.
* <p>
* Encountering the end of input having partially satisfied the read is always considered an
* error, and will result in an {@link EOFException} being thrown.
*
* @param target A target array into which data should be written.
* @param offset The offset into the target array at which to write.
* @param length The number of bytes to read from the input.
* @param allowEndOfInput True if encountering the end of the input having read no data is
* allowed, and should result in {@code false} being returned. False if it should be
* considered an error, causing an {@link EOFException} to be thrown.
* @return True if the read was successful. False if the end of the input was encountered having
* read no data.
* @throws EOFException If the end of input was encountered having partially satisfied the read
* (i.e. having read at least one byte, but fewer than {@code length}), or if no bytes were
* read and {@code allowEndOfInput} is false.
* @throws IOException If an error occurs reading from the input.
* @throws InterruptedException If the thread has been interrupted.
*/
boolean readFully(byte[] target, int offset, int length, boolean allowEndOfInput)
throws IOException, InterruptedException;
/**
* Equivalent to {@code readFully(target, offset, length, false)}.
*
* @param target A target array into which data should be written.
* @param offset The offset into the target array at which to write.
* @param length The number of bytes to read from the input.
* @throws EOFException If the end of input was encountered.
* @throws IOException If an error occurs reading from the input.
* @throws InterruptedException If the thread is interrupted.
*/
void readFully(byte[] target, int offset, int length) throws IOException, InterruptedException;
/**
* Like {@link #readFully(byte[], int, int)}, except the data is skipped instead of read.
* <p>
* Encountering the end of input is always considered an error, and will result in an
* {@link EOFException} being thrown.
*
* @param length The number of bytes to skip from the input.
* @throws EOFException If the end of input was encountered.
* @throws IOException If an error occurs reading from the input.
* @throws InterruptedException If the thread is interrupted.
*/
void skipFully(int length) throws IOException, InterruptedException;
/**
* The current position (byte offset) in the stream.
*
* @return The position (byte offset) in the stream.
*/
long getPosition();
/**
* Returns the length of the source stream, or {@link C#LENGTH_UNBOUNDED} if it is unknown.
*
* @return The length of the source stream, or {@link C#LENGTH_UNBOUNDED}.
*/
long getLength();
}
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.extractor;
/**
* Receives stream level data extracted by an {@link Extractor}.
*/
public interface ExtractorOutput {
/**
* Invoked when the {@link Extractor} identifies the existence of a track in the stream.
* <p>
* Returns a {@link TrackOutput} that will receive track level data belonging to the track.
*
* @param trackId A track identifier.
* @return The {@link TrackOutput} that should receive track level data belonging to the track.
*/
TrackOutput track(int trackId);
/**
* Invoked when all tracks have been identified, meaning that {@link #track(int)} will not be
* invoked again.
*/
void endTracks();
}
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.extractor;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.util.ParsableByteArray;
/**
* Receives track level data extracted by an {@link Extractor}.
*/
public interface TrackOutput {
/**
* Invoked when the {@link MediaFormat} of the track has been extracted from the stream.
*
* @param format The extracted {@link MediaFormat}.
*/
void format(MediaFormat format);
/**
* Invoked to write sample data to the output.
*
* @param data A {@link ParsableByteArray} from which to read the sample data.
* @param length The number of bytes to read.
*/
void sampleData(ParsableByteArray data, int length);
/**
* Invoked when metadata associated with a sample has been extracted from the stream.
* <p>
* The corresponding sample data will have already been passed to the output via calls to
* {@link #sampleData(ParsableByteArray, int)}.
*
* @param timeUs The media timestamp associated with the sample, in microseconds.
* @param flags Flags associated with the sample. See {@link SampleHolder#flags}.
* @param size The size of the sample data, in bytes.
* @param offset The number of bytes that have been passed to
* {@link #sampleData(ParsableByteArray, int)} since the last byte belonging to the sample
* whose metadata is being passed.
* @param encryptionKey The encryption key associated with the sample. May be null.
*/
void sampleMetadata(long timeUs, int flags, int size, int offset, byte[] encryptionKey);
}
...@@ -13,13 +13,11 @@ ...@@ -13,13 +13,11 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package com.google.android.exoplayer.hls.parser; package com.google.android.exoplayer.extractor.ts;
import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.extractor.Extractor;
import com.google.android.exoplayer.SampleHolder; import com.google.android.exoplayer.extractor.ExtractorInput;
import com.google.android.exoplayer.upstream.BufferPool; import com.google.android.exoplayer.extractor.ExtractorOutput;
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.ParsableByteArray; import com.google.android.exoplayer.util.ParsableByteArray;
import java.io.IOException; import java.io.IOException;
...@@ -28,82 +26,38 @@ import java.io.IOException; ...@@ -28,82 +26,38 @@ import java.io.IOException;
* Facilitates the extraction of AAC samples from elementary audio files formatted as AAC with ADTS * Facilitates the extraction of AAC samples from elementary audio files formatted as AAC with ADTS
* headers. * headers.
*/ */
public class AdtsExtractor extends HlsExtractor { public class AdtsExtractor implements Extractor {
private static final int MAX_PACKET_SIZE = 200; private static final int MAX_PACKET_SIZE = 200;
private final long firstSampleTimestamp; private final long firstSampleTimestamp;
private final ParsableByteArray packetBuffer; private final ParsableByteArray packetBuffer;
private final AdtsReader adtsReader;
// Accessed only by the loading thread. // Accessed only by the loading thread.
private AdtsReader adtsReader;
private boolean firstPacket; private boolean firstPacket;
// Accessed by both the loading and consuming threads.
private volatile boolean prepared;
public AdtsExtractor(boolean shouldSpliceIn, long firstSampleTimestamp, BufferPool bufferPool) { public AdtsExtractor(long firstSampleTimestamp) {
super(shouldSpliceIn);
this.firstSampleTimestamp = firstSampleTimestamp; this.firstSampleTimestamp = firstSampleTimestamp;
packetBuffer = new ParsableByteArray(MAX_PACKET_SIZE); packetBuffer = new ParsableByteArray(MAX_PACKET_SIZE);
adtsReader = new AdtsReader(bufferPool);
firstPacket = true; firstPacket = true;
} }
@Override @Override
public int getTrackCount() { public void init(ExtractorOutput output) {
Assertions.checkState(prepared); adtsReader = new AdtsReader(output.track(0));
return 1; output.endTracks();
} }
@Override @Override
public MediaFormat getFormat(int track) { public int read(ExtractorInput input)
Assertions.checkState(prepared); throws IOException, InterruptedException {
return adtsReader.getMediaFormat(); int bytesRead = input.read(packetBuffer.data, 0, MAX_PACKET_SIZE);
}
@Override
public boolean isPrepared() {
return prepared;
}
@Override
public void release() {
adtsReader.release();
}
@Override
public long getLargestSampleTimestamp() {
return adtsReader.getLargestParsedTimestampUs();
}
@Override
public boolean getSample(int track, SampleHolder holder) {
Assertions.checkState(prepared);
Assertions.checkState(track == 0);
return adtsReader.getSample(holder);
}
@Override
public void discardUntil(int track, long timeUs) {
Assertions.checkState(prepared);
Assertions.checkState(track == 0);
adtsReader.discardUntil(timeUs);
}
@Override
public boolean hasSamples(int track) {
Assertions.checkState(prepared);
Assertions.checkState(track == 0);
return !adtsReader.isEmpty();
}
@Override
public int read(DataSource dataSource) throws IOException {
int bytesRead = dataSource.read(packetBuffer.data, 0, MAX_PACKET_SIZE);
if (bytesRead == -1) { if (bytesRead == -1) {
return -1; return RESULT_END_OF_INPUT;
} }
// Feed whatever data we have to the reader, regardless of whether the read finished or not.
packetBuffer.setPosition(0); packetBuffer.setPosition(0);
packetBuffer.setLimit(bytesRead); packetBuffer.setLimit(bytesRead);
...@@ -111,16 +65,7 @@ public class AdtsExtractor extends HlsExtractor { ...@@ -111,16 +65,7 @@ public class AdtsExtractor extends HlsExtractor {
// unnecessary to copy the data through packetBuffer. // unnecessary to copy the data through packetBuffer.
adtsReader.consume(packetBuffer, firstSampleTimestamp, firstPacket); adtsReader.consume(packetBuffer, firstSampleTimestamp, firstPacket);
firstPacket = false; firstPacket = false;
if (!prepared) { return RESULT_CONTINUE;
prepared = adtsReader.hasMediaFormat();
}
return bytesRead;
}
@Override
protected SampleQueue getSampleQueue(int track) {
Assertions.checkState(track == 0);
return adtsReader;
} }
} }
...@@ -13,11 +13,11 @@ ...@@ -13,11 +13,11 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package com.google.android.exoplayer.hls.parser; package com.google.android.exoplayer.extractor.ts;
import com.google.android.exoplayer.C; import com.google.android.exoplayer.C;
import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.upstream.BufferPool; import com.google.android.exoplayer.extractor.TrackOutput;
import com.google.android.exoplayer.util.CodecSpecificDataUtil; import com.google.android.exoplayer.util.CodecSpecificDataUtil;
import com.google.android.exoplayer.util.MimeTypes; import com.google.android.exoplayer.util.MimeTypes;
import com.google.android.exoplayer.util.ParsableBitArray; import com.google.android.exoplayer.util.ParsableBitArray;
...@@ -48,15 +48,16 @@ import java.util.Collections; ...@@ -48,15 +48,16 @@ import java.util.Collections;
private boolean lastByteWasFF; private boolean lastByteWasFF;
private boolean hasCrc; private boolean hasCrc;
// Parsed from the header. // Used when parsing the header.
private boolean hasOutputFormat;
private long frameDurationUs; private long frameDurationUs;
private int sampleSize; private int sampleSize;
// Used when reading the samples. // Used when reading the samples.
private long timeUs; private long timeUs;
public AdtsReader(BufferPool bufferPool) { public AdtsReader(TrackOutput output) {
super(bufferPool); super(output);
adtsScratch = new ParsableBitArray(new byte[HEADER_SIZE + CRC_SIZE]); adtsScratch = new ParsableBitArray(new byte[HEADER_SIZE + CRC_SIZE]);
state = STATE_FINDING_SYNC; state = STATE_FINDING_SYNC;
} }
...@@ -78,17 +79,16 @@ import java.util.Collections; ...@@ -78,17 +79,16 @@ import java.util.Collections;
int targetLength = hasCrc ? HEADER_SIZE + CRC_SIZE : HEADER_SIZE; int targetLength = hasCrc ? HEADER_SIZE + CRC_SIZE : HEADER_SIZE;
if (continueRead(data, adtsScratch.getData(), targetLength)) { if (continueRead(data, adtsScratch.getData(), targetLength)) {
parseHeader(); parseHeader();
startSample(timeUs);
bytesRead = 0; bytesRead = 0;
state = STATE_READING_SAMPLE; state = STATE_READING_SAMPLE;
} }
break; break;
case STATE_READING_SAMPLE: case STATE_READING_SAMPLE:
int bytesToRead = Math.min(data.bytesLeft(), sampleSize - bytesRead); int bytesToRead = Math.min(data.bytesLeft(), sampleSize - bytesRead);
appendData(data, bytesToRead); output.sampleData(data, bytesToRead);
bytesRead += bytesToRead; bytesRead += bytesToRead;
if (bytesRead == sampleSize) { if (bytesRead == sampleSize) {
commitSample(true); output.sampleMetadata(timeUs, C.SAMPLE_FLAG_SYNC, sampleSize, 0, null);
timeUs += frameDurationUs; timeUs += frameDurationUs;
bytesRead = 0; bytesRead = 0;
state = STATE_FINDING_SYNC; state = STATE_FINDING_SYNC;
...@@ -152,7 +152,7 @@ import java.util.Collections; ...@@ -152,7 +152,7 @@ import java.util.Collections;
private void parseHeader() { private void parseHeader() {
adtsScratch.setPosition(0); adtsScratch.setPosition(0);
if (!hasMediaFormat()) { if (!hasOutputFormat) {
int audioObjectType = adtsScratch.readBits(2) + 1; int audioObjectType = adtsScratch.readBits(2) + 1;
int sampleRateIndex = adtsScratch.readBits(4); int sampleRateIndex = adtsScratch.readBits(4);
adtsScratch.skipBits(1); adtsScratch.skipBits(1);
...@@ -167,7 +167,8 @@ import java.util.Collections; ...@@ -167,7 +167,8 @@ import java.util.Collections;
MediaFormat.NO_VALUE, audioParams.second, audioParams.first, MediaFormat.NO_VALUE, audioParams.second, audioParams.first,
Collections.singletonList(audioSpecificConfig)); Collections.singletonList(audioSpecificConfig));
frameDurationUs = (C.MICROS_PER_SECOND * 1024L) / mediaFormat.sampleRate; frameDurationUs = (C.MICROS_PER_SECOND * 1024L) / mediaFormat.sampleRate;
setMediaFormat(mediaFormat); output.format(mediaFormat);
hasOutputFormat = true;
} else { } else {
adtsScratch.skipBits(10); adtsScratch.skipBits(10);
} }
......
...@@ -13,18 +13,23 @@ ...@@ -13,18 +13,23 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package com.google.android.exoplayer.hls.parser; package com.google.android.exoplayer.extractor.ts;
import com.google.android.exoplayer.upstream.BufferPool; import com.google.android.exoplayer.extractor.TrackOutput;
import com.google.android.exoplayer.util.ParsableByteArray; import com.google.android.exoplayer.util.ParsableByteArray;
/** /**
* Extracts individual samples from an elementary media stream, preserving original order. * Extracts individual samples from an elementary media stream, preserving original order.
*/ */
/* package */ abstract class ElementaryStreamReader extends SampleQueue { /* package */ abstract class ElementaryStreamReader {
protected ElementaryStreamReader(BufferPool bufferPool) { protected final TrackOutput output;
super(bufferPool);
/**
* @param output A {@link TrackOutput} to which samples should be written.
*/
protected ElementaryStreamReader(TrackOutput output) {
this.output = output;
} }
/** /**
......
...@@ -13,16 +13,19 @@ ...@@ -13,16 +13,19 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package com.google.android.exoplayer.hls.parser; package com.google.android.exoplayer.extractor.ts;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.mp4.Mp4Util; import com.google.android.exoplayer.extractor.TrackOutput;
import com.google.android.exoplayer.upstream.BufferPool;
import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.H264Util;
import com.google.android.exoplayer.util.MimeTypes; import com.google.android.exoplayer.util.MimeTypes;
import com.google.android.exoplayer.util.ParsableBitArray; import com.google.android.exoplayer.util.ParsableBitArray;
import com.google.android.exoplayer.util.ParsableByteArray; import com.google.android.exoplayer.util.ParsableByteArray;
import android.util.Log;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
...@@ -32,29 +35,59 @@ import java.util.List; ...@@ -32,29 +35,59 @@ import java.util.List;
*/ */
/* package */ class H264Reader extends ElementaryStreamReader { /* package */ class H264Reader extends ElementaryStreamReader {
private static final String TAG = "H264Reader";
private static final int NAL_UNIT_TYPE_IDR = 5; private static final int NAL_UNIT_TYPE_IDR = 5;
private static final int NAL_UNIT_TYPE_SEI = 6; private static final int NAL_UNIT_TYPE_SEI = 6;
private static final int NAL_UNIT_TYPE_SPS = 7; private static final int NAL_UNIT_TYPE_SPS = 7;
private static final int NAL_UNIT_TYPE_PPS = 8; private static final int NAL_UNIT_TYPE_PPS = 8;
private static final int NAL_UNIT_TYPE_AUD = 9; private static final int NAL_UNIT_TYPE_AUD = 9;
private static final int EXTENDED_SAR = 0xFF;
private static final float[] ASPECT_RATIO_IDC_VALUES = new float[] {
1f /* Unspecified. Assume square */,
1f,
12f / 11f,
10f / 11f,
16f / 11f,
40f / 33f,
24f / 11f,
20f / 11f,
32f / 11f,
80f / 33f,
18f / 11f,
15f / 11f,
64f / 33f,
160f / 99f,
4f / 3f,
3f / 2f,
2f
};
private final SeiReader seiReader; private final SeiReader seiReader;
private final boolean[] prefixFlags; private final boolean[] prefixFlags;
private final NalUnitTargetBuffer sps; private final NalUnitTargetBuffer sps;
private final NalUnitTargetBuffer pps; private final NalUnitTargetBuffer pps;
private final NalUnitTargetBuffer sei; private final NalUnitTargetBuffer sei;
private final ParsableByteArray seiWrapper;
private boolean hasOutputFormat;
private int scratchEscapeCount; private int scratchEscapeCount;
private int[] scratchEscapePositions; private int[] scratchEscapePositions;
private boolean writingSample;
private boolean isKeyframe; private boolean isKeyframe;
private long samplePosition;
private long sampleTimeUs;
private long totalBytesWritten;
public H264Reader(BufferPool bufferPool, SeiReader seiReader) { public H264Reader(TrackOutput output, SeiReader seiReader) {
super(bufferPool); super(output);
this.seiReader = seiReader; this.seiReader = seiReader;
prefixFlags = new boolean[3]; prefixFlags = new boolean[3];
sps = new NalUnitTargetBuffer(NAL_UNIT_TYPE_SPS, 128); sps = new NalUnitTargetBuffer(NAL_UNIT_TYPE_SPS, 128);
pps = new NalUnitTargetBuffer(NAL_UNIT_TYPE_PPS, 128); pps = new NalUnitTargetBuffer(NAL_UNIT_TYPE_PPS, 128);
sei = new NalUnitTargetBuffer(NAL_UNIT_TYPE_SEI, 128); sei = new NalUnitTargetBuffer(NAL_UNIT_TYPE_SEI, 128);
seiWrapper = new ParsableByteArray();
scratchEscapePositions = new int[10]; scratchEscapePositions = new int[10];
} }
...@@ -66,11 +99,12 @@ import java.util.List; ...@@ -66,11 +99,12 @@ import java.util.List;
byte[] dataArray = data.data; byte[] dataArray = data.data;
// Append the data to the buffer. // Append the data to the buffer.
appendData(data, data.bytesLeft()); totalBytesWritten += data.bytesLeft();
output.sampleData(data, data.bytesLeft());
// Scan the appended data, processing NAL units as they are encountered // Scan the appended data, processing NAL units as they are encountered
while (offset < limit) { while (offset < limit) {
int nextNalUnitOffset = Mp4Util.findNalUnit(dataArray, offset, limit, prefixFlags); int nextNalUnitOffset = H264Util.findNalUnit(dataArray, offset, limit, prefixFlags);
if (nextNalUnitOffset < limit) { if (nextNalUnitOffset < limit) {
// We've seen the start of a NAL unit. // We've seen the start of a NAL unit.
...@@ -81,16 +115,21 @@ import java.util.List; ...@@ -81,16 +115,21 @@ import java.util.List;
feedNalUnitTargetBuffersData(dataArray, offset, nextNalUnitOffset); feedNalUnitTargetBuffersData(dataArray, offset, nextNalUnitOffset);
} }
int nalUnitType = Mp4Util.getNalUnitType(dataArray, nextNalUnitOffset); int nalUnitType = H264Util.getNalUnitType(dataArray, nextNalUnitOffset);
int nalUnitOffsetInData = nextNalUnitOffset - limit; int bytesWrittenPastNalUnit = limit - nextNalUnitOffset;
if (nalUnitType == NAL_UNIT_TYPE_AUD) { if (nalUnitType == NAL_UNIT_TYPE_AUD) {
if (writingSample()) { if (writingSample) {
if (isKeyframe && !hasMediaFormat() && sps.isCompleted() && pps.isCompleted()) { if (isKeyframe && !hasOutputFormat && sps.isCompleted() && pps.isCompleted()) {
parseMediaFormat(sps, pps); parseMediaFormat(sps, pps);
} }
commitSample(isKeyframe, nalUnitOffsetInData); int flags = isKeyframe ? C.SAMPLE_FLAG_SYNC : 0;
int size = (int) (totalBytesWritten - samplePosition) - bytesWrittenPastNalUnit;
output.sampleMetadata(sampleTimeUs, flags, size, bytesWrittenPastNalUnit, null);
writingSample = false;
} }
startSample(pesTimeUs, nalUnitOffsetInData); writingSample = true;
samplePosition = totalBytesWritten - bytesWrittenPastNalUnit;
sampleTimeUs = pesTimeUs;
isKeyframe = false; isKeyframe = false;
} else if (nalUnitType == NAL_UNIT_TYPE_IDR) { } else if (nalUnitType == NAL_UNIT_TYPE_IDR) {
isKeyframe = true; isKeyframe = true;
...@@ -117,7 +156,7 @@ import java.util.List; ...@@ -117,7 +156,7 @@ import java.util.List;
} }
private void feedNalUnitTargetBuffersStart(int nalUnitType) { private void feedNalUnitTargetBuffersStart(int nalUnitType) {
if (!hasMediaFormat()) { if (!hasOutputFormat) {
sps.startNalUnit(nalUnitType); sps.startNalUnit(nalUnitType);
pps.startNalUnit(nalUnitType); pps.startNalUnit(nalUnitType);
} }
...@@ -125,7 +164,7 @@ import java.util.List; ...@@ -125,7 +164,7 @@ import java.util.List;
} }
private void feedNalUnitTargetBuffersData(byte[] dataArray, int offset, int limit) { private void feedNalUnitTargetBuffersData(byte[] dataArray, int offset, int limit) {
if (!hasMediaFormat()) { if (!hasOutputFormat) {
sps.appendToNalUnit(dataArray, offset, limit); sps.appendToNalUnit(dataArray, offset, limit);
pps.appendToNalUnit(dataArray, offset, limit); pps.appendToNalUnit(dataArray, offset, limit);
} }
...@@ -137,7 +176,8 @@ import java.util.List; ...@@ -137,7 +176,8 @@ import java.util.List;
pps.endNalUnit(discardPadding); pps.endNalUnit(discardPadding);
if (sei.endNalUnit(discardPadding)) { if (sei.endNalUnit(discardPadding)) {
int unescapedLength = unescapeStream(sei.nalData, sei.nalLength); int unescapedLength = unescapeStream(sei.nalData, sei.nalLength);
seiReader.read(sei.nalData, 0, unescapedLength, pesTimeUs); seiWrapper.reset(sei.nalData, unescapedLength);
seiReader.consume(seiWrapper, pesTimeUs, true);
} }
} }
...@@ -228,9 +268,29 @@ import java.util.List; ...@@ -228,9 +268,29 @@ import java.util.List;
frameHeight -= (frameCropTopOffset + frameCropBottomOffset) * cropUnitY; frameHeight -= (frameCropTopOffset + frameCropBottomOffset) * cropUnitY;
} }
// Set the format. float pixelWidthHeightRatio = 1;
setMediaFormat(MediaFormat.createVideoFormat(MimeTypes.VIDEO_H264, MediaFormat.NO_VALUE, boolean vuiParametersPresentFlag = bitArray.readBit();
frameWidth, frameHeight, initializationData)); if (vuiParametersPresentFlag) {
boolean aspectRatioInfoPresentFlag = bitArray.readBit();
if (aspectRatioInfoPresentFlag) {
int aspectRatioIdc = bitArray.readBits(8);
if (aspectRatioIdc == EXTENDED_SAR) {
int sarWidth = bitArray.readBits(16);
int sarHeight = bitArray.readBits(16);
if (sarWidth != 0 && sarHeight != 0) {
pixelWidthHeightRatio = (float) sarWidth / sarHeight;
}
} else if (aspectRatioIdc < ASPECT_RATIO_IDC_VALUES.length) {
pixelWidthHeightRatio = ASPECT_RATIO_IDC_VALUES[aspectRatioIdc];
} else {
Log.w(TAG, "Unexpected aspect_ratio_idc value: " + aspectRatioIdc);
}
}
}
output.format(MediaFormat.createVideoFormat(MimeTypes.VIDEO_H264, MediaFormat.NO_VALUE,
C.UNKNOWN_TIME_US, frameWidth, frameHeight, pixelWidthHeightRatio, initializationData));
hasOutputFormat = true;
} }
private void skipScalingList(ParsableBitArray bitArray, int size) { private void skipScalingList(ParsableBitArray bitArray, int size) {
......
...@@ -13,10 +13,11 @@ ...@@ -13,10 +13,11 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package com.google.android.exoplayer.hls.parser; package com.google.android.exoplayer.extractor.ts;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.upstream.BufferPool; import com.google.android.exoplayer.extractor.TrackOutput;
import com.google.android.exoplayer.util.ParsableByteArray; import com.google.android.exoplayer.util.ParsableByteArray;
/** /**
...@@ -24,24 +25,32 @@ import com.google.android.exoplayer.util.ParsableByteArray; ...@@ -24,24 +25,32 @@ import com.google.android.exoplayer.util.ParsableByteArray;
*/ */
/* package */ class Id3Reader extends ElementaryStreamReader { /* package */ class Id3Reader extends ElementaryStreamReader {
public Id3Reader(BufferPool bufferPool) { private boolean writingSample;
super(bufferPool); private long sampleTimeUs;
setMediaFormat(MediaFormat.createId3Format()); private int sampleSize;
public Id3Reader(TrackOutput output) {
super(output);
output.format(MediaFormat.createId3Format());
} }
@Override @Override
public void consume(ParsableByteArray data, long pesTimeUs, boolean startOfPacket) { public void consume(ParsableByteArray data, long pesTimeUs, boolean startOfPacket) {
if (startOfPacket) { if (startOfPacket) {
startSample(pesTimeUs); writingSample = true;
sampleTimeUs = pesTimeUs;
sampleSize = 0;
} }
if (writingSample()) { if (writingSample) {
appendData(data, data.bytesLeft()); sampleSize += data.bytesLeft();
output.sampleData(data, data.bytesLeft());
} }
} }
@Override @Override
public void packetFinished() { public void packetFinished() {
commitSample(true); output.sampleMetadata(sampleTimeUs, C.SAMPLE_FLAG_SYNC, sampleSize, 0, null);
writingSample = false;
} }
} }
...@@ -13,11 +13,12 @@ ...@@ -13,11 +13,12 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package com.google.android.exoplayer.hls.parser; package com.google.android.exoplayer.extractor.ts;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.extractor.TrackOutput;
import com.google.android.exoplayer.text.eia608.Eia608Parser; import com.google.android.exoplayer.text.eia608.Eia608Parser;
import com.google.android.exoplayer.upstream.BufferPool;
import com.google.android.exoplayer.util.ParsableByteArray; import com.google.android.exoplayer.util.ParsableByteArray;
/** /**
...@@ -26,20 +27,17 @@ import com.google.android.exoplayer.util.ParsableByteArray; ...@@ -26,20 +27,17 @@ import com.google.android.exoplayer.util.ParsableByteArray;
* TODO: Technically, we shouldn't allow a sample to be read from the queue until we're sure that * TODO: Technically, we shouldn't allow a sample to be read from the queue until we're sure that
* a sample with an earlier timestamp won't be added to it. * a sample with an earlier timestamp won't be added to it.
*/ */
/* package */ class SeiReader extends SampleQueue { /* package */ class SeiReader extends ElementaryStreamReader {
private final ParsableByteArray seiBuffer; public SeiReader(TrackOutput output) {
super(output);
public SeiReader(BufferPool bufferPool) { output.format(MediaFormat.createEia608Format());
super(bufferPool);
setMediaFormat(MediaFormat.createEia608Format());
seiBuffer = new ParsableByteArray();
} }
public void read(byte[] data, int position, int limit, long pesTimeUs) { @Override
seiBuffer.reset(data, limit); public void consume(ParsableByteArray seiBuffer, long pesTimeUs, boolean startOfPacket) {
// Skip the NAL prefix and type. // Skip the NAL prefix and type.
seiBuffer.setPosition(position + 4); seiBuffer.skip(4);
int b; int b;
while (seiBuffer.bytesLeft() > 1 /* last byte will be rbsp_trailing_bits */) { while (seiBuffer.bytesLeft() > 1 /* last byte will be rbsp_trailing_bits */) {
...@@ -57,13 +55,17 @@ import com.google.android.exoplayer.util.ParsableByteArray; ...@@ -57,13 +55,17 @@ import com.google.android.exoplayer.util.ParsableByteArray;
} while (b == 0xFF); } while (b == 0xFF);
// Process the payload. We only support EIA-608 payloads currently. // Process the payload. We only support EIA-608 payloads currently.
if (Eia608Parser.isSeiMessageEia608(payloadType, payloadSize, seiBuffer)) { if (Eia608Parser.isSeiMessageEia608(payloadType, payloadSize, seiBuffer)) {
startSample(pesTimeUs); output.sampleData(seiBuffer, payloadSize);
appendData(seiBuffer, payloadSize); output.sampleMetadata(pesTimeUs, C.SAMPLE_FLAG_SYNC, payloadSize, 0, null);
commitSample(true);
} else { } else {
seiBuffer.skip(payloadSize); seiBuffer.skip(payloadSize);
} }
} }
} }
@Override
public void packetFinished() {
// Do nothing.
}
} }
...@@ -13,14 +13,12 @@ ...@@ -13,14 +13,12 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package com.google.android.exoplayer.hls.parser; package com.google.android.exoplayer.extractor.ts;
import com.google.android.exoplayer.C; import com.google.android.exoplayer.C;
import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.extractor.Extractor;
import com.google.android.exoplayer.SampleHolder; import com.google.android.exoplayer.extractor.ExtractorInput;
import com.google.android.exoplayer.upstream.BufferPool; import com.google.android.exoplayer.extractor.ExtractorOutput;
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.ParsableBitArray; import com.google.android.exoplayer.util.ParsableBitArray;
import com.google.android.exoplayer.util.ParsableByteArray; import com.google.android.exoplayer.util.ParsableByteArray;
...@@ -32,7 +30,7 @@ import java.io.IOException; ...@@ -32,7 +30,7 @@ import java.io.IOException;
/** /**
* Facilitates the extraction of data from the MPEG-2 TS container format. * Facilitates the extraction of data from the MPEG-2 TS container format.
*/ */
public final class TsExtractor extends HlsExtractor { public final class TsExtractor implements Extractor {
private static final String TAG = "TsExtractor"; private static final String TAG = "TsExtractor";
...@@ -50,119 +48,43 @@ public final class TsExtractor extends HlsExtractor { ...@@ -50,119 +48,43 @@ public final class TsExtractor extends HlsExtractor {
private static final long MAX_PTS = 0x1FFFFFFFFL; private static final long MAX_PTS = 0x1FFFFFFFFL;
private final ParsableByteArray tsPacketBuffer; private final ParsableByteArray tsPacketBuffer;
private final SparseArray<SampleQueue> sampleQueues; // Indexed by streamType private final SparseArray<ElementaryStreamReader> streamReaders; // Indexed by streamType
private final SparseArray<TsPayloadReader> tsPayloadReaders; // Indexed by pid private final SparseArray<TsPayloadReader> tsPayloadReaders; // Indexed by pid
private final BufferPool bufferPool;
private final long firstSampleTimestamp; private final long firstSampleTimestamp;
private final ParsableBitArray tsScratch; private final ParsableBitArray tsScratch;
// Accessed only by the loading thread. // Accessed only by the loading thread.
private int tsPacketBytesRead; private ExtractorOutput output;
private long timestampOffsetUs; private long timestampOffsetUs;
private long lastPts; private long lastPts;
// Accessed by both the loading and consuming threads. public TsExtractor(long firstSampleTimestamp) {
private volatile boolean prepared;
public TsExtractor(boolean shouldSpliceIn, long firstSampleTimestamp, BufferPool bufferPool) {
super(shouldSpliceIn);
this.firstSampleTimestamp = firstSampleTimestamp; this.firstSampleTimestamp = firstSampleTimestamp;
this.bufferPool = bufferPool;
tsScratch = new ParsableBitArray(new byte[3]); tsScratch = new ParsableBitArray(new byte[3]);
tsPacketBuffer = new ParsableByteArray(TS_PACKET_SIZE); tsPacketBuffer = new ParsableByteArray(TS_PACKET_SIZE);
sampleQueues = new SparseArray<SampleQueue>(); streamReaders = new SparseArray<ElementaryStreamReader>();
tsPayloadReaders = new SparseArray<TsPayloadReader>(); tsPayloadReaders = new SparseArray<TsPayloadReader>();
tsPayloadReaders.put(TS_PAT_PID, new PatReader()); tsPayloadReaders.put(TS_PAT_PID, new PatReader());
lastPts = Long.MIN_VALUE; lastPts = Long.MIN_VALUE;
} }
@Override @Override
public int getTrackCount() { public void init(ExtractorOutput output) {
Assertions.checkState(prepared); this.output = output;
return sampleQueues.size();
}
@Override
public MediaFormat getFormat(int track) {
Assertions.checkState(prepared);
return sampleQueues.valueAt(track).getMediaFormat();
}
@Override
public boolean isPrepared() {
return prepared;
}
@Override
public void release() {
for (int i = 0; i < sampleQueues.size(); i++) {
sampleQueues.valueAt(i).release();
}
}
@Override
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 @Override
public boolean hasSamples(int track) { public int read(ExtractorInput input)
Assertions.checkState(prepared); throws IOException, InterruptedException {
return !sampleQueues.valueAt(track).isEmpty(); if (!input.readFully(tsPacketBuffer.data, 0, TS_PACKET_SIZE, true)) {
return RESULT_END_OF_INPUT;
} }
private boolean checkPrepared() {
int pesPayloadReaderCount = sampleQueues.size();
if (pesPayloadReaderCount == 0) {
return false;
}
for (int i = 0; i < pesPayloadReaderCount; i++) {
if (!sampleQueues.valueAt(i).hasMediaFormat()) {
return false;
}
}
return true;
}
@Override
public int read(DataSource dataSource) throws IOException {
int bytesRead = dataSource.read(tsPacketBuffer.data, tsPacketBytesRead,
TS_PACKET_SIZE - tsPacketBytesRead);
if (bytesRead == -1) {
return -1;
}
tsPacketBytesRead += bytesRead;
if (tsPacketBytesRead < TS_PACKET_SIZE) {
// We haven't read the whole packet yet.
return bytesRead;
}
// Reset before reading the packet.
tsPacketBytesRead = 0;
tsPacketBuffer.setPosition(0); tsPacketBuffer.setPosition(0);
tsPacketBuffer.setLimit(TS_PACKET_SIZE); tsPacketBuffer.setLimit(TS_PACKET_SIZE);
int syncByte = tsPacketBuffer.readUnsignedByte(); int syncByte = tsPacketBuffer.readUnsignedByte();
if (syncByte != TS_SYNC_BYTE) { if (syncByte != TS_SYNC_BYTE) {
return bytesRead; return RESULT_CONTINUE;
} }
tsPacketBuffer.readBytes(tsScratch, 3); tsPacketBuffer.readBytes(tsScratch, 3);
...@@ -185,20 +107,11 @@ public final class TsExtractor extends HlsExtractor { ...@@ -185,20 +107,11 @@ public final class TsExtractor extends HlsExtractor {
if (payloadExists) { if (payloadExists) {
TsPayloadReader payloadReader = tsPayloadReaders.get(pid); TsPayloadReader payloadReader = tsPayloadReaders.get(pid);
if (payloadReader != null) { if (payloadReader != null) {
payloadReader.consume(tsPacketBuffer, payloadUnitStartIndicator); payloadReader.consume(tsPacketBuffer, payloadUnitStartIndicator, output);
}
} }
if (!prepared) {
prepared = checkPrepared();
} }
return bytesRead; return RESULT_CONTINUE;
}
@Override
protected SampleQueue getSampleQueue(int track) {
return sampleQueues.valueAt(track);
} }
/** /**
...@@ -233,7 +146,8 @@ public final class TsExtractor extends HlsExtractor { ...@@ -233,7 +146,8 @@ public final class TsExtractor extends HlsExtractor {
*/ */
private abstract static class TsPayloadReader { private abstract static class TsPayloadReader {
public abstract void consume(ParsableByteArray data, boolean payloadUnitStartIndicator); public abstract void consume(ParsableByteArray data, boolean payloadUnitStartIndicator,
ExtractorOutput output);
} }
...@@ -249,7 +163,8 @@ public final class TsExtractor extends HlsExtractor { ...@@ -249,7 +163,8 @@ public final class TsExtractor extends HlsExtractor {
} }
@Override @Override
public void consume(ParsableByteArray data, boolean payloadUnitStartIndicator) { public void consume(ParsableByteArray data, boolean payloadUnitStartIndicator,
ExtractorOutput output) {
// Skip pointer. // Skip pointer.
if (payloadUnitStartIndicator) { if (payloadUnitStartIndicator) {
int pointerField = data.readUnsignedByte(); int pointerField = data.readUnsignedByte();
...@@ -288,7 +203,8 @@ public final class TsExtractor extends HlsExtractor { ...@@ -288,7 +203,8 @@ public final class TsExtractor extends HlsExtractor {
} }
@Override @Override
public void consume(ParsableByteArray data, boolean payloadUnitStartIndicator) { public void consume(ParsableByteArray data, boolean payloadUnitStartIndicator,
ExtractorOutput output) {
// Skip pointer. // Skip pointer.
if (payloadUnitStartIndicator) { if (payloadUnitStartIndicator) {
int pointerField = data.readUnsignedByte(); int pointerField = data.readUnsignedByte();
...@@ -325,7 +241,7 @@ public final class TsExtractor extends HlsExtractor { ...@@ -325,7 +241,7 @@ public final class TsExtractor extends HlsExtractor {
data.skip(esInfoLength); data.skip(esInfoLength);
entriesSize -= esInfoLength + 5; entriesSize -= esInfoLength + 5;
if (sampleQueues.get(streamType) != null) { if (streamReaders.get(streamType) != null) {
continue; continue;
} }
...@@ -336,25 +252,26 @@ public final class TsExtractor extends HlsExtractor { ...@@ -336,25 +252,26 @@ public final class TsExtractor extends HlsExtractor {
pesPayloadReader = new MpaReader(bufferPool); pesPayloadReader = new MpaReader(bufferPool);
break; break;
case TS_STREAM_TYPE_AAC: case TS_STREAM_TYPE_AAC:
pesPayloadReader = new AdtsReader(bufferPool); pesPayloadReader = new AdtsReader(output.track(TS_STREAM_TYPE_AAC));
break; break;
case TS_STREAM_TYPE_H264: case TS_STREAM_TYPE_H264:
SeiReader seiReader = new SeiReader(bufferPool); SeiReader seiReader = new SeiReader(output.track(TS_STREAM_TYPE_EIA608));
sampleQueues.put(TS_STREAM_TYPE_EIA608, seiReader); streamReaders.put(TS_STREAM_TYPE_EIA608, seiReader);
pesPayloadReader = new H264Reader(bufferPool, seiReader); pesPayloadReader = new H264Reader(output.track(TS_STREAM_TYPE_H264),
seiReader);
break; break;
case TS_STREAM_TYPE_ID3: case TS_STREAM_TYPE_ID3:
pesPayloadReader = new Id3Reader(bufferPool); pesPayloadReader = new Id3Reader(output.track(TS_STREAM_TYPE_ID3));
break; break;
} }
if (pesPayloadReader != null) { if (pesPayloadReader != null) {
sampleQueues.put(streamType, pesPayloadReader); streamReaders.put(streamType, pesPayloadReader);
tsPayloadReaders.put(elementaryPid, new PesReader(pesPayloadReader)); tsPayloadReaders.put(elementaryPid, new PesReader(pesPayloadReader));
} }
} }
// Skip CRC_32. output.endTracks();
} }
} }
...@@ -393,7 +310,8 @@ public final class TsExtractor extends HlsExtractor { ...@@ -393,7 +310,8 @@ public final class TsExtractor extends HlsExtractor {
} }
@Override @Override
public void consume(ParsableByteArray data, boolean payloadUnitStartIndicator) { public void consume(ParsableByteArray data, boolean payloadUnitStartIndicator,
ExtractorOutput output) {
if (payloadUnitStartIndicator) { if (payloadUnitStartIndicator) {
switch (state) { switch (state) {
case STATE_FINDING_HEADER: case STATE_FINDING_HEADER:
......
...@@ -13,27 +13,44 @@ ...@@ -13,27 +13,44 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package com.google.android.exoplayer.hls.parser; package com.google.android.exoplayer.hls;
import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.SampleHolder; import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.extractor.DefaultTrackOutput;
import com.google.android.exoplayer.extractor.Extractor;
import com.google.android.exoplayer.extractor.ExtractorInput;
import com.google.android.exoplayer.extractor.ExtractorOutput;
import com.google.android.exoplayer.extractor.TrackOutput;
import com.google.android.exoplayer.upstream.BufferPool;
import com.google.android.exoplayer.util.Assertions;
import android.util.SparseArray;
import java.io.IOException; import java.io.IOException;
/** /**
* Facilitates extraction of media samples for HLS playbacks. * Wraps a {@link Extractor}, adding functionality to enable reading of the extracted samples.
*/ */
// TODO: Consider consolidating more common logic in this base class. public final class HlsExtractorWrapper implements ExtractorOutput {
public abstract class HlsExtractor {
private final BufferPool bufferPool;
private final Extractor extractor;
private final SparseArray<DefaultTrackOutput> sampleQueues;
private final boolean shouldSpliceIn; private final boolean shouldSpliceIn;
private volatile boolean tracksBuilt;
// Accessed only by the consuming thread. // Accessed only by the consuming thread.
private boolean prepared;
private boolean spliceConfigured; private boolean spliceConfigured;
public HlsExtractor(boolean shouldSpliceIn) { public HlsExtractorWrapper(BufferPool bufferPool, Extractor extractor, boolean shouldSpliceIn) {
this.bufferPool = bufferPool;
this.extractor = extractor;
this.shouldSpliceIn = shouldSpliceIn; this.shouldSpliceIn = shouldSpliceIn;
sampleQueues = new SparseArray<DefaultTrackOutput>();
extractor.init(this);
} }
/** /**
...@@ -50,7 +67,7 @@ public abstract class HlsExtractor { ...@@ -50,7 +67,7 @@ public abstract class HlsExtractor {
* *
* @param nextExtractor The extractor being spliced to. * @param nextExtractor The extractor being spliced to.
*/ */
public final void configureSpliceTo(HlsExtractor nextExtractor) { public final void configureSpliceTo(HlsExtractorWrapper nextExtractor) {
if (spliceConfigured || !nextExtractor.shouldSpliceIn || !nextExtractor.isPrepared()) { if (spliceConfigured || !nextExtractor.shouldSpliceIn || !nextExtractor.isPrepared()) {
// The splice is already configured, or the next extractor doesn't want to be spliced in, or // The splice is already configured, or the next extractor doesn't want to be spliced in, or
// the next extractor isn't ready to be spliced in. // the next extractor isn't ready to be spliced in.
...@@ -59,7 +76,9 @@ public abstract class HlsExtractor { ...@@ -59,7 +76,9 @@ public abstract class HlsExtractor {
boolean spliceConfigured = true; boolean spliceConfigured = true;
int trackCount = getTrackCount(); int trackCount = getTrackCount();
for (int i = 0; i < trackCount; i++) { for (int i = 0; i < trackCount; i++) {
spliceConfigured &= getSampleQueue(i).configureSpliceTo(nextExtractor.getSampleQueue(i)); DefaultTrackOutput currentSampleQueue = sampleQueues.valueAt(i);
DefaultTrackOutput nextSampleQueue = nextExtractor.sampleQueues.valueAt(i);
spliceConfigured &= currentSampleQueue.configureSpliceTo(nextSampleQueue);
} }
this.spliceConfigured = spliceConfigured; this.spliceConfigured = spliceConfigured;
return; return;
...@@ -72,7 +91,9 @@ public abstract class HlsExtractor { ...@@ -72,7 +91,9 @@ public abstract class HlsExtractor {
* *
* @return The number of available tracks. * @return The number of available tracks.
*/ */
public abstract int getTrackCount(); public int getTrackCount() {
return sampleQueues.size();
}
/** /**
* Gets the format of the specified track. * Gets the format of the specified track.
...@@ -82,28 +103,49 @@ public abstract class HlsExtractor { ...@@ -82,28 +103,49 @@ public abstract class HlsExtractor {
* @param track The track index. * @param track The track index.
* @return The corresponding format. * @return The corresponding format.
*/ */
public abstract MediaFormat getFormat(int track); public MediaFormat getFormat(int track) {
return sampleQueues.valueAt(track).getFormat();
}
/** /**
* Whether the extractor is prepared. * Whether the extractor is prepared.
* *
* @return True if the extractor is prepared. False otherwise. * @return True if the extractor is prepared. False otherwise.
*/ */
public abstract boolean isPrepared(); public boolean isPrepared() {
if (!prepared && tracksBuilt) {
for (int i = 0; i < sampleQueues.size(); i++) {
if (!sampleQueues.valueAt(i).hasFormat()) {
return false;
}
}
prepared = true;
}
return prepared;
}
/** /**
* Releases the extractor, recycling any pending or incomplete samples to the sample pool. * Clears queues for all tracks, returning all allocations to the buffer pool.
* <p>
* This method should not be called whilst {@link #read(DataSource)} is also being invoked.
*/ */
public abstract void release(); public void clear() {
for (int i = 0; i < sampleQueues.size(); i++) {
sampleQueues.valueAt(i).clear();
}
}
/** /**
* Gets the largest timestamp of any sample parsed by the extractor. * Gets the largest timestamp of any sample parsed by the extractor.
* *
* @return The largest timestamp, or {@link Long#MIN_VALUE} if no samples have been parsed. * @return The largest timestamp, or {@link Long#MIN_VALUE} if no samples have been parsed.
*/ */
public abstract long getLargestSampleTimestamp(); public long getLargestParsedTimestampUs() {
long largestParsedTimestampUs = Long.MIN_VALUE;
for (int i = 0; i < sampleQueues.size(); i++) {
largestParsedTimestampUs = Math.max(largestParsedTimestampUs,
sampleQueues.valueAt(i).getLargestParsedTimestampUs());
}
return largestParsedTimestampUs;
}
/** /**
* Gets the next sample for the specified track. * Gets the next sample for the specified track.
...@@ -112,7 +154,10 @@ public abstract class HlsExtractor { ...@@ -112,7 +154,10 @@ public abstract class HlsExtractor {
* @param holder A {@link SampleHolder} into which the sample should be read. * @param holder A {@link SampleHolder} into which the sample should be read.
* @return True if a sample was read. False otherwise. * @return True if a sample was read. False otherwise.
*/ */
public abstract boolean getSample(int track, SampleHolder holder); public boolean getSample(int track, SampleHolder holder) {
Assertions.checkState(isPrepared());
return sampleQueues.valueAt(track).getSample(holder);
}
/** /**
* Discards samples for the specified track up to the specified time. * Discards samples for the specified track up to the specified time.
...@@ -120,7 +165,10 @@ public abstract class HlsExtractor { ...@@ -120,7 +165,10 @@ public abstract class HlsExtractor {
* @param track The track from which samples should be discarded. * @param track The track from which samples should be discarded.
* @param timeUs The time up to which samples should be discarded, in microseconds. * @param timeUs The time up to which samples should be discarded, in microseconds.
*/ */
public abstract void discardUntil(int track, long timeUs); public void discardUntil(int track, long timeUs) {
Assertions.checkState(isPrepared());
sampleQueues.valueAt(track).discardUntil(timeUs);
}
/** /**
* Whether samples are available for reading from {@link #getSample(int, SampleHolder)} for the * Whether samples are available for reading from {@link #getSample(int, SampleHolder)} for the
...@@ -129,23 +177,36 @@ public abstract class HlsExtractor { ...@@ -129,23 +177,36 @@ public abstract class HlsExtractor {
* @return True if samples are available for reading from {@link #getSample(int, SampleHolder)} * @return True if samples are available for reading from {@link #getSample(int, SampleHolder)}
* for the specified track. False otherwise. * for the specified track. False otherwise.
*/ */
public abstract boolean hasSamples(int track); public boolean hasSamples(int track) {
Assertions.checkState(isPrepared());
return !sampleQueues.valueAt(track).isEmpty();
}
/** /**
* Reads up to a single TS packet. * Reads from the provided {@link ExtractorInput}.
* *
* @param dataSource The {@link DataSource} from which to read. * @param input The {@link ExtractorInput} from which to read.
* @return One of {@link Extractor#RESULT_CONTINUE} and {@link Extractor#RESULT_END_OF_INPUT}.
* @throws IOException If an error occurred reading from the source. * @throws IOException If an error occurred reading from the source.
* @return The number of bytes read from the source. * @throws InterruptedException If the thread was interrupted.
*/ */
public abstract int read(DataSource dataSource) throws IOException; public int read(ExtractorInput input) throws IOException, InterruptedException {
int result = extractor.read(input);
return result;
}
/** // ExtractorOutput implementation.
* Gets the {@link SampleQueue} for the specified track.
* @Override
* @param track The track index. public TrackOutput track(int id) {
* @return The corresponding sample queue. DefaultTrackOutput sampleQueue = new DefaultTrackOutput(bufferPool);
*/ sampleQueues.put(id, sampleQueue);
protected abstract SampleQueue getSampleQueue(int track); return sampleQueue;
}
@Override
public void endTracks() {
this.tracksBuilt = true;
}
} }
...@@ -15,8 +15,6 @@ ...@@ -15,8 +15,6 @@
*/ */
package com.google.android.exoplayer.hls; package com.google.android.exoplayer.hls;
import android.net.Uri;
import java.util.List; import java.util.List;
/** /**
...@@ -25,10 +23,12 @@ import java.util.List; ...@@ -25,10 +23,12 @@ import java.util.List;
public final class HlsMasterPlaylist extends HlsPlaylist { public final class HlsMasterPlaylist extends HlsPlaylist {
public final List<Variant> variants; public final List<Variant> variants;
public final List<Subtitle> subtitles;
public HlsMasterPlaylist(Uri baseUri, List<Variant> variants) { public HlsMasterPlaylist(String baseUri, List<Variant> variants, List<Subtitle> subtitles) {
super(baseUri, HlsPlaylist.TYPE_MASTER); super(baseUri, HlsPlaylist.TYPE_MASTER);
this.variants = variants; this.variants = variants;
this.subtitles = subtitles;
} }
} }
...@@ -17,8 +17,6 @@ package com.google.android.exoplayer.hls; ...@@ -17,8 +17,6 @@ package com.google.android.exoplayer.hls;
import com.google.android.exoplayer.C; import com.google.android.exoplayer.C;
import android.net.Uri;
import java.util.List; import java.util.List;
/** /**
...@@ -30,24 +28,25 @@ public final class HlsMediaPlaylist extends HlsPlaylist { ...@@ -30,24 +28,25 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
* Media segment reference. * Media segment reference.
*/ */
public static final class Segment implements Comparable<Long> { public static final class Segment implements Comparable<Long> {
public final boolean discontinuity; public final boolean discontinuity;
public final double durationSecs; public final double durationSecs;
public final String url; public final String url;
public final long startTimeUs; public final long startTimeUs;
public final String encryptionMethod; public final boolean isEncrypted;
public final String encryptionKeyUri; public final String encryptionKeyUri;
public final String encryptionIV; public final String encryptionIV;
public final int byterangeOffset; public final int byterangeOffset;
public final int byterangeLength; public final int byterangeLength;
public Segment(String uri, double durationSecs, boolean discontinuity, long startTimeUs, public Segment(String uri, double durationSecs, boolean discontinuity, long startTimeUs,
String encryptionMethod, String encryptionKeyUri, String encryptionIV, boolean isEncrypted, String encryptionKeyUri, String encryptionIV, int byterangeOffset,
int byterangeOffset, int byterangeLength) { int byterangeLength) {
this.url = uri; this.url = uri;
this.durationSecs = durationSecs; this.durationSecs = durationSecs;
this.discontinuity = discontinuity; this.discontinuity = discontinuity;
this.startTimeUs = startTimeUs; this.startTimeUs = startTimeUs;
this.encryptionMethod = encryptionMethod; this.isEncrypted = isEncrypted;
this.encryptionKeyUri = encryptionKeyUri; this.encryptionKeyUri = encryptionKeyUri;
this.encryptionIV = encryptionIV; this.encryptionIV = encryptionIV;
this.byterangeOffset = byterangeOffset; this.byterangeOffset = byterangeOffset;
...@@ -70,7 +69,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist { ...@@ -70,7 +69,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
public final boolean live; public final boolean live;
public final long durationUs; public final long durationUs;
public HlsMediaPlaylist(Uri baseUri, int mediaSequence, int targetDurationSecs, int version, public HlsMediaPlaylist(String baseUri, int mediaSequence, int targetDurationSecs, int version,
boolean live, List<Segment> segments) { boolean live, List<Segment> segments) {
super(baseUri, HlsPlaylist.TYPE_MEDIA); super(baseUri, HlsPlaylist.TYPE_MEDIA);
this.mediaSequence = mediaSequence; this.mediaSequence = mediaSequence;
......
...@@ -23,7 +23,10 @@ import java.util.regex.Pattern; ...@@ -23,7 +23,10 @@ import java.util.regex.Pattern;
/** /**
* Utility methods for HLS manifest parsing. * Utility methods for HLS manifest parsing.
*/ */
/* package */ class HlsParserUtil { /* package */ final class HlsParserUtil {
private static final String BOOLEAN_YES = "YES";
private static final String BOOLEAN_NO = "NO";
private HlsParserUtil() {} private HlsParserUtil() {}
...@@ -36,6 +39,16 @@ import java.util.regex.Pattern; ...@@ -36,6 +39,16 @@ import java.util.regex.Pattern;
throw new ParserException(String.format("Couldn't match %s tag in %s", tag, line)); throw new ParserException(String.format("Couldn't match %s tag in %s", tag, line));
} }
public static int parseIntAttr(String line, Pattern pattern, String tag)
throws ParserException {
return Integer.parseInt(parseStringAttr(line, pattern, tag));
}
public static double parseDoubleAttr(String line, Pattern pattern, String tag)
throws ParserException {
return Double.parseDouble(parseStringAttr(line, pattern, tag));
}
public static String parseOptionalStringAttr(String line, Pattern pattern) { public static String parseOptionalStringAttr(String line, Pattern pattern) {
Matcher matcher = pattern.matcher(line); Matcher matcher = pattern.matcher(line);
if (matcher.find() && matcher.groupCount() == 1) { if (matcher.find() && matcher.groupCount() == 1) {
...@@ -44,14 +57,16 @@ import java.util.regex.Pattern; ...@@ -44,14 +57,16 @@ import java.util.regex.Pattern;
return null; return null;
} }
public static int parseIntAttr(String line, Pattern pattern, String tag) public static boolean parseOptionalBooleanAttr(String line, Pattern pattern) {
throws ParserException { Matcher matcher = pattern.matcher(line);
return Integer.parseInt(parseStringAttr(line, pattern, tag)); if (matcher.find() && matcher.groupCount() == 1) {
return BOOLEAN_YES.equals(matcher.group(1));
}
return false;
} }
public static double parseDoubleAttr(String line, Pattern pattern, String tag) public static Pattern compileBooleanAttrPattern(String attrName) {
throws ParserException { return Pattern.compile(attrName + "=(" + BOOLEAN_YES + "|" + BOOLEAN_NO + ")");
return Double.parseDouble(parseStringAttr(line, pattern, tag));
} }
} }
...@@ -15,7 +15,6 @@ ...@@ -15,7 +15,6 @@
*/ */
package com.google.android.exoplayer.hls; package com.google.android.exoplayer.hls;
import android.net.Uri;
/** /**
* Represents an HLS playlist. * Represents an HLS playlist.
...@@ -25,10 +24,10 @@ public abstract class HlsPlaylist { ...@@ -25,10 +24,10 @@ public abstract class HlsPlaylist {
public final static int TYPE_MASTER = 0; public final static int TYPE_MASTER = 0;
public final static int TYPE_MEDIA = 1; public final static int TYPE_MEDIA = 1;
public final Uri baseUri; public final String baseUri;
public final int type; public final int type;
protected HlsPlaylist(Uri baseUri, int type) { protected HlsPlaylist(String baseUri, int type) {
this.baseUri = baseUri; this.baseUri = baseUri;
this.type = type; this.type = type;
} }
......
...@@ -15,13 +15,13 @@ ...@@ -15,13 +15,13 @@
*/ */
package com.google.android.exoplayer.hls; package com.google.android.exoplayer.hls;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.MediaFormatHolder; import com.google.android.exoplayer.MediaFormatHolder;
import com.google.android.exoplayer.SampleHolder; import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.SampleSource; import com.google.android.exoplayer.SampleSource;
import com.google.android.exoplayer.TrackInfo; import com.google.android.exoplayer.TrackInfo;
import com.google.android.exoplayer.TrackRenderer; import com.google.android.exoplayer.TrackRenderer;
import com.google.android.exoplayer.hls.parser.HlsExtractor;
import com.google.android.exoplayer.upstream.Loader; import com.google.android.exoplayer.upstream.Loader;
import com.google.android.exoplayer.upstream.Loader.Loadable; import com.google.android.exoplayer.upstream.Loader.Loadable;
import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.Assertions;
...@@ -44,7 +44,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { ...@@ -44,7 +44,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
private static final int NO_RESET_PENDING = -1; private static final int NO_RESET_PENDING = -1;
private final HlsChunkSource chunkSource; private final HlsChunkSource chunkSource;
private final LinkedList<HlsExtractor> extractors; private final LinkedList<HlsExtractorWrapper> extractors;
private final boolean frameAccurateSeeking; private final boolean frameAccurateSeeking;
private final int minLoadableRetryCount; private final int minLoadableRetryCount;
...@@ -83,7 +83,8 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { ...@@ -83,7 +83,8 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
this.frameAccurateSeeking = frameAccurateSeeking; this.frameAccurateSeeking = frameAccurateSeeking;
this.remainingReleaseCount = downstreamRendererCount; this.remainingReleaseCount = downstreamRendererCount;
this.minLoadableRetryCount = minLoadableRetryCount; this.minLoadableRetryCount = minLoadableRetryCount;
extractors = new LinkedList<HlsExtractor>(); this.pendingResetPositionUs = NO_RESET_PENDING;
extractors = new LinkedList<HlsExtractorWrapper>();
} }
@Override @Override
...@@ -96,7 +97,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { ...@@ -96,7 +97,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
} }
continueBufferingInternal(); continueBufferingInternal();
if (!extractors.isEmpty()) { if (!extractors.isEmpty()) {
HlsExtractor extractor = extractors.getFirst(); HlsExtractorWrapper extractor = extractors.getFirst();
if (extractor.isPrepared()) { if (extractor.isPrepared()) {
trackCount = extractor.getTrackCount(); trackCount = extractor.getTrackCount();
trackEnabledStates = new boolean[trackCount]; trackEnabledStates = new boolean[trackCount];
...@@ -190,12 +191,16 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { ...@@ -190,12 +191,16 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
return DISCONTINUITY_READ; return DISCONTINUITY_READ;
} }
if (onlyReadDiscontinuity || isPendingReset() || extractors.isEmpty()) { if (onlyReadDiscontinuity) {
return NOTHING_READ;
}
if (isPendingReset()) {
maybeThrowLoadableException(); maybeThrowLoadableException();
return NOTHING_READ; return NOTHING_READ;
} }
HlsExtractor extractor = getCurrentExtractor(); HlsExtractorWrapper extractor = getCurrentExtractor();
if (extractors.size() > 1) { if (extractors.size() > 1) {
// If there's more than one extractor, attempt to configure a seamless splice from the // If there's more than one extractor, attempt to configure a seamless splice from the
// current one to the next one. // current one to the next one.
...@@ -223,7 +228,8 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { ...@@ -223,7 +228,8 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
} }
if (extractor.getSample(track, sampleHolder)) { if (extractor.getSample(track, sampleHolder)) {
sampleHolder.decodeOnly = frameAccurateSeeking && sampleHolder.timeUs < lastSeekPositionUs; boolean decodeOnly = frameAccurateSeeking && sampleHolder.timeUs < lastSeekPositionUs;
sampleHolder.flags |= decodeOnly ? C.SAMPLE_FLAG_DECODE_ONLY : 0;
return SAMPLE_READ; return SAMPLE_READ;
} }
...@@ -240,10 +246,11 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { ...@@ -240,10 +246,11 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
Assertions.checkState(prepared); Assertions.checkState(prepared);
Assertions.checkState(enabledTrackCount > 0); Assertions.checkState(enabledTrackCount > 0);
lastSeekPositionUs = positionUs; lastSeekPositionUs = positionUs;
if (pendingResetPositionUs == positionUs || downstreamPositionUs == positionUs) { if ((isPendingReset() ? pendingResetPositionUs : downstreamPositionUs) == positionUs) {
downstreamPositionUs = positionUs;
return; return;
} }
// TODO: Optimize the seek for the case where the position is already buffered.
downstreamPositionUs = positionUs; downstreamPositionUs = positionUs;
for (int i = 0; i < pendingDiscontinuities.length; i++) { for (int i = 0; i < pendingDiscontinuities.length; i++) {
pendingDiscontinuities[i] = true; pendingDiscontinuities[i] = true;
...@@ -260,9 +267,9 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { ...@@ -260,9 +267,9 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
} else if (loadingFinished) { } else if (loadingFinished) {
return TrackRenderer.END_OF_TRACK_US; return TrackRenderer.END_OF_TRACK_US;
} else { } else {
long largestSampleTimestamp = extractors.getLast().getLargestSampleTimestamp(); long largestParsedTimestampUs = extractors.getLast().getLargestParsedTimestampUs();
return largestSampleTimestamp == Long.MIN_VALUE ? downstreamPositionUs return largestParsedTimestampUs == Long.MIN_VALUE ? downstreamPositionUs
: largestSampleTimestamp; : largestParsedTimestampUs;
} }
} }
...@@ -328,17 +335,17 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { ...@@ -328,17 +335,17 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
* *
* @return The current extractor from which samples should be read. Guaranteed to be non-null. * @return The current extractor from which samples should be read. Guaranteed to be non-null.
*/ */
private HlsExtractor getCurrentExtractor() { private HlsExtractorWrapper getCurrentExtractor() {
HlsExtractor extractor = extractors.getFirst(); HlsExtractorWrapper extractor = extractors.getFirst();
while (extractors.size() > 1 && !haveSamplesForEnabledTracks(extractor)) { while (extractors.size() > 1 && !haveSamplesForEnabledTracks(extractor)) {
// We're finished reading from the extractor for all tracks, and so can discard it. // We're finished reading from the extractor for all tracks, and so can discard it.
extractors.removeFirst().release(); extractors.removeFirst().clear();
extractor = extractors.getFirst(); extractor = extractors.getFirst();
} }
return extractor; return extractor;
} }
private void discardSamplesForDisabledTracks(HlsExtractor extractor, long timeUs) { private void discardSamplesForDisabledTracks(HlsExtractorWrapper extractor, long timeUs) {
if (!extractor.isPrepared()) { if (!extractor.isPrepared()) {
return; return;
} }
...@@ -349,7 +356,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { ...@@ -349,7 +356,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
} }
} }
private boolean haveSamplesForEnabledTracks(HlsExtractor extractor) { private boolean haveSamplesForEnabledTracks(HlsExtractorWrapper extractor) {
if (!extractor.isPrepared()) { if (!extractor.isPrepared()) {
return false; return false;
} }
...@@ -381,7 +388,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { ...@@ -381,7 +388,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
private void clearState() { private void clearState() {
for (int i = 0; i < extractors.size(); i++) { for (int i = 0; i < extractors.size(); i++) {
extractors.get(i).release(); extractors.get(i).clear();
} }
extractors.clear(); extractors.clear();
clearCurrentLoadable(); clearCurrentLoadable();
......
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.hls;
/**
* Subtitle media tag.
*/
public final class Subtitle {
public final String name;
public final String uri;
public final String language;
public final boolean isDefault;
public final boolean autoSelect;
public Subtitle(String name, String uri, String language, boolean isDefault, boolean autoSelect) {
this.name = name;
this.uri = uri;
this.language = language;
this.autoSelect = autoSelect;
this.isDefault = isDefault;
}
}
...@@ -15,7 +15,9 @@ ...@@ -15,7 +15,9 @@
*/ */
package com.google.android.exoplayer.hls; package com.google.android.exoplayer.hls;
import com.google.android.exoplayer.hls.parser.HlsExtractor; import com.google.android.exoplayer.extractor.DefaultExtractorInput;
import com.google.android.exoplayer.extractor.Extractor;
import com.google.android.exoplayer.extractor.ExtractorInput;
import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DataSpec; import com.google.android.exoplayer.upstream.DataSpec;
...@@ -26,8 +28,6 @@ import java.io.IOException; ...@@ -26,8 +28,6 @@ import java.io.IOException;
*/ */
public final class TsChunk extends HlsChunk { public final class TsChunk extends HlsChunk {
private static final byte[] SCRATCH_SPACE = new byte[4096];
/** /**
* The index of the variant in the master playlist. * The index of the variant in the master playlist.
*/ */
...@@ -51,7 +51,7 @@ public final class TsChunk extends HlsChunk { ...@@ -51,7 +51,7 @@ public final class TsChunk extends HlsChunk {
/** /**
* The extractor into which this chunk is being consumed. * The extractor into which this chunk is being consumed.
*/ */
public final HlsExtractor extractor; public final HlsExtractorWrapper extractor;
private int loadPosition; private int loadPosition;
private volatile boolean loadFinished; private volatile boolean loadFinished;
...@@ -67,7 +67,7 @@ public final class TsChunk extends HlsChunk { ...@@ -67,7 +67,7 @@ public final class TsChunk extends HlsChunk {
* @param chunkIndex The index of the chunk. * @param chunkIndex The index of the chunk.
* @param isLastChunk True if this is the last chunk in the media. False otherwise. * @param isLastChunk True if this is the last chunk in the media. False otherwise.
*/ */
public TsChunk(DataSource dataSource, DataSpec dataSpec, HlsExtractor extractor, public TsChunk(DataSource dataSource, DataSpec dataSpec, HlsExtractorWrapper extractor,
int variantIndex, long startTimeUs, long endTimeUs, int chunkIndex, boolean isLastChunk) { int variantIndex, long startTimeUs, long endTimeUs, int chunkIndex, boolean isLastChunk) {
super(dataSource, dataSpec); super(dataSource, dataSpec);
this.extractor = extractor; this.extractor = extractor;
...@@ -102,30 +102,23 @@ public final class TsChunk extends HlsChunk { ...@@ -102,30 +102,23 @@ public final class TsChunk extends HlsChunk {
@Override @Override
public void load() throws IOException, InterruptedException { public void load() throws IOException, InterruptedException {
ExtractorInput input;
try { try {
dataSource.open(dataSpec); input = new DefaultExtractorInput(dataSource, 0, dataSource.open(dataSpec));
int bytesRead = 0;
int bytesSkipped = 0;
// If we previously fed part of this chunk to the extractor, skip it this time. // If we previously fed part of this chunk to the extractor, skip it this time.
// TODO: Ideally we'd construct a dataSpec that only loads the remainder of the data here, // TODO: Ideally we'd construct a dataSpec that only loads the remainder of the data here,
// rather than loading the whole chunk again and then skipping data we previously loaded. To // rather than loading the whole chunk again and then skipping data we previously loaded. To
// do this is straightforward for non-encrypted content, but more complicated for content // do this is straightforward for non-encrypted content, but more complicated for content
// encrypted with AES, for which we'll need to modify the way that decryption is performed. // encrypted with AES, for which we'll need to modify the way that decryption is performed.
while (bytesRead != -1 && !loadCanceled && bytesSkipped < loadPosition) { input.skipFully(loadPosition);
int skipLength = Math.min(loadPosition - bytesSkipped, SCRATCH_SPACE.length); try {
bytesRead = dataSource.read(SCRATCH_SPACE, 0, skipLength); int result = Extractor.RESULT_CONTINUE;
if (bytesRead != -1) { while (result == Extractor.RESULT_CONTINUE && !loadCanceled) {
bytesSkipped += bytesRead; result = extractor.read(input);
}
}
// Feed the remaining data into the extractor.
while (bytesRead != -1 && !loadCanceled) {
bytesRead = extractor.read(dataSource);
if (bytesRead != -1) {
loadPosition += bytesRead;
} }
} finally {
loadPosition = (int) input.getPosition();
} }
loadFinished = !loadCanceled;
} finally { } finally {
dataSource.close(); dataSource.close();
} }
......
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.metadata;
/**
* A metadata that contains parsed ID3 GEOB (General Encapsulated Object) frame data associated
* with time indices.
*/
public class GeobMetadata {
public static final String TYPE = "GEOB";
public final String mimeType;
public final String filename;
public final String description;
public final byte[] data;
public GeobMetadata(String mimeType, String filename, String description, byte[] data) {
this.mimeType = mimeType;
this.filename = filename;
this.description = description;
this.data = data;
}
}
...@@ -29,6 +29,11 @@ import java.util.Map; ...@@ -29,6 +29,11 @@ import java.util.Map;
*/ */
public class Id3Parser implements MetadataParser<Map<String, Object>> { public class Id3Parser implements MetadataParser<Map<String, Object>> {
private static final int ID3_TEXT_ENCODING_ISO_8859_1 = 0;
private static final int ID3_TEXT_ENCODING_UTF_16 = 1;
private static final int ID3_TEXT_ENCODING_UTF_16BE = 2;
private static final int ID3_TEXT_ENCODING_UTF_8 = 3;
@Override @Override
public boolean canParse(String mimeType) { public boolean canParse(String mimeType) {
return mimeType.equals(MimeTypes.APPLICATION_ID3); return mimeType.equals(MimeTypes.APPLICATION_ID3);
...@@ -60,13 +65,48 @@ public class Id3Parser implements MetadataParser<Map<String, Object>> { ...@@ -60,13 +65,48 @@ public class Id3Parser implements MetadataParser<Map<String, Object>> {
byte[] frame = new byte[frameSize - 1]; byte[] frame = new byte[frameSize - 1];
id3Data.readBytes(frame, 0, frameSize - 1); id3Data.readBytes(frame, 0, frameSize - 1);
int firstZeroIndex = indexOf(frame, 0, (byte) 0); int firstZeroIndex = indexOfEOS(frame, 0, encoding);
String description = new String(frame, 0, firstZeroIndex, charset); String description = new String(frame, 0, firstZeroIndex, charset);
int valueStartIndex = indexOfNot(frame, firstZeroIndex, (byte) 0); int valueStartIndex = firstZeroIndex + delimiterLength(encoding);
int valueEndIndex = indexOf(frame, valueStartIndex, (byte) 0); int valueEndIndex = indexOfEOS(frame, valueStartIndex, encoding);
String value = new String(frame, valueStartIndex, valueEndIndex - valueStartIndex, String value = new String(frame, valueStartIndex, valueEndIndex - valueStartIndex,
charset); charset);
metadata.put(TxxxMetadata.TYPE, new TxxxMetadata(description, value)); metadata.put(TxxxMetadata.TYPE, new TxxxMetadata(description, value));
} else if (frameId0 == 'P' && frameId1 == 'R' && frameId2 == 'I' && frameId3 == 'V') {
// Check frame ID == PRIV
byte[] frame = new byte[frameSize];
id3Data.readBytes(frame, 0, frameSize);
int firstZeroIndex = indexOf(frame, 0, (byte) 0);
String owner = new String(frame, 0, firstZeroIndex, "ISO-8859-1");
byte[] privateData = new byte[frameSize - firstZeroIndex - 1];
System.arraycopy(frame, firstZeroIndex + 1, privateData, 0, frameSize - firstZeroIndex - 1);
metadata.put(PrivMetadata.TYPE, new PrivMetadata(owner, privateData));
} else if (frameId0 == 'G' && frameId1 == 'E' && frameId2 == 'O' && frameId3 == 'B') {
// Check frame ID == GEOB
int encoding = id3Data.readUnsignedByte();
String charset = getCharsetName(encoding);
byte[] frame = new byte[frameSize - 1];
id3Data.readBytes(frame, 0, frameSize - 1);
int firstZeroIndex = indexOf(frame, 0, (byte) 0);
String mimeType = new String(frame, 0, firstZeroIndex, "ISO-8859-1");
int filenameStartIndex = firstZeroIndex + 1;
int filenameEndIndex = indexOfEOS(frame, filenameStartIndex, encoding);
String filename = new String(frame, filenameStartIndex,
filenameEndIndex - filenameStartIndex, charset);
int descriptionStartIndex = filenameEndIndex + delimiterLength(encoding);
int descriptionEndIndex = indexOfEOS(frame, descriptionStartIndex, encoding);
String description = new String(frame, descriptionStartIndex,
descriptionEndIndex - descriptionStartIndex, charset);
int objectDataSize = frameSize - 1 /* encoding byte */ - descriptionEndIndex
- delimiterLength(encoding);
byte[] objectData = new byte[objectDataSize];
System.arraycopy(frame, descriptionEndIndex + delimiterLength(encoding), objectData, 0,
objectDataSize);
metadata.put(GeobMetadata.TYPE, new GeobMetadata(mimeType, filename,
description, objectData));
} else { } else {
String type = String.format("%c%c%c%c", frameId0, frameId1, frameId2, frameId3); String type = String.format("%c%c%c%c", frameId0, frameId1, frameId2, frameId3);
byte[] frame = new byte[frameSize]; byte[] frame = new byte[frameSize];
...@@ -89,15 +129,30 @@ public class Id3Parser implements MetadataParser<Map<String, Object>> { ...@@ -89,15 +129,30 @@ public class Id3Parser implements MetadataParser<Map<String, Object>> {
return data.length; return data.length;
} }
private static int indexOfNot(byte[] data, int fromIndex, byte key) { private static int indexOfEOS(byte[] data, int fromIndex, int encodingByte) {
for (int i = fromIndex; i < data.length; i++) { int terminationPos = indexOf(data, fromIndex, (byte) 0);
if (data[i] != key) {
return i; // For single byte encoding charsets, we are done
if (encodingByte == ID3_TEXT_ENCODING_ISO_8859_1 || encodingByte == ID3_TEXT_ENCODING_UTF_8) {
return terminationPos;
} }
// Otherwise, look for a two zero bytes
while (terminationPos < data.length - 1) {
if (data[terminationPos + 1] == (byte) 0) {
return terminationPos;
}
terminationPos = indexOf(data, terminationPos + 1, (byte) 0);
} }
return data.length; return data.length;
} }
private static int delimiterLength(int encodingByte) {
return (encodingByte == ID3_TEXT_ENCODING_ISO_8859_1
|| encodingByte == ID3_TEXT_ENCODING_UTF_8) ? 1 : 2;
}
/** /**
* Parses an ID3 header. * Parses an ID3 header.
* *
...@@ -142,13 +197,13 @@ public class Id3Parser implements MetadataParser<Map<String, Object>> { ...@@ -142,13 +197,13 @@ public class Id3Parser implements MetadataParser<Map<String, Object>> {
*/ */
private static String getCharsetName(int encodingByte) { private static String getCharsetName(int encodingByte) {
switch (encodingByte) { switch (encodingByte) {
case 0: case ID3_TEXT_ENCODING_ISO_8859_1:
return "ISO-8859-1"; return "ISO-8859-1";
case 1: case ID3_TEXT_ENCODING_UTF_16:
return "UTF-16"; return "UTF-16";
case 2: case ID3_TEXT_ENCODING_UTF_16BE:
return "UTF-16BE"; return "UTF-16BE";
case 3: case ID3_TEXT_ENCODING_UTF_8:
return "UTF-8"; return "UTF-8";
default: default:
return "ISO-8859-1"; return "ISO-8859-1";
......
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.metadata;
/**
* A metadata that contains parsed ID3 PRIV (Private) frame data associated
* with time indices.
*/
public class PrivMetadata {
public static final String TYPE = "PRIV";
public final String owner;
public final byte[] privateData;
public PrivMetadata(String owner, byte[] privateData) {
this.owner = owner;
this.privateData = privateData;
}
}
...@@ -24,6 +24,19 @@ import java.util.List; ...@@ -24,6 +24,19 @@ import java.util.List;
public abstract class Atom { public abstract class Atom {
/** Size of an atom header, in bytes. */
public static final int ATOM_HEADER_SIZE = 8;
/** Size of a long atom header, in bytes. */
public static final int LONG_ATOM_HEADER_SIZE = 16;
/** Size of a full atom header, in bytes. */
public static final int FULL_ATOM_HEADER_SIZE = 12;
/** Value for the first 32 bits of atomSize when the atom size is actually a long value. */
public static final int LONG_SIZE_PREFIX = 1;
public static final int TYPE_ftyp = getAtomTypeInteger("ftyp");
public static final int TYPE_avc1 = getAtomTypeInteger("avc1"); public static final int TYPE_avc1 = getAtomTypeInteger("avc1");
public static final int TYPE_avc3 = getAtomTypeInteger("avc3"); public static final int TYPE_avc3 = getAtomTypeInteger("avc3");
public static final int TYPE_esds = getAtomTypeInteger("esds"); public static final int TYPE_esds = getAtomTypeInteger("esds");
...@@ -153,6 +166,20 @@ public abstract class Atom { ...@@ -153,6 +166,20 @@ public abstract class Atom {
} }
/**
* Parses the version number out of the additional integer component of a full atom.
*/
public static int parseFullAtomVersion(int fullAtomInt) {
return 0x000000FF & (fullAtomInt >> 24);
}
/**
* Parses the atom flags out of the additional integer component of a full atom.
*/
public static int parseFullAtomFlags(int fullAtomInt) {
return 0x00FFFFFF & fullAtomInt;
}
private static String getAtomTypeString(int type) { private static String getAtomTypeString(int type) {
return "" + (char) (type >> 24) return "" + (char) (type >> 24)
+ (char) ((type >> 16) & 0xFF) + (char) ((type >> 16) & 0xFF)
......
...@@ -22,6 +22,9 @@ import com.google.android.exoplayer.util.Util; ...@@ -22,6 +22,9 @@ import com.google.android.exoplayer.util.Util;
/** Sample table for a track in an MP4 file. */ /** Sample table for a track in an MP4 file. */
public final class Mp4TrackSampleTable { public final class Mp4TrackSampleTable {
/** Sample index when no sample is available. */
public static final int NO_SAMPLE = -1;
/** Sample offsets in bytes. */ /** Sample offsets in bytes. */
public final long[] offsets; public final long[] offsets;
/** Sample sizes in bytes. */ /** Sample sizes in bytes. */
...@@ -53,7 +56,7 @@ public final class Mp4TrackSampleTable { ...@@ -53,7 +56,7 @@ public final class Mp4TrackSampleTable {
* timestamp, if one is available. * timestamp, if one is available.
* *
* @param timeUs Timestamp adjacent to which to find a synchronization sample. * @param timeUs Timestamp adjacent to which to find a synchronization sample.
* @return Index of the synchronization sample, or {@link Mp4Util#NO_SAMPLE} if none. * @return Index of the synchronization sample, or {@link #NO_SAMPLE} if none.
*/ */
public int getIndexOfEarlierOrEqualSynchronizationSample(long timeUs) { public int getIndexOfEarlierOrEqualSynchronizationSample(long timeUs) {
int startIndex = Util.binarySearchFloor(timestampsUs, timeUs, true, false); int startIndex = Util.binarySearchFloor(timestampsUs, timeUs, true, false);
...@@ -63,7 +66,7 @@ public final class Mp4TrackSampleTable { ...@@ -63,7 +66,7 @@ public final class Mp4TrackSampleTable {
} }
} }
return Mp4Util.NO_SAMPLE; return NO_SAMPLE;
} }
/** /**
...@@ -71,7 +74,7 @@ public final class Mp4TrackSampleTable { ...@@ -71,7 +74,7 @@ public final class Mp4TrackSampleTable {
* if one is available. * if one is available.
* *
* @param timeUs Timestamp adjacent to which to find a synchronization sample. * @param timeUs Timestamp adjacent to which to find a synchronization sample.
* @return index Index of the synchronization sample, or {@link Mp4Util#NO_SAMPLE} if none. * @return index Index of the synchronization sample, or {@link #NO_SAMPLE} if none.
*/ */
public int getIndexOfLaterOrEqualSynchronizationSample(long timeUs) { public int getIndexOfLaterOrEqualSynchronizationSample(long timeUs) {
int startIndex = Util.binarySearchCeil(timestampsUs, timeUs, true, false); int startIndex = Util.binarySearchCeil(timestampsUs, timeUs, true, false);
...@@ -81,7 +84,7 @@ public final class Mp4TrackSampleTable { ...@@ -81,7 +84,7 @@ public final class Mp4TrackSampleTable {
} }
} }
return Mp4Util.NO_SAMPLE; return NO_SAMPLE;
} }
} }
...@@ -30,6 +30,7 @@ import com.google.android.exoplayer.chunk.MediaChunk; ...@@ -30,6 +30,7 @@ import com.google.android.exoplayer.chunk.MediaChunk;
import com.google.android.exoplayer.chunk.parser.Extractor; import com.google.android.exoplayer.chunk.parser.Extractor;
import com.google.android.exoplayer.chunk.parser.mp4.FragmentedMp4Extractor; import com.google.android.exoplayer.chunk.parser.mp4.FragmentedMp4Extractor;
import com.google.android.exoplayer.chunk.parser.mp4.TrackEncryptionBox; import com.google.android.exoplayer.chunk.parser.mp4.TrackEncryptionBox;
import com.google.android.exoplayer.drm.DrmInitData;
import com.google.android.exoplayer.mp4.Track; import com.google.android.exoplayer.mp4.Track;
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.ProtectionElement; import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.ProtectionElement;
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.StreamElement; import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.StreamElement;
...@@ -38,6 +39,7 @@ import com.google.android.exoplayer.upstream.DataSource; ...@@ -38,6 +39,7 @@ import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DataSpec; import com.google.android.exoplayer.upstream.DataSpec;
import com.google.android.exoplayer.util.CodecSpecificDataUtil; import com.google.android.exoplayer.util.CodecSpecificDataUtil;
import com.google.android.exoplayer.util.ManifestFetcher; import com.google.android.exoplayer.util.ManifestFetcher;
import com.google.android.exoplayer.util.MimeTypes;
import android.net.Uri; import android.net.Uri;
import android.os.SystemClock; import android.os.SystemClock;
...@@ -48,8 +50,6 @@ import java.io.IOException; ...@@ -48,8 +50,6 @@ import java.io.IOException;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.UUID;
/** /**
* An {@link ChunkSource} for SmoothStreaming. * An {@link ChunkSource} for SmoothStreaming.
...@@ -71,7 +71,7 @@ public class SmoothStreamingChunkSource implements ChunkSource { ...@@ -71,7 +71,7 @@ public class SmoothStreamingChunkSource implements ChunkSource {
private final int maxHeight; private final int maxHeight;
private final SparseArray<FragmentedMp4Extractor> extractors; private final SparseArray<FragmentedMp4Extractor> extractors;
private final Map<UUID, byte[]> psshInfo; private final DrmInitData drmInitData;
private final SmoothStreamingFormat[] formats; private final SmoothStreamingFormat[] formats;
private SmoothStreamingManifest currentManifest; private SmoothStreamingManifest currentManifest;
...@@ -143,9 +143,11 @@ public class SmoothStreamingChunkSource implements ChunkSource { ...@@ -143,9 +143,11 @@ public class SmoothStreamingChunkSource implements ChunkSource {
byte[] keyId = getKeyId(protectionElement.data); byte[] keyId = getKeyId(protectionElement.data);
trackEncryptionBoxes = new TrackEncryptionBox[1]; trackEncryptionBoxes = new TrackEncryptionBox[1];
trackEncryptionBoxes[0] = new TrackEncryptionBox(true, INITIALIZATION_VECTOR_SIZE, keyId); trackEncryptionBoxes[0] = new TrackEncryptionBox(true, INITIALIZATION_VECTOR_SIZE, keyId);
psshInfo = Collections.singletonMap(protectionElement.uuid, protectionElement.data); DrmInitData.Mapped drmInitData = new DrmInitData.Mapped(MimeTypes.VIDEO_MP4);
drmInitData.put(protectionElement.uuid, protectionElement.data);
this.drmInitData = drmInitData;
} else { } else {
psshInfo = null; drmInitData = null;
} }
int trackCount = trackIndices != null ? trackIndices.length : streamElement.tracks.length; int trackCount = trackIndices != null ? trackIndices.length : streamElement.tracks.length;
...@@ -299,7 +301,7 @@ public class SmoothStreamingChunkSource implements ChunkSource { ...@@ -299,7 +301,7 @@ public class SmoothStreamingChunkSource implements ChunkSource {
Uri uri = streamElement.buildRequestUri(selectedFormat.trackIndex, chunkIndex); Uri uri = streamElement.buildRequestUri(selectedFormat.trackIndex, chunkIndex);
Chunk mediaChunk = newMediaChunk(selectedFormat, uri, null, Chunk mediaChunk = newMediaChunk(selectedFormat, uri, null,
extractors.get(Integer.parseInt(selectedFormat.id)), psshInfo, dataSource, extractors.get(Integer.parseInt(selectedFormat.id)), drmInitData, dataSource,
currentAbsoluteChunkIndex, isLastChunk, chunkStartTimeUs, nextChunkStartTimeUs, 0); currentAbsoluteChunkIndex, isLastChunk, chunkStartTimeUs, nextChunkStartTimeUs, 0);
out.chunk = mediaChunk; out.chunk = mediaChunk;
} }
...@@ -365,7 +367,7 @@ public class SmoothStreamingChunkSource implements ChunkSource { ...@@ -365,7 +367,7 @@ public class SmoothStreamingChunkSource implements ChunkSource {
} }
private static MediaChunk newMediaChunk(Format formatInfo, Uri uri, String cacheKey, private static MediaChunk newMediaChunk(Format formatInfo, Uri uri, String cacheKey,
Extractor extractor, Map<UUID, byte[]> psshInfo, DataSource dataSource, int chunkIndex, Extractor extractor, DrmInitData drmInitData, DataSource dataSource, int chunkIndex,
boolean isLast, long chunkStartTimeUs, long nextChunkStartTimeUs, int trigger) { boolean isLast, long chunkStartTimeUs, long nextChunkStartTimeUs, int trigger) {
int nextChunkIndex = isLast ? -1 : chunkIndex + 1; int nextChunkIndex = isLast ? -1 : chunkIndex + 1;
long nextStartTimeUs = isLast ? -1 : nextChunkStartTimeUs; long nextStartTimeUs = isLast ? -1 : nextChunkStartTimeUs;
...@@ -374,7 +376,7 @@ public class SmoothStreamingChunkSource implements ChunkSource { ...@@ -374,7 +376,7 @@ public class SmoothStreamingChunkSource implements ChunkSource {
// In SmoothStreaming each chunk contains sample timestamps relative to the start of the chunk. // In SmoothStreaming each chunk contains sample timestamps relative to the start of the chunk.
// To convert them the absolute timestamps, we need to set sampleOffsetUs to -chunkStartTimeUs. // To convert them the absolute timestamps, we need to set sampleOffsetUs to -chunkStartTimeUs.
return new ContainerMediaChunk(dataSource, dataSpec, formatInfo, trigger, chunkStartTimeUs, return new ContainerMediaChunk(dataSource, dataSpec, formatInfo, trigger, chunkStartTimeUs,
nextStartTimeUs, nextChunkIndex, extractor, psshInfo, false, -chunkStartTimeUs); nextStartTimeUs, nextChunkIndex, extractor, drmInitData, false, -chunkStartTimeUs);
} }
private static byte[] getKeyId(byte[] initData) { private static byte[] getKeyId(byte[] initData) {
......
...@@ -17,6 +17,7 @@ package com.google.android.exoplayer.smoothstreaming; ...@@ -17,6 +17,7 @@ package com.google.android.exoplayer.smoothstreaming;
import com.google.android.exoplayer.C; import com.google.android.exoplayer.C;
import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.UriUtil;
import com.google.android.exoplayer.util.Util; import com.google.android.exoplayer.util.Util;
import android.net.Uri; import android.net.Uri;
...@@ -197,14 +198,14 @@ public class SmoothStreamingManifest { ...@@ -197,14 +198,14 @@ public class SmoothStreamingManifest {
public final TrackElement[] tracks; public final TrackElement[] tracks;
public final int chunkCount; public final int chunkCount;
private final Uri baseUri; private final String baseUri;
private final String chunkTemplate; private final String chunkTemplate;
private final List<Long> chunkStartTimes; private final List<Long> chunkStartTimes;
private final long[] chunkStartTimesUs; private final long[] chunkStartTimesUs;
private final long lastChunkDurationUs; private final long lastChunkDurationUs;
public StreamElement(Uri baseUri, String chunkTemplate, int type, String subType, public StreamElement(String baseUri, String chunkTemplate, int type, String subType,
long timescale, String name, int qualityLevels, int maxWidth, int maxHeight, long timescale, String name, int qualityLevels, int maxWidth, int maxHeight,
int displayWidth, int displayHeight, String language, TrackElement[] tracks, int displayWidth, int displayHeight, String language, TrackElement[] tracks,
List<Long> chunkStartTimes, long lastChunkDuration) { List<Long> chunkStartTimes, long lastChunkDuration) {
...@@ -274,7 +275,7 @@ public class SmoothStreamingManifest { ...@@ -274,7 +275,7 @@ public class SmoothStreamingManifest {
String chunkUrl = chunkTemplate String chunkUrl = chunkTemplate
.replace(URL_PLACEHOLDER_BITRATE, Integer.toString(tracks[track].bitrate)) .replace(URL_PLACEHOLDER_BITRATE, Integer.toString(tracks[track].bitrate))
.replace(URL_PLACEHOLDER_START_TIME, chunkStartTimes.get(chunkIndex).toString()); .replace(URL_PLACEHOLDER_START_TIME, chunkStartTimes.get(chunkIndex).toString());
return Util.getMergedUri(baseUri, chunkUrl); return UriUtil.resolveToUri(baseUri, chunkUrl);
} }
} }
......
...@@ -23,9 +23,7 @@ import com.google.android.exoplayer.upstream.NetworkLoadable; ...@@ -23,9 +23,7 @@ import com.google.android.exoplayer.upstream.NetworkLoadable;
import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.CodecSpecificDataUtil; import com.google.android.exoplayer.util.CodecSpecificDataUtil;
import com.google.android.exoplayer.util.MimeTypes; import com.google.android.exoplayer.util.MimeTypes;
import com.google.android.exoplayer.util.Util;
import android.net.Uri;
import android.util.Base64; import android.util.Base64;
import android.util.Pair; import android.util.Pair;
...@@ -65,8 +63,8 @@ public class SmoothStreamingManifestParser implements ...@@ -65,8 +63,8 @@ public class SmoothStreamingManifestParser implements
try { try {
XmlPullParser xmlParser = xmlParserFactory.newPullParser(); XmlPullParser xmlParser = xmlParserFactory.newPullParser();
xmlParser.setInput(inputStream, null); xmlParser.setInput(inputStream, null);
SmoothStreamMediaParser smoothStreamMediaParser = new SmoothStreamMediaParser(null, SmoothStreamMediaParser smoothStreamMediaParser =
Util.parseBaseUri(connectionUrl)); new SmoothStreamMediaParser(null, connectionUrl);
return (SmoothStreamingManifest) smoothStreamMediaParser.parse(xmlParser); return (SmoothStreamingManifest) smoothStreamMediaParser.parse(xmlParser);
} catch (XmlPullParserException e) { } catch (XmlPullParserException e) {
throw new ParserException(e); throw new ParserException(e);
...@@ -89,13 +87,13 @@ public class SmoothStreamingManifestParser implements ...@@ -89,13 +87,13 @@ public class SmoothStreamingManifestParser implements
*/ */
private static abstract class ElementParser { private static abstract class ElementParser {
private final Uri baseUri; private final String baseUri;
private final String tag; private final String tag;
private final ElementParser parent; private final ElementParser parent;
private final List<Pair<String, Object>> normalizedAttributes; private final List<Pair<String, Object>> normalizedAttributes;
public ElementParser(ElementParser parent, Uri baseUri, String tag) { public ElementParser(ElementParser parent, String baseUri, String tag) {
this.parent = parent; this.parent = parent;
this.baseUri = baseUri; this.baseUri = baseUri;
this.tag = tag; this.tag = tag;
...@@ -158,7 +156,7 @@ public class SmoothStreamingManifestParser implements ...@@ -158,7 +156,7 @@ public class SmoothStreamingManifestParser implements
} }
} }
private ElementParser newChildParser(ElementParser parent, String name, Uri baseUri) { private ElementParser newChildParser(ElementParser parent, String name, String baseUri) {
if (TrackElementParser.TAG.equals(name)) { if (TrackElementParser.TAG.equals(name)) {
return new TrackElementParser(parent, baseUri); return new TrackElementParser(parent, baseUri);
} else if (ProtectionElementParser.TAG.equals(name)) { } else if (ProtectionElementParser.TAG.equals(name)) {
...@@ -342,7 +340,7 @@ public class SmoothStreamingManifestParser implements ...@@ -342,7 +340,7 @@ public class SmoothStreamingManifestParser implements
private ProtectionElement protectionElement; private ProtectionElement protectionElement;
private List<StreamElement> streamElements; private List<StreamElement> streamElements;
public SmoothStreamMediaParser(ElementParser parent, Uri baseUri) { public SmoothStreamMediaParser(ElementParser parent, String baseUri) {
super(parent, baseUri, TAG); super(parent, baseUri, TAG);
lookAheadCount = -1; lookAheadCount = -1;
protectionElement = null; protectionElement = null;
...@@ -392,7 +390,7 @@ public class SmoothStreamingManifestParser implements ...@@ -392,7 +390,7 @@ public class SmoothStreamingManifestParser implements
private UUID uuid; private UUID uuid;
private byte[] initData; private byte[] initData;
public ProtectionElementParser(ElementParser parent, Uri baseUri) { public ProtectionElementParser(ElementParser parent, String baseUri) {
super(parent, baseUri, TAG); super(parent, baseUri, TAG);
} }
...@@ -455,7 +453,7 @@ public class SmoothStreamingManifestParser implements ...@@ -455,7 +453,7 @@ public class SmoothStreamingManifestParser implements
private static final String KEY_FRAGMENT_START_TIME = "t"; private static final String KEY_FRAGMENT_START_TIME = "t";
private static final String KEY_FRAGMENT_REPEAT_COUNT = "r"; private static final String KEY_FRAGMENT_REPEAT_COUNT = "r";
private final Uri baseUri; private final String baseUri;
private final List<TrackElement> tracks; private final List<TrackElement> tracks;
private int type; private int type;
...@@ -473,7 +471,7 @@ public class SmoothStreamingManifestParser implements ...@@ -473,7 +471,7 @@ public class SmoothStreamingManifestParser implements
private long lastChunkDuration; private long lastChunkDuration;
public StreamElementParser(ElementParser parent, Uri baseUri) { public StreamElementParser(ElementParser parent, String baseUri) {
super(parent, baseUri, TAG); super(parent, baseUri, TAG);
this.baseUri = baseUri; this.baseUri = baseUri;
tracks = new LinkedList<TrackElement>(); tracks = new LinkedList<TrackElement>();
...@@ -615,7 +613,7 @@ public class SmoothStreamingManifestParser implements ...@@ -615,7 +613,7 @@ public class SmoothStreamingManifestParser implements
private int nalUnitLengthField; private int nalUnitLengthField;
private String content; private String content;
public TrackElementParser(ElementParser parent, Uri baseUri) { public TrackElementParser(ElementParser parent, String baseUri) {
super(parent, baseUri, TAG); super(parent, baseUri, TAG);
this.csd = new LinkedList<byte[]>(); this.csd = new LinkedList<byte[]>();
} }
......
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
package com.google.android.exoplayer.source; package com.google.android.exoplayer.source;
import com.google.android.exoplayer.C; import com.google.android.exoplayer.C;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.MediaFormatHolder; import com.google.android.exoplayer.MediaFormatHolder;
import com.google.android.exoplayer.SampleHolder; import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.SampleSource; import com.google.android.exoplayer.SampleSource;
...@@ -62,9 +63,14 @@ public final class DefaultSampleSource implements SampleSource { ...@@ -62,9 +63,14 @@ public final class DefaultSampleSource implements SampleSource {
if (sampleExtractor.prepare()) { if (sampleExtractor.prepare()) {
prepared = true; prepared = true;
trackInfos = sampleExtractor.getTrackInfos(); int trackCount = sampleExtractor.getTrackCount();
trackStates = new int[trackInfos.length]; trackStates = new int[trackCount];
pendingDiscontinuities = new boolean[trackInfos.length]; pendingDiscontinuities = new boolean[trackCount];
trackInfos = new TrackInfo[trackCount];
for (int track = 0; track < trackCount; track++) {
MediaFormat mediaFormat = sampleExtractor.getMediaFormat(track);
trackInfos[track] = new TrackInfo(mediaFormat.mimeType, mediaFormat.durationUs);
}
} }
return prepared; return prepared;
...@@ -119,7 +125,8 @@ public final class DefaultSampleSource implements SampleSource { ...@@ -119,7 +125,8 @@ public final class DefaultSampleSource implements SampleSource {
return NOTHING_READ; return NOTHING_READ;
} }
if (trackStates[track] != TRACK_STATE_FORMAT_SENT) { if (trackStates[track] != TRACK_STATE_FORMAT_SENT) {
sampleExtractor.getTrackMediaFormat(track, formatHolder); formatHolder.format = sampleExtractor.getMediaFormat(track);
formatHolder.drmInitData = sampleExtractor.getDrmInitData(track);
trackStates[track] = TRACK_STATE_FORMAT_SENT; trackStates[track] = TRACK_STATE_FORMAT_SENT;
return FORMAT_READ; return FORMAT_READ;
} }
......
...@@ -15,14 +15,13 @@ ...@@ -15,14 +15,13 @@
*/ */
package com.google.android.exoplayer.source; package com.google.android.exoplayer.source;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.MediaFormatHolder;
import com.google.android.exoplayer.SampleHolder; import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.SampleSource; import com.google.android.exoplayer.SampleSource;
import com.google.android.exoplayer.TrackInfo;
import com.google.android.exoplayer.TrackRenderer; import com.google.android.exoplayer.TrackRenderer;
import com.google.android.exoplayer.drm.DrmInitData;
import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.MimeTypes;
import com.google.android.exoplayer.util.Util; import com.google.android.exoplayer.util.Util;
import android.annotation.TargetApi; import android.annotation.TargetApi;
...@@ -53,8 +52,6 @@ public final class FrameworkSampleExtractor implements SampleExtractor { ...@@ -53,8 +52,6 @@ public final class FrameworkSampleExtractor implements SampleExtractor {
private final MediaExtractor mediaExtractor; private final MediaExtractor mediaExtractor;
private TrackInfo[] trackInfos;
/** /**
* Instantiates a new sample extractor reading from the specified {@code uri}. * Instantiates a new sample extractor reading from the specified {@code uri}.
* *
...@@ -106,25 +103,10 @@ public final class FrameworkSampleExtractor implements SampleExtractor { ...@@ -106,25 +103,10 @@ public final class FrameworkSampleExtractor implements SampleExtractor {
mediaExtractor.setDataSource(fileDescriptor, fileDescriptorOffset, fileDescriptorLength); mediaExtractor.setDataSource(fileDescriptor, fileDescriptorOffset, fileDescriptorLength);
} }
int trackCount = mediaExtractor.getTrackCount();
trackInfos = new TrackInfo[trackCount];
for (int i = 0; i < trackCount; i++) {
android.media.MediaFormat format = mediaExtractor.getTrackFormat(i);
long durationUs = format.containsKey(android.media.MediaFormat.KEY_DURATION)
? format.getLong(android.media.MediaFormat.KEY_DURATION) : C.UNKNOWN_TIME_US;
String mime = format.getString(android.media.MediaFormat.KEY_MIME);
trackInfos[i] = new TrackInfo(mime, durationUs);
}
return true; return true;
} }
@Override @Override
public TrackInfo[] getTrackInfos() {
return trackInfos;
}
@Override
public void selectTrack(int index) { public void selectTrack(int index) {
mediaExtractor.selectTrack(index); mediaExtractor.selectTrack(index);
} }
...@@ -151,10 +133,18 @@ public final class FrameworkSampleExtractor implements SampleExtractor { ...@@ -151,10 +133,18 @@ public final class FrameworkSampleExtractor implements SampleExtractor {
} }
@Override @Override
public void getTrackMediaFormat(int track, MediaFormatHolder mediaFormatHolder) { public int getTrackCount() {
mediaFormatHolder.format = return mediaExtractor.getTrackCount();
MediaFormat.createFromFrameworkMediaFormatV16(mediaExtractor.getTrackFormat(track)); }
mediaFormatHolder.drmInitData = Util.SDK_INT >= 18 ? getPsshInfoV18() : null;
@Override
public MediaFormat getMediaFormat(int track) {
return MediaFormat.createFromFrameworkMediaFormatV16(mediaExtractor.getTrackFormat(track));
}
@Override
public DrmInitData getDrmInitData(int track) {
return Util.SDK_INT >= 18 ? getDrmInitDataV18() : null;
} }
@Override @Override
...@@ -173,7 +163,7 @@ public final class FrameworkSampleExtractor implements SampleExtractor { ...@@ -173,7 +163,7 @@ public final class FrameworkSampleExtractor implements SampleExtractor {
} }
sampleHolder.timeUs = mediaExtractor.getSampleTime(); sampleHolder.timeUs = mediaExtractor.getSampleTime();
sampleHolder.flags = mediaExtractor.getSampleFlags(); sampleHolder.flags = mediaExtractor.getSampleFlags();
if ((sampleHolder.flags & MediaExtractor.SAMPLE_FLAG_ENCRYPTED) != 0) { if (sampleHolder.isEncrypted()) {
sampleHolder.cryptoInfo.setFromExtractorV16(mediaExtractor); sampleHolder.cryptoInfo.setFromExtractorV16(mediaExtractor);
} }
...@@ -188,9 +178,15 @@ public final class FrameworkSampleExtractor implements SampleExtractor { ...@@ -188,9 +178,15 @@ public final class FrameworkSampleExtractor implements SampleExtractor {
} }
@TargetApi(18) @TargetApi(18)
private Map<UUID, byte[]> getPsshInfoV18() { private DrmInitData getDrmInitDataV18() {
// MediaExtractor only supports psshInfo for MP4, so it's ok to hard code the mimeType here.
Map<UUID, byte[]> psshInfo = mediaExtractor.getPsshInfo(); Map<UUID, byte[]> psshInfo = mediaExtractor.getPsshInfo();
return (psshInfo == null || psshInfo.isEmpty()) ? null : psshInfo; if (psshInfo == null || psshInfo.isEmpty()) {
return null;
}
DrmInitData.Mapped drmInitData = new DrmInitData.Mapped(MimeTypes.VIDEO_MP4);
drmInitData.putAll(psshInfo);
return drmInitData;
} }
} }
...@@ -16,11 +16,10 @@ ...@@ -16,11 +16,10 @@
package com.google.android.exoplayer.source; package com.google.android.exoplayer.source;
import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.MediaFormatHolder;
import com.google.android.exoplayer.SampleHolder; import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.SampleSource; import com.google.android.exoplayer.SampleSource;
import com.google.android.exoplayer.TrackInfo;
import com.google.android.exoplayer.TrackRenderer; import com.google.android.exoplayer.TrackRenderer;
import com.google.android.exoplayer.drm.DrmInitData;
import java.io.IOException; import java.io.IOException;
...@@ -28,7 +27,7 @@ import java.io.IOException; ...@@ -28,7 +27,7 @@ import java.io.IOException;
* Extractor for reading track metadata and samples stored in tracks. * Extractor for reading track metadata and samples stored in tracks.
* *
* <p>Call {@link #prepare} until it returns {@code true}, then access track metadata via * <p>Call {@link #prepare} until it returns {@code true}, then access track metadata via
* {@link #getTrackInfos} and {@link #getTrackMediaFormat}. * {@link #getMediaFormat}.
* *
* <p>Pass indices of tracks to read from to {@link #selectTrack}. A track can later be deselected * <p>Pass indices of tracks to read from to {@link #selectTrack}. A track can later be deselected
* by calling {@link #deselectTrack}. It is safe to select/deselect tracks after reading sample * by calling {@link #deselectTrack}. It is safe to select/deselect tracks after reading sample
...@@ -46,9 +45,6 @@ public interface SampleExtractor { ...@@ -46,9 +45,6 @@ public interface SampleExtractor {
*/ */
boolean prepare() throws IOException; boolean prepare() throws IOException;
/** Returns track information about all tracks that can be selected. */
TrackInfo[] getTrackInfos();
/** Selects the track at {@code index} for reading sample data. */ /** Selects the track at {@code index} for reading sample data. */
void selectTrack(int index); void selectTrack(int index);
...@@ -75,8 +71,14 @@ public interface SampleExtractor { ...@@ -75,8 +71,14 @@ public interface SampleExtractor {
*/ */
void seekTo(long positionUs); void seekTo(long positionUs);
/** Stores the {@link MediaFormat} of {@code track}. */ /** Returns the number of tracks, if {@link #prepare} has returned {@code true}. */
void getTrackMediaFormat(int track, MediaFormatHolder mediaFormatHolder); int getTrackCount();
/** Returns the {@link MediaFormat} of {@code track}. */
MediaFormat getMediaFormat(int track);
/** Returns the DRM initialization data for {@code track}. */
DrmInitData getDrmInitData(int track);
/** /**
* Reads the next sample in the track at index {@code track} into {@code sampleHolder}, returning * Reads the next sample in the track at index {@code track} into {@code sampleHolder}, returning
......
...@@ -179,7 +179,7 @@ public class TextTrackRenderer extends TrackRenderer implements Callback { ...@@ -179,7 +179,7 @@ public class TextTrackRenderer extends TrackRenderer implements Callback {
SampleHolder sampleHolder = parserHelper.getSampleHolder(); SampleHolder sampleHolder = parserHelper.getSampleHolder();
sampleHolder.clearData(); sampleHolder.clearData();
int result = source.readData(trackIndex, positionUs, formatHolder, sampleHolder, false); int result = source.readData(trackIndex, positionUs, formatHolder, sampleHolder, false);
if (result == SampleSource.SAMPLE_READ && !sampleHolder.decodeOnly) { if (result == SampleSource.SAMPLE_READ && !sampleHolder.isDecodeOnly()) {
parserHelper.startParseOperation(); parserHelper.startParseOperation();
textRendererNeedsUpdate = false; textRendererNeedsUpdate = false;
} else if (result == SampleSource.END_OF_STREAM) { } else if (result == SampleSource.END_OF_STREAM) {
......
...@@ -57,6 +57,8 @@ package com.google.android.exoplayer.text.eia608; ...@@ -57,6 +57,8 @@ package com.google.android.exoplayer.text.eia608;
public static final byte CARRIAGE_RETURN = 0x2D; public static final byte CARRIAGE_RETURN = 0x2D;
public static final byte ERASE_NON_DISPLAYED_MEMORY = 0x2E; public static final byte ERASE_NON_DISPLAYED_MEMORY = 0x2E;
public static final byte BACKSPACE = 0x21;
public static final byte MID_ROW_CHAN_1 = 0x11; public static final byte MID_ROW_CHAN_1 = 0x11;
public static final byte MID_ROW_CHAN_2 = 0x19; public static final byte MID_ROW_CHAN_2 = 0x19;
......
...@@ -82,6 +82,26 @@ public class Eia608Parser { ...@@ -82,6 +82,26 @@ public class Eia608Parser {
0xFB // 3F: 251 'û' "Latin small letter U with circumflex" 0xFB // 3F: 251 'û' "Latin small letter U with circumflex"
}; };
// Extended Spanish/Miscellaneous and French char set.
private static final int[] SPECIAL_ES_FR_CHARACTER_SET = new int[] {
// Spanish and misc.
0xC1, 0xC9, 0xD3, 0xDA, 0xDC, 0xFC, 0x2018, 0xA1,
0x2A, 0x27, 0x2014, 0xA9, 0x2120, 0x2022, 0x201C, 0x201D,
// French.
0xC0, 0xC2, 0xC7, 0xC8, 0xCA, 0xCB, 0xEB, 0xCE,
0xCF, 0xEF, 0xD4, 0xD9, 0xF9, 0xDB, 0xAB, 0xBB
};
//Extended Portuguese and German/Danish char set.
private static final int[] SPECIAL_PT_DE_CHARACTER_SET = new int[] {
// Portuguese.
0xC3, 0xE3, 0xCD, 0xCC, 0xEC, 0xD2, 0xF2, 0xD5,
0xF5, 0x7B, 0x7D, 0x5C, 0x5E, 0x5F, 0x7C, 0x7E,
// German/Danish.
0xC4, 0xE4, 0xD6, 0xF6, 0xDF, 0xA5, 0xA4, 0x2502,
0xC5, 0xE5, 0xD8, 0xF8, 0x250C, 0x2510, 0x2514, 0x2518
};
private final ParsableBitArray seiBuffer; private final ParsableBitArray seiBuffer;
private final StringBuilder stringBuilder; private final StringBuilder stringBuilder;
private final ArrayList<ClosedCaption> captions; private final ArrayList<ClosedCaption> captions;
...@@ -134,31 +154,45 @@ public class Eia608Parser { ...@@ -134,31 +154,45 @@ public class Eia608Parser {
} }
// Special North American character set. // Special North American character set.
if ((ccData1 == 0x11) && ((ccData2 & 0x70) == 0x30)) { // ccData2 - P|0|1|1|X|X|X|X
if ((ccData1 == 0x11 || ccData1 == 0x19)
&& ((ccData2 & 0x70) == 0x30)) {
stringBuilder.append(getSpecialChar(ccData2)); stringBuilder.append(getSpecialChar(ccData2));
continue; continue;
} }
// Extended Spanish/Miscellaneous and French character set.
// ccData2 - P|0|1|X|X|X|X|X
if ((ccData1 == 0x12 || ccData1 == 0x1A)
&& ((ccData2 & 0x60) == 0x20)) {
backspace(); // Remove standard equivalent of the special extended char.
stringBuilder.append(getExtendedEsFrChar(ccData2));
continue;
}
// Extended Portuguese and German/Danish character set.
// ccData2 - P|0|1|X|X|X|X|X
if ((ccData1 == 0x13 || ccData1 == 0x1B)
&& ((ccData2 & 0x60) == 0x20)) {
backspace(); // Remove standard equivalent of the special extended char.
stringBuilder.append(getExtendedPtDeChar(ccData2));
continue;
}
// Control character. // Control character.
if (ccData1 < 0x20) { if (ccData1 < 0x20) {
if (stringBuilder.length() > 0) { addCtrl(ccData1, ccData2);
captions.add(new ClosedCaptionText(stringBuilder.toString()));
stringBuilder.setLength(0);
}
captions.add(new ClosedCaptionCtrl(ccData1, ccData2));
continue; continue;
} }
// Basic North American character set. // Basic North American character set.
stringBuilder.append(getChar(ccData1)); stringBuilder.append(getChar(ccData1));
if (ccData2 != 0) { if (ccData2 >= 0x20) {
stringBuilder.append(getChar(ccData2)); stringBuilder.append(getChar(ccData2));
} }
} }
if (stringBuilder.length() > 0) { addBufferedText();
captions.add(new ClosedCaptionText(stringBuilder.toString()));
}
if (captions.isEmpty()) { if (captions.isEmpty()) {
return null; return null;
...@@ -166,7 +200,7 @@ public class Eia608Parser { ...@@ -166,7 +200,7 @@ public class Eia608Parser {
ClosedCaption[] captionArray = new ClosedCaption[captions.size()]; ClosedCaption[] captionArray = new ClosedCaption[captions.size()];
captions.toArray(captionArray); captions.toArray(captionArray);
return new ClosedCaptionList(sampleHolder.timeUs, sampleHolder.decodeOnly, captionArray); return new ClosedCaptionList(sampleHolder.timeUs, sampleHolder.isDecodeOnly(), captionArray);
} }
private static char getChar(byte ccData) { private static char getChar(byte ccData) {
...@@ -179,6 +213,32 @@ public class Eia608Parser { ...@@ -179,6 +213,32 @@ public class Eia608Parser {
return (char) SPECIAL_CHARACTER_SET[index]; return (char) SPECIAL_CHARACTER_SET[index];
} }
private static char getExtendedEsFrChar(byte ccData) {
int index = ccData & 0x1F;
return (char) SPECIAL_ES_FR_CHARACTER_SET[index];
}
private static char getExtendedPtDeChar(byte ccData) {
int index = ccData & 0x1F;
return (char) SPECIAL_PT_DE_CHARACTER_SET[index];
}
private void addBufferedText() {
if (stringBuilder.length() > 0) {
captions.add(new ClosedCaptionText(stringBuilder.toString()));
stringBuilder.setLength(0);
}
}
private void addCtrl(byte ccData1, byte ccData2) {
addBufferedText();
captions.add(new ClosedCaptionCtrl(ccData1, ccData2));
}
private void backspace() {
addCtrl((byte) 0x14, ClosedCaptionCtrl.BACKSPACE);
}
/** /**
* Inspects an sei message to determine whether it contains EIA-608. * Inspects an sei message to determine whether it contains EIA-608.
* <p> * <p>
......
No preview for this file type
No preview for this file type
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment