diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 68794310bd..b727e4ab49 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,5 +1,22 @@ # Release notes # +### 2.9.5 ### + +* HLS: Parse `CHANNELS` attribute from `EXT-X-MEDIA` tag. +* ConcatenatingMediaSource: + * Add `Handler` parameter to methods that take a callback `Runnable`. + * Fix issue with dropped messages when releasing the source + ([#5464](https://github.com/google/ExoPlayer/issues/5464)). +* ExtractorMediaSource: Fix issue that could cause the player to get stuck + buffering at the end of the media. +* PlayerView: Fix issue preventing `OnClickListener` from receiving events + ([#5433](https://github.com/google/ExoPlayer/issues/5433)). +* IMA extension: Upgrade IMA dependency to 3.10.6. +* Cronet extension: Upgrade Cronet dependency to 71.3578.98. +* OkHttp extension: Upgrade OkHttp dependency to 3.12.1. +* MP3: Wider fix for issue where streams would play twice on some Samsung + devices ([#4519](https://github.com/google/ExoPlayer/issues/4519)). + ### 2.9.4 ### * IMA extension: Clear ads loader listeners on release @@ -1160,7 +1177,7 @@ [here](https://medium.com/google-exoplayer/customizing-exoplayers-ui-components-728cf55ee07a#.9ewjg7avi). * Robustness improvements when handling MediaSource timeline changes and MediaPeriod transitions. -* EIA608: Support for caption styling and positioning. +* CEA-608: Support for caption styling and positioning. * MPEG-TS: Improved support: * Support injection of custom TS payload readers. * Support injection of custom section payload readers. @@ -1404,8 +1421,8 @@ V2 release. (#801). * MP3: Fix playback of some streams when stream length is unknown. * ID3: Support multiple frames of the same type in a single tag. -* EIA608: Correctly handle repeated control characters, fixing an issue in which - captions would immediately disappear. +* CEA-608: Correctly handle repeated control characters, fixing an issue in + which captions would immediately disappear. * AVC3: Fix decoder failures on some MediaTek devices in the case where the first buffer fed to the decoder does not start with SPS/PPS NAL units. * Misc bug fixes. diff --git a/constants.gradle b/constants.gradle index 716ddbadba..16fb3420ce 100644 --- a/constants.gradle +++ b/constants.gradle @@ -13,8 +13,8 @@ // limitations under the License. project.ext { // ExoPlayer version and version code. - releaseVersion = '2.9.4' - releaseVersionCode = 2009004 + releaseVersion = '2.9.5' + releaseVersionCode = 2009005 // Important: ExoPlayer specifies a minSdkVersion of 14 because various // components provided by the library may be of use on older devices. // However, please note that the core media playback functionality provided diff --git a/extensions/cronet/build.gradle b/extensions/cronet/build.gradle index 7d8c217b58..520edfe1d1 100644 --- a/extensions/cronet/build.gradle +++ b/extensions/cronet/build.gradle @@ -30,7 +30,7 @@ android { } dependencies { - api 'org.chromium.net:cronet-embedded:66.3359.158' + api 'org.chromium.net:cronet-embedded:71.3578.98' implementation project(modulePrefix + 'library-core') implementation 'com.android.support:support-annotations:' + supportLibraryVersion testImplementation project(modulePrefix + 'library') diff --git a/extensions/ima/build.gradle b/extensions/ima/build.gradle index 22196ff3ab..4d6302c898 100644 --- a/extensions/ima/build.gradle +++ b/extensions/ima/build.gradle @@ -31,13 +31,13 @@ android { } dependencies { - api 'com.google.ads.interactivemedia.v3:interactivemedia:3.10.2' + api 'com.google.ads.interactivemedia.v3:interactivemedia:3.10.6' implementation project(modulePrefix + 'library-core') - implementation 'com.google.android.gms:play-services-ads:17.1.1' + implementation 'com.google.android.gms:play-services-ads:17.1.2' // These dependencies are necessary to force the supportLibraryVersion of // com.android.support:support-v4 and com.android.support:customtabs to be // used. Else older versions are used, for example via: - // com.google.android.gms:play-services-ads:17.1.1 + // com.google.android.gms:play-services-ads:17.1.2 // |-- com.android.support:customtabs:26.1.0 implementation 'com.android.support:support-v4:' + supportLibraryVersion implementation 'com.android.support:customtabs:' + supportLibraryVersion diff --git a/extensions/okhttp/build.gradle b/extensions/okhttp/build.gradle index 4e6b11c495..78825a6277 100644 --- a/extensions/okhttp/build.gradle +++ b/extensions/okhttp/build.gradle @@ -34,7 +34,7 @@ dependencies { implementation project(modulePrefix + 'library-core') implementation 'com.android.support:support-annotations:' + supportLibraryVersion compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion - api 'com.squareup.okhttp3:okhttp:3.11.0' + api 'com.squareup.okhttp3:okhttp:3.12.1' } ext { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java index cc16c43b05..50832dd5af 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2; import android.content.Context; +import android.media.MediaCodec; import android.os.Handler; import android.os.Looper; import android.support.annotation.IntDef; @@ -85,15 +86,18 @@ public class DefaultRenderersFactory implements RenderersFactory { protected static final int MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY = 50; private final Context context; - private final @Nullable DrmSessionManager drmSessionManager; - private final @ExtensionRendererMode int extensionRendererMode; - private final long allowedVideoJoiningTimeMs; + @Nullable private DrmSessionManager drmSessionManager; + @ExtensionRendererMode private int extensionRendererMode; + private long allowedVideoJoiningTimeMs; + private boolean playClearSamplesWithoutKeys; + private MediaCodecSelector mediaCodecSelector; - /** - * @param context A {@link Context}. - */ + /** @param context A {@link Context}. */ public DefaultRenderersFactory(Context context) { - this(context, EXTENSION_RENDERER_MODE_OFF); + this.context = context; + extensionRendererMode = EXTENSION_RENDERER_MODE_OFF; + allowedVideoJoiningTimeMs = DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS; + mediaCodecSelector = MediaCodecSelector.DEFAULT; } /** @@ -108,19 +112,20 @@ public class DefaultRenderersFactory implements RenderersFactory { } /** - * @param context A {@link Context}. - * @param extensionRendererMode The extension renderer mode, which determines if and how available - * extension renderers are used. Note that extensions must be included in the application - * build for them to be considered available. + * @deprecated Use {@link #DefaultRenderersFactory(Context)} and {@link + * #setExtensionRendererMode(int)}. */ + @Deprecated + @SuppressWarnings("deprecation") public DefaultRenderersFactory( Context context, @ExtensionRendererMode int extensionRendererMode) { this(context, extensionRendererMode, DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS); } /** - * @deprecated Use {@link #DefaultRenderersFactory(Context, int)} and pass {@link - * DrmSessionManager} directly to {@link SimpleExoPlayer} or {@link ExoPlayerFactory}. + * @deprecated Use {@link #DefaultRenderersFactory(Context)} and {@link + * #setExtensionRendererMode(int)}, and pass {@link DrmSessionManager} directly to {@link + * SimpleExoPlayer} or {@link ExoPlayerFactory}. */ @Deprecated @SuppressWarnings("deprecation") @@ -132,26 +137,22 @@ public class DefaultRenderersFactory implements RenderersFactory { } /** - * @param context A {@link Context}. - * @param extensionRendererMode The extension renderer mode, which determines if and how available - * extension renderers are used. Note that extensions must be included in the application - * build for them to be considered available. - * @param allowedVideoJoiningTimeMs The maximum duration for which video renderers can attempt to - * seamlessly join an ongoing playback. + * @deprecated Use {@link #DefaultRenderersFactory(Context)}, {@link + * #setExtensionRendererMode(int)} and {@link #setAllowedVideoJoiningTimeMs(long)}. */ + @Deprecated + @SuppressWarnings("deprecation") public DefaultRenderersFactory( Context context, @ExtensionRendererMode int extensionRendererMode, long allowedVideoJoiningTimeMs) { - this.context = context; - this.extensionRendererMode = extensionRendererMode; - this.allowedVideoJoiningTimeMs = allowedVideoJoiningTimeMs; - this.drmSessionManager = null; + this(context, null, extensionRendererMode, allowedVideoJoiningTimeMs); } /** - * @deprecated Use {@link #DefaultRenderersFactory(Context, int, long)} and pass {@link - * DrmSessionManager} directly to {@link SimpleExoPlayer} or {@link ExoPlayerFactory}. + * @deprecated Use {@link #DefaultRenderersFactory(Context)}, {@link + * #setExtensionRendererMode(int)} and {@link #setAllowedVideoJoiningTimeMs(long)}, and pass + * {@link DrmSessionManager} directly to {@link SimpleExoPlayer} or {@link ExoPlayerFactory}. */ @Deprecated public DefaultRenderersFactory( @@ -163,6 +164,70 @@ public class DefaultRenderersFactory implements RenderersFactory { this.extensionRendererMode = extensionRendererMode; this.allowedVideoJoiningTimeMs = allowedVideoJoiningTimeMs; this.drmSessionManager = drmSessionManager; + mediaCodecSelector = MediaCodecSelector.DEFAULT; + } + + /** + * Sets the extension renderer mode, which determines if and how available extension renderers are + * used. Note that extensions must be included in the application build for them to be considered + * available. + * + *

The default value is {@link #EXTENSION_RENDERER_MODE_OFF}. + * + * @param extensionRendererMode The extension renderer mode. + * @return This factory, for convenience. + */ + public DefaultRenderersFactory setExtensionRendererMode( + @ExtensionRendererMode int extensionRendererMode) { + this.extensionRendererMode = extensionRendererMode; + return this; + } + + /** + * Sets whether renderers are permitted to play clear regions of encrypted media prior to having + * obtained the keys necessary to decrypt encrypted regions of the media. For encrypted media that + * starts with a short clear region, this allows playback to begin in parallel with key + * acquisition, which can reduce startup latency. + * + *

The default value is {@code false}. + * + * @param playClearSamplesWithoutKeys Whether renderers are permitted to play clear regions of + * encrypted media prior to having obtained the keys necessary to decrypt encrypted regions of + * the media. + * @return This factory, for convenience. + */ + public DefaultRenderersFactory setPlayClearSamplesWithoutKeys( + boolean playClearSamplesWithoutKeys) { + this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys; + return this; + } + + /** + * Sets a {@link MediaCodecSelector} for use by {@link MediaCodec} based renderers. + * + *

The default value is {@link MediaCodecSelector#DEFAULT}. + * + * @param mediaCodecSelector The {@link MediaCodecSelector}. + * @return This factory, for convenience. + */ + public DefaultRenderersFactory setMediaCodecSelector(MediaCodecSelector mediaCodecSelector) { + this.mediaCodecSelector = mediaCodecSelector; + return this; + } + + /** + * Sets the maximum duration for which video renderers can attempt to seamlessly join an ongoing + * playback. + * + *

The default value is {@link #DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS}. + * + * @param allowedVideoJoiningTimeMs The maximum duration for which video renderers can attempt to + * seamlessly join an ongoing playback, in milliseconds. + * @return This factory, for convenience. + */ + public DefaultRenderersFactory setAllowedVideoJoiningTimeMs(long allowedVideoJoiningTimeMs) { + this.allowedVideoJoiningTimeMs = allowedVideoJoiningTimeMs; + return this; } @Override @@ -177,10 +242,26 @@ public class DefaultRenderersFactory implements RenderersFactory { drmSessionManager = this.drmSessionManager; } ArrayList renderersList = new ArrayList<>(); - buildVideoRenderers(context, drmSessionManager, allowedVideoJoiningTimeMs, - eventHandler, videoRendererEventListener, extensionRendererMode, renderersList); - buildAudioRenderers(context, drmSessionManager, buildAudioProcessors(), - eventHandler, audioRendererEventListener, extensionRendererMode, renderersList); + buildVideoRenderers( + context, + extensionRendererMode, + mediaCodecSelector, + drmSessionManager, + playClearSamplesWithoutKeys, + eventHandler, + videoRendererEventListener, + allowedVideoJoiningTimeMs, + renderersList); + buildAudioRenderers( + context, + extensionRendererMode, + mediaCodecSelector, + drmSessionManager, + playClearSamplesWithoutKeys, + buildAudioProcessors(), + eventHandler, + audioRendererEventListener, + renderersList); buildTextRenderers(context, textRendererOutput, eventHandler.getLooper(), extensionRendererMode, renderersList); buildMetadataRenderers(context, metadataRendererOutput, eventHandler.getLooper(), @@ -194,27 +275,36 @@ public class DefaultRenderersFactory implements RenderersFactory { * Builds video renderers for use by the player. * * @param context The {@link Context} associated with the player. - * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the player - * will not be used for DRM protected playbacks. - * @param allowedVideoJoiningTimeMs The maximum duration in milliseconds for which video - * renderers can attempt to seamlessly join an ongoing playback. + * @param extensionRendererMode The extension renderer mode. + * @param mediaCodecSelector A decoder selector. + * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the player will + * not be used for DRM protected playbacks. + * @param playClearSamplesWithoutKeys Whether renderers are permitted to play clear regions of + * encrypted media prior to having obtained the keys necessary to decrypt encrypted regions of + * the media. * @param eventHandler A handler associated with the main thread's looper. * @param eventListener An event listener. - * @param extensionRendererMode The extension renderer mode. + * @param allowedVideoJoiningTimeMs The maximum duration for which video renderers can attempt to + * seamlessly join an ongoing playback, in milliseconds. * @param out An array to which the built renderers should be appended. */ - protected void buildVideoRenderers(Context context, + protected void buildVideoRenderers( + Context context, + @ExtensionRendererMode int extensionRendererMode, + MediaCodecSelector mediaCodecSelector, @Nullable DrmSessionManager drmSessionManager, - long allowedVideoJoiningTimeMs, Handler eventHandler, - VideoRendererEventListener eventListener, @ExtensionRendererMode int extensionRendererMode, + boolean playClearSamplesWithoutKeys, + Handler eventHandler, + VideoRendererEventListener eventListener, + long allowedVideoJoiningTimeMs, ArrayList out) { out.add( new MediaCodecVideoRenderer( context, - MediaCodecSelector.DEFAULT, + mediaCodecSelector, allowedVideoJoiningTimeMs, drmSessionManager, - /* playClearSamplesWithoutKeys= */ false, + playClearSamplesWithoutKeys, eventHandler, eventListener, MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY)); @@ -261,26 +351,35 @@ public class DefaultRenderersFactory implements RenderersFactory { * Builds audio renderers for use by the player. * * @param context The {@link Context} associated with the player. - * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the player - * will not be used for DRM protected playbacks. - * @param audioProcessors An array of {@link AudioProcessor}s that will process PCM audio - * buffers before output. May be empty. + * @param extensionRendererMode The extension renderer mode. + * @param mediaCodecSelector A decoder selector. + * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the player will + * not be used for DRM protected playbacks. + * @param playClearSamplesWithoutKeys Whether renderers are permitted to play clear regions of + * encrypted media prior to having obtained the keys necessary to decrypt encrypted regions of + * the media. + * @param audioProcessors An array of {@link AudioProcessor}s that will process PCM audio buffers + * before output. May be empty. * @param eventHandler A handler to use when invoking event listeners and outputs. * @param eventListener An event listener. - * @param extensionRendererMode The extension renderer mode. * @param out An array to which the built renderers should be appended. */ - protected void buildAudioRenderers(Context context, + protected void buildAudioRenderers( + Context context, + @ExtensionRendererMode int extensionRendererMode, + MediaCodecSelector mediaCodecSelector, @Nullable DrmSessionManager drmSessionManager, - AudioProcessor[] audioProcessors, Handler eventHandler, - AudioRendererEventListener eventListener, @ExtensionRendererMode int extensionRendererMode, + boolean playClearSamplesWithoutKeys, + AudioProcessor[] audioProcessors, + Handler eventHandler, + AudioRendererEventListener eventListener, ArrayList out) { out.add( new MediaCodecAudioRenderer( context, - MediaCodecSelector.DEFAULT, + mediaCodecSelector, drmSessionManager, - /* playClearSamplesWithoutKeys= */ false, + playClearSamplesWithoutKeys, eventHandler, eventListener, AudioCapabilities.getCapabilities(context), diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java index 81f4285a08..6c2a6f527c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java @@ -97,7 +97,8 @@ public final class ExoPlayerFactory { LoadControl loadControl, @Nullable DrmSessionManager drmSessionManager, @DefaultRenderersFactory.ExtensionRendererMode int extensionRendererMode) { - RenderersFactory renderersFactory = new DefaultRenderersFactory(context, extensionRendererMode); + RenderersFactory renderersFactory = + new DefaultRenderersFactory(context).setExtensionRendererMode(extensionRendererMode); return newSimpleInstance( context, renderersFactory, trackSelector, loadControl, drmSessionManager); } @@ -127,7 +128,9 @@ public final class ExoPlayerFactory { @DefaultRenderersFactory.ExtensionRendererMode int extensionRendererMode, long allowedVideoJoiningTimeMs) { RenderersFactory renderersFactory = - new DefaultRenderersFactory(context, extensionRendererMode, allowedVideoJoiningTimeMs); + new DefaultRenderersFactory(context) + .setExtensionRendererMode(extensionRendererMode) + .setAllowedVideoJoiningTimeMs(allowedVideoJoiningTimeMs); return newSimpleInstance( context, renderersFactory, trackSelector, loadControl, drmSessionManager); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java index 36723c5d73..e3a2e1cd27 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java @@ -29,11 +29,11 @@ public final class ExoPlayerLibraryInfo { /** The version of the library expressed as a string, for example "1.2.3". */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa. - public static final String VERSION = "2.9.4"; + public static final String VERSION = "2.9.5"; /** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - public static final String VERSION_SLASHY = "ExoPlayerLib/2.9.4"; + public static final String VERSION_SLASHY = "ExoPlayerLib/2.9.5"; /** * The version of the library expressed as an integer, for example 1002003. @@ -43,7 +43,7 @@ public final class ExoPlayerLibraryInfo { * integer version 123045006 (123-045-006). */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - public static final int VERSION_INT = 2009004; + public static final int VERSION_INT = 2009005; /** * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java index 429510bcaf..7ee25d07d1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java @@ -419,7 +419,7 @@ public final class DefaultAudioSink implements AudioSink { isInputPcm = Util.isEncodingLinearPcm(inputEncoding); shouldConvertHighResIntPcmToFloat = enableConvertHighResIntPcmToFloat - && supportsOutput(channelCount, C.ENCODING_PCM_32BIT) + && supportsOutput(channelCount, C.ENCODING_PCM_FLOAT) && Util.isEncodingHighResolutionIntegerPcm(inputEncoding); if (isInputPcm) { pcmFrameSize = Util.getPcmFrameSize(inputEncoding, channelCount); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java index a5506e2cfb..88805d9362 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java @@ -50,7 +50,8 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact FLAG_IGNORE_H264_STREAM, FLAG_DETECT_ACCESS_UNITS, FLAG_IGNORE_SPLICE_INFO_STREAM, - FLAG_OVERRIDE_CAPTION_DESCRIPTORS + FLAG_OVERRIDE_CAPTION_DESCRIPTORS, + FLAG_IGNORE_HDMV_DTS_STREAM }) public @interface Flags {} @@ -86,6 +87,12 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact * closedCaptionFormats} will be ignored if the PMT contains closed captions service descriptors. */ public static final int FLAG_OVERRIDE_CAPTION_DESCRIPTORS = 1 << 5; + /** + * Prevents the creation of {@link DtsReader} instances when receiving {@link + * TsExtractor#TS_STREAM_TYPE_HDMV_DTS} as stream type. Enabling this flag prevents a stream type + * collision between HDMV DTS audio and SCTE-35 subtitles. + */ + public static final int FLAG_IGNORE_HDMV_DTS_STREAM = 1 << 6; private static final int DESCRIPTOR_TAG_CAPTION_SERVICE = 0x86; @@ -142,8 +149,12 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact case TsExtractor.TS_STREAM_TYPE_AC3: case TsExtractor.TS_STREAM_TYPE_E_AC3: return new PesReader(new Ac3Reader(esInfo.language)); - case TsExtractor.TS_STREAM_TYPE_DTS: case TsExtractor.TS_STREAM_TYPE_HDMV_DTS: + if (isSet(FLAG_IGNORE_HDMV_DTS_STREAM)) { + return null; + } + // Fall through. + case TsExtractor.TS_STREAM_TYPE_DTS: return new PesReader(new DtsReader(esInfo.language)); case TsExtractor.TS_STREAM_TYPE_H262: return new PesReader(new H262Reader(buildUserDataReader(esInfo))); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java index 9ae50179c3..f09cae9949 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java @@ -59,8 +59,6 @@ public final class MediaCodecUtil { private static final String TAG = "MediaCodecUtil"; private static final Pattern PROFILE_PATTERN = Pattern.compile("^\\D?(\\d+)$"); - private static final RawAudioCodecComparator RAW_AUDIO_CODEC_COMPARATOR = - new RawAudioCodecComparator(); private static final HashMap> decoderInfosCache = new HashMap<>(); @@ -312,32 +310,6 @@ public final class MediaCodecUtil { return false; } - // Work around https://github.com/google/ExoPlayer/issues/398. - if (Util.SDK_INT < 18 && "OMX.SEC.MP3.Decoder".equals(name)) { - return false; - } - - // Work around https://github.com/google/ExoPlayer/issues/4519. - if ("OMX.SEC.mp3.dec".equals(name) - && (Util.MODEL.startsWith("GT-I9152") - || Util.MODEL.startsWith("GT-I9515") - || Util.MODEL.startsWith("GT-P5220") - || Util.MODEL.startsWith("GT-S7580") - || Util.MODEL.startsWith("SM-G350") - || Util.MODEL.startsWith("SM-G386") - || Util.MODEL.startsWith("SM-T231") - || Util.MODEL.startsWith("SM-T530") - || Util.MODEL.startsWith("SCH-I535") - || Util.MODEL.startsWith("SPH-L710"))) { - return false; - } - if ("OMX.brcm.audio.mp3.decoder".equals(name) - && (Util.MODEL.startsWith("GT-I9152") - || Util.MODEL.startsWith("GT-S7580") - || Util.MODEL.startsWith("SM-G350"))) { - return false; - } - // Work around https://github.com/google/ExoPlayer/issues/1528 and // https://github.com/google/ExoPlayer/issues/3171. if (Util.SDK_INT < 18 && "OMX.MTK.AUDIO.DECODER.AAC".equals(name) @@ -424,7 +396,18 @@ public final class MediaCodecUtil { */ private static void applyWorkarounds(String mimeType, List decoderInfos) { if (MimeTypes.AUDIO_RAW.equals(mimeType)) { - Collections.sort(decoderInfos, RAW_AUDIO_CODEC_COMPARATOR); + Collections.sort(decoderInfos, new RawAudioCodecComparator()); + } else if (Util.SDK_INT < 21 && decoderInfos.size() > 1) { + String firstCodecName = decoderInfos.get(0).name; + if ("OMX.SEC.mp3.dec".equals(firstCodecName) + || "OMX.SEC.MP3.Decoder".equals(firstCodecName) + || "OMX.brcm.audio.mp3.decoder".equals(firstCodecName)) { + // Prefer OMX.google codecs over OMX.SEC.mp3.dec, OMX.SEC.MP3.Decoder and + // OMX.brcm.audio.mp3.decoder on older devices. See: + // https://github.com/google/ExoPlayer/issues/398 and + // https://github.com/google/ExoPlayer/issues/4519. + Collections.sort(decoderInfos, new PreferOmxGoogleCodecComparator()); + } } } @@ -730,6 +713,18 @@ public final class MediaCodecUtil { } } + /** Comparator for preferring OMX.google media codecs. */ + private static final class PreferOmxGoogleCodecComparator implements Comparator { + @Override + public int compare(MediaCodecInfo a, MediaCodecInfo b) { + return scoreMediaCodecInfo(a) - scoreMediaCodecInfo(b); + } + + private static int scoreMediaCodecInfo(MediaCodecInfo mediaCodecInfo) { + return mediaCodecInfo.name.startsWith("OMX.google") ? -1 : 0; + } + } + static { AVC_PROFILE_NUMBER_TO_CONST = new SparseIntArray(); AVC_PROFILE_NUMBER_TO_CONST.put(66, CodecProfileLevel.AVCProfileBaseline); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java index c93afdb249..7baea9979f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.source; import android.os.Handler; import android.os.Message; +import android.support.annotation.GuardedBy; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.Pair; @@ -35,9 +36,11 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.IdentityHashMap; import java.util.List; import java.util.Map; +import java.util.Set; /** * Concatenates multiple {@link MediaSource}s. The list of {@link MediaSource}s can be modified @@ -50,25 +53,31 @@ public class ConcatenatingMediaSource extends CompositeMediaSource mediaSourcesPublic; - // Accessed on the playback thread. + @GuardedBy("this") + private final Set pendingOnCompletionActions; + + @GuardedBy("this") + @Nullable + private Handler playbackThreadHandler; + + // Accessed on the playback thread only. private final List mediaSourceHolders; private final Map mediaSourceByMediaPeriod; private final Map mediaSourceByUid; - private final List pendingOnCompletionActions; private final boolean isAtomic; private final boolean useLazyPreparation; private final Timeline.Window window; private final Timeline.Period period; - @Nullable private Handler playbackThreadHandler; - @Nullable private Handler applicationThreadHandler; - private boolean listenerNotificationScheduled; + private boolean timelineUpdateScheduled; + private Set nextTimelineUpdateOnCompletionActions; private ShuffleOrder shuffleOrder; private int windowCount; private int periodCount; @@ -127,7 +136,8 @@ public class ConcatenatingMediaSource extends CompositeMediaSource(); this.mediaSourcesPublic = new ArrayList<>(); this.mediaSourceHolders = new ArrayList<>(); - this.pendingOnCompletionActions = new ArrayList<>(); + this.nextTimelineUpdateOnCompletionActions = new HashSet<>(); + this.pendingOnCompletionActions = new HashSet<>(); this.isAtomic = isAtomic; this.useLazyPreparation = useLazyPreparation; window = new Timeline.Window(); @@ -141,19 +151,20 @@ public class ConcatenatingMediaSource extends CompositeMediaSource mediaSources) { - addMediaSources(mediaSourcesPublic.size(), mediaSources, null); + addPublicMediaSources( + mediaSourcesPublic.size(), + mediaSources, + /* handler= */ null, + /* onCompletionAction= */ null); } /** @@ -197,12 +218,13 @@ public class ConcatenatingMediaSource extends CompositeMediaSource mediaSources, @Nullable Runnable actionOnCompletion) { - addMediaSources(mediaSourcesPublic.size(), mediaSources, actionOnCompletion); + Collection mediaSources, Handler handler, Runnable onCompletionAction) { + addPublicMediaSources(mediaSourcesPublic.size(), mediaSources, handler, onCompletionAction); } /** @@ -214,7 +236,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource mediaSources) { - addMediaSources(index, mediaSources, null); + addPublicMediaSources(index, mediaSources, /* handler= */ null, /* onCompletionAction= */ null); } /** @@ -224,26 +246,16 @@ public class ConcatenatingMediaSource extends CompositeMediaSource mediaSources, @Nullable Runnable actionOnCompletion) { - for (MediaSource mediaSource : mediaSources) { - Assertions.checkNotNull(mediaSource); - } - List mediaSourceHolders = new ArrayList<>(mediaSources.size()); - for (MediaSource mediaSource : mediaSources) { - mediaSourceHolders.add(new MediaSourceHolder(mediaSource)); - } - mediaSourcesPublic.addAll(index, mediaSourceHolders); - if (playbackThreadHandler != null && !mediaSources.isEmpty()) { - playbackThreadHandler - .obtainMessage(MSG_ADD, new MessageData<>(index, mediaSourceHolders, actionOnCompletion)) - .sendToTarget(); - } else if (actionOnCompletion != null) { - actionOnCompletion.run(); - } + int index, + Collection mediaSources, + Handler handler, + Runnable onCompletionAction) { + addPublicMediaSources(index, mediaSources, handler, onCompletionAction); } /** @@ -259,26 +271,27 @@ public class ConcatenatingMediaSource extends CompositeMediaSourceNote: If you want to move the instance, it's preferable to use {@link #moveMediaSource(int, - * int, Runnable)} instead. + * int, Handler, Runnable)} instead. * *

Note: If you want to remove a set of contiguous sources, it's preferable to use {@link - * #removeMediaSourceRange(int, int, Runnable)} instead. + * #removeMediaSourceRange(int, int, Handler, Runnable)} instead. * * @param index The index at which the media source will be removed. This index must be in the * range of 0 <= index < {@link #getSize()}. - * @param actionOnCompletion A {@link Runnable} which is executed immediately after the media + * @param handler The {@link Handler} to run {@code onCompletionAction}. + * @param onCompletionAction A {@link Runnable} which is executed immediately after the media * source has been removed from the playlist. */ public final synchronized void removeMediaSource( - int index, @Nullable Runnable actionOnCompletion) { - removeMediaSourceRange(index, index + 1, actionOnCompletion); + int index, Handler handler, Runnable onCompletionAction) { + removePublicMediaSources(index, index + 1, handler, onCompletionAction); } /** @@ -296,7 +309,8 @@ public class ConcatenatingMediaSource extends CompositeMediaSource(fromIndex, toIndex, actionOnCompletion)) - .sendToTarget(); - } else if (actionOnCompletion != null) { - actionOnCompletion.run(); - } + int fromIndex, int toIndex, Handler handler, Runnable onCompletionAction) { + removePublicMediaSources(fromIndex, toIndex, handler, onCompletionAction); } /** @@ -342,7 +344,8 @@ public class ConcatenatingMediaSource extends CompositeMediaSource(currentIndex, newIndex, actionOnCompletion)) - .sendToTarget(); - } else if (actionOnCompletion != null) { - actionOnCompletion.run(); - } + int currentIndex, int newIndex, Handler handler, Runnable onCompletionAction) { + movePublicMediaSource(currentIndex, newIndex, handler, onCompletionAction); } /** Clears the playlist. */ public final synchronized void clear() { - clear(/* actionOnCompletion= */ null); + removeMediaSourceRange(0, getSize()); } /** * Clears the playlist and executes a custom action on completion. * - * @param actionOnCompletion A {@link Runnable} which is executed immediately after the playlist + * @param handler The {@link Handler} to run {@code onCompletionAction}. + * @param onCompletionAction A {@link Runnable} which is executed immediately after the playlist * has been cleared. */ - public final synchronized void clear(@Nullable Runnable actionOnCompletion) { - removeMediaSourceRange(0, getSize(), actionOnCompletion); + public final synchronized void clear(Handler handler, Runnable onCompletionAction) { + removeMediaSourceRange(0, getSize(), handler, onCompletionAction); } /** Returns the number of media sources in the playlist. */ @@ -410,41 +402,24 @@ public class ConcatenatingMediaSource extends CompositeMediaSource(/* index= */ 0, shuffleOrder, actionOnCompletion)) - .sendToTarget(); - } else { - this.shuffleOrder = - shuffleOrder.getLength() > 0 ? shuffleOrder.cloneAndClear() : shuffleOrder; - if (actionOnCompletion != null) { - actionOnCompletion.run(); - } - } + ShuffleOrder shuffleOrder, Handler handler, Runnable onCompletionAction) { + setPublicShuffleOrder(shuffleOrder, handler, onCompletionAction); } + // CompositeMediaSource implementation. + @Override @Nullable public Object getTag() { @@ -458,13 +433,12 @@ public class ConcatenatingMediaSource extends CompositeMediaSource mediaSources, + @Nullable Handler handler, + @Nullable Runnable onCompletionAction) { + Assertions.checkArgument((handler == null) == (onCompletionAction == null)); + Handler playbackThreadHandler = this.playbackThreadHandler; + for (MediaSource mediaSource : mediaSources) { + Assertions.checkNotNull(mediaSource); + } + List mediaSourceHolders = new ArrayList<>(mediaSources.size()); + for (MediaSource mediaSource : mediaSources) { + mediaSourceHolders.add(new MediaSourceHolder(mediaSource)); + } + mediaSourcesPublic.addAll(index, mediaSourceHolders); + if (playbackThreadHandler != null && !mediaSources.isEmpty()) { + HandlerAndRunnable callbackAction = createOnCompletionAction(handler, onCompletionAction); + playbackThreadHandler + .obtainMessage(MSG_ADD, new MessageData<>(index, mediaSourceHolders, callbackAction)) + .sendToTarget(); + } else if (onCompletionAction != null && handler != null) { + handler.post(onCompletionAction); + } + } + + @GuardedBy("this") + private void removePublicMediaSources( + int fromIndex, + int toIndex, + @Nullable Handler handler, + @Nullable Runnable onCompletionAction) { + Assertions.checkArgument((handler == null) == (onCompletionAction == null)); + Handler playbackThreadHandler = this.playbackThreadHandler; + Util.removeRange(mediaSourcesPublic, fromIndex, toIndex); + if (playbackThreadHandler != null) { + HandlerAndRunnable callbackAction = createOnCompletionAction(handler, onCompletionAction); + playbackThreadHandler + .obtainMessage(MSG_REMOVE, new MessageData<>(fromIndex, toIndex, callbackAction)) + .sendToTarget(); + } else if (onCompletionAction != null && handler != null) { + handler.post(onCompletionAction); + } + } + + @GuardedBy("this") + private void movePublicMediaSource( + int currentIndex, + int newIndex, + @Nullable Handler handler, + @Nullable Runnable onCompletionAction) { + Assertions.checkArgument((handler == null) == (onCompletionAction == null)); + Handler playbackThreadHandler = this.playbackThreadHandler; + mediaSourcesPublic.add(newIndex, mediaSourcesPublic.remove(currentIndex)); + if (playbackThreadHandler != null) { + HandlerAndRunnable callbackAction = createOnCompletionAction(handler, onCompletionAction); + playbackThreadHandler + .obtainMessage(MSG_MOVE, new MessageData<>(currentIndex, newIndex, callbackAction)) + .sendToTarget(); + } else if (onCompletionAction != null && handler != null) { + handler.post(onCompletionAction); + } + } + + @GuardedBy("this") + private void setPublicShuffleOrder( + ShuffleOrder shuffleOrder, @Nullable Handler handler, @Nullable Runnable onCompletionAction) { + Assertions.checkArgument((handler == null) == (onCompletionAction == null)); + Handler playbackThreadHandler = this.playbackThreadHandler; + if (playbackThreadHandler != null) { + int size = getSize(); + if (shuffleOrder.getLength() != size) { + shuffleOrder = + shuffleOrder + .cloneAndClear() + .cloneAndInsert(/* insertionIndex= */ 0, /* insertionCount= */ size); + } + HandlerAndRunnable callbackAction = createOnCompletionAction(handler, onCompletionAction); + playbackThreadHandler + .obtainMessage( + MSG_SET_SHUFFLE_ORDER, + new MessageData<>(/* index= */ 0, shuffleOrder, callbackAction)) + .sendToTarget(); + } else { + this.shuffleOrder = + shuffleOrder.getLength() > 0 ? shuffleOrder.cloneAndClear() : shuffleOrder; + if (onCompletionAction != null && handler != null) { + handler.post(onCompletionAction); + } + } + } + + @GuardedBy("this") + @Nullable + private HandlerAndRunnable createOnCompletionAction( + @Nullable Handler handler, @Nullable Runnable runnable) { + if (handler == null || runnable == null) { + return null; + } + HandlerAndRunnable handlerAndRunnable = new HandlerAndRunnable(handler, runnable); + pendingOnCompletionActions.add(handlerAndRunnable); + return handlerAndRunnable; + } + + // Internal methods. Called on the playback thread. + @SuppressWarnings("unchecked") private boolean handleMessage(Message msg) { - if (playbackThreadHandler == null) { - // Stale event. - return false; - } switch (msg.what) { case MSG_ADD: MessageData> addMessage = (MessageData>) Util.castNonNull(msg.obj); shuffleOrder = shuffleOrder.cloneAndInsert(addMessage.index, addMessage.customData.size()); addMediaSourcesInternal(addMessage.index, addMessage.customData); - scheduleListenerNotification(addMessage.actionOnCompletion); + scheduleTimelineUpdate(addMessage.onCompletionAction); break; case MSG_REMOVE: MessageData removeMessage = (MessageData) Util.castNonNull(msg.obj); @@ -576,30 +659,27 @@ public class ConcatenatingMediaSource extends CompositeMediaSource= fromIndex; index--) { removeMediaSourceInternal(index); } - scheduleListenerNotification(removeMessage.actionOnCompletion); + scheduleTimelineUpdate(removeMessage.onCompletionAction); break; case MSG_MOVE: MessageData moveMessage = (MessageData) Util.castNonNull(msg.obj); shuffleOrder = shuffleOrder.cloneAndRemove(moveMessage.index, moveMessage.index + 1); shuffleOrder = shuffleOrder.cloneAndInsert(moveMessage.customData, 1); moveMediaSourceInternal(moveMessage.index, moveMessage.customData); - scheduleListenerNotification(moveMessage.actionOnCompletion); + scheduleTimelineUpdate(moveMessage.onCompletionAction); break; case MSG_SET_SHUFFLE_ORDER: MessageData shuffleOrderMessage = (MessageData) Util.castNonNull(msg.obj); shuffleOrder = shuffleOrderMessage.customData; - scheduleListenerNotification(shuffleOrderMessage.actionOnCompletion); + scheduleTimelineUpdate(shuffleOrderMessage.onCompletionAction); break; - case MSG_NOTIFY_LISTENER: - notifyListener(); + case MSG_UPDATE_TIMELINE: + updateTimelineAndScheduleOnCompletionActions(); break; case MSG_ON_COMPLETION: - List actionsOnCompletion = (List) Util.castNonNull(msg.obj); - Handler handler = Assertions.checkNotNull(applicationThreadHandler); - for (int i = 0; i < actionsOnCompletion.size(); i++) { - handler.post(actionsOnCompletion.get(i)); - } + Set actions = (Set) Util.castNonNull(msg.obj); + dispatchOnCompletionActions(actions); break; default: throw new IllegalStateException(); @@ -607,34 +687,46 @@ public class ConcatenatingMediaSource extends CompositeMediaSource actionsOnCompletion = - pendingOnCompletionActions.isEmpty() - ? Collections.emptyList() - : new ArrayList<>(pendingOnCompletionActions); - pendingOnCompletionActions.clear(); + private void updateTimelineAndScheduleOnCompletionActions() { + timelineUpdateScheduled = false; + Set onCompletionActions = nextTimelineUpdateOnCompletionActions; + nextTimelineUpdateOnCompletionActions = new HashSet<>(); refreshSourceInfo( new ConcatenatedTimeline( mediaSourceHolders, windowCount, periodCount, shuffleOrder, isAtomic), /* manifest= */ null); - if (!actionsOnCompletion.isEmpty()) { - Assertions.checkNotNull(playbackThreadHandler) - .obtainMessage(MSG_ON_COMPLETION, actionsOnCompletion) - .sendToTarget(); + getPlaybackThreadHandlerOnPlaybackThread() + .obtainMessage(MSG_ON_COMPLETION, onCompletionActions) + .sendToTarget(); + } + + @SuppressWarnings("GuardedBy") + private Handler getPlaybackThreadHandlerOnPlaybackThread() { + // Write access to this value happens on the playback thread only, so playback thread reads + // don't need to be synchronized. + return Assertions.checkNotNull(playbackThreadHandler); + } + + private synchronized void dispatchOnCompletionActions( + Set onCompletionActions) { + for (HandlerAndRunnable pendingAction : onCompletionActions) { + pendingAction.dispatch(); } + pendingOnCompletionActions.removeAll(onCompletionActions); } private void addMediaSourcesInternal( @@ -733,7 +825,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSourceNote: To loop a {@link MediaSource} indefinitely, it is usually better to use {@link - * ExoPlayer#setRepeatMode(int)}. + * ExoPlayer#setRepeatMode(int)} instead of this class. */ public final class LoopingMediaSource extends CompositeMediaSource { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java index 388aa29ce9..5b35ee946a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java @@ -1319,8 +1319,9 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } synchronized (MediaCodecVideoRenderer.class) { if (!evaluatedDeviceNeedsSetOutputSurfaceWorkaround) { - if (Util.SDK_INT <= 27 && "dangal".equals(Util.DEVICE)) { - // Dangal is affected on API level 27: https://github.com/google/ExoPlayer/issues/5169. + if (Util.SDK_INT <= 27 && ("dangal".equals(Util.DEVICE) || "HWEML".equals(Util.DEVICE))) { + // A small number of devices are affected on API level 27: + // https://github.com/google/ExoPlayer/issues/5169. deviceNeedsSetOutputSurfaceWorkaround = true; } else if (Util.SDK_INT >= 27) { // In general, devices running API level 27 or later should be unaffected. Do nothing. diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java index dd1221f160..9ec5078faf 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java @@ -17,13 +17,16 @@ package com.google.android.exoplayer2.source; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import android.os.ConditionVariable; +import android.os.Handler; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; +import com.google.android.exoplayer2.source.MediaSource.SourceInfoRefreshListener; import com.google.android.exoplayer2.source.ShuffleOrder.DefaultShuffleOrder; import com.google.android.exoplayer2.testutil.DummyMainThread; import com.google.android.exoplayer2.testutil.FakeMediaSource; @@ -41,7 +44,6 @@ import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.Mockito; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; @@ -415,57 +417,59 @@ public final class ConcatenatingMediaSourceTest { @Test public void testCustomCallbackBeforePreparationAddSingle() { - Runnable runnable = Mockito.mock(Runnable.class); + Runnable runnable = mock(Runnable.class); - mediaSource.addMediaSource(createFakeMediaSource(), runnable); + mediaSource.addMediaSource(createFakeMediaSource(), new Handler(), runnable); verify(runnable).run(); } @Test public void testCustomCallbackBeforePreparationAddMultiple() { - Runnable runnable = Mockito.mock(Runnable.class); + Runnable runnable = mock(Runnable.class); mediaSource.addMediaSources( Arrays.asList(new MediaSource[] {createFakeMediaSource(), createFakeMediaSource()}), + new Handler(), runnable); verify(runnable).run(); } @Test public void testCustomCallbackBeforePreparationAddSingleWithIndex() { - Runnable runnable = Mockito.mock(Runnable.class); + Runnable runnable = mock(Runnable.class); - mediaSource.addMediaSource(/* index */ 0, createFakeMediaSource(), runnable); + mediaSource.addMediaSource(/* index */ 0, createFakeMediaSource(), new Handler(), runnable); verify(runnable).run(); } @Test public void testCustomCallbackBeforePreparationAddMultipleWithIndex() { - Runnable runnable = Mockito.mock(Runnable.class); + Runnable runnable = mock(Runnable.class); mediaSource.addMediaSources( /* index */ 0, Arrays.asList(new MediaSource[] {createFakeMediaSource(), createFakeMediaSource()}), + new Handler(), runnable); verify(runnable).run(); } @Test public void testCustomCallbackBeforePreparationRemove() { - Runnable runnable = Mockito.mock(Runnable.class); + Runnable runnable = mock(Runnable.class); mediaSource.addMediaSource(createFakeMediaSource()); - mediaSource.removeMediaSource(/* index */ 0, runnable); + mediaSource.removeMediaSource(/* index */ 0, new Handler(), runnable); verify(runnable).run(); } @Test public void testCustomCallbackBeforePreparationMove() { - Runnable runnable = Mockito.mock(Runnable.class); + Runnable runnable = mock(Runnable.class); mediaSource.addMediaSources( Arrays.asList(new MediaSource[] {createFakeMediaSource(), createFakeMediaSource()})); - mediaSource.moveMediaSource(/* fromIndex */ 1, /* toIndex */ 0, runnable); + mediaSource.moveMediaSource(/* fromIndex */ 1, /* toIndex */ 0, new Handler(), runnable); verify(runnable).run(); } @@ -476,7 +480,8 @@ public final class ConcatenatingMediaSourceTest { testRunner.prepareSource(); final TimelineGrabber timelineGrabber = new TimelineGrabber(testRunner); dummyMainThread.runOnMainThread( - () -> mediaSource.addMediaSource(createFakeMediaSource(), timelineGrabber)); + () -> + mediaSource.addMediaSource(createFakeMediaSource(), new Handler(), timelineGrabber)); Timeline timeline = timelineGrabber.assertTimelineChangeBlocking(); assertThat(timeline.getWindowCount()).isEqualTo(1); } finally { @@ -495,6 +500,7 @@ public final class ConcatenatingMediaSourceTest { mediaSource.addMediaSources( Arrays.asList( new MediaSource[] {createFakeMediaSource(), createFakeMediaSource()}), + new Handler(), timelineGrabber)); Timeline timeline = timelineGrabber.assertTimelineChangeBlocking(); assertThat(timeline.getWindowCount()).isEqualTo(2); @@ -511,7 +517,8 @@ public final class ConcatenatingMediaSourceTest { final TimelineGrabber timelineGrabber = new TimelineGrabber(testRunner); dummyMainThread.runOnMainThread( () -> - mediaSource.addMediaSource(/* index */ 0, createFakeMediaSource(), timelineGrabber)); + mediaSource.addMediaSource( + /* index */ 0, createFakeMediaSource(), new Handler(), timelineGrabber)); Timeline timeline = timelineGrabber.assertTimelineChangeBlocking(); assertThat(timeline.getWindowCount()).isEqualTo(1); } finally { @@ -531,6 +538,7 @@ public final class ConcatenatingMediaSourceTest { /* index */ 0, Arrays.asList( new MediaSource[] {createFakeMediaSource(), createFakeMediaSource()}), + new Handler(), timelineGrabber)); Timeline timeline = timelineGrabber.assertTimelineChangeBlocking(); assertThat(timeline.getWindowCount()).isEqualTo(2); @@ -549,7 +557,7 @@ public final class ConcatenatingMediaSourceTest { final TimelineGrabber timelineGrabber = new TimelineGrabber(testRunner); dummyMainThread.runOnMainThread( - () -> mediaSource.removeMediaSource(/* index */ 0, timelineGrabber)); + () -> mediaSource.removeMediaSource(/* index */ 0, new Handler(), timelineGrabber)); Timeline timeline = timelineGrabber.assertTimelineChangeBlocking(); assertThat(timeline.getWindowCount()).isEqualTo(0); } finally { @@ -571,7 +579,9 @@ public final class ConcatenatingMediaSourceTest { final TimelineGrabber timelineGrabber = new TimelineGrabber(testRunner); dummyMainThread.runOnMainThread( - () -> mediaSource.moveMediaSource(/* fromIndex */ 1, /* toIndex */ 0, timelineGrabber)); + () -> + mediaSource.moveMediaSource( + /* fromIndex */ 1, /* toIndex */ 0, new Handler(), timelineGrabber)); Timeline timeline = timelineGrabber.assertTimelineChangeBlocking(); assertThat(timeline.getWindowCount()).isEqualTo(2); } finally { @@ -819,7 +829,7 @@ public final class ConcatenatingMediaSourceTest { testRunner.prepareSource(); final TimelineGrabber timelineGrabber = new TimelineGrabber(testRunner); - dummyMainThread.runOnMainThread(() -> mediaSource.clear(timelineGrabber)); + dummyMainThread.runOnMainThread(() -> mediaSource.clear(new Handler(), timelineGrabber)); Timeline timeline = timelineGrabber.assertTimelineChangeBlocking(); assertThat(timeline.isEmpty()).isTrue(); @@ -964,8 +974,9 @@ public final class ConcatenatingMediaSourceTest { @Test public void testCustomCallbackBeforePreparationSetShuffleOrder() throws Exception { - Runnable runnable = Mockito.mock(Runnable.class); - mediaSource.setShuffleOrder(new ShuffleOrder.UnshuffledShuffleOrder(/* length= */ 0), runnable); + Runnable runnable = mock(Runnable.class); + mediaSource.setShuffleOrder( + new ShuffleOrder.UnshuffledShuffleOrder(/* length= */ 0), new Handler(), runnable); verify(runnable).run(); } @@ -981,7 +992,9 @@ public final class ConcatenatingMediaSourceTest { dummyMainThread.runOnMainThread( () -> mediaSource.setShuffleOrder( - new ShuffleOrder.UnshuffledShuffleOrder(/* length= */ 3), timelineGrabber)); + new ShuffleOrder.UnshuffledShuffleOrder(/* length= */ 3), + new Handler(), + timelineGrabber)); Timeline timeline = timelineGrabber.assertTimelineChangeBlocking(); assertThat(timeline.getFirstWindowIndex(/* shuffleModeEnabled= */ true)).isEqualTo(0); } finally { diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java index 242711431c..9e13d6fa0f 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java @@ -101,6 +101,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser variableDefinitions) { + String channelsString = parseOptionalStringAttr(line, REGEX_CHANNELS, variableDefinitions); + return channelsString != null + ? Integer.parseInt(Util.splitAtFirst(channelsString, "/")[0]) + : Format.NO_VALUE; + } + private static @Nullable SchemeData parsePlayReadySchemeData( String line, Map variableDefinitions) throws ParserException { String keyFormatVersions = diff --git a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java index 9701171ce9..8b69ba0db2 100644 --- a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java +++ b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java @@ -81,6 +81,18 @@ public class HlsMasterPlaylistParserTest { + "CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128\n" + "http://example.com/low.m3u8\n"; + private static final String PLAYLIST_WITH_CHANNELS_ATTRIBUTE = + " #EXTM3U \n" + + "#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"audio\",CHANNELS=\"6\",NAME=\"Eng6\"," + + "URI=\"something.m3u8\"\n" + + "#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"audio\",CHANNELS=\"2/6\",NAME=\"Eng26\"," + + "URI=\"something2.m3u8\"\n" + + "#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"audio\",NAME=\"Eng\"," + + "URI=\"something3.m3u8\"\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=1280000," + + "CODECS=\"mp4a.40.2,avc1.66.30\",AUDIO=\"audio\",RESOLUTION=304x128\n" + + "http://example.com/low.m3u8\n"; + private static final String PLAYLIST_WITHOUT_CC = " #EXTM3U \n" + "#EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS," @@ -216,6 +228,17 @@ public class HlsMasterPlaylistParserTest { assertThat(closedCaptionFormat.language).isEqualTo("es"); } + @Test + public void testPlaylistWithChannelsAttribute() throws IOException { + HlsMasterPlaylist playlist = + parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_CHANNELS_ATTRIBUTE); + List audios = playlist.audios; + assertThat(audios).hasSize(3); + assertThat(audios.get(0).format.channelCount).isEqualTo(6); + assertThat(audios.get(1).format.channelCount).isEqualTo(2); + assertThat(audios.get(2).format.channelCount).isEqualTo(Format.NO_VALUE); + } + @Test public void testPlaylistWithoutClosedCaptions() throws IOException { HlsMasterPlaylist playlist = parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITHOUT_CC); diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java index 83f5b70cbb..9742d0005a 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java @@ -758,10 +758,6 @@ public class PlayerView extends FrameLayout { @Override public boolean dispatchKeyEvent(KeyEvent event) { if (player != null && player.isPlayingAd()) { - // Focus any overlay UI now, in case it's provided by a WebView whose contents may update - // dynamically. This is needed to make the "Skip ad" button focused on Android TV when using - // IMA [Internal: b/62371030]. - overlayFrameLayout.requestFocus(); return super.dispatchKeyEvent(event); } boolean isDpadWhenControlHidden = @@ -1035,6 +1031,12 @@ public class PlayerView extends FrameLayout { if (ev.getActionMasked() != MotionEvent.ACTION_DOWN) { return false; } + return performClick(); + } + + @Override + public boolean performClick() { + super.performClick(); return toggleControllerVisibility(); } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/DebugRenderersFactory.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/DebugRenderersFactory.java index 627b5b72f3..4ea2d7d754 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/DebugRenderersFactory.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/DebugRenderersFactory.java @@ -20,6 +20,7 @@ import android.content.Context; import android.media.MediaCodec; import android.media.MediaCrypto; import android.os.Handler; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.DefaultRenderersFactory; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; @@ -37,23 +38,38 @@ import java.util.ArrayList; /** * A debug extension of {@link DefaultRenderersFactory}. Provides a video renderer that performs - * video buffer timestamp assertions. + * video buffer timestamp assertions, and modifies the default value for {@link + * #setAllowedVideoJoiningTimeMs(long)} to be {@code 0}. */ @TargetApi(16) public class DebugRenderersFactory extends DefaultRenderersFactory { public DebugRenderersFactory(Context context) { - super(context, DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF, 0); + super(context); + setAllowedVideoJoiningTimeMs(0); } @Override - protected void buildVideoRenderers(Context context, - DrmSessionManager drmSessionManager, long allowedVideoJoiningTimeMs, - Handler eventHandler, VideoRendererEventListener eventListener, - @ExtensionRendererMode int extensionRendererMode, ArrayList out) { - out.add(new DebugMediaCodecVideoRenderer(context, MediaCodecSelector.DEFAULT, - allowedVideoJoiningTimeMs, drmSessionManager, eventHandler, eventListener, - MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY)); + protected void buildVideoRenderers( + Context context, + @ExtensionRendererMode int extensionRendererMode, + MediaCodecSelector mediaCodecSelector, + @Nullable DrmSessionManager drmSessionManager, + boolean playClearSamplesWithoutKeys, + Handler eventHandler, + VideoRendererEventListener eventListener, + long allowedVideoJoiningTimeMs, + ArrayList out) { + out.add( + new DebugMediaCodecVideoRenderer( + context, + mediaCodecSelector, + allowedVideoJoiningTimeMs, + drmSessionManager, + playClearSamplesWithoutKeys, + eventHandler, + eventListener, + MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY)); } /** @@ -72,12 +88,24 @@ public class DebugRenderersFactory extends DefaultRenderersFactory { private int minimumInsertIndex; private boolean skipToPositionBeforeRenderingFirstFrame; - public DebugMediaCodecVideoRenderer(Context context, MediaCodecSelector mediaCodecSelector, - long allowedJoiningTimeMs, DrmSessionManager drmSessionManager, - Handler eventHandler, VideoRendererEventListener eventListener, + public DebugMediaCodecVideoRenderer( + Context context, + MediaCodecSelector mediaCodecSelector, + long allowedJoiningTimeMs, + DrmSessionManager drmSessionManager, + boolean playClearSamplesWithoutKeys, + Handler eventHandler, + VideoRendererEventListener eventListener, int maxDroppedFrameCountToNotify) { - super(context, mediaCodecSelector, allowedJoiningTimeMs, drmSessionManager, false, - eventHandler, eventListener, maxDroppedFrameCountToNotify); + super( + context, + mediaCodecSelector, + allowedJoiningTimeMs, + drmSessionManager, + playClearSamplesWithoutKeys, + eventHandler, + eventListener, + maxDroppedFrameCountToNotify); } @Override diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/RobolectricUtil.java b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/RobolectricUtil.java index dc7781fd90..1e7f09bacf 100644 --- a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/RobolectricUtil.java +++ b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/RobolectricUtil.java @@ -37,6 +37,7 @@ import org.robolectric.shadows.ShadowMessageQueue; public final class RobolectricUtil { private static final AtomicLong sequenceNumberGenerator = new AtomicLong(0); + private static final int ANY_MESSAGE = Integer.MIN_VALUE; private RobolectricUtil() {} @@ -110,7 +111,8 @@ public final class RobolectricUtil { boolean isRemoved = false; for (RemovedMessage removedMessage : removedMessages) { if (removedMessage.handler == target - && removedMessage.what == pendingMessage.message.what + && (removedMessage.what == ANY_MESSAGE + || removedMessage.what == pendingMessage.message.what) && (removedMessage.object == null || removedMessage.object == pendingMessage.message.obj) && pendingMessage.sequenceNumber < removedMessage.sequenceNumber) { @@ -179,6 +181,15 @@ public final class RobolectricUtil { ((CustomLooper) shadowOf(looper)).removeMessages(handler, what, object); } } + + @Implementation + public void removeCallbacksAndMessages(Handler handler, Object object) { + Looper looper = ShadowLooper.getLooperForThread(looperThread); + if (shadowOf(looper) instanceof CustomLooper + && shadowOf(looper) != ShadowLooper.getShadowMainLooper()) { + ((CustomLooper) shadowOf(looper)).removeMessages(handler, ANY_MESSAGE, object); + } + } } private static final class PendingMessage implements Comparable {