Merge pull request #444 from google/dev

dev -> dev-webm-vp9-opus
This commit is contained in:
ojw28 2015-05-11 21:17:17 +01:00
commit dd41d14ac3
45 changed files with 452 additions and 210 deletions

View File

@ -20,7 +20,7 @@ buildscript {
jcenter() jcenter()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:1.0.0' classpath 'com.android.tools.build:gradle:1.2.2'
classpath 'com.novoda:bintray-release:0.2.7' classpath 'com.novoda:bintray-release:0.2.7'
} }
} }

View File

@ -14,12 +14,12 @@
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
android { android {
compileSdkVersion 21 compileSdkVersion 22
buildToolsVersion "21.1.2" buildToolsVersion "22.0.1"
defaultConfig { defaultConfig {
minSdkVersion 16 minSdkVersion 16
targetSdkVersion 21 targetSdkVersion 22
} }
buildTypes { buildTypes {
release { release {

View File

@ -25,7 +25,7 @@
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-sdk android:minSdkVersion="16" android:targetSdkVersion="21"/> <uses-sdk android:minSdkVersion="16" android:targetSdkVersion="22"/>
<application <application
android:label="@string/application_name" android:label="@string/application_name"

View File

@ -25,7 +25,6 @@ import java.net.CookieHandler;
import java.net.CookieManager; import java.net.CookieManager;
import java.net.CookiePolicy; import java.net.CookiePolicy;
import java.net.HttpURLConnection; import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL; import java.net.URL;
import java.util.Map; import java.util.Map;
@ -52,7 +51,7 @@ public class DemoUtil {
} }
public static byte[] executePost(String url, byte[] data, Map<String, String> requestProperties) public static byte[] executePost(String url, byte[] data, Map<String, String> requestProperties)
throws MalformedURLException, IOException { throws IOException {
HttpURLConnection urlConnection = null; HttpURLConnection urlConnection = null;
try { try {
urlConnection = (HttpURLConnection) new URL(url).openConnection(); urlConnection = (HttpURLConnection) new URL(url).openConnection();

View File

@ -229,7 +229,8 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback,
return new DashRendererBuilder(this, userAgent, contentUri.toString(), return new DashRendererBuilder(this, userAgent, contentUri.toString(),
new WidevineTestMediaDrmCallback(contentId), debugTextView, audioCapabilities); new WidevineTestMediaDrmCallback(contentId), debugTextView, audioCapabilities);
case DemoUtil.TYPE_HLS: case DemoUtil.TYPE_HLS:
return new HlsRendererBuilder(this, userAgent, contentUri.toString(), debugTextView); return new HlsRendererBuilder(this, userAgent, contentUri.toString(), debugTextView,
audioCapabilities);
case DemoUtil.TYPE_M4A: // There are no file format differences between M4A and MP4. case DemoUtil.TYPE_M4A: // There are no file format differences between M4A and MP4.
case DemoUtil.TYPE_MP4: case DemoUtil.TYPE_MP4:
return new ExtractorRendererBuilder(userAgent, contentUri, debugTextView, return new ExtractorRendererBuilder(userAgent, contentUri, debugTextView,
@ -239,7 +240,7 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback,
new Mp3Extractor()); new Mp3Extractor());
case DemoUtil.TYPE_TS: case DemoUtil.TYPE_TS:
return new ExtractorRendererBuilder(userAgent, contentUri, debugTextView, return new ExtractorRendererBuilder(userAgent, contentUri, debugTextView,
new TsExtractor()); new TsExtractor(0, audioCapabilities));
case DemoUtil.TYPE_AAC: case DemoUtil.TYPE_AAC:
return new ExtractorRendererBuilder(userAgent, contentUri, debugTextView, return new ExtractorRendererBuilder(userAgent, contentUri, debugTextView,
new AdtsExtractor()); new AdtsExtractor());

View File

@ -130,8 +130,6 @@ import java.util.Locale;
public static final Sample[] MISC = new Sample[] { public static final Sample[] MISC = new Sample[] {
new Sample("Dizzy", "http://html5demos.com/assets/dizzy.mp4", new Sample("Dizzy", "http://html5demos.com/assets/dizzy.mp4",
DemoUtil.TYPE_MP4), DemoUtil.TYPE_MP4),
new Sample("Dizzy (https->http redirect)", "https://goo.gl/MtUDEj",
DemoUtil.TYPE_MP4),
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_AAC), DemoUtil.TYPE_AAC),

View File

@ -22,8 +22,6 @@ import android.media.MediaDrm.KeyRequest;
import android.media.MediaDrm.ProvisionRequest; import android.media.MediaDrm.ProvisionRequest;
import android.text.TextUtils; import android.text.TextUtils;
import org.apache.http.client.ClientProtocolException;
import java.io.IOException; import java.io.IOException;
import java.util.UUID; import java.util.UUID;
@ -43,8 +41,7 @@ public class WidevineTestMediaDrmCallback implements MediaDrmCallback {
} }
@Override @Override
public byte[] executeProvisionRequest(UUID uuid, ProvisionRequest request) public byte[] executeProvisionRequest(UUID uuid, ProvisionRequest request) throws IOException {
throws ClientProtocolException, IOException {
String url = request.getDefaultUrl() + "&signedRequest=" + new String(request.getData()); String url = request.getDefaultUrl() + "&signedRequest=" + new String(request.getData());
return DemoUtil.executePost(url, null, null); return DemoUtil.executePost(url, null, null);
} }

View File

@ -59,7 +59,6 @@ import com.google.android.exoplayer.util.Util;
import android.annotation.TargetApi; import android.annotation.TargetApi;
import android.content.Context; import android.content.Context;
import android.media.AudioFormat;
import android.media.MediaCodec; import android.media.MediaCodec;
import android.media.UnsupportedSchemeException; import android.media.UnsupportedSchemeException;
import android.os.Handler; import android.os.Handler;
@ -249,7 +248,6 @@ public class DashRendererBuilder implements RendererBuilder,
// Build the audio chunk sources. // Build the audio chunk sources.
List<ChunkSource> audioChunkSourceList = new ArrayList<ChunkSource>(); List<ChunkSource> audioChunkSourceList = new ArrayList<ChunkSource>();
List<String> audioTrackNameList = new ArrayList<String>(); List<String> audioTrackNameList = new ArrayList<String>();
int audioEncoding = AudioFormat.ENCODING_PCM_16BIT;
if (audioAdaptationSet != null) { if (audioAdaptationSet != null) {
DataSource audioDataSource = new DefaultUriDataSource(userAgent, bandwidthMeter); DataSource audioDataSource = new DefaultUriDataSource(userAgent, bandwidthMeter);
FormatEvaluator audioEvaluator = new FormatEvaluator.FixedEvaluator(); FormatEvaluator audioEvaluator = new FormatEvaluator.FixedEvaluator();
@ -275,7 +273,6 @@ public class DashRendererBuilder implements RendererBuilder,
continue; continue;
} }
audioEncoding = encoding;
for (int j = audioRepresentations.size() - 1; j >= 0; j--) { for (int j = audioRepresentations.size() - 1; j >= 0; j--) {
if (!audioRepresentations.get(j).format.codecs.equals(codec)) { if (!audioRepresentations.get(j).format.codecs.equals(codec)) {
audioTrackNameList.remove(j); audioTrackNameList.remove(j);
@ -303,7 +300,7 @@ public class DashRendererBuilder implements RendererBuilder,
AUDIO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true, mainHandler, player, AUDIO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true, mainHandler, player,
DemoPlayer.TYPE_AUDIO); DemoPlayer.TYPE_AUDIO);
audioRenderer = new MediaCodecAudioTrackRenderer(audioSampleSource, drmSessionManager, true, audioRenderer = new MediaCodecAudioTrackRenderer(audioSampleSource, drmSessionManager, true,
mainHandler, player, audioEncoding); mainHandler, player);
} }
// Build the text chunk sources. // Build the text chunk sources.

View File

@ -57,7 +57,7 @@ import android.widget.TextView;
} }
@Override @Override
protected int doPrepare() throws ExoPlaybackException { protected int doPrepare(long positionUs) throws ExoPlaybackException {
maybeFail(); maybeFail();
return STATE_PREPARED; return STATE_PREPARED;
} }

View File

@ -19,6 +19,7 @@ import com.google.android.exoplayer.MediaCodecAudioTrackRenderer;
import com.google.android.exoplayer.MediaCodecUtil.DecoderQueryException; import com.google.android.exoplayer.MediaCodecUtil.DecoderQueryException;
import com.google.android.exoplayer.MediaCodecVideoTrackRenderer; import com.google.android.exoplayer.MediaCodecVideoTrackRenderer;
import com.google.android.exoplayer.TrackRenderer; import com.google.android.exoplayer.TrackRenderer;
import com.google.android.exoplayer.audio.AudioCapabilities;
import com.google.android.exoplayer.chunk.VideoFormatSelectorUtil; import com.google.android.exoplayer.chunk.VideoFormatSelectorUtil;
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.DemoPlayer.RendererBuilderCallback; import com.google.android.exoplayer.demo.player.DemoPlayer.RendererBuilderCallback;
@ -56,15 +57,18 @@ public class HlsRendererBuilder implements RendererBuilder, ManifestCallback<Hls
private final String userAgent; private final String userAgent;
private final String url; private final String url;
private final TextView debugTextView; private final TextView debugTextView;
private final AudioCapabilities audioCapabilities;
private DemoPlayer player; private DemoPlayer player;
private RendererBuilderCallback callback; private RendererBuilderCallback callback;
public HlsRendererBuilder(Context context, String userAgent, String url, TextView debugTextView) { public HlsRendererBuilder(Context context, String userAgent, String url, TextView debugTextView,
AudioCapabilities audioCapabilities) {
this.context = context; this.context = context;
this.userAgent = userAgent; this.userAgent = userAgent;
this.url = url; this.url = url;
this.debugTextView = debugTextView; this.debugTextView = debugTextView;
this.audioCapabilities = audioCapabilities;
} }
@Override @Override
@ -101,7 +105,7 @@ public class HlsRendererBuilder implements RendererBuilder, ManifestCallback<Hls
DataSource dataSource = new DefaultUriDataSource(userAgent, bandwidthMeter); DataSource dataSource = new DefaultUriDataSource(userAgent, bandwidthMeter);
HlsChunkSource chunkSource = new HlsChunkSource(dataSource, url, manifest, bandwidthMeter, HlsChunkSource chunkSource = new HlsChunkSource(dataSource, url, manifest, bandwidthMeter,
variantIndices, HlsChunkSource.ADAPTIVE_MODE_SPLICE); variantIndices, HlsChunkSource.ADAPTIVE_MODE_SPLICE, audioCapabilities);
HlsSampleSource sampleSource = new HlsSampleSource(chunkSource, true, 3, REQUESTED_BUFFER_SIZE, HlsSampleSource sampleSource = new HlsSampleSource(chunkSource, true, 3, REQUESTED_BUFFER_SIZE,
REQUESTED_BUFFER_DURATION_MS, mainHandler, player, DemoPlayer.TYPE_VIDEO); REQUESTED_BUFFER_DURATION_MS, mainHandler, player, DemoPlayer.TYPE_VIDEO);
MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(sampleSource, MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(sampleSource,

View File

@ -8,6 +8,6 @@
# project structure. # project structure.
# Project target. # Project target.
target=android-21 target=android-22
android.library=false android.library=false
android.library.reference.1=../../../library/src/main android.library.reference.1=../../../library/src/main

View File

@ -15,8 +15,8 @@ apply plugin: 'com.android.library'
apply plugin: 'bintray-release' apply plugin: 'bintray-release'
android { android {
compileSdkVersion 21 compileSdkVersion 22
buildToolsVersion "21.1.2" buildToolsVersion "22.0.1"
defaultConfig { defaultConfig {
// Important: ExoPlayerLib specifies a minSdkVersion of 9 because // Important: ExoPlayerLib specifies a minSdkVersion of 9 because
@ -25,7 +25,7 @@ android {
// functionality provided by the library requires API level 16 or // functionality provided by the library requires API level 16 or
// greater. // greater.
minSdkVersion 9 minSdkVersion 9
targetSdkVersion 21 targetSdkVersion 22
} }
buildTypes { buildTypes {

View File

@ -27,6 +27,6 @@
the library may be of use on older devices. However, please note that the core video playback the library may be of use on older devices. However, please note that the core video playback
functionality provided by the library requires API level 16 or greater. functionality provided by the library requires API level 16 or greater.
--> -->
<uses-sdk android:minSdkVersion="9" android:targetSdkVersion="21"/> <uses-sdk android:minSdkVersion="9" android:targetSdkVersion="22"/>
</manifest> </manifest>

View File

@ -18,14 +18,14 @@ package com.google.android.exoplayer;
/** /**
* A {@link TrackRenderer} that does nothing. * A {@link TrackRenderer} that does nothing.
* <p> * <p>
* This renderer returns {@link TrackRenderer#STATE_IGNORE} from {@link #doPrepare()} in order to * This renderer returns {@link TrackRenderer#STATE_IGNORE} from {@link #doPrepare(long)} in order
* request that it should be ignored. {@link IllegalStateException} is thrown from all methods that * to request that it should be ignored. {@link IllegalStateException} is thrown from all methods
* are documented to indicate that they should not be invoked unless the renderer is prepared. * that are documented to indicate that they should not be invoked unless the renderer is prepared.
*/ */
public class DummyTrackRenderer extends TrackRenderer { public class DummyTrackRenderer extends TrackRenderer {
@Override @Override
protected int doPrepare() throws ExoPlaybackException { protected int doPrepare(long positionUs) throws ExoPlaybackException {
return STATE_IGNORE; return STATE_IGNORE;
} }

View File

@ -101,7 +101,7 @@ public interface ExoPlayer {
* The default minimum duration of data that must be buffered for playback to start or resume * The default minimum duration of data that must be buffered for playback to start or resume
* following a user action such as a seek. * following a user action such as a seek.
*/ */
public static final int DEFAULT_MIN_BUFFER_MS = 500; public static final int DEFAULT_MIN_BUFFER_MS = 2500;
/** /**
* The default minimum duration of data that must be buffered for playback to resume * The default minimum duration of data that must be buffered for playback to resume

View File

@ -264,7 +264,7 @@ import java.util.List;
boolean prepared = true; boolean prepared = true;
for (int i = 0; i < renderers.length; i++) { for (int i = 0; i < renderers.length; i++) {
if (renderers[i].getState() == TrackRenderer.STATE_UNPREPARED) { if (renderers[i].getState() == TrackRenderer.STATE_UNPREPARED) {
int state = renderers[i].prepare(); int state = renderers[i].prepare(positionUs);
if (state == TrackRenderer.STATE_UNPREPARED) { if (state == TrackRenderer.STATE_UNPREPARED) {
prepared = false; prepared = false;
} }

View File

@ -128,7 +128,7 @@ public final class FrameworkSampleSource implements SampleSource {
} }
@Override @Override
public boolean prepare() throws IOException { public boolean prepare(long positionUs) throws IOException {
if (!prepared) { if (!prepared) {
extractor = new MediaExtractor(); extractor = new MediaExtractor();
if (context != null) { if (context != null) {

View File

@ -21,9 +21,7 @@ import com.google.android.exoplayer.drm.DrmSessionManager;
import com.google.android.exoplayer.util.MimeTypes; import com.google.android.exoplayer.util.MimeTypes;
import android.annotation.TargetApi; import android.annotation.TargetApi;
import android.media.AudioFormat;
import android.media.MediaCodec; import android.media.MediaCodec;
import android.media.MediaFormat;
import android.media.audiofx.Virtualizer; import android.media.audiofx.Virtualizer;
import android.os.Handler; import android.os.Handler;
@ -71,7 +69,6 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer {
private final EventListener eventListener; private final EventListener eventListener;
private final AudioTrack audioTrack; private final AudioTrack audioTrack;
private final int encoding;
private int audioSessionId; private int audioSessionId;
private long currentPositionUs; private long currentPositionUs;
@ -124,50 +121,27 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer {
*/ */
public MediaCodecAudioTrackRenderer(SampleSource source, DrmSessionManager drmSessionManager, public MediaCodecAudioTrackRenderer(SampleSource source, DrmSessionManager drmSessionManager,
boolean playClearSamplesWithoutKeys, Handler eventHandler, EventListener eventListener) { boolean playClearSamplesWithoutKeys, Handler eventHandler, EventListener eventListener) {
this(source, drmSessionManager, playClearSamplesWithoutKeys, eventHandler, eventListener,
AudioFormat.ENCODING_PCM_16BIT);
}
/**
* @param source The upstream source from which the renderer obtains samples.
* @param drmSessionManager For use with encrypted content. May be null if support for encrypted
* content is not required.
* @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions.
* For example a media file may start with a short clear region so as to allow playback to
* begin in parallel with key acquisision. This parameter specifies whether the renderer is
* permitted to play clear regions of encrypted media files before {@code drmSessionManager}
* has obtained the keys necessary to decrypt encrypted regions of the media.
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
* null if delivery of events is not required.
* @param eventListener A listener of events. May be null if delivery of events is not required.
* @param encoding One of the {@code AudioFormat.ENCODING_*} constants specifying the audio
* encoding.
*/
public MediaCodecAudioTrackRenderer(SampleSource source, DrmSessionManager drmSessionManager,
boolean playClearSamplesWithoutKeys, Handler eventHandler, EventListener eventListener,
int encoding) {
super(source, drmSessionManager, playClearSamplesWithoutKeys, eventHandler, eventListener); super(source, drmSessionManager, playClearSamplesWithoutKeys, eventHandler, eventListener);
this.eventListener = eventListener; this.eventListener = eventListener;
this.audioSessionId = AudioTrack.SESSION_ID_NOT_SET; this.audioSessionId = AudioTrack.SESSION_ID_NOT_SET;
this.audioTrack = new AudioTrack(); this.audioTrack = new AudioTrack();
this.encoding = encoding;
} }
@Override @Override
protected DecoderInfo getDecoderInfo(String mimeType, boolean requiresSecureDecoder) protected DecoderInfo getDecoderInfo(String mimeType, boolean requiresSecureDecoder)
throws DecoderQueryException { throws DecoderQueryException {
if (encoding == AudioFormat.ENCODING_AC3 || encoding == AudioFormat.ENCODING_E_AC3) { if (MimeTypes.isPassthroughAudio(mimeType)) {
return new DecoderInfo(RAW_DECODER_NAME, true); return new DecoderInfo(RAW_DECODER_NAME, true);
} }
return super.getDecoderInfo(mimeType, requiresSecureDecoder); return super.getDecoderInfo(mimeType, requiresSecureDecoder);
} }
@Override @Override
protected void configureCodec(MediaCodec codec, String codecName, MediaFormat format, protected void configureCodec(MediaCodec codec, String codecName,
android.media.MediaCrypto crypto) { android.media.MediaFormat format, android.media.MediaCrypto crypto) {
if (RAW_DECODER_NAME.equals(codecName)) { if (RAW_DECODER_NAME.equals(codecName)) {
// Override the MIME type used to configure the codec if we are using a passthrough decoder. // Override the MIME type used to configure the codec if we are using a passthrough decoder.
String mimeType = format.getString(MediaFormat.KEY_MIME); String mimeType = format.getString(android.media.MediaFormat.KEY_MIME);
format.setString(android.media.MediaFormat.KEY_MIME, MimeTypes.AUDIO_RAW); format.setString(android.media.MediaFormat.KEY_MIME, MimeTypes.AUDIO_RAW);
codec.configure(format, null, crypto, 0); codec.configure(format, null, crypto, 0);
format.setString(android.media.MediaFormat.KEY_MIME, mimeType); format.setString(android.media.MediaFormat.KEY_MIME, mimeType);
@ -193,8 +167,13 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer {
} }
@Override @Override
protected void onOutputFormatChanged(MediaFormat format) { protected void onOutputFormatChanged(MediaFormat inputFormat,
audioTrack.reconfigure(format, encoding, 0); android.media.MediaFormat outputFormat) {
if (MimeTypes.isPassthroughAudio(inputFormat.mimeType)) {
audioTrack.reconfigure(inputFormat.getFrameworkMediaFormatV16());
} else {
audioTrack.reconfigure(outputFormat);
}
} }
/** /**

View File

@ -243,9 +243,9 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
} }
@Override @Override
protected int doPrepare() throws ExoPlaybackException { protected int doPrepare(long positionUs) throws ExoPlaybackException {
try { try {
boolean sourcePrepared = source.prepare(); boolean sourcePrepared = source.prepare(positionUs);
if (!sourcePrepared) { if (!sourcePrepared) {
return TrackRenderer.STATE_UNPREPARED; return TrackRenderer.STATE_UNPREPARED;
} }
@ -742,9 +742,11 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
* <p> * <p>
* The default implementation is a no-op. * The default implementation is a no-op.
* *
* @param format The new output format. * @param inputFormat The format of media input to the codec.
* @param outputFormat The new output format.
*/ */
protected void onOutputFormatChanged(android.media.MediaFormat format) { protected void onOutputFormatChanged(MediaFormat inputFormat,
android.media.MediaFormat outputFormat) {
// Do nothing. // Do nothing.
} }
@ -818,7 +820,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
} }
if (outputIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { if (outputIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
onOutputFormatChanged(codec.getOutputFormat()); onOutputFormatChanged(format, codec.getOutputFormat());
codecCounters.outputFormatChangedCount++; codecCounters.outputFormatChangedCount++;
return true; return true;
} else if (outputIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) { } else if (outputIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {

View File

@ -381,15 +381,17 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer {
} }
@Override @Override
protected void onOutputFormatChanged(android.media.MediaFormat format) { protected void onOutputFormatChanged(MediaFormat inputFormat,
boolean hasCrop = format.containsKey(KEY_CROP_RIGHT) && format.containsKey(KEY_CROP_LEFT) android.media.MediaFormat outputFormat) {
&& format.containsKey(KEY_CROP_BOTTOM) && format.containsKey(KEY_CROP_TOP); boolean hasCrop = outputFormat.containsKey(KEY_CROP_RIGHT)
&& outputFormat.containsKey(KEY_CROP_LEFT) && outputFormat.containsKey(KEY_CROP_BOTTOM)
&& outputFormat.containsKey(KEY_CROP_TOP);
currentWidth = hasCrop currentWidth = hasCrop
? format.getInteger(KEY_CROP_RIGHT) - format.getInteger(KEY_CROP_LEFT) + 1 ? outputFormat.getInteger(KEY_CROP_RIGHT) - outputFormat.getInteger(KEY_CROP_LEFT) + 1
: format.getInteger(android.media.MediaFormat.KEY_WIDTH); : outputFormat.getInteger(android.media.MediaFormat.KEY_WIDTH);
currentHeight = hasCrop currentHeight = hasCrop
? format.getInteger(KEY_CROP_BOTTOM) - format.getInteger(KEY_CROP_TOP) + 1 ? outputFormat.getInteger(KEY_CROP_BOTTOM) - outputFormat.getInteger(KEY_CROP_TOP) + 1
: format.getInteger(android.media.MediaFormat.KEY_HEIGHT); : outputFormat.getInteger(android.media.MediaFormat.KEY_HEIGHT);
} }
@Override @Override

View File

@ -57,10 +57,11 @@ public interface SampleSource {
* and formats). If insufficient data is available then the call will return {@code false} rather * and formats). If insufficient data is available then the call will return {@code false} rather
* than block. The method can be called repeatedly until the return value indicates success. * than block. The method can be called repeatedly until the return value indicates success.
* *
* @param positionUs The player's current playback position.
* @return True if the source was prepared successfully, false otherwise. * @return True if the source was prepared successfully, false otherwise.
* @throws IOException If an error occurred preparing the source. * @throws IOException If an error occurred preparing the source.
*/ */
public boolean prepare() throws IOException; public boolean prepare(long positionUs) throws IOException;
/** /**
* Returns the number of tracks exposed by the source. * Returns the number of tracks exposed by the source.

View File

@ -108,11 +108,12 @@ public abstract class TrackRenderer implements ExoPlayerComponent {
* Prepares the renderer. This method is non-blocking, and hence it may be necessary to call it * Prepares the renderer. This method is non-blocking, and hence it may be necessary to call it
* more than once in order to transition the renderer into the prepared state. * more than once in order to transition the renderer into the prepared state.
* *
* @param positionUs The player's current playback position.
* @return The current state (one of the STATE_* constants), for convenience. * @return The current state (one of the STATE_* constants), for convenience.
*/ */
/* package */ final int prepare() throws ExoPlaybackException { /* package */ final int prepare(long positionUs) throws ExoPlaybackException {
Assertions.checkState(state == TrackRenderer.STATE_UNPREPARED); Assertions.checkState(state == TrackRenderer.STATE_UNPREPARED);
state = doPrepare(); state = doPrepare(positionUs);
Assertions.checkState(state == TrackRenderer.STATE_UNPREPARED || Assertions.checkState(state == TrackRenderer.STATE_UNPREPARED ||
state == TrackRenderer.STATE_PREPARED || state == TrackRenderer.STATE_PREPARED ||
state == TrackRenderer.STATE_IGNORE); state == TrackRenderer.STATE_IGNORE);
@ -127,11 +128,12 @@ public abstract class TrackRenderer implements ExoPlayerComponent {
* This method should return quickly, and should not block if the renderer is currently unable to * This method should return quickly, and should not block if the renderer is currently unable to
* make any useful progress. * make any useful progress.
* *
* @param positionUs The player's current playback position.
* @return The new state of the renderer. One of {@link #STATE_UNPREPARED}, * @return The new state of the renderer. One of {@link #STATE_UNPREPARED},
* {@link #STATE_PREPARED} and {@link #STATE_IGNORE}. * {@link #STATE_PREPARED} and {@link #STATE_IGNORE}.
* @throws ExoPlaybackException If an error occurs. * @throws ExoPlaybackException If an error occurs.
*/ */
protected abstract int doPrepare() throws ExoPlaybackException; protected abstract int doPrepare(long positionUs) throws ExoPlaybackException;
/** /**
* Enable the renderer. * Enable the renderer.

View File

@ -18,6 +18,7 @@ package com.google.android.exoplayer.audio;
import com.google.android.exoplayer.C; import com.google.android.exoplayer.C;
import com.google.android.exoplayer.util.Ac3Util; import com.google.android.exoplayer.util.Ac3Util;
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;
@ -315,24 +316,21 @@ public final class AudioTrack {
} }
/** /**
* Reconfigures the audio track to play back media in {@code format}. The encoding is assumed to * Reconfigures the audio track to play back media in {@code format}, inferring a buffer size from
* be {@link AudioFormat#ENCODING_PCM_16BIT}. * the format.
*/ */
public void reconfigure(MediaFormat format) { public void reconfigure(MediaFormat format) {
reconfigure(format, AudioFormat.ENCODING_PCM_16BIT, 0); reconfigure(format, 0);
} }
/** /**
* Reconfigures the audio track to play back media in {@code format}. Buffers passed to * Reconfigures the audio track to play back media in {@code format}.
* {@link #handleBuffer} must use the specified {@code encoding}, which should be a constant from
* {@link AudioFormat}.
* *
* @param format Specifies the channel count and sample rate to play back. * @param format Specifies the channel count and sample rate to play back.
* @param encoding The format in which audio is represented.
* @param specifiedBufferSize A specific size for the playback buffer in bytes, or 0 to use a * @param specifiedBufferSize A specific size for the playback buffer in bytes, or 0 to use a
* size inferred from the format. * size inferred from the format.
*/ */
public void reconfigure(MediaFormat format, int encoding, int specifiedBufferSize) { public void reconfigure(MediaFormat format, int specifiedBufferSize) {
int channelCount = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT); int channelCount = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
int channelConfig; int channelConfig;
switch (channelCount) { switch (channelCount) {
@ -353,8 +351,10 @@ public final class AudioTrack {
} }
int sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE); int sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE);
String mimeType = format.getString(MediaFormat.KEY_MIME);
// TODO: Does channelConfig determine channelCount? // TODO: Does channelConfig determine channelCount?
int encoding = MimeTypes.getEncodingForMimeType(mimeType);
boolean isAc3 = encoding == C.ENCODING_AC3 || encoding == C.ENCODING_E_AC3; boolean isAc3 = encoding == C.ENCODING_AC3 || encoding == C.ENCODING_E_AC3;
if (isInitialized() && this.sampleRate == sampleRate && this.channelConfig == channelConfig if (isInitialized() && this.sampleRate == sampleRate && this.channelConfig == channelConfig
&& !this.isAc3 && !isAc3) { && !this.isAc3 && !isAc3) {
@ -423,12 +423,23 @@ public final class AudioTrack {
return RESULT_BUFFER_CONSUMED; return RESULT_BUFFER_CONSUMED;
} }
// As a workaround for an issue where an an AC-3 audio track continues to play data written // Workarounds for issues with AC-3 passthrough AudioTracks on API versions 21/22:
// while it is paused, stop writing so its buffer empties. See [Internal: b/18899620]. if (Util.SDK_INT <= 22 && isAc3) {
if (isAc3 && audioTrack.getPlayState() == android.media.AudioTrack.PLAYSTATE_PAUSED) { // An AC-3 audio track continues to play data written while it is paused. Stop writing so its
// buffer empties. See [Internal: b/18899620].
if (audioTrack.getPlayState() == android.media.AudioTrack.PLAYSTATE_PAUSED) {
return 0; return 0;
} }
// A new AC-3 audio track's playback position continues to increase from the old track's
// position for a short time after is has been released. Avoid writing data until the playback
// head position actually returns to zero.
if (audioTrack.getPlayState() == android.media.AudioTrack.PLAYSTATE_STOPPED
&& audioTrackUtil.getPlaybackHeadPosition() != 0) {
return 0;
}
}
int result = 0; int result = 0;
if (temporaryBufferSize == 0) { if (temporaryBufferSize == 0) {
if (isAc3 && ac3Bitrate == UNKNOWN_AC3_BITRATE) { if (isAc3 && ac3Bitrate == UNKNOWN_AC3_BITRATE) {
@ -637,7 +648,8 @@ public final class AudioTrack {
} }
if (systemClockUs - lastTimestampSampleTimeUs >= MIN_TIMESTAMP_SAMPLE_INTERVAL_US) { if (systemClockUs - lastTimestampSampleTimeUs >= MIN_TIMESTAMP_SAMPLE_INTERVAL_US) {
audioTimestampSet = audioTrackUtil.updateTimestamp(); // Don't use AudioTrack.getTimestamp() on AC-3 tracks, as it gives an incorrect timestamp.
audioTimestampSet = !isAc3 && audioTrackUtil.updateTimestamp();
if (audioTimestampSet) { if (audioTimestampSet) {
// Perform sanity checks on the timestamp. // Perform sanity checks on the timestamp.
long audioTimestampUs = audioTrackUtil.getTimestampNanoTime() / 1000; long audioTimestampUs = audioTrackUtil.getTimestampNanoTime() / 1000;
@ -739,7 +751,7 @@ public final class AudioTrack {
private static class AudioTrackUtil { private static class AudioTrackUtil {
protected android.media.AudioTrack audioTrack; protected android.media.AudioTrack audioTrack;
private boolean enablePassthroughWorkaround; private boolean isPassthrough;
private int sampleRate; private int sampleRate;
private long lastRawPlaybackHeadPosition; private long lastRawPlaybackHeadPosition;
private long rawPlaybackHeadWrapCount; private long rawPlaybackHeadWrapCount;
@ -749,14 +761,11 @@ public final class AudioTrack {
* Reconfigures the audio track utility helper to use the specified {@code audioTrack}. * Reconfigures the audio track utility helper to use the specified {@code audioTrack}.
* *
* @param audioTrack The audio track to wrap. * @param audioTrack The audio track to wrap.
* @param enablePassthroughWorkaround Whether to work around an issue where the playback head * @param isPassthrough Whether the audio track is used for passthrough (e.g. AC-3) playback.
* position jumps back to zero on a paused passthrough/direct audio track. See
* [Internal: b/19187573].
*/ */
public void reconfigure(android.media.AudioTrack audioTrack, public void reconfigure(android.media.AudioTrack audioTrack, boolean isPassthrough) {
boolean enablePassthroughWorkaround) {
this.audioTrack = audioTrack; this.audioTrack = audioTrack;
this.enablePassthroughWorkaround = enablePassthroughWorkaround; this.isPassthrough = isPassthrough;
lastRawPlaybackHeadPosition = 0; lastRawPlaybackHeadPosition = 0;
rawPlaybackHeadWrapCount = 0; rawPlaybackHeadWrapCount = 0;
passthroughWorkaroundPauseOffset = 0; passthroughWorkaroundPauseOffset = 0;
@ -767,14 +776,14 @@ public final class AudioTrack {
/** /**
* Returns whether the audio track should behave as though it has pending data. This is to work * Returns whether the audio track should behave as though it has pending data. This is to work
* around an issue where AC-3 audio tracks can't be paused, so we empty their buffers when * around an issue on platform API versions 21/22 where AC-3 audio tracks can't be paused, so we
* paused. In this case, they should still behave as if they have pending data, otherwise * empty their buffers when paused. In this case, they should still behave as if they have
* writing will never resume. * pending data, otherwise writing will never resume.
* *
* @see #handleBuffer * @see #handleBuffer
*/ */
public boolean overrideHasPendingData() { public boolean overrideHasPendingData() {
return enablePassthroughWorkaround return Util.SDK_INT <= 22 && isPassthrough
&& audioTrack.getPlayState() == android.media.AudioTrack.PLAYSTATE_PAUSED && audioTrack.getPlayState() == android.media.AudioTrack.PLAYSTATE_PAUSED
&& audioTrack.getPlaybackHeadPosition() == 0; && audioTrack.getPlaybackHeadPosition() == 0;
} }
@ -790,8 +799,16 @@ public final class AudioTrack {
*/ */
public long getPlaybackHeadPosition() { public long getPlaybackHeadPosition() {
long rawPlaybackHeadPosition = 0xFFFFFFFFL & audioTrack.getPlaybackHeadPosition(); long rawPlaybackHeadPosition = 0xFFFFFFFFL & audioTrack.getPlaybackHeadPosition();
if (enablePassthroughWorkaround) { if (Util.SDK_INT <= 22 && isPassthrough) {
if (audioTrack.getPlayState() == android.media.AudioTrack.PLAYSTATE_PAUSED // Work around issues with passthrough/direct AudioTracks on platform API versions 21/22:
// - After resetting, the new AudioTrack's playback position continues to increase for a
// short time from the old AudioTrack's position, while in the PLAYSTATE_STOPPED state.
// - The playback head position jumps back to zero on paused passthrough/direct audio
// tracks. See [Internal: b/19187573].
if (audioTrack.getPlayState() == android.media.AudioTrack.PLAYSTATE_STOPPED) {
// Prevent detecting a wrapped position.
lastRawPlaybackHeadPosition = rawPlaybackHeadPosition;
} else if (audioTrack.getPlayState() == android.media.AudioTrack.PLAYSTATE_PAUSED
&& rawPlaybackHeadPosition == 0) { && rawPlaybackHeadPosition == 0) {
passthroughWorkaroundPauseOffset = lastRawPlaybackHeadPosition; passthroughWorkaroundPauseOffset = lastRawPlaybackHeadPosition;
} }
@ -868,9 +885,8 @@ public final class AudioTrack {
} }
@Override @Override
public void reconfigure(android.media.AudioTrack audioTrack, public void reconfigure(android.media.AudioTrack audioTrack, boolean isPassthrough) {
boolean enablePassthroughWorkaround) { super.reconfigure(audioTrack, isPassthrough);
super.reconfigure(audioTrack, enablePassthroughWorkaround);
rawTimestampFramePositionWrapCount = 0; rawTimestampFramePositionWrapCount = 0;
lastRawTimestampFramePosition = 0; lastRawTimestampFramePosition = 0;
lastTimestampFramePosition = 0; lastTimestampFramePosition = 0;

View File

@ -87,7 +87,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback {
private long currentLoadStartTimeMs; private long currentLoadStartTimeMs;
private MediaFormat downstreamMediaFormat; private MediaFormat downstreamMediaFormat;
private volatile Format downstreamFormat; private Format downstreamFormat;
public ChunkSampleSource(ChunkSource chunkSource, LoadControl loadControl, public ChunkSampleSource(ChunkSource chunkSource, LoadControl loadControl,
int bufferSizeContribution, boolean frameAccurateSeeking) { int bufferSizeContribution, boolean frameAccurateSeeking) {
@ -120,18 +120,8 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback {
pendingResetPositionUs = NO_RESET_PENDING; pendingResetPositionUs = NO_RESET_PENDING;
} }
/**
* Exposes the current downstream format for debugging purposes. Can be called from any thread.
*
* @return The current downstream format.
*/
@Deprecated
public Format getFormat() {
return downstreamFormat;
}
@Override @Override
public boolean prepare() { public boolean prepare(long positionUs) {
Assertions.checkState(state == STATE_UNPREPARED); Assertions.checkState(state == STATE_UNPREPARED);
loader = new Loader("Loader:" + chunkSource.getTrackInfo().mimeType); loader = new Loader("Loader:" + chunkSource.getTrackInfo().mimeType);
state = STATE_PREPARED; state = STATE_PREPARED;

View File

@ -84,7 +84,7 @@ public interface FormatEvaluator {
/** /**
* Always selects the first format. * Always selects the first format.
*/ */
public static class FixedEvaluator implements FormatEvaluator { public static final class FixedEvaluator implements FormatEvaluator {
@Override @Override
public void enable() { public void enable() {
@ -107,7 +107,7 @@ public interface FormatEvaluator {
/** /**
* Selects randomly between the available formats. * Selects randomly between the available formats.
*/ */
public static class RandomEvaluator implements FormatEvaluator { public static final class RandomEvaluator implements FormatEvaluator {
private final Random random; private final Random random;
@ -145,7 +145,7 @@ public interface FormatEvaluator {
* reference implementation only. It is recommended that application developers implement their * reference implementation only. It is recommended that application developers implement their
* own adaptive evaluator to more precisely suit their use case. * own adaptive evaluator to more precisely suit their use case.
*/ */
public static class AdaptiveEvaluator implements FormatEvaluator { public static final class AdaptiveEvaluator implements FormatEvaluator {
public static final int DEFAULT_MAX_INITIAL_BITRATE = 800000; public static final int DEFAULT_MAX_INITIAL_BITRATE = 800000;
@ -259,8 +259,9 @@ public interface FormatEvaluator {
/** /**
* Compute the ideal format ignoring buffer health. * Compute the ideal format ignoring buffer health.
*/ */
protected Format determineIdealFormat(Format[] formats, long bitrateEstimate) { private Format determineIdealFormat(Format[] formats, long bitrateEstimate) {
long effectiveBitrate = computeEffectiveBitrateEstimate(bitrateEstimate); long effectiveBitrate = bitrateEstimate == BandwidthMeter.NO_ESTIMATE
? maxInitialBitrate : (long) (bitrateEstimate * bandwidthFraction);
for (int i = 0; i < formats.length; i++) { for (int i = 0; i < formats.length; i++) {
Format format = formats[i]; Format format = formats[i];
if (format.bitrate <= effectiveBitrate) { if (format.bitrate <= effectiveBitrate) {
@ -271,14 +272,6 @@ public interface FormatEvaluator {
return formats[formats.length - 1]; return formats[formats.length - 1];
} }
/**
* Apply overhead factor, or default value in absence of estimate.
*/
protected long computeEffectiveBitrateEstimate(long bitrateEstimate) {
return bitrateEstimate == BandwidthMeter.NO_ESTIMATE
? maxInitialBitrate : (long) (bitrateEstimate * bandwidthFraction);
}
} }
} }

View File

@ -419,7 +419,9 @@ public class DashChunkSource implements ChunkSource {
(ChunkIndex) initializationChunk.getSeekMap(), (ChunkIndex) initializationChunk.getSeekMap(),
initializationChunk.dataSpec.uri.toString()); initializationChunk.dataSpec.uri.toString());
} }
if (initializationChunk.hasDrmInitData()) { // The null check avoids overwriting drmInitData obtained from the manifest with drmInitData
// obtained from the stream, as per DASH IF Interoperability Recommendations V3.0, 7.5.3.
if (drmInitData == null && initializationChunk.hasDrmInitData()) {
drmInitData = initializationChunk.getDrmInitData(); drmInitData = initializationChunk.getDrmInitData();
} }
} }

View File

@ -24,10 +24,12 @@ import com.google.android.exoplayer.dash.mpd.SegmentBase.SingleSegmentBase;
import com.google.android.exoplayer.upstream.UriLoadable; import com.google.android.exoplayer.upstream.UriLoadable;
import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.MimeTypes; import com.google.android.exoplayer.util.MimeTypes;
import com.google.android.exoplayer.util.ParsableByteArray;
import com.google.android.exoplayer.util.UriUtil; import com.google.android.exoplayer.util.UriUtil;
import com.google.android.exoplayer.util.Util; import com.google.android.exoplayer.util.Util;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Base64;
import org.xml.sax.helpers.DefaultHandler; import org.xml.sax.helpers.DefaultHandler;
import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParser;
@ -41,6 +43,7 @@ import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.UUID;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@ -270,11 +273,27 @@ public class MediaPresentationDescriptionParser extends DefaultHandler
protected ContentProtection parseContentProtection(XmlPullParser xpp) protected ContentProtection parseContentProtection(XmlPullParser xpp)
throws XmlPullParserException, IOException { throws XmlPullParserException, IOException {
String schemeIdUri = xpp.getAttributeValue(null, "schemeIdUri"); String schemeIdUri = xpp.getAttributeValue(null, "schemeIdUri");
return buildContentProtection(schemeIdUri); UUID uuid = null;
byte[] data = null;
do {
xpp.next();
// The cenc:pssh element is defined in 23001-7:2015
if (isStartTag(xpp, "cenc:pssh") && xpp.next() == XmlPullParser.TEXT) {
byte[] decodedData = Base64.decode(xpp.getText(), Base64.DEFAULT);
ParsableByteArray psshAtom = new ParsableByteArray(decodedData);
psshAtom.skipBytes(12);
uuid = new UUID(psshAtom.readLong(), psshAtom.readLong());
int dataSize = psshAtom.readInt();
data = new byte[dataSize];
psshAtom.readBytes(data, 0, dataSize);
}
} while (!isEndTag(xpp, "ContentProtection"));
return buildContentProtection(schemeIdUri, uuid, data);
} }
protected ContentProtection buildContentProtection(String schemeIdUri) { protected ContentProtection buildContentProtection(String schemeIdUri, UUID uuid, byte[] data) {
return new ContentProtection(schemeIdUri, null, null); return new ContentProtection(schemeIdUri, uuid, data);
} }
/** /**

View File

@ -72,6 +72,7 @@ public class ExtractorSampleSource implements SampleSource, ExtractorOutput, Loa
private boolean prepared; private boolean prepared;
private int enabledTrackCount; private int enabledTrackCount;
private TrackInfo[] trackInfos; private TrackInfo[] trackInfos;
private long maxTrackDurationUs;
private boolean[] pendingMediaFormat; private boolean[] pendingMediaFormat;
private boolean[] pendingDiscontinuities; private boolean[] pendingDiscontinuities;
private boolean[] trackEnabledStates; private boolean[] trackEnabledStates;
@ -137,7 +138,7 @@ public class ExtractorSampleSource implements SampleSource, ExtractorOutput, Loa
} }
@Override @Override
public boolean prepare() throws IOException { public boolean prepare(long positionUs) throws IOException {
if (prepared) { if (prepared) {
return true; return true;
} }
@ -156,9 +157,13 @@ public class ExtractorSampleSource implements SampleSource, ExtractorOutput, Loa
pendingDiscontinuities = new boolean[trackCount]; pendingDiscontinuities = new boolean[trackCount];
pendingMediaFormat = new boolean[trackCount]; pendingMediaFormat = new boolean[trackCount];
trackInfos = new TrackInfo[trackCount]; trackInfos = new TrackInfo[trackCount];
maxTrackDurationUs = C.UNKNOWN_TIME_US;
for (int i = 0; i < trackCount; i++) { for (int i = 0; i < trackCount; i++) {
MediaFormat format = sampleQueues.valueAt(i).getFormat(); MediaFormat format = sampleQueues.valueAt(i).getFormat();
trackInfos[i] = new TrackInfo(format.mimeType, format.durationUs); trackInfos[i] = new TrackInfo(format.mimeType, format.durationUs);
if (format.durationUs != C.UNKNOWN_TIME_US && format.durationUs > maxTrackDurationUs) {
maxTrackDurationUs = format.durationUs;
}
} }
prepared = true; prepared = true;
return true; return true;
@ -448,6 +453,11 @@ public class ExtractorSampleSource implements SampleSource, ExtractorOutput, Loa
loadable = createLoadableFromStart(); loadable = createLoadableFromStart();
} else { } else {
Assertions.checkState(isPendingReset()); Assertions.checkState(isPendingReset());
if (maxTrackDurationUs != C.UNKNOWN_TIME_US && pendingResetPositionUs >= maxTrackDurationUs) {
loadingFinished = true;
pendingResetPositionUs = NO_RESET_PENDING;
return;
}
loadable = createLoadableFromPositionUs(pendingResetPositionUs); loadable = createLoadableFromPositionUs(pendingResetPositionUs);
pendingResetPositionUs = NO_RESET_PENDING; pendingResetPositionUs = NO_RESET_PENDING;
} }

View File

@ -137,7 +137,7 @@ import com.google.android.exoplayer.util.Util;
} }
long position = (long) ((1f / 256) * fx * sizeBytes) + firstFramePosition; long position = (long) ((1f / 256) * fx * sizeBytes) + firstFramePosition;
return inputLength != C.LENGTH_UNBOUNDED ? Math.min(position, inputLength) : position; return inputLength != C.LENGTH_UNBOUNDED ? Math.min(position, inputLength - 1) : position;
} }
@Override @Override

View File

@ -534,8 +534,18 @@ import java.util.List;
childPosition += childAtomSize; childPosition += childAtomSize;
} }
out.mediaFormat = MediaFormat.createAudioFormat( // Set the MIME type for ac-3/ec-3 atoms even if the dac3/dec3 child atom is missing.
MimeTypes.AUDIO_AAC, sampleSize, durationUs, channelCount, sampleRate, String mimeType;
if (atomType == Atom.TYPE_ac_3) {
mimeType = MimeTypes.AUDIO_AC3;
} else if (atomType == Atom.TYPE_ec_3) {
mimeType = MimeTypes.AUDIO_EC3;
} else {
mimeType = MimeTypes.AUDIO_AAC;
}
out.mediaFormat = MediaFormat.createAudioFormat(mimeType, sampleSize, durationUs, channelCount,
sampleRate,
initializationData == null ? null : Collections.singletonList(initializationData)); initializationData == null ? null : Collections.singletonList(initializationData));
} }

View File

@ -16,6 +16,7 @@
package com.google.android.exoplayer.extractor.ts; package com.google.android.exoplayer.extractor.ts;
import com.google.android.exoplayer.C; import com.google.android.exoplayer.C;
import com.google.android.exoplayer.audio.AudioCapabilities;
import com.google.android.exoplayer.extractor.Extractor; import com.google.android.exoplayer.extractor.Extractor;
import com.google.android.exoplayer.extractor.ExtractorInput; import com.google.android.exoplayer.extractor.ExtractorInput;
import com.google.android.exoplayer.extractor.ExtractorOutput; import com.google.android.exoplayer.extractor.ExtractorOutput;
@ -43,7 +44,7 @@ public final class TsExtractor implements Extractor, SeekMap {
private static final int TS_STREAM_TYPE_AAC = 0x0F; private static final int TS_STREAM_TYPE_AAC = 0x0F;
private static final int TS_STREAM_TYPE_ATSC_AC3 = 0x81; private static final int TS_STREAM_TYPE_ATSC_AC3 = 0x81;
private static final int TS_STREAM_TYPE_DVB_AC3 = 0x06; private static final int TS_STREAM_TYPE_ATSC_E_AC3 = 0x87;
private static final int TS_STREAM_TYPE_H264 = 0x1B; private static final int TS_STREAM_TYPE_H264 = 0x1B;
private static final int TS_STREAM_TYPE_ID3 = 0x15; private static final int TS_STREAM_TYPE_ID3 = 0x15;
private static final int TS_STREAM_TYPE_EIA608 = 0x100; // 0xFF + 1 private static final int TS_STREAM_TYPE_EIA608 = 0x100; // 0xFF + 1
@ -52,6 +53,7 @@ public final class TsExtractor implements Extractor, SeekMap {
private final ParsableByteArray tsPacketBuffer; private final ParsableByteArray tsPacketBuffer;
private final SparseBooleanArray streamTypes; private final SparseBooleanArray streamTypes;
private final SparseBooleanArray allowedPassthroughStreamTypes;
private final SparseArray<TsPayloadReader> tsPayloadReaders; // Indexed by pid private final SparseArray<TsPayloadReader> tsPayloadReaders; // Indexed by pid
private final long firstSampleTimestampUs; private final long firstSampleTimestampUs;
private final ParsableBitArray tsScratch; private final ParsableBitArray tsScratch;
@ -62,14 +64,15 @@ public final class TsExtractor implements Extractor, SeekMap {
private long lastPts; private long lastPts;
public TsExtractor() { public TsExtractor() {
this(0); this(0, null);
} }
public TsExtractor(long firstSampleTimestampUs) { public TsExtractor(long firstSampleTimestampUs, AudioCapabilities audioCapabilities) {
this.firstSampleTimestampUs = firstSampleTimestampUs; this.firstSampleTimestampUs = firstSampleTimestampUs;
tsScratch = new ParsableBitArray(new byte[3]); tsScratch = new ParsableBitArray(new byte[3]);
tsPacketBuffer = new ParsableByteArray(TS_PACKET_SIZE); tsPacketBuffer = new ParsableByteArray(TS_PACKET_SIZE);
streamTypes = new SparseBooleanArray(); streamTypes = new SparseBooleanArray();
allowedPassthroughStreamTypes = getPassthroughStreamTypes(audioCapabilities);
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;
@ -174,6 +177,24 @@ public final class TsExtractor implements Extractor, SeekMap {
return timeUs + timestampOffsetUs; return timeUs + timestampOffsetUs;
} }
/**
* Returns a sparse boolean array of stream types that can be played back based on
* {@code audioCapabilities}.
*/
private static SparseBooleanArray getPassthroughStreamTypes(AudioCapabilities audioCapabilities) {
SparseBooleanArray streamTypes = new SparseBooleanArray();
if (audioCapabilities != null) {
if (audioCapabilities.supportsEncoding(C.ENCODING_AC3)) {
streamTypes.put(TS_STREAM_TYPE_ATSC_AC3, true);
}
if (audioCapabilities.supportsEncoding(C.ENCODING_E_AC3)) {
// TODO: Uncomment when Ac3Reader supports enhanced AC-3.
// streamTypes.put(TS_STREAM_TYPE_ATSC_E_AC3, true);
}
}
return streamTypes;
}
/** /**
* Parses TS packet payload data. * Parses TS packet payload data.
*/ */
@ -308,13 +329,17 @@ public final class TsExtractor implements Extractor, SeekMap {
continue; continue;
} }
// TODO: Detect and read DVB AC-3 streams with Ac3Reader.
ElementaryStreamReader pesPayloadReader = null; ElementaryStreamReader pesPayloadReader = null;
switch (streamType) { switch (streamType) {
case TS_STREAM_TYPE_AAC: case TS_STREAM_TYPE_AAC:
pesPayloadReader = new AdtsReader(output.track(TS_STREAM_TYPE_AAC)); pesPayloadReader = new AdtsReader(output.track(TS_STREAM_TYPE_AAC));
break; break;
case TS_STREAM_TYPE_ATSC_E_AC3:
case TS_STREAM_TYPE_ATSC_AC3: case TS_STREAM_TYPE_ATSC_AC3:
case TS_STREAM_TYPE_DVB_AC3: if (!allowedPassthroughStreamTypes.get(streamType)) {
continue;
}
pesPayloadReader = new Ac3Reader(output.track(streamType)); pesPayloadReader = new Ac3Reader(output.track(streamType));
break; break;
case TS_STREAM_TYPE_H264: case TS_STREAM_TYPE_H264:

View File

@ -17,6 +17,7 @@ package com.google.android.exoplayer.hls;
import com.google.android.exoplayer.C; import com.google.android.exoplayer.C;
import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.audio.AudioCapabilities;
import com.google.android.exoplayer.chunk.BaseChunkSampleSourceEventListener; import com.google.android.exoplayer.chunk.BaseChunkSampleSourceEventListener;
import com.google.android.exoplayer.chunk.Chunk; import com.google.android.exoplayer.chunk.Chunk;
import com.google.android.exoplayer.chunk.DataChunk; import com.google.android.exoplayer.chunk.DataChunk;
@ -125,6 +126,7 @@ public class HlsChunkSource {
private final int maxHeight; private final int maxHeight;
private final long minBufferDurationToSwitchUpUs; private final long minBufferDurationToSwitchUpUs;
private final long maxBufferDurationToSwitchDownUs; private final long maxBufferDurationToSwitchDownUs;
private final AudioCapabilities audioCapabilities;
/* package */ byte[] scratchSpace; /* package */ byte[] scratchSpace;
/* package */ final HlsMediaPlaylist[] mediaPlaylists; /* package */ final HlsMediaPlaylist[] mediaPlaylists;
@ -140,9 +142,11 @@ public class HlsChunkSource {
private byte[] encryptionIv; private byte[] encryptionIv;
public HlsChunkSource(DataSource dataSource, String playlistUrl, HlsPlaylist playlist, public HlsChunkSource(DataSource dataSource, String playlistUrl, HlsPlaylist playlist,
BandwidthMeter bandwidthMeter, int[] variantIndices, int adaptiveMode) { BandwidthMeter bandwidthMeter, int[] variantIndices, int adaptiveMode,
AudioCapabilities audioCapabilities) {
this(dataSource, playlistUrl, playlist, bandwidthMeter, variantIndices, adaptiveMode, this(dataSource, playlistUrl, playlist, bandwidthMeter, variantIndices, adaptiveMode,
DEFAULT_MIN_BUFFER_TO_SWITCH_UP_MS, DEFAULT_MAX_BUFFER_TO_SWITCH_DOWN_MS); DEFAULT_MIN_BUFFER_TO_SWITCH_UP_MS, DEFAULT_MAX_BUFFER_TO_SWITCH_DOWN_MS,
audioCapabilities);
} }
/** /**
@ -160,13 +164,17 @@ public class HlsChunkSource {
* for a switch to a higher quality variant to be considered. * for a switch to a higher quality variant to be considered.
* @param maxBufferDurationToSwitchDownMs The maximum duration of media that needs to be buffered * @param maxBufferDurationToSwitchDownMs The maximum duration of media that needs to be buffered
* for a switch to a lower quality variant to be considered. * for a switch to a lower quality variant to be considered.
* @param audioCapabilities The audio capabilities for playback on this device, or {@code null} if
* the default capabilities should be assumed.
*/ */
public HlsChunkSource(DataSource dataSource, String playlistUrl, HlsPlaylist playlist, public HlsChunkSource(DataSource dataSource, String playlistUrl, HlsPlaylist playlist,
BandwidthMeter bandwidthMeter, int[] variantIndices, int adaptiveMode, BandwidthMeter bandwidthMeter, int[] variantIndices, int adaptiveMode,
long minBufferDurationToSwitchUpMs, long maxBufferDurationToSwitchDownMs) { long minBufferDurationToSwitchUpMs, long maxBufferDurationToSwitchDownMs,
AudioCapabilities audioCapabilities) {
this.dataSource = dataSource; this.dataSource = dataSource;
this.bandwidthMeter = bandwidthMeter; this.bandwidthMeter = bandwidthMeter;
this.adaptiveMode = adaptiveMode; this.adaptiveMode = adaptiveMode;
this.audioCapabilities = audioCapabilities;
minBufferDurationToSwitchUpUs = minBufferDurationToSwitchUpMs * 1000; minBufferDurationToSwitchUpUs = minBufferDurationToSwitchUpMs * 1000;
maxBufferDurationToSwitchDownUs = maxBufferDurationToSwitchDownMs * 1000; maxBufferDurationToSwitchDownUs = maxBufferDurationToSwitchDownMs * 1000;
baseUri = playlist.baseUri; baseUri = playlist.baseUri;
@ -235,16 +243,14 @@ public class HlsChunkSource {
public Chunk getChunkOperation(TsChunk previousTsChunk, long seekPositionUs, public Chunk getChunkOperation(TsChunk previousTsChunk, long seekPositionUs,
long playbackPositionUs) { long playbackPositionUs) {
int nextFormatIndex; int nextFormatIndex;
boolean switchingVariant;
boolean switchingVariantSpliced; boolean switchingVariantSpliced;
if (adaptiveMode == ADAPTIVE_MODE_NONE) { if (adaptiveMode == ADAPTIVE_MODE_NONE) {
nextFormatIndex = formatIndex; nextFormatIndex = formatIndex;
switchingVariant = false;
switchingVariantSpliced = false; switchingVariantSpliced = false;
} else { } else {
nextFormatIndex = getNextFormatIndex(previousTsChunk, playbackPositionUs); nextFormatIndex = getNextFormatIndex(previousTsChunk, playbackPositionUs);
switchingVariant = nextFormatIndex != formatIndex; switchingVariantSpliced = nextFormatIndex != formatIndex
switchingVariantSpliced = switchingVariant && adaptiveMode == ADAPTIVE_MODE_SPLICE; && adaptiveMode == ADAPTIVE_MODE_SPLICE;
} }
int variantIndex = getVariantIndex(enabledFormats[nextFormatIndex]); int variantIndex = getVariantIndex(enabledFormats[nextFormatIndex]);
@ -331,10 +337,11 @@ public class HlsChunkSource {
// Configure the extractor that will read the chunk. // Configure the extractor that will read the chunk.
HlsExtractorWrapper extractorWrapper; HlsExtractorWrapper extractorWrapper;
if (previousTsChunk == null || segment.discontinuity || switchingVariant || liveDiscontinuity) { if (previousTsChunk == null || segment.discontinuity || !format.equals(previousTsChunk.format)
|| liveDiscontinuity) {
Extractor extractor = chunkUri.getLastPathSegment().endsWith(AAC_FILE_EXTENSION) Extractor extractor = chunkUri.getLastPathSegment().endsWith(AAC_FILE_EXTENSION)
? new AdtsExtractor(startTimeUs) ? new AdtsExtractor(startTimeUs)
: new TsExtractor(startTimeUs); : new TsExtractor(startTimeUs, audioCapabilities);
extractorWrapper = new HlsExtractorWrapper(trigger, format, startTimeUs, extractor, extractorWrapper = new HlsExtractorWrapper(trigger, format, startTimeUs, extractor,
switchingVariantSpliced); switchingVariantSpliced);
} else { } else {

View File

@ -125,15 +125,12 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
} }
@Override @Override
public boolean prepare() throws IOException { public boolean prepare(long positionUs) throws IOException {
if (prepared) { if (prepared) {
return true; return true;
} }
if (loader == null) {
loader = new Loader("Loader:HLS");
}
continueBufferingInternal();
if (!extractors.isEmpty()) { if (!extractors.isEmpty()) {
// We're not prepared, but we might have loaded what we need.
HlsExtractorWrapper extractor = extractors.getFirst(); HlsExtractorWrapper extractor = extractors.getFirst();
if (extractor.isPrepared()) { if (extractor.isPrepared()) {
trackCount = extractor.getTrackCount(); trackCount = extractor.getTrackCount();
@ -146,12 +143,23 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
trackInfos[i] = new TrackInfo(format.mimeType, chunkSource.getDurationUs()); trackInfos[i] = new TrackInfo(format.mimeType, chunkSource.getDurationUs());
} }
prepared = true; prepared = true;
return true;
} }
} }
if (!prepared) { // We're not prepared and we haven't loaded what we need.
if (loader == null) {
loader = new Loader("Loader:HLS");
}
if (!loader.isLoading()) {
// We're going to have to start loading a chunk to get what we need for preparation. We should
// attempt to load the chunk at positionUs, so that we'll already be loading the correct chunk
// in the common case where the renderer is subsequently enabled at this position.
pendingResetPositionUs = positionUs;
downstreamPositionUs = positionUs;
}
maybeStartLoading();
maybeThrowLoadableException(); maybeThrowLoadableException();
} return false;
return prepared;
} }
@Override @Override
@ -345,7 +353,12 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
if (!currentLoadableExceptionFatal) { if (!currentLoadableExceptionFatal) {
clearCurrentLoadable(); clearCurrentLoadable();
} }
if (enabledTrackCount > 0) {
maybeStartLoading(); maybeStartLoading();
} else {
clearState();
allocator.trim(0);
}
} }
@Override @Override

View File

@ -90,9 +90,9 @@ public class MetadataTrackRenderer<T> extends TrackRenderer implements Callback
} }
@Override @Override
protected int doPrepare() throws ExoPlaybackException { protected int doPrepare(long positionUs) throws ExoPlaybackException {
try { try {
boolean sourcePrepared = source.prepare(); boolean sourcePrepared = source.prepare(positionUs);
if (!sourcePrepared) { if (!sourcePrepared) {
return TrackRenderer.STATE_UNPREPARED; return TrackRenderer.STATE_UNPREPARED;
} }

View File

@ -80,9 +80,9 @@ public class TextTrackRenderer extends TrackRenderer implements Callback {
} }
@Override @Override
protected int doPrepare() throws ExoPlaybackException { protected int doPrepare(long positionUs) throws ExoPlaybackException {
try { try {
boolean sourcePrepared = source.prepare(); boolean sourcePrepared = source.prepare(positionUs);
if (!sourcePrepared) { if (!sourcePrepared) {
return TrackRenderer.STATE_UNPREPARED; return TrackRenderer.STATE_UNPREPARED;
} }

View File

@ -90,9 +90,9 @@ public class Eia608TrackRenderer extends TrackRenderer implements Callback {
} }
@Override @Override
protected int doPrepare() throws ExoPlaybackException { protected int doPrepare(long positionUs) throws ExoPlaybackException {
try { try {
boolean sourcePrepared = source.prepare(); boolean sourcePrepared = source.prepare(positionUs);
if (!sourcePrepared) { if (!sourcePrepared) {
return TrackRenderer.STATE_UNPREPARED; return TrackRenderer.STATE_UNPREPARED;
} }

View File

@ -18,7 +18,24 @@ package com.google.android.exoplayer.upstream;
/** /**
* Provides estimates of the currently available bandwidth. * Provides estimates of the currently available bandwidth.
*/ */
public interface BandwidthMeter { public interface BandwidthMeter extends TransferListener {
/**
* Interface definition for a callback to be notified of {@link BandwidthMeter} events.
*/
public interface EventListener {
/**
* Invoked periodically to indicate that bytes have been transferred.
*
* @param elapsedMs The time taken to transfer the bytes, in milliseconds.
* @param bytes The number of bytes transferred.
* @param bitrate The estimated bitrate in bits/sec, or {@link #NO_ESTIMATE} if no estimate
* is available. Note that this estimate is typically derived from more information than
* {@code bytes} and {@code elapsedMs}.
*/
void onBandwidthSample(int elapsedMs, long bytes, long bitrate);
}
/** /**
* Indicates no bandwidth estimate is available. * Indicates no bandwidth estimate is available.

View File

@ -26,25 +26,7 @@ import android.os.Handler;
* Counts transferred bytes while transfers are open and creates a bandwidth sample and updated * Counts transferred bytes while transfers are open and creates a bandwidth sample and updated
* bandwidth estimate each time a transfer ends. * bandwidth estimate each time a transfer ends.
*/ */
public class DefaultBandwidthMeter implements BandwidthMeter, TransferListener { public class DefaultBandwidthMeter implements BandwidthMeter {
/**
* Interface definition for a callback to be notified of {@link DefaultBandwidthMeter} events.
*/
public interface EventListener {
/**
* Invoked periodically to indicate that bytes have been transferred.
*
* @param elapsedMs The time taken to transfer the bytes, in milliseconds.
* @param bytes The number of bytes transferred.
* @param bitrate The estimated bitrate in bits/sec, or {@link #NO_ESTIMATE} if no estimate
* is available. Note that this estimate is typically derived from more information than
* {@code bytes} and {@code elapsedMs}.
*/
void onBandwidthSample(int elapsedMs, long bytes, long bitrate);
}
private static final int DEFAULT_MAX_WEIGHT = 2000; private static final int DEFAULT_MAX_WEIGHT = 2000;

View File

@ -28,6 +28,8 @@ import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.InterruptedIOException; import java.io.InterruptedIOException;
import java.net.HttpURLConnection; import java.net.HttpURLConnection;
import java.net.NoRouteToHostException;
import java.net.ProtocolException;
import java.net.URL; import java.net.URL;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
@ -38,17 +40,30 @@ import java.util.regex.Pattern;
/** /**
* A {@link HttpDataSource} that uses Android's {@link HttpURLConnection}. * A {@link HttpDataSource} that uses Android's {@link HttpURLConnection}.
* <p>
* By default this implementation will not follow cross-protocol redirects (i.e. redirects from
* HTTP to HTTPS or vice versa). Cross-protocol redirects can be enabled by using the
* {@link #DefaultHttpDataSource(String, Predicate, TransferListener, int, int, boolean)}
* constructor and passing {@code true} as the final argument.
*/ */
public class DefaultHttpDataSource implements HttpDataSource { public class DefaultHttpDataSource implements HttpDataSource {
/**
* The default connection timeout, in milliseconds.
*/
public static final int DEFAULT_CONNECT_TIMEOUT_MILLIS = 8 * 1000; public static final int DEFAULT_CONNECT_TIMEOUT_MILLIS = 8 * 1000;
/**
* The default read timeout, in milliseconds.
*/
public static final int DEFAULT_READ_TIMEOUT_MILLIS = 8 * 1000; public static final int DEFAULT_READ_TIMEOUT_MILLIS = 8 * 1000;
private static final int MAX_REDIRECTS = 20; // Same limit as okhttp.
private static final String TAG = "HttpDataSource"; private static final String TAG = "HttpDataSource";
private static final Pattern CONTENT_RANGE_HEADER = private static final Pattern CONTENT_RANGE_HEADER =
Pattern.compile("^bytes (\\d+)-(\\d+)/(\\d+)$"); Pattern.compile("^bytes (\\d+)-(\\d+)/(\\d+)$");
private static final AtomicReference<byte[]> skipBufferReference = new AtomicReference<byte[]>(); private static final AtomicReference<byte[]> skipBufferReference = new AtomicReference<byte[]>();
private final boolean allowCrossProtocolRedirects;
private final int connectTimeoutMillis; private final int connectTimeoutMillis;
private final int readTimeoutMillis; private final int readTimeoutMillis;
private final String userAgent; private final String userAgent;
@ -103,12 +118,33 @@ public class DefaultHttpDataSource implements HttpDataSource {
*/ */
public DefaultHttpDataSource(String userAgent, Predicate<String> contentTypePredicate, public DefaultHttpDataSource(String userAgent, Predicate<String> contentTypePredicate,
TransferListener listener, int connectTimeoutMillis, int readTimeoutMillis) { TransferListener listener, int connectTimeoutMillis, int readTimeoutMillis) {
this(userAgent, contentTypePredicate, listener, connectTimeoutMillis, readTimeoutMillis, false);
}
/**
* @param userAgent The User-Agent string that should be used.
* @param contentTypePredicate An optional {@link Predicate}. If a content type is
* rejected by the predicate then a {@link HttpDataSource.InvalidContentTypeException} is
* thrown from {@link #open(DataSpec)}.
* @param listener An optional listener.
* @param connectTimeoutMillis The connection timeout, in milliseconds. A timeout of zero is
* interpreted as an infinite timeout. Pass {@link #DEFAULT_CONNECT_TIMEOUT_MILLIS} to use
* the default value.
* @param readTimeoutMillis The read timeout, in milliseconds. A timeout of zero is interpreted
* as an infinite timeout. Pass {@link #DEFAULT_READ_TIMEOUT_MILLIS} to use the default value.
* @param allowCrossProtocolRedirects Whether cross-protocol redirects (i.e. redirects from HTTP
* to HTTPS and vice versa) are enabled.
*/
public DefaultHttpDataSource(String userAgent, Predicate<String> contentTypePredicate,
TransferListener listener, int connectTimeoutMillis, int readTimeoutMillis,
boolean allowCrossProtocolRedirects) {
this.userAgent = Assertions.checkNotEmpty(userAgent); this.userAgent = Assertions.checkNotEmpty(userAgent);
this.contentTypePredicate = contentTypePredicate; this.contentTypePredicate = contentTypePredicate;
this.listener = listener; this.listener = listener;
this.requestProperties = new HashMap<String, String>(); this.requestProperties = new HashMap<String, String>();
this.connectTimeoutMillis = connectTimeoutMillis; this.connectTimeoutMillis = connectTimeoutMillis;
this.readTimeoutMillis = readTimeoutMillis; this.readTimeoutMillis = readTimeoutMillis;
this.allowCrossProtocolRedirects = allowCrossProtocolRedirects;
} }
@Override @Override
@ -283,8 +319,58 @@ public class DefaultHttpDataSource implements HttpDataSource {
return bytesToRead == C.LENGTH_UNBOUNDED ? bytesToRead : bytesToRead - bytesRead; return bytesToRead == C.LENGTH_UNBOUNDED ? bytesToRead : bytesToRead - bytesRead;
} }
/**
* Establishes a connection, following redirects to do so where permitted.
*/
private HttpURLConnection makeConnection(DataSpec dataSpec) throws IOException { private HttpURLConnection makeConnection(DataSpec dataSpec) throws IOException {
URL url = new URL(dataSpec.uri.toString()); URL url = new URL(dataSpec.uri.toString());
long position = dataSpec.position;
long length = dataSpec.length;
boolean allowGzip = (dataSpec.flags & DataSpec.FLAG_ALLOW_GZIP) != 0;
if (!allowCrossProtocolRedirects) {
// HttpURLConnection disallows cross-protocol redirects, but otherwise performs redirection
// automatically. This is the behavior we want, so use it.
HttpURLConnection connection = configureConnection(url, position, length, allowGzip);
connection.connect();
return connection;
}
// We need to handle redirects ourselves to allow cross-protocol redirects.
int redirectCount = 0;
while (redirectCount++ <= MAX_REDIRECTS) {
HttpURLConnection connection = configureConnection(url, position, length, allowGzip);
connection.setInstanceFollowRedirects(false);
connection.connect();
int responseCode = connection.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_MULT_CHOICE
|| responseCode == HttpURLConnection.HTTP_MOVED_PERM
|| responseCode == HttpURLConnection.HTTP_MOVED_TEMP
|| responseCode == HttpURLConnection.HTTP_SEE_OTHER
|| responseCode == 307 /* HTTP_TEMP_REDIRECT */
|| responseCode == 308 /* HTTP_PERM_REDIRECT */) {
String location = connection.getHeaderField("Location");
connection.disconnect();
url = handleRedirect(url, location);
} else {
return connection;
}
}
// If we get here we've been redirected more times than are permitted.
throw new NoRouteToHostException("Too many redirects: " + redirectCount);
}
/**
* Configures a connection, but does not open it.
*
* @param url The url to connect to.
* @param position The byte offset of the requested data.
* @param length The length of the requested data, or {@link C#LENGTH_UNBOUNDED}.
* @param allowGzip Whether to allow the use of gzip.
*/
private HttpURLConnection configureConnection(URL url, long position, long length,
boolean allowGzip) throws IOException {
HttpURLConnection connection = (HttpURLConnection) url.openConnection(); HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setConnectTimeout(connectTimeoutMillis); connection.setConnectTimeout(connectTimeoutMillis);
connection.setReadTimeout(readTimeoutMillis); connection.setReadTimeout(readTimeoutMillis);
@ -294,28 +380,56 @@ public class DefaultHttpDataSource implements HttpDataSource {
connection.setRequestProperty(property.getKey(), property.getValue()); connection.setRequestProperty(property.getKey(), property.getValue());
} }
} }
setRangeHeader(connection, dataSpec); if (!(position == 0 && length == C.LENGTH_UNBOUNDED)) {
connection.setRequestProperty("User-Agent", userAgent); String rangeRequest = "bytes=" + position + "-";
if ((dataSpec.flags & DataSpec.FLAG_ALLOW_GZIP) == 0) { if (length != C.LENGTH_UNBOUNDED) {
connection.setRequestProperty("Accept-Encoding", "identity"); rangeRequest += (position + length - 1);
}
connection.connect();
return connection;
}
private void setRangeHeader(HttpURLConnection connection, DataSpec dataSpec) {
if (dataSpec.position == 0 && dataSpec.length == C.LENGTH_UNBOUNDED) {
// Not required.
return;
}
String rangeRequest = "bytes=" + dataSpec.position + "-";
if (dataSpec.length != C.LENGTH_UNBOUNDED) {
rangeRequest += (dataSpec.position + dataSpec.length - 1);
} }
connection.setRequestProperty("Range", rangeRequest); connection.setRequestProperty("Range", rangeRequest);
} }
connection.setRequestProperty("User-Agent", userAgent);
if (!allowGzip) {
connection.setRequestProperty("Accept-Encoding", "identity");
}
return connection;
}
private long getContentLength(HttpURLConnection connection) { /**
* Handles a redirect.
*
* @param originalUrl The original URL.
* @param location The Location header in the response.
* @return The next URL.
* @throws IOException If redirection isn't possible.
*/
private static URL handleRedirect(URL originalUrl, String location) throws IOException {
if (location == null) {
throw new ProtocolException("Null location redirect");
}
// Form the new url.
URL url = new URL(originalUrl, location);
// Check that the protocol of the new url is supported.
String protocol = url.getProtocol();
if (!"https".equals(protocol) && !"http".equals(protocol)) {
throw new ProtocolException("Unsupported protocol redirect: " + protocol);
}
// Currently this method is only called if allowCrossProtocolRedirects is true, and so the code
// below isn't required. If we ever decide to handle redirects ourselves when cross-protocol
// redirects are disabled, we'll need to uncomment this block of code.
// if (!allowCrossProtocolRedirects && !protocol.equals(originalUrl.getProtocol())) {
// throw new ProtocolException("Disallowed cross-protocol redirect ("
// + originalUrl.getProtocol() + " to " + protocol + ")");
// }
return url;
}
/**
* Attempts to extract the length of the content from the response headers of an open connection.
*
* @param connection The open connection.
* @return The extracted length, or {@link C#LENGTH_UNBOUNDED}.
*/
private static long getContentLength(HttpURLConnection connection) {
long contentLength = C.LENGTH_UNBOUNDED; long contentLength = C.LENGTH_UNBOUNDED;
String contentLengthHeader = connection.getHeaderField("Content-Length"); String contentLengthHeader = connection.getHeaderField("Content-Length");
if (!TextUtils.isEmpty(contentLengthHeader)) { if (!TextUtils.isEmpty(contentLengthHeader)) {
@ -429,6 +543,9 @@ public class DefaultHttpDataSource implements HttpDataSource {
return read; return read;
} }
/**
* Closes the current connection, if there is one.
*/
private void closeConnection() { private void closeConnection() {
if (connection != null) { if (connection != null) {
connection.disconnect(); connection.disconnect();

View File

@ -36,15 +36,36 @@ public final class DefaultUriDataSource implements UriDataSource {
private UriDataSource dataSource; private UriDataSource dataSource;
/** /**
* Constructs a new data source that delegates to a {@link FileDataSource} for file URIs and an * Constructs a new data source that delegates to a {@link FileDataSource} for file URIs and a
* {@link DefaultHttpDataSource} for other URIs. * {@link DefaultHttpDataSource} for other URIs.
* <p>
* The constructed instance will not follow cross-protocol redirects (i.e. redirects from HTTP to
* HTTPS or vice versa) when fetching remote data. Cross-protocol redirects can be enabled by
* using the {@link #DefaultUriDataSource(String, TransferListener, boolean)} constructor and
* passing {@code true} as the final argument.
* *
* @param userAgent The User-Agent string that should be used when requesting remote data. * @param userAgent The User-Agent string that should be used when requesting remote data.
* @param transferListener An optional listener. * @param transferListener An optional listener.
*/ */
public DefaultUriDataSource(String userAgent, TransferListener transferListener) { public DefaultUriDataSource(String userAgent, TransferListener transferListener) {
this(userAgent, transferListener, false);
}
/**
* Constructs a new data source that delegates to a {@link FileDataSource} for file URIs and a
* {@link DefaultHttpDataSource} for other URIs.
*
* @param userAgent The User-Agent string that should be used when requesting remote data.
* @param transferListener An optional listener.
* @param allowCrossProtocolRedirects Whether cross-protocol redirects (i.e. redirects from HTTP
* to HTTPS and vice versa) are enabled when fetching remote data..
*/
public DefaultUriDataSource(String userAgent, TransferListener transferListener,
boolean allowCrossProtocolRedirects) {
this(new FileDataSource(transferListener), this(new FileDataSource(transferListener),
new DefaultHttpDataSource(userAgent, null, transferListener)); new DefaultHttpDataSource(userAgent, null, transferListener,
DefaultHttpDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS,
DefaultHttpDataSource.DEFAULT_READ_TIMEOUT_MILLIS, allowCrossProtocolRedirects));
} }
/** /**

View File

@ -15,6 +15,11 @@
*/ */
package com.google.android.exoplayer.util; package com.google.android.exoplayer.util;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.audio.AudioCapabilities;
import android.media.AudioFormat;
/** /**
* Defines common MIME types and helper methods. * Defines common MIME types and helper methods.
*/ */
@ -119,4 +124,37 @@ public class MimeTypes {
return mimeType.equals(APPLICATION_TTML); return mimeType.equals(APPLICATION_TTML);
} }
/**
* Returns the output audio encoding that will result from processing input in {@code mimeType}.
* For non-passthrough audio formats, this is always {@link AudioFormat#ENCODING_PCM_16BIT}. For
* passthrough formats it will be one of {@link AudioFormat}'s other {@code ENCODING_*} constants.
* For non-audio formats, {@link AudioFormat#ENCODING_INVALID} will be returned.
*
* @param mimeType The MIME type of media that will be decoded (or passed through).
* @return The corresponding {@link AudioFormat} encoding.
*/
public static int getEncodingForMimeType(String mimeType) {
if (AUDIO_AC3.equals(mimeType)) {
return C.ENCODING_AC3;
}
if (AUDIO_EC3.equals(mimeType)) {
return C.ENCODING_E_AC3;
}
// All other audio formats will be decoded to 16-bit PCM.
return isAudio(mimeType) ? AudioFormat.ENCODING_PCM_16BIT : AudioFormat.ENCODING_INVALID;
}
/**
* Returns whether the specified {@code mimeType} represents audio that can be played via
* passthrough if the device supports it.
*
* @param mimeType The MIME type of input media.
* @return Whether the audio can be played via passthrough. If this method returns {@code true},
* it is still necessary to check the {@link AudioCapabilities} for device support.
*/
public static boolean isPassthroughAudio(String mimeType) {
return AUDIO_AC3.equals(mimeType) || AUDIO_EC3.equals(mimeType);
}
} }

View File

@ -8,5 +8,5 @@
# project structure. # project structure.
# Project target. # Project target.
target=android-21 target=android-22
android.library=true android.library=true

View File

@ -17,7 +17,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.google.android.exoplayer.tests"> package="com.google.android.exoplayer.tests">
<uses-sdk android:minSdkVersion="9" android:targetSdkVersion="21"/> <uses-sdk android:minSdkVersion="9" android:targetSdkVersion="22"/>
<application> <application>
<uses-library android:name="android.test.runner"/> <uses-library android:name="android.test.runner"/>

View File

@ -579,7 +579,7 @@ public class Mp4ExtractorTest extends TestCase {
try { try {
switch (message.what) { switch (message.what) {
case MSG_PREPARE: case MSG_PREPARE:
if (!source.prepare()) { if (!source.prepare(0)) {
sendEmptyMessage(MSG_PREPARE); sendEmptyMessage(MSG_PREPARE);
} else { } else {
// Select the video track and get its metadata. // Select the video track and get its metadata.

View File

@ -11,4 +11,4 @@
#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt #proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
# Project target. # Project target.
target=android-21 target=android-22