From 3079bf1d7502dd14ae7356a1a4e64523d5ccc5a8 Mon Sep 17 00:00:00 2001 From: Sebastian Roth Date: Thu, 7 May 2015 16:35:53 +0800 Subject: [PATCH 01/19] gradle updates --- build.gradle | 2 +- demo/build.gradle | 6 +++--- library/build.gradle | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) 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/library/build.gradle b/library/build.gradle index 45b6e69fe5..9518cdd896 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -15,8 +15,8 @@ apply plugin: 'com.android.library' apply plugin: 'bintray-release' android { - compileSdkVersion 21 - buildToolsVersion "21.1.2" + compileSdkVersion 22 + buildToolsVersion "22.0.1" defaultConfig { // Important: ExoPlayerLib specifies a minSdkVersion of 9 because @@ -25,7 +25,7 @@ android { // functionality provided by the library requires API level 16 or // greater. minSdkVersion 9 - targetSdkVersion 21 + targetSdkVersion 22 } buildTypes { From de5bce3400129b4b6400237e64cc3e70bcf4044c Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 8 May 2015 17:02:23 +0100 Subject: [PATCH 02/19] Apply passthrough workarounds only on platform API versions 21/22. --- .../android/exoplayer/audio/AudioTrack.java | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) 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..fc1e3125bc 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 @@ -423,9 +423,11 @@ 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) { + // As a workaround for an issue on platform API versions 21/22 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 (Util.SDK_INT <= 22 && isAc3 + && audioTrack.getPlayState() == android.media.AudioTrack.PLAYSTATE_PAUSED) { return 0; } @@ -739,7 +741,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 +751,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 +766,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,7 +789,9 @@ public final class AudioTrack { */ public long getPlaybackHeadPosition() { long rawPlaybackHeadPosition = 0xFFFFFFFFL & audioTrack.getPlaybackHeadPosition(); - if (enablePassthroughWorkaround) { + if (Util.SDK_INT <= 22 && isPassthrough) { + // Work around an issue on platform API versions 21/22 where 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_PAUSED && rawPlaybackHeadPosition == 0) { passthroughWorkaroundPauseOffset = lastRawPlaybackHeadPosition; @@ -868,9 +869,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; From a1083d360ad3ed77facf2ce543c4440363b37daf Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 8 May 2015 17:03:05 +0100 Subject: [PATCH 03/19] Prevent wrapping detection on new passthrough AudioTracks. --- .../google/android/exoplayer/audio/AudioTrack.java | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) 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 fc1e3125bc..75bf3bcf5f 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 @@ -790,9 +790,15 @@ public final class AudioTrack { public long getPlaybackHeadPosition() { long rawPlaybackHeadPosition = 0xFFFFFFFFL & audioTrack.getPlaybackHeadPosition(); if (Util.SDK_INT <= 22 && isPassthrough) { - // Work around an issue on platform API versions 21/22 where 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_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) { passthroughWorkaroundPauseOffset = lastRawPlaybackHeadPosition; } From 3360f5eda5fde56e51154b1182ffafde6caafbaf Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 8 May 2015 17:04:21 +0100 Subject: [PATCH 04/19] Enable passthrough based on the input MIME type. --- .../demo/player/DashRendererBuilder.java | 5 +-- .../MediaCodecAudioTrackRenderer.java | 43 +++++-------------- .../exoplayer/MediaCodecTrackRenderer.java | 8 ++-- .../MediaCodecVideoTrackRenderer.java | 16 ++++--- .../android/exoplayer/audio/AudioTrack.java | 16 +++---- .../exoplayer/extractor/mp4/AtomParsers.java | 14 +++++- .../android/exoplayer/util/MimeTypes.java | 38 ++++++++++++++++ 7 files changed, 84 insertions(+), 56 deletions(-) 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/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..58bba1d2c1 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java @@ -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/audio/AudioTrack.java b/library/src/main/java/com/google/android/exoplayer/audio/AudioTrack.java index 75bf3bcf5f..4fe7c24245 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) { 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/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); + } + } From 4527539efe053715b33e29a59966e7fd039d2423 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 8 May 2015 17:05:04 +0100 Subject: [PATCH 05/19] Handle cenc:pssh elements in DASH manifests. Issue: #407 --- .../exoplayer/dash/DashChunkSource.java | 4 ++- .../MediaPresentationDescriptionParser.java | 25 ++++++++++++++++--- 2 files changed, 25 insertions(+), 4 deletions(-) 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); } /** From 54b71a57436f4622a3661918f5553dd1fd6c2355 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 8 May 2015 17:05:54 +0100 Subject: [PATCH 06/19] Allow cross-protocol redirects. Issue: #423 --- .../android/exoplayer/demo/Samples.java | 2 - .../upstream/DefaultHttpDataSource.java | 141 ++++++++++++++++-- .../upstream/DefaultUriDataSource.java | 25 +++- 3 files changed, 152 insertions(+), 16 deletions(-) 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/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)); } /** From 7437ee39d81f86ff61cdc1f9d8784dbf165a9c4c Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 8 May 2015 17:06:27 +0100 Subject: [PATCH 07/19] Remove deprecated method. --- .../android/exoplayer/chunk/ChunkSampleSource.java | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) 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..6bc5e21652 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,16 +120,6 @@ 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() { Assertions.checkState(state == STATE_UNPREPARED); From d8af120b98b7fbbba6e5abaaf540c29ef502bbff Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 8 May 2015 17:06:59 +0100 Subject: [PATCH 08/19] Fix treating all DVB data as AC-3. Issue #434 --- .../com/google/android/exoplayer/extractor/ts/TsExtractor.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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..5afded8639 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 @@ -43,7 +43,6 @@ 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_H264 = 0x1B; private static final int TS_STREAM_TYPE_ID3 = 0x15; private static final int TS_STREAM_TYPE_EIA608 = 0x100; // 0xFF + 1 @@ -308,13 +307,13 @@ 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_AC3: - case TS_STREAM_TYPE_DVB_AC3: pesPayloadReader = new Ac3Reader(output.track(streamType)); break; case TS_STREAM_TYPE_H264: From d9071710cfda98f743f575a8c7f8f64a0e834f84 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 8 May 2015 17:08:13 +0100 Subject: [PATCH 09/19] Read AC-3 tracks in MPEG TSs only if AC-3 playback is supported. Partly fixes #434 as the AC-3 stream will now be ignored if the audio capabilities don't allow it to be played back. --- .../exoplayer/demo/PlayerActivity.java | 5 ++-- .../demo/player/HlsRendererBuilder.java | 8 +++-- .../exoplayer/extractor/ts/TsExtractor.java | 30 +++++++++++++++++-- .../android/exoplayer/hls/HlsChunkSource.java | 16 +++++++--- 4 files changed, 49 insertions(+), 10 deletions(-) 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/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 tsPayloadReaders; // Indexed by pid private final long firstSampleTimestampUs; private final ParsableBitArray tsScratch; @@ -61,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; @@ -173,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. */ @@ -313,7 +335,11 @@ public final class TsExtractor implements Extractor, SeekMap { 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: + 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..7afa4be013 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; @@ -334,7 +342,7 @@ public class HlsChunkSource { if (previousTsChunk == null || segment.discontinuity || switchingVariant || 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 { From 9f77c4009e5d8dc51e5df08592dead8c636f73ef Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 8 May 2015 17:08:59 +0100 Subject: [PATCH 10/19] Clip seek position to the inputLength - 1. --- .../com/google/android/exoplayer/extractor/mp3/XingSeeker.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 861d6749ef24b4b1b97855418dc9d7fe36a933b2 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 8 May 2015 17:09:39 +0100 Subject: [PATCH 11/19] Remove ability to extend the default FormatEvaluator implementations. --- .../exoplayer/chunk/FormatEvaluator.java | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) 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); - } - } } From dd5eabdf4a5c6e0de641656d8bc7db2d3042c853 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 11 May 2015 21:01:35 +0100 Subject: [PATCH 12/19] Signal EoS when seeking ExtractorSampleSource to the end of a track. --- .../exoplayer/extractor/ExtractorSampleSource.java | 10 ++++++++++ 1 file changed, 10 insertions(+) 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..cf0e8a2c28 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; @@ -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; } From 770ad7f06ff4cdba1021c7423a7916517c5065c9 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 11 May 2015 21:02:38 +0100 Subject: [PATCH 13/19] Fix edge case in HlsSampleSource. 1. prepare() needs to load a TsChunk to actually prepare the source. 2. Source is prepared, but no tracks are enabled (this is why it's an edge case - no-one is likely to be doing this!). 3. The TsChunk load completes. We should not load additional chunks in this case. --- .../com/google/android/exoplayer/hls/HlsSampleSource.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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..36c1517fab 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 @@ -345,7 +345,12 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { if (!currentLoadableExceptionFatal) { clearCurrentLoadable(); } - maybeStartLoading(); + if (enabledTrackCount > 0) { + maybeStartLoading(); + } else { + clearState(); + allocator.trim(0); + } } @Override From b0abda43ecdf2dfa1d0c2f2f0070a965ed9666e9 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 11 May 2015 21:03:30 +0100 Subject: [PATCH 14/19] Ensure we configure a new extractor when we need one. Issue: #400 --- .../com/google/android/exoplayer/hls/HlsChunkSource.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) 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 7afa4be013..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 @@ -243,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]); @@ -339,7 +337,8 @@ 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, audioCapabilities); From 12d05a0917a85a9fa5b7dea8e3f322fe2a618f70 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 11 May 2015 21:04:51 +0100 Subject: [PATCH 15/19] Bump target API level to 22. --- demo/src/main/AndroidManifest.xml | 2 +- .../java/com/google/android/exoplayer/demo/DemoUtil.java | 3 +-- .../android/exoplayer/demo/WidevineTestMediaDrmCallback.java | 5 +---- demo/src/main/project.properties | 2 +- library/src/main/AndroidManifest.xml | 2 +- library/src/main/project.properties | 2 +- library/src/test/AndroidManifest.xml | 2 +- library/src/test/project.properties | 2 +- 8 files changed, 8 insertions(+), 12 deletions(-) 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/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/project.properties b/demo/src/main/project.properties index 77dfd37843..4fdc858b92 100644 --- a/demo/src/main/project.properties +++ b/demo/src/main/project.properties @@ -8,6 +8,6 @@ # project structure. # Project target. -target=android-21 +target=android-22 android.library=false android.library.reference.1=../../../library/src/main diff --git a/library/src/main/AndroidManifest.xml b/library/src/main/AndroidManifest.xml index 39ee60e58d..6327960ac1 100644 --- a/library/src/main/AndroidManifest.xml +++ b/library/src/main/AndroidManifest.xml @@ -27,6 +27,6 @@ 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. --> - + 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/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 From 116a18848f51dc61cd7512824d20bda088c62cc1 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 11 May 2015 21:05:37 +0100 Subject: [PATCH 16/19] Alter default exo buffertime value to 2500ms. --- .../src/main/java/com/google/android/exoplayer/ExoPlayer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 64cc380fe15435f3c497f417c5723c7adf57c4f2 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 11 May 2015 21:10:20 +0100 Subject: [PATCH 17/19] Avoid loading first chunk when preparing HLS for non-zero position. This also fixes a technical mistake where HlsChunkSource is fed seekPositionUs=-1 when obtaining the first chunk. This is wrong, but the usage of this variable within HlsChunkSource enforces that the seek must stay within bounds, so we get away with it. Issue: #385 --- .../demo/player/DebugTrackRenderer.java | 2 +- .../android/exoplayer/DummyTrackRenderer.java | 8 +++---- .../exoplayer/ExoPlayerImplInternal.java | 2 +- .../exoplayer/FrameworkSampleSource.java | 2 +- .../exoplayer/MediaCodecTrackRenderer.java | 4 ++-- .../android/exoplayer/SampleSource.java | 3 ++- .../android/exoplayer/TrackRenderer.java | 8 ++++--- .../exoplayer/chunk/ChunkSampleSource.java | 2 +- .../extractor/ExtractorSampleSource.java | 2 +- .../exoplayer/hls/HlsSampleSource.java | 24 ++++++++++++------- .../metadata/MetadataTrackRenderer.java | 4 ++-- .../exoplayer/text/TextTrackRenderer.java | 4 ++-- .../text/eia608/Eia608TrackRenderer.java | 4 ++-- .../extractor/mp4/Mp4ExtractorTest.java | 2 +- 14 files changed, 41 insertions(+), 30 deletions(-) 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/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/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/MediaCodecTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java index 58bba1d2c1..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; } 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/chunk/ChunkSampleSource.java b/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java index 6bc5e21652..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 @@ -121,7 +121,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback { } @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/extractor/ExtractorSampleSource.java b/library/src/main/java/com/google/android/exoplayer/extractor/ExtractorSampleSource.java index cf0e8a2c28..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 @@ -138,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; } 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 36c1517fab..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 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/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. From 79c7798d841239aad034c232bdc9e6abd664b113 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 11 May 2015 21:11:53 +0100 Subject: [PATCH 18/19] Reorganize BandwidthMeter interface --- .../exoplayer/upstream/BandwidthMeter.java | 19 +++++++++++++++++- .../upstream/DefaultBandwidthMeter.java | 20 +------------------ 2 files changed, 19 insertions(+), 20 deletions(-) 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; From 166c2f7cc0c48961ca5efd15a9bb14261345ba15 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 11 May 2015 21:16:02 +0100 Subject: [PATCH 19/19] Fix two issues related to seeking with AC-3 output. When a passthrough AudioTrack is replaced (due to seeking) the new one behaves as if it is still emptying data from the old one, with its playback position advancing until it runs out of data. Data written while the 'old' AudioTrack was emptying would be discarded, so avoid writing to the new AudioTrack while the old one is still emptying. Also avoid using AudioTrack.getTimestamp with passthrough tracks, as this causes the playback position to jump to a position that breaks audio/video synchronization. --- .../android/exoplayer/audio/AudioTrack.java | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) 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 4fe7c24245..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 @@ -423,12 +423,21 @@ public final class AudioTrack { return RESULT_BUFFER_CONSUMED; } - // As a workaround for an issue on platform API versions 21/22 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 (Util.SDK_INT <= 22 && 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; @@ -639,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;