diff --git a/build.gradle b/build.gradle index 2864587d3f..a342c5673b 100644 --- a/build.gradle +++ b/build.gradle @@ -20,7 +20,7 @@ buildscript { jcenter() } 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' } } diff --git a/demo/build.gradle b/demo/build.gradle index b7c53a67c8..5a95cfff41 100644 --- a/demo/build.gradle +++ b/demo/build.gradle @@ -14,12 +14,12 @@ apply plugin: 'com.android.application' android { - compileSdkVersion 21 - buildToolsVersion "21.1.2" + compileSdkVersion 22 + buildToolsVersion "22.0.1" defaultConfig { minSdkVersion 16 - targetSdkVersion 21 + targetSdkVersion 22 } buildTypes { release { diff --git a/demo/src/main/AndroidManifest.xml b/demo/src/main/AndroidManifest.xml index 3a233ef204..ffe34972a6 100644 --- a/demo/src/main/AndroidManifest.xml +++ b/demo/src/main/AndroidManifest.xml @@ -25,7 +25,7 @@ - + requestProperties) - throws MalformedURLException, IOException { + throws IOException { HttpURLConnection urlConnection = null; try { urlConnection = (HttpURLConnection) new URL(url).openConnection(); diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/PlayerActivity.java b/demo/src/main/java/com/google/android/exoplayer/demo/PlayerActivity.java index 7f17e98a59..e824ed0115 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/PlayerActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/PlayerActivity.java @@ -229,7 +229,8 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback, return new DashRendererBuilder(this, userAgent, contentUri.toString(), new WidevineTestMediaDrmCallback(contentId), debugTextView, audioCapabilities); 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_MP4: return new ExtractorRendererBuilder(userAgent, contentUri, debugTextView, @@ -239,7 +240,7 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback, new Mp3Extractor()); case DemoUtil.TYPE_TS: return new ExtractorRendererBuilder(userAgent, contentUri, debugTextView, - new TsExtractor()); + new TsExtractor(0, audioCapabilities)); case DemoUtil.TYPE_AAC: return new ExtractorRendererBuilder(userAgent, contentUri, debugTextView, new AdtsExtractor()); diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/Samples.java b/demo/src/main/java/com/google/android/exoplayer/demo/Samples.java index d053b5708c..f68bb58892 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/Samples.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/Samples.java @@ -130,8 +130,6 @@ import java.util.Locale; public static final Sample[] MISC = new Sample[] { new Sample("Dizzy", "http://html5demos.com/assets/dizzy.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/" + "streaming/examples/bipbop_4x3/gear0/fileSequence0.aac", DemoUtil.TYPE_AAC), diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/WidevineTestMediaDrmCallback.java b/demo/src/main/java/com/google/android/exoplayer/demo/WidevineTestMediaDrmCallback.java index 378c74c202..1f29f6b7a1 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/WidevineTestMediaDrmCallback.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/WidevineTestMediaDrmCallback.java @@ -22,8 +22,6 @@ import android.media.MediaDrm.KeyRequest; import android.media.MediaDrm.ProvisionRequest; import android.text.TextUtils; -import org.apache.http.client.ClientProtocolException; - import java.io.IOException; import java.util.UUID; @@ -43,8 +41,7 @@ public class WidevineTestMediaDrmCallback implements MediaDrmCallback { } @Override - public byte[] executeProvisionRequest(UUID uuid, ProvisionRequest request) - throws ClientProtocolException, IOException { + public byte[] executeProvisionRequest(UUID uuid, ProvisionRequest request) throws IOException { String url = request.getDefaultUrl() + "&signedRequest=" + new String(request.getData()); return DemoUtil.executePost(url, null, null); } diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/player/DashRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/player/DashRendererBuilder.java index 26933cf5e4..2b0de68c3d 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/player/DashRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/player/DashRendererBuilder.java @@ -59,7 +59,6 @@ import com.google.android.exoplayer.util.Util; import android.annotation.TargetApi; import android.content.Context; -import android.media.AudioFormat; import android.media.MediaCodec; import android.media.UnsupportedSchemeException; import android.os.Handler; @@ -249,7 +248,6 @@ public class DashRendererBuilder implements RendererBuilder, // Build the audio chunk sources. List audioChunkSourceList = new ArrayList(); List audioTrackNameList = new ArrayList(); - int audioEncoding = AudioFormat.ENCODING_PCM_16BIT; if (audioAdaptationSet != null) { DataSource audioDataSource = new DefaultUriDataSource(userAgent, bandwidthMeter); FormatEvaluator audioEvaluator = new FormatEvaluator.FixedEvaluator(); @@ -275,7 +273,6 @@ public class DashRendererBuilder implements RendererBuilder, continue; } - audioEncoding = encoding; for (int j = audioRepresentations.size() - 1; j >= 0; j--) { if (!audioRepresentations.get(j).format.codecs.equals(codec)) { audioTrackNameList.remove(j); @@ -303,7 +300,7 @@ public class DashRendererBuilder implements RendererBuilder, AUDIO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true, mainHandler, player, DemoPlayer.TYPE_AUDIO); audioRenderer = new MediaCodecAudioTrackRenderer(audioSampleSource, drmSessionManager, true, - mainHandler, player, audioEncoding); + mainHandler, player); } // Build the text chunk sources. diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/player/DebugTrackRenderer.java b/demo/src/main/java/com/google/android/exoplayer/demo/player/DebugTrackRenderer.java index fe76f22044..2a846f46b2 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/player/DebugTrackRenderer.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/player/DebugTrackRenderer.java @@ -57,7 +57,7 @@ import android.widget.TextView; } @Override - protected int doPrepare() throws ExoPlaybackException { + protected int doPrepare(long positionUs) throws ExoPlaybackException { maybeFail(); return STATE_PREPARED; } diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/player/HlsRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/player/HlsRendererBuilder.java index de1b087be8..171433c0d8 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/player/HlsRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/player/HlsRendererBuilder.java @@ -19,6 +19,7 @@ import com.google.android.exoplayer.MediaCodecAudioTrackRenderer; import com.google.android.exoplayer.MediaCodecUtil.DecoderQueryException; import com.google.android.exoplayer.MediaCodecVideoTrackRenderer; 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.demo.player.DemoPlayer.RendererBuilder; import com.google.android.exoplayer.demo.player.DemoPlayer.RendererBuilderCallback; @@ -56,15 +57,18 @@ public class HlsRendererBuilder implements RendererBuilder, ManifestCallback - + diff --git a/library/src/main/java/com/google/android/exoplayer/DummyTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/DummyTrackRenderer.java index 4dd5ef4a42..39aec48781 100644 --- a/library/src/main/java/com/google/android/exoplayer/DummyTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/DummyTrackRenderer.java @@ -18,14 +18,14 @@ package com.google.android.exoplayer; /** * A {@link TrackRenderer} that does nothing. *

- * This renderer returns {@link TrackRenderer#STATE_IGNORE} from {@link #doPrepare()} in order to - * request that it should be ignored. {@link IllegalStateException} is thrown from all methods that - * are documented to indicate that they should not be invoked unless the renderer is prepared. + * This renderer returns {@link TrackRenderer#STATE_IGNORE} from {@link #doPrepare(long)} in order + * to request that it should be ignored. {@link IllegalStateException} is thrown from all methods + * that are documented to indicate that they should not be invoked unless the renderer is prepared. */ public class DummyTrackRenderer extends TrackRenderer { @Override - protected int doPrepare() throws ExoPlaybackException { + protected int doPrepare(long positionUs) throws ExoPlaybackException { return STATE_IGNORE; } diff --git a/library/src/main/java/com/google/android/exoplayer/ExoPlayer.java b/library/src/main/java/com/google/android/exoplayer/ExoPlayer.java index b6ec3c12d1..1eecc358e1 100644 --- a/library/src/main/java/com/google/android/exoplayer/ExoPlayer.java +++ b/library/src/main/java/com/google/android/exoplayer/ExoPlayer.java @@ -101,7 +101,7 @@ public interface ExoPlayer { * The default minimum duration of data that must be buffered for playback to start or resume * 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 diff --git a/library/src/main/java/com/google/android/exoplayer/ExoPlayerImplInternal.java b/library/src/main/java/com/google/android/exoplayer/ExoPlayerImplInternal.java index bf45253b52..2c87cd7133 100644 --- a/library/src/main/java/com/google/android/exoplayer/ExoPlayerImplInternal.java +++ b/library/src/main/java/com/google/android/exoplayer/ExoPlayerImplInternal.java @@ -264,7 +264,7 @@ import java.util.List; boolean prepared = true; for (int i = 0; i < renderers.length; i++) { if (renderers[i].getState() == TrackRenderer.STATE_UNPREPARED) { - int state = renderers[i].prepare(); + int state = renderers[i].prepare(positionUs); if (state == TrackRenderer.STATE_UNPREPARED) { prepared = false; } diff --git a/library/src/main/java/com/google/android/exoplayer/FrameworkSampleSource.java b/library/src/main/java/com/google/android/exoplayer/FrameworkSampleSource.java index 0d3fc61437..8eab665aa8 100644 --- a/library/src/main/java/com/google/android/exoplayer/FrameworkSampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/FrameworkSampleSource.java @@ -128,7 +128,7 @@ public final class FrameworkSampleSource implements SampleSource { } @Override - public boolean prepare() throws IOException { + public boolean prepare(long positionUs) throws IOException { if (!prepared) { extractor = new MediaExtractor(); if (context != null) { diff --git a/library/src/main/java/com/google/android/exoplayer/MediaCodecAudioTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/MediaCodecAudioTrackRenderer.java index ba98373319..48b54db597 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaCodecAudioTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecAudioTrackRenderer.java @@ -21,9 +21,7 @@ import com.google.android.exoplayer.drm.DrmSessionManager; import com.google.android.exoplayer.util.MimeTypes; import android.annotation.TargetApi; -import android.media.AudioFormat; import android.media.MediaCodec; -import android.media.MediaFormat; import android.media.audiofx.Virtualizer; import android.os.Handler; @@ -71,7 +69,6 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer { private final EventListener eventListener; private final AudioTrack audioTrack; - private final int encoding; private int audioSessionId; private long currentPositionUs; @@ -124,50 +121,27 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer { */ public MediaCodecAudioTrackRenderer(SampleSource source, DrmSessionManager drmSessionManager, 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); this.eventListener = eventListener; this.audioSessionId = AudioTrack.SESSION_ID_NOT_SET; this.audioTrack = new AudioTrack(); - this.encoding = encoding; } @Override protected DecoderInfo getDecoderInfo(String mimeType, boolean requiresSecureDecoder) throws DecoderQueryException { - if (encoding == AudioFormat.ENCODING_AC3 || encoding == AudioFormat.ENCODING_E_AC3) { + if (MimeTypes.isPassthroughAudio(mimeType)) { return new DecoderInfo(RAW_DECODER_NAME, true); } return super.getDecoderInfo(mimeType, requiresSecureDecoder); } @Override - protected void configureCodec(MediaCodec codec, String codecName, MediaFormat format, - android.media.MediaCrypto crypto) { + protected void configureCodec(MediaCodec codec, String codecName, + android.media.MediaFormat format, android.media.MediaCrypto crypto) { if (RAW_DECODER_NAME.equals(codecName)) { // 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); codec.configure(format, null, crypto, 0); format.setString(android.media.MediaFormat.KEY_MIME, mimeType); @@ -193,8 +167,13 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer { } @Override - protected void onOutputFormatChanged(MediaFormat format) { - audioTrack.reconfigure(format, encoding, 0); + protected void onOutputFormatChanged(MediaFormat inputFormat, + android.media.MediaFormat outputFormat) { + if (MimeTypes.isPassthroughAudio(inputFormat.mimeType)) { + audioTrack.reconfigure(inputFormat.getFrameworkMediaFormatV16()); + } else { + audioTrack.reconfigure(outputFormat); + } } /** diff --git a/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java index 364a3f4f79..6f8dcbd6be 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java @@ -243,9 +243,9 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { } @Override - protected int doPrepare() throws ExoPlaybackException { + protected int doPrepare(long positionUs) throws ExoPlaybackException { try { - boolean sourcePrepared = source.prepare(); + boolean sourcePrepared = source.prepare(positionUs); if (!sourcePrepared) { return TrackRenderer.STATE_UNPREPARED; } @@ -742,9 +742,11 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { *

* 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. } @@ -818,7 +820,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { } if (outputIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { - onOutputFormatChanged(codec.getOutputFormat()); + onOutputFormatChanged(format, codec.getOutputFormat()); codecCounters.outputFormatChangedCount++; return true; } else if (outputIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) { diff --git a/library/src/main/java/com/google/android/exoplayer/MediaCodecVideoTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/MediaCodecVideoTrackRenderer.java index b843901543..49949f91a2 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaCodecVideoTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecVideoTrackRenderer.java @@ -381,15 +381,17 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer { } @Override - protected void onOutputFormatChanged(android.media.MediaFormat format) { - boolean hasCrop = format.containsKey(KEY_CROP_RIGHT) && format.containsKey(KEY_CROP_LEFT) - && format.containsKey(KEY_CROP_BOTTOM) && format.containsKey(KEY_CROP_TOP); + protected void onOutputFormatChanged(MediaFormat inputFormat, + android.media.MediaFormat outputFormat) { + boolean hasCrop = outputFormat.containsKey(KEY_CROP_RIGHT) + && outputFormat.containsKey(KEY_CROP_LEFT) && outputFormat.containsKey(KEY_CROP_BOTTOM) + && outputFormat.containsKey(KEY_CROP_TOP); currentWidth = hasCrop - ? format.getInteger(KEY_CROP_RIGHT) - format.getInteger(KEY_CROP_LEFT) + 1 - : format.getInteger(android.media.MediaFormat.KEY_WIDTH); + ? outputFormat.getInteger(KEY_CROP_RIGHT) - outputFormat.getInteger(KEY_CROP_LEFT) + 1 + : outputFormat.getInteger(android.media.MediaFormat.KEY_WIDTH); currentHeight = hasCrop - ? format.getInteger(KEY_CROP_BOTTOM) - format.getInteger(KEY_CROP_TOP) + 1 - : format.getInteger(android.media.MediaFormat.KEY_HEIGHT); + ? outputFormat.getInteger(KEY_CROP_BOTTOM) - outputFormat.getInteger(KEY_CROP_TOP) + 1 + : outputFormat.getInteger(android.media.MediaFormat.KEY_HEIGHT); } @Override diff --git a/library/src/main/java/com/google/android/exoplayer/SampleSource.java b/library/src/main/java/com/google/android/exoplayer/SampleSource.java index 9a3d40819b..b7a93e3f65 100644 --- a/library/src/main/java/com/google/android/exoplayer/SampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/SampleSource.java @@ -57,10 +57,11 @@ public interface SampleSource { * 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. * + * @param positionUs The player's current playback position. * @return True if the source was prepared successfully, false otherwise. * @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. diff --git a/library/src/main/java/com/google/android/exoplayer/TrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/TrackRenderer.java index 8d5534e0b7..b28a2fbc9d 100644 --- a/library/src/main/java/com/google/android/exoplayer/TrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/TrackRenderer.java @@ -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 * 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. */ - /* package */ final int prepare() throws ExoPlaybackException { + /* package */ final int prepare(long positionUs) throws ExoPlaybackException { Assertions.checkState(state == TrackRenderer.STATE_UNPREPARED); - state = doPrepare(); + state = doPrepare(positionUs); Assertions.checkState(state == TrackRenderer.STATE_UNPREPARED || state == TrackRenderer.STATE_PREPARED || 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 * make any useful progress. * + * @param positionUs The player's current playback position. * @return The new state of the renderer. One of {@link #STATE_UNPREPARED}, * {@link #STATE_PREPARED} and {@link #STATE_IGNORE}. * @throws ExoPlaybackException If an error occurs. */ - protected abstract int doPrepare() throws ExoPlaybackException; + protected abstract int doPrepare(long positionUs) throws ExoPlaybackException; /** * Enable the renderer. diff --git a/library/src/main/java/com/google/android/exoplayer/audio/AudioTrack.java b/library/src/main/java/com/google/android/exoplayer/audio/AudioTrack.java index 30f8f0dbf7..96271a2774 100644 --- a/library/src/main/java/com/google/android/exoplayer/audio/AudioTrack.java +++ b/library/src/main/java/com/google/android/exoplayer/audio/AudioTrack.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer.audio; import com.google.android.exoplayer.C; import com.google.android.exoplayer.util.Ac3Util; import com.google.android.exoplayer.util.Assertions; +import com.google.android.exoplayer.util.MimeTypes; import com.google.android.exoplayer.util.Util; import android.annotation.TargetApi; @@ -315,24 +316,21 @@ public final class AudioTrack { } /** - * Reconfigures the audio track to play back media in {@code format}. The encoding is assumed to - * be {@link AudioFormat#ENCODING_PCM_16BIT}. + * Reconfigures the audio track to play back media in {@code format}, inferring a buffer size from + * the 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 - * {@link #handleBuffer} must use the specified {@code encoding}, which should be a constant from - * {@link AudioFormat}. + * Reconfigures the audio track to play back media in {@code format}. * * @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 * 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 channelConfig; switch (channelCount) { @@ -353,8 +351,10 @@ public final class AudioTrack { } int sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE); + String mimeType = format.getString(MediaFormat.KEY_MIME); // TODO: Does channelConfig determine channelCount? + int encoding = MimeTypes.getEncodingForMimeType(mimeType); boolean isAc3 = encoding == C.ENCODING_AC3 || encoding == C.ENCODING_E_AC3; if (isInitialized() && this.sampleRate == sampleRate && this.channelConfig == channelConfig && !this.isAc3 && !isAc3) { @@ -423,10 +423,21 @@ public final class AudioTrack { return RESULT_BUFFER_CONSUMED; } - // As a workaround for an issue where an 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 (isAc3 && audioTrack.getPlayState() == android.media.AudioTrack.PLAYSTATE_PAUSED) { - return 0; + // Workarounds for issues with AC-3 passthrough AudioTracks on API versions 21/22: + if (Util.SDK_INT <= 22 && isAc3) { + // 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; + } + + // 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; @@ -637,7 +648,8 @@ public final class AudioTrack { } 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) { // Perform sanity checks on the timestamp. long audioTimestampUs = audioTrackUtil.getTimestampNanoTime() / 1000; @@ -739,7 +751,7 @@ public final class AudioTrack { private static class AudioTrackUtil { protected android.media.AudioTrack audioTrack; - private boolean enablePassthroughWorkaround; + private boolean isPassthrough; private int sampleRate; private long lastRawPlaybackHeadPosition; private long rawPlaybackHeadWrapCount; @@ -749,14 +761,11 @@ public final class AudioTrack { * Reconfigures the audio track utility helper to use the specified {@code audioTrack}. * * @param audioTrack The audio track to wrap. - * @param enablePassthroughWorkaround Whether to work around an issue where the playback head - * position jumps back to zero on a paused passthrough/direct audio track. See - * [Internal: b/19187573]. + * @param isPassthrough Whether the audio track is used for passthrough (e.g. AC-3) playback. */ - public void reconfigure(android.media.AudioTrack audioTrack, - boolean enablePassthroughWorkaround) { + public void reconfigure(android.media.AudioTrack audioTrack, boolean isPassthrough) { this.audioTrack = audioTrack; - this.enablePassthroughWorkaround = enablePassthroughWorkaround; + this.isPassthrough = isPassthrough; lastRawPlaybackHeadPosition = 0; rawPlaybackHeadWrapCount = 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 - * around an issue where AC-3 audio tracks can't be paused, so we empty their buffers when - * paused. In this case, they should still behave as if they have pending data, otherwise - * writing will never resume. + * around an issue on platform API versions 21/22 where AC-3 audio tracks can't be paused, so we + * empty their buffers when paused. In this case, they should still behave as if they have + * pending data, otherwise writing will never resume. * * @see #handleBuffer */ public boolean overrideHasPendingData() { - return enablePassthroughWorkaround + return Util.SDK_INT <= 22 && isPassthrough && audioTrack.getPlayState() == android.media.AudioTrack.PLAYSTATE_PAUSED && audioTrack.getPlaybackHeadPosition() == 0; } @@ -790,8 +799,16 @@ public final class AudioTrack { */ public long getPlaybackHeadPosition() { long rawPlaybackHeadPosition = 0xFFFFFFFFL & audioTrack.getPlaybackHeadPosition(); - if (enablePassthroughWorkaround) { - if (audioTrack.getPlayState() == android.media.AudioTrack.PLAYSTATE_PAUSED + if (Util.SDK_INT <= 22 && isPassthrough) { + // 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) { passthroughWorkaroundPauseOffset = lastRawPlaybackHeadPosition; } @@ -868,9 +885,8 @@ public final class AudioTrack { } @Override - public void reconfigure(android.media.AudioTrack audioTrack, - boolean enablePassthroughWorkaround) { - super.reconfigure(audioTrack, enablePassthroughWorkaround); + public void reconfigure(android.media.AudioTrack audioTrack, boolean isPassthrough) { + super.reconfigure(audioTrack, isPassthrough); rawTimestampFramePositionWrapCount = 0; lastRawTimestampFramePosition = 0; lastTimestampFramePosition = 0; diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java b/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java index bf3f26e4b1..a023a31244 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java @@ -87,7 +87,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback { private long currentLoadStartTimeMs; private MediaFormat downstreamMediaFormat; - private volatile Format downstreamFormat; + private Format downstreamFormat; public ChunkSampleSource(ChunkSource chunkSource, LoadControl loadControl, int bufferSizeContribution, boolean frameAccurateSeeking) { @@ -120,18 +120,8 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback { 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 - public boolean prepare() { + public boolean prepare(long positionUs) { Assertions.checkState(state == STATE_UNPREPARED); loader = new Loader("Loader:" + chunkSource.getTrackInfo().mimeType); state = STATE_PREPARED; diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/FormatEvaluator.java b/library/src/main/java/com/google/android/exoplayer/chunk/FormatEvaluator.java index 2a1cc561bb..7c88ff68a0 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/FormatEvaluator.java +++ b/library/src/main/java/com/google/android/exoplayer/chunk/FormatEvaluator.java @@ -84,7 +84,7 @@ public interface FormatEvaluator { /** * Always selects the first format. */ - public static class FixedEvaluator implements FormatEvaluator { + public static final class FixedEvaluator implements FormatEvaluator { @Override public void enable() { @@ -107,7 +107,7 @@ public interface FormatEvaluator { /** * Selects randomly between the available formats. */ - public static class RandomEvaluator implements FormatEvaluator { + public static final class RandomEvaluator implements FormatEvaluator { private final Random random; @@ -145,7 +145,7 @@ public interface FormatEvaluator { * reference implementation only. It is recommended that application developers implement their * 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; @@ -259,8 +259,9 @@ public interface FormatEvaluator { /** * Compute the ideal format ignoring buffer health. */ - protected Format determineIdealFormat(Format[] formats, long bitrateEstimate) { - long effectiveBitrate = computeEffectiveBitrateEstimate(bitrateEstimate); + private Format determineIdealFormat(Format[] formats, long bitrateEstimate) { + long effectiveBitrate = bitrateEstimate == BandwidthMeter.NO_ESTIMATE + ? maxInitialBitrate : (long) (bitrateEstimate * bandwidthFraction); for (int i = 0; i < formats.length; i++) { Format format = formats[i]; if (format.bitrate <= effectiveBitrate) { @@ -271,14 +272,6 @@ public interface FormatEvaluator { 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); - } - } } diff --git a/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java b/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java index 515a94716a..226c1237c3 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java @@ -419,7 +419,9 @@ public class DashChunkSource implements ChunkSource { (ChunkIndex) initializationChunk.getSeekMap(), 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(); } } diff --git a/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionParser.java b/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionParser.java index 6b6664793c..e509ffeefe 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionParser.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionParser.java @@ -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.util.Assertions; 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.Util; import android.text.TextUtils; +import android.util.Base64; import org.xml.sax.helpers.DefaultHandler; import org.xmlpull.v1.XmlPullParser; @@ -41,6 +43,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; +import java.util.UUID; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -270,11 +273,27 @@ public class MediaPresentationDescriptionParser extends DefaultHandler protected ContentProtection parseContentProtection(XmlPullParser xpp) throws XmlPullParserException, IOException { 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) { - return new ContentProtection(schemeIdUri, null, null); + protected ContentProtection buildContentProtection(String schemeIdUri, UUID uuid, byte[] data) { + return new ContentProtection(schemeIdUri, uuid, data); } /** diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/ExtractorSampleSource.java b/library/src/main/java/com/google/android/exoplayer/extractor/ExtractorSampleSource.java index 59bf9d113c..b84401eb76 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/ExtractorSampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/ExtractorSampleSource.java @@ -72,6 +72,7 @@ public class ExtractorSampleSource implements SampleSource, ExtractorOutput, Loa private boolean prepared; private int enabledTrackCount; private TrackInfo[] trackInfos; + private long maxTrackDurationUs; private boolean[] pendingMediaFormat; private boolean[] pendingDiscontinuities; private boolean[] trackEnabledStates; @@ -137,7 +138,7 @@ public class ExtractorSampleSource implements SampleSource, ExtractorOutput, Loa } @Override - public boolean prepare() throws IOException { + public boolean prepare(long positionUs) throws IOException { if (prepared) { return true; } @@ -156,9 +157,13 @@ public class ExtractorSampleSource implements SampleSource, ExtractorOutput, Loa pendingDiscontinuities = new boolean[trackCount]; pendingMediaFormat = new boolean[trackCount]; trackInfos = new TrackInfo[trackCount]; + maxTrackDurationUs = C.UNKNOWN_TIME_US; for (int i = 0; i < trackCount; i++) { MediaFormat format = sampleQueues.valueAt(i).getFormat(); trackInfos[i] = new TrackInfo(format.mimeType, format.durationUs); + if (format.durationUs != C.UNKNOWN_TIME_US && format.durationUs > maxTrackDurationUs) { + maxTrackDurationUs = format.durationUs; + } } prepared = true; return true; @@ -448,6 +453,11 @@ public class ExtractorSampleSource implements SampleSource, ExtractorOutput, Loa loadable = createLoadableFromStart(); } else { Assertions.checkState(isPendingReset()); + if (maxTrackDurationUs != C.UNKNOWN_TIME_US && pendingResetPositionUs >= maxTrackDurationUs) { + loadingFinished = true; + pendingResetPositionUs = NO_RESET_PENDING; + return; + } loadable = createLoadableFromPositionUs(pendingResetPositionUs); pendingResetPositionUs = NO_RESET_PENDING; } diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/mp3/XingSeeker.java b/library/src/main/java/com/google/android/exoplayer/extractor/mp3/XingSeeker.java index 25fd30f89e..bbc3ae4c8a 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/mp3/XingSeeker.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/mp3/XingSeeker.java @@ -137,7 +137,7 @@ import com.google.android.exoplayer.util.Util; } 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 diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/mp4/AtomParsers.java b/library/src/main/java/com/google/android/exoplayer/extractor/mp4/AtomParsers.java index 2f6fb9fc22..42c3f2e70e 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/mp4/AtomParsers.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/mp4/AtomParsers.java @@ -534,8 +534,18 @@ import java.util.List; childPosition += childAtomSize; } - out.mediaFormat = MediaFormat.createAudioFormat( - MimeTypes.AUDIO_AAC, sampleSize, durationUs, channelCount, sampleRate, + // Set the MIME type for ac-3/ec-3 atoms even if the dac3/dec3 child atom is missing. + 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)); } diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/ts/TsExtractor.java b/library/src/main/java/com/google/android/exoplayer/extractor/ts/TsExtractor.java index cde096dfad..670f3ea8ec 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/ts/TsExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/ts/TsExtractor.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer.extractor.ts; 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.ExtractorInput; 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_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_ID3 = 0x15; 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 SparseBooleanArray streamTypes; + private final SparseBooleanArray allowedPassthroughStreamTypes; private final SparseArray tsPayloadReaders; // Indexed by pid private final long firstSampleTimestampUs; private final ParsableBitArray tsScratch; @@ -62,14 +64,15 @@ public final class TsExtractor implements Extractor, SeekMap { private long lastPts; public TsExtractor() { - this(0); + this(0, null); } - public TsExtractor(long firstSampleTimestampUs) { + public TsExtractor(long firstSampleTimestampUs, AudioCapabilities audioCapabilities) { this.firstSampleTimestampUs = firstSampleTimestampUs; tsScratch = new ParsableBitArray(new byte[3]); tsPacketBuffer = new ParsableByteArray(TS_PACKET_SIZE); streamTypes = new SparseBooleanArray(); + allowedPassthroughStreamTypes = getPassthroughStreamTypes(audioCapabilities); tsPayloadReaders = new SparseArray(); tsPayloadReaders.put(TS_PAT_PID, new PatReader()); lastPts = Long.MIN_VALUE; @@ -174,6 +177,24 @@ public final class TsExtractor implements Extractor, SeekMap { 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. */ @@ -308,13 +329,17 @@ public final class TsExtractor implements Extractor, SeekMap { continue; } + // TODO: Detect and read DVB AC-3 streams with Ac3Reader. ElementaryStreamReader pesPayloadReader = null; switch (streamType) { case TS_STREAM_TYPE_AAC: pesPayloadReader = new AdtsReader(output.track(TS_STREAM_TYPE_AAC)); break; + case TS_STREAM_TYPE_ATSC_E_AC3: case TS_STREAM_TYPE_ATSC_AC3: - case TS_STREAM_TYPE_DVB_AC3: + if (!allowedPassthroughStreamTypes.get(streamType)) { + continue; + } pesPayloadReader = new Ac3Reader(output.track(streamType)); break; case TS_STREAM_TYPE_H264: diff --git a/library/src/main/java/com/google/android/exoplayer/hls/HlsChunkSource.java b/library/src/main/java/com/google/android/exoplayer/hls/HlsChunkSource.java index 2883782af0..2873dfcad0 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/HlsChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/HlsChunkSource.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer.hls; import com.google.android.exoplayer.C; 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.Chunk; import com.google.android.exoplayer.chunk.DataChunk; @@ -125,6 +126,7 @@ public class HlsChunkSource { private final int maxHeight; private final long minBufferDurationToSwitchUpUs; private final long maxBufferDurationToSwitchDownUs; + private final AudioCapabilities audioCapabilities; /* package */ byte[] scratchSpace; /* package */ final HlsMediaPlaylist[] mediaPlaylists; @@ -140,9 +142,11 @@ public class HlsChunkSource { private byte[] encryptionIv; 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, - 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. * @param maxBufferDurationToSwitchDownMs The maximum duration of media that needs to be buffered * 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, BandwidthMeter bandwidthMeter, int[] variantIndices, int adaptiveMode, - long minBufferDurationToSwitchUpMs, long maxBufferDurationToSwitchDownMs) { + long minBufferDurationToSwitchUpMs, long maxBufferDurationToSwitchDownMs, + AudioCapabilities audioCapabilities) { this.dataSource = dataSource; this.bandwidthMeter = bandwidthMeter; this.adaptiveMode = adaptiveMode; + this.audioCapabilities = audioCapabilities; minBufferDurationToSwitchUpUs = minBufferDurationToSwitchUpMs * 1000; maxBufferDurationToSwitchDownUs = maxBufferDurationToSwitchDownMs * 1000; baseUri = playlist.baseUri; @@ -235,16 +243,14 @@ public class HlsChunkSource { public Chunk getChunkOperation(TsChunk previousTsChunk, long seekPositionUs, long playbackPositionUs) { int nextFormatIndex; - boolean switchingVariant; boolean switchingVariantSpliced; if (adaptiveMode == ADAPTIVE_MODE_NONE) { nextFormatIndex = formatIndex; - switchingVariant = false; switchingVariantSpliced = false; } else { nextFormatIndex = getNextFormatIndex(previousTsChunk, playbackPositionUs); - switchingVariant = nextFormatIndex != formatIndex; - switchingVariantSpliced = switchingVariant && adaptiveMode == ADAPTIVE_MODE_SPLICE; + switchingVariantSpliced = nextFormatIndex != formatIndex + && adaptiveMode == ADAPTIVE_MODE_SPLICE; } int variantIndex = getVariantIndex(enabledFormats[nextFormatIndex]); @@ -331,10 +337,11 @@ public class HlsChunkSource { // Configure the extractor that will read the chunk. 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) ? new AdtsExtractor(startTimeUs) - : new TsExtractor(startTimeUs); + : new TsExtractor(startTimeUs, audioCapabilities); extractorWrapper = new HlsExtractorWrapper(trigger, format, startTimeUs, extractor, switchingVariantSpliced); } else { diff --git a/library/src/main/java/com/google/android/exoplayer/hls/HlsSampleSource.java b/library/src/main/java/com/google/android/exoplayer/hls/HlsSampleSource.java index 38239448be..d78b23be67 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/HlsSampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/HlsSampleSource.java @@ -125,15 +125,12 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { } @Override - public boolean prepare() throws IOException { + public boolean prepare(long positionUs) throws IOException { if (prepared) { return true; } - if (loader == null) { - loader = new Loader("Loader:HLS"); - } - continueBufferingInternal(); if (!extractors.isEmpty()) { + // We're not prepared, but we might have loaded what we need. HlsExtractorWrapper extractor = extractors.getFirst(); if (extractor.isPrepared()) { trackCount = extractor.getTrackCount(); @@ -146,12 +143,23 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { trackInfos[i] = new TrackInfo(format.mimeType, chunkSource.getDurationUs()); } prepared = true; + return true; } } - if (!prepared) { - maybeThrowLoadableException(); + // We're not prepared and we haven't loaded what we need. + if (loader == null) { + loader = new Loader("Loader:HLS"); } - return prepared; + 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(); + return false; } @Override @@ -345,7 +353,12 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { if (!currentLoadableExceptionFatal) { clearCurrentLoadable(); } - maybeStartLoading(); + if (enabledTrackCount > 0) { + maybeStartLoading(); + } else { + clearState(); + allocator.trim(0); + } } @Override diff --git a/library/src/main/java/com/google/android/exoplayer/metadata/MetadataTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/metadata/MetadataTrackRenderer.java index 147a222c4f..c6cd9aa458 100644 --- a/library/src/main/java/com/google/android/exoplayer/metadata/MetadataTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/metadata/MetadataTrackRenderer.java @@ -90,9 +90,9 @@ public class MetadataTrackRenderer extends TrackRenderer implements Callback } @Override - protected int doPrepare() throws ExoPlaybackException { + protected int doPrepare(long positionUs) throws ExoPlaybackException { try { - boolean sourcePrepared = source.prepare(); + boolean sourcePrepared = source.prepare(positionUs); if (!sourcePrepared) { return TrackRenderer.STATE_UNPREPARED; } diff --git a/library/src/main/java/com/google/android/exoplayer/text/TextTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/text/TextTrackRenderer.java index 60a3fe84c1..3827e88a26 100644 --- a/library/src/main/java/com/google/android/exoplayer/text/TextTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/text/TextTrackRenderer.java @@ -80,9 +80,9 @@ public class TextTrackRenderer extends TrackRenderer implements Callback { } @Override - protected int doPrepare() throws ExoPlaybackException { + protected int doPrepare(long positionUs) throws ExoPlaybackException { try { - boolean sourcePrepared = source.prepare(); + boolean sourcePrepared = source.prepare(positionUs); if (!sourcePrepared) { return TrackRenderer.STATE_UNPREPARED; } diff --git a/library/src/main/java/com/google/android/exoplayer/text/eia608/Eia608TrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/text/eia608/Eia608TrackRenderer.java index 664b549cf9..48438ce1b4 100644 --- a/library/src/main/java/com/google/android/exoplayer/text/eia608/Eia608TrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/text/eia608/Eia608TrackRenderer.java @@ -90,9 +90,9 @@ public class Eia608TrackRenderer extends TrackRenderer implements Callback { } @Override - protected int doPrepare() throws ExoPlaybackException { + protected int doPrepare(long positionUs) throws ExoPlaybackException { try { - boolean sourcePrepared = source.prepare(); + boolean sourcePrepared = source.prepare(positionUs); if (!sourcePrepared) { return TrackRenderer.STATE_UNPREPARED; } diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/BandwidthMeter.java b/library/src/main/java/com/google/android/exoplayer/upstream/BandwidthMeter.java index 5bbffb6c1f..f8f7eb24af 100644 --- a/library/src/main/java/com/google/android/exoplayer/upstream/BandwidthMeter.java +++ b/library/src/main/java/com/google/android/exoplayer/upstream/BandwidthMeter.java @@ -18,7 +18,24 @@ package com.google.android.exoplayer.upstream; /** * 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. diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/DefaultBandwidthMeter.java b/library/src/main/java/com/google/android/exoplayer/upstream/DefaultBandwidthMeter.java index ce2197e2cf..f2c7ec49b1 100644 --- a/library/src/main/java/com/google/android/exoplayer/upstream/DefaultBandwidthMeter.java +++ b/library/src/main/java/com/google/android/exoplayer/upstream/DefaultBandwidthMeter.java @@ -26,25 +26,7 @@ import android.os.Handler; * Counts transferred bytes while transfers are open and creates a bandwidth sample and updated * bandwidth estimate each time a transfer ends. */ -public class DefaultBandwidthMeter implements BandwidthMeter, TransferListener { - - /** - * 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); - - } +public class DefaultBandwidthMeter implements BandwidthMeter { private static final int DEFAULT_MAX_WEIGHT = 2000; diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/DefaultHttpDataSource.java b/library/src/main/java/com/google/android/exoplayer/upstream/DefaultHttpDataSource.java index ff7c69240c..621c49b32a 100644 --- a/library/src/main/java/com/google/android/exoplayer/upstream/DefaultHttpDataSource.java +++ b/library/src/main/java/com/google/android/exoplayer/upstream/DefaultHttpDataSource.java @@ -28,6 +28,8 @@ import java.io.IOException; import java.io.InputStream; import java.io.InterruptedIOException; import java.net.HttpURLConnection; +import java.net.NoRouteToHostException; +import java.net.ProtocolException; import java.net.URL; import java.util.HashMap; import java.util.List; @@ -38,17 +40,30 @@ import java.util.regex.Pattern; /** * A {@link HttpDataSource} that uses Android's {@link HttpURLConnection}. + *

+ * 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 { + /** + * The default connection timeout, in milliseconds. + */ 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; + private static final int MAX_REDIRECTS = 20; // Same limit as okhttp. private static final String TAG = "HttpDataSource"; private static final Pattern CONTENT_RANGE_HEADER = Pattern.compile("^bytes (\\d+)-(\\d+)/(\\d+)$"); private static final AtomicReference skipBufferReference = new AtomicReference(); + private final boolean allowCrossProtocolRedirects; private final int connectTimeoutMillis; private final int readTimeoutMillis; private final String userAgent; @@ -103,12 +118,33 @@ public class DefaultHttpDataSource implements HttpDataSource { */ public DefaultHttpDataSource(String userAgent, Predicate contentTypePredicate, 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 contentTypePredicate, + TransferListener listener, int connectTimeoutMillis, int readTimeoutMillis, + boolean allowCrossProtocolRedirects) { this.userAgent = Assertions.checkNotEmpty(userAgent); this.contentTypePredicate = contentTypePredicate; this.listener = listener; this.requestProperties = new HashMap(); this.connectTimeoutMillis = connectTimeoutMillis; this.readTimeoutMillis = readTimeoutMillis; + this.allowCrossProtocolRedirects = allowCrossProtocolRedirects; } @Override @@ -283,8 +319,58 @@ public class DefaultHttpDataSource implements HttpDataSource { 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 { 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(); connection.setConnectTimeout(connectTimeoutMillis); connection.setReadTimeout(readTimeoutMillis); @@ -294,28 +380,56 @@ public class DefaultHttpDataSource implements HttpDataSource { connection.setRequestProperty(property.getKey(), property.getValue()); } } - setRangeHeader(connection, dataSpec); + if (!(position == 0 && length == C.LENGTH_UNBOUNDED)) { + String rangeRequest = "bytes=" + position + "-"; + if (length != C.LENGTH_UNBOUNDED) { + rangeRequest += (position + length - 1); + } + connection.setRequestProperty("Range", rangeRequest); + } connection.setRequestProperty("User-Agent", userAgent); - if ((dataSpec.flags & DataSpec.FLAG_ALLOW_GZIP) == 0) { + if (!allowGzip) { connection.setRequestProperty("Accept-Encoding", "identity"); } - connection.connect(); return connection; } - private void setRangeHeader(HttpURLConnection connection, DataSpec dataSpec) { - if (dataSpec.position == 0 && dataSpec.length == C.LENGTH_UNBOUNDED) { - // Not required. - return; + /** + * 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"); } - String rangeRequest = "bytes=" + dataSpec.position + "-"; - if (dataSpec.length != C.LENGTH_UNBOUNDED) { - rangeRequest += (dataSpec.position + dataSpec.length - 1); + // 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); } - connection.setRequestProperty("Range", rangeRequest); + // 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; } - private long getContentLength(HttpURLConnection connection) { + /** + * 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; String contentLengthHeader = connection.getHeaderField("Content-Length"); if (!TextUtils.isEmpty(contentLengthHeader)) { @@ -429,6 +543,9 @@ public class DefaultHttpDataSource implements HttpDataSource { return read; } + /** + * Closes the current connection, if there is one. + */ private void closeConnection() { if (connection != null) { connection.disconnect(); diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/DefaultUriDataSource.java b/library/src/main/java/com/google/android/exoplayer/upstream/DefaultUriDataSource.java index 9c20861bf5..fa225bf266 100644 --- a/library/src/main/java/com/google/android/exoplayer/upstream/DefaultUriDataSource.java +++ b/library/src/main/java/com/google/android/exoplayer/upstream/DefaultUriDataSource.java @@ -36,15 +36,36 @@ public final class DefaultUriDataSource implements UriDataSource { 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. + *

+ * 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 transferListener An optional listener. */ 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), - new DefaultHttpDataSource(userAgent, null, transferListener)); + new DefaultHttpDataSource(userAgent, null, transferListener, + DefaultHttpDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS, + DefaultHttpDataSource.DEFAULT_READ_TIMEOUT_MILLIS, allowCrossProtocolRedirects)); } /** diff --git a/library/src/main/java/com/google/android/exoplayer/util/MimeTypes.java b/library/src/main/java/com/google/android/exoplayer/util/MimeTypes.java index 8ed514174a..3a6ea01c6f 100644 --- a/library/src/main/java/com/google/android/exoplayer/util/MimeTypes.java +++ b/library/src/main/java/com/google/android/exoplayer/util/MimeTypes.java @@ -15,6 +15,11 @@ */ 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. */ @@ -119,4 +124,37 @@ public class MimeTypes { 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); + } + } diff --git a/library/src/main/project.properties b/library/src/main/project.properties index b756f4487f..95228a4fc6 100644 --- a/library/src/main/project.properties +++ b/library/src/main/project.properties @@ -8,5 +8,5 @@ # project structure. # Project target. -target=android-21 +target=android-22 android.library=true diff --git a/library/src/test/AndroidManifest.xml b/library/src/test/AndroidManifest.xml index 517161f3b9..71bb5c3a66 100644 --- a/library/src/test/AndroidManifest.xml +++ b/library/src/test/AndroidManifest.xml @@ -17,7 +17,7 @@ - + diff --git a/library/src/test/java/com/google/android/exoplayer/extractor/mp4/Mp4ExtractorTest.java b/library/src/test/java/com/google/android/exoplayer/extractor/mp4/Mp4ExtractorTest.java index 42bb872b31..9e6c290bec 100644 --- a/library/src/test/java/com/google/android/exoplayer/extractor/mp4/Mp4ExtractorTest.java +++ b/library/src/test/java/com/google/android/exoplayer/extractor/mp4/Mp4ExtractorTest.java @@ -579,7 +579,7 @@ public class Mp4ExtractorTest extends TestCase { try { switch (message.what) { case MSG_PREPARE: - if (!source.prepare()) { + if (!source.prepare(0)) { sendEmptyMessage(MSG_PREPARE); } else { // Select the video track and get its metadata. diff --git a/library/src/test/project.properties b/library/src/test/project.properties index 6e18427a42..00cf62bacc 100644 --- a/library/src/test/project.properties +++ b/library/src/test/project.properties @@ -11,4 +11,4 @@ #proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt # Project target. -target=android-21 +target=android-22