commit
de39925ce9
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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')
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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<FrameworkMediaCrypto> drmSessionManager;
|
||||
private final @ExtensionRendererMode int extensionRendererMode;
|
||||
private final long allowedVideoJoiningTimeMs;
|
||||
@Nullable private DrmSessionManager<FrameworkMediaCrypto> 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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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<Renderer> 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<FrameworkMediaCrypto> drmSessionManager,
|
||||
long allowedVideoJoiningTimeMs, Handler eventHandler,
|
||||
VideoRendererEventListener eventListener, @ExtensionRendererMode int extensionRendererMode,
|
||||
boolean playClearSamplesWithoutKeys,
|
||||
Handler eventHandler,
|
||||
VideoRendererEventListener eventListener,
|
||||
long allowedVideoJoiningTimeMs,
|
||||
ArrayList<Renderer> 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<FrameworkMediaCrypto> drmSessionManager,
|
||||
AudioProcessor[] audioProcessors, Handler eventHandler,
|
||||
AudioRendererEventListener eventListener, @ExtensionRendererMode int extensionRendererMode,
|
||||
boolean playClearSamplesWithoutKeys,
|
||||
AudioProcessor[] audioProcessors,
|
||||
Handler eventHandler,
|
||||
AudioRendererEventListener eventListener,
|
||||
ArrayList<Renderer> out) {
|
||||
out.add(
|
||||
new MediaCodecAudioRenderer(
|
||||
context,
|
||||
MediaCodecSelector.DEFAULT,
|
||||
mediaCodecSelector,
|
||||
drmSessionManager,
|
||||
/* playClearSamplesWithoutKeys= */ false,
|
||||
playClearSamplesWithoutKeys,
|
||||
eventHandler,
|
||||
eventListener,
|
||||
AudioCapabilities.getCapabilities(context),
|
||||
|
@ -97,7 +97,8 @@ public final class ExoPlayerFactory {
|
||||
LoadControl loadControl,
|
||||
@Nullable DrmSessionManager<FrameworkMediaCrypto> 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);
|
||||
}
|
||||
|
@ -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}
|
||||
|
@ -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);
|
||||
|
@ -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)));
|
||||
|
@ -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<CodecKey, List<MediaCodecInfo>> 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<MediaCodecInfo> 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<MediaCodecInfo> {
|
||||
@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);
|
||||
|
@ -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<MediaSourceHo
|
||||
private static final int MSG_REMOVE = 1;
|
||||
private static final int MSG_MOVE = 2;
|
||||
private static final int MSG_SET_SHUFFLE_ORDER = 3;
|
||||
private static final int MSG_NOTIFY_LISTENER = 4;
|
||||
private static final int MSG_UPDATE_TIMELINE = 4;
|
||||
private static final int MSG_ON_COMPLETION = 5;
|
||||
|
||||
// Accessed on the app thread.
|
||||
// Accessed on any thread.
|
||||
@GuardedBy("this")
|
||||
private final List<MediaSourceHolder> mediaSourcesPublic;
|
||||
|
||||
// Accessed on the playback thread.
|
||||
@GuardedBy("this")
|
||||
private final Set<HandlerAndRunnable> pendingOnCompletionActions;
|
||||
|
||||
@GuardedBy("this")
|
||||
@Nullable
|
||||
private Handler playbackThreadHandler;
|
||||
|
||||
// Accessed on the playback thread only.
|
||||
private final List<MediaSourceHolder> mediaSourceHolders;
|
||||
private final Map<MediaPeriod, MediaSourceHolder> mediaSourceByMediaPeriod;
|
||||
private final Map<Object, MediaSourceHolder> mediaSourceByUid;
|
||||
private final List<Runnable> 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<HandlerAndRunnable> nextTimelineUpdateOnCompletionActions;
|
||||
private ShuffleOrder shuffleOrder;
|
||||
private int windowCount;
|
||||
private int periodCount;
|
||||
@ -127,7 +136,8 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
||||
this.mediaSourceByUid = new HashMap<>();
|
||||
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<MediaSourceHo
|
||||
* @param mediaSource The {@link MediaSource} to be added to the list.
|
||||
*/
|
||||
public final synchronized void addMediaSource(MediaSource mediaSource) {
|
||||
addMediaSource(mediaSourcesPublic.size(), mediaSource, null);
|
||||
addMediaSource(mediaSourcesPublic.size(), mediaSource);
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends a {@link MediaSource} to the playlist and executes a custom action on completion.
|
||||
*
|
||||
* @param mediaSource The {@link MediaSource} to be added to the list.
|
||||
* @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 added to the playlist.
|
||||
*/
|
||||
public final synchronized void addMediaSource(
|
||||
MediaSource mediaSource, @Nullable Runnable actionOnCompletion) {
|
||||
addMediaSource(mediaSourcesPublic.size(), mediaSource, actionOnCompletion);
|
||||
MediaSource mediaSource, Handler handler, Runnable onCompletionAction) {
|
||||
addMediaSource(mediaSourcesPublic.size(), mediaSource, handler, onCompletionAction);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -164,7 +175,11 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
||||
* @param mediaSource The {@link MediaSource} to be added to the list.
|
||||
*/
|
||||
public final synchronized void addMediaSource(int index, MediaSource mediaSource) {
|
||||
addMediaSource(index, mediaSource, null);
|
||||
addPublicMediaSources(
|
||||
index,
|
||||
Collections.singletonList(mediaSource),
|
||||
/* handler= */ null,
|
||||
/* onCompletionAction= */ null);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -173,12 +188,14 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
||||
* @param index The index at which the new {@link MediaSource} will be inserted. This index must
|
||||
* be in the range of 0 <= index <= {@link #getSize()}.
|
||||
* @param mediaSource The {@link MediaSource} to be added to the list.
|
||||
* @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 added to the playlist.
|
||||
*/
|
||||
public final synchronized void addMediaSource(
|
||||
int index, MediaSource mediaSource, @Nullable Runnable actionOnCompletion) {
|
||||
addMediaSources(index, Collections.singletonList(mediaSource), actionOnCompletion);
|
||||
int index, MediaSource mediaSource, Handler handler, Runnable onCompletionAction) {
|
||||
addPublicMediaSources(
|
||||
index, Collections.singletonList(mediaSource), handler, onCompletionAction);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -188,7 +205,11 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
||||
* sources are added in the order in which they appear in this collection.
|
||||
*/
|
||||
public final synchronized void addMediaSources(Collection<MediaSource> mediaSources) {
|
||||
addMediaSources(mediaSourcesPublic.size(), mediaSources, null);
|
||||
addPublicMediaSources(
|
||||
mediaSourcesPublic.size(),
|
||||
mediaSources,
|
||||
/* handler= */ null,
|
||||
/* onCompletionAction= */ null);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -197,12 +218,13 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
||||
*
|
||||
* @param mediaSources A collection of {@link MediaSource}s to be added to the list. The media
|
||||
* sources are added in the order in which they appear in this collection.
|
||||
* @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
|
||||
* sources have been added to the playlist.
|
||||
*/
|
||||
public final synchronized void addMediaSources(
|
||||
Collection<MediaSource> mediaSources, @Nullable Runnable actionOnCompletion) {
|
||||
addMediaSources(mediaSourcesPublic.size(), mediaSources, actionOnCompletion);
|
||||
Collection<MediaSource> mediaSources, Handler handler, Runnable onCompletionAction) {
|
||||
addPublicMediaSources(mediaSourcesPublic.size(), mediaSources, handler, onCompletionAction);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -214,7 +236,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
||||
* sources are added in the order in which they appear in this collection.
|
||||
*/
|
||||
public final synchronized void addMediaSources(int index, Collection<MediaSource> mediaSources) {
|
||||
addMediaSources(index, mediaSources, null);
|
||||
addPublicMediaSources(index, mediaSources, /* handler= */ null, /* onCompletionAction= */ null);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -224,26 +246,16 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
||||
* be in the range of 0 <= index <= {@link #getSize()}.
|
||||
* @param mediaSources A collection of {@link MediaSource}s to be added to the list. The media
|
||||
* sources are added in the order in which they appear in this collection.
|
||||
* @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
|
||||
* sources have been added to the playlist.
|
||||
*/
|
||||
public final synchronized void addMediaSources(
|
||||
int index, Collection<MediaSource> mediaSources, @Nullable Runnable actionOnCompletion) {
|
||||
for (MediaSource mediaSource : mediaSources) {
|
||||
Assertions.checkNotNull(mediaSource);
|
||||
}
|
||||
List<MediaSourceHolder> 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<MediaSource> mediaSources,
|
||||
Handler handler,
|
||||
Runnable onCompletionAction) {
|
||||
addPublicMediaSources(index, mediaSources, handler, onCompletionAction);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -259,26 +271,27 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
||||
* range of 0 <= index < {@link #getSize()}.
|
||||
*/
|
||||
public final synchronized void removeMediaSource(int index) {
|
||||
removeMediaSource(index, null);
|
||||
removePublicMediaSources(index, index + 1, /* handler= */ null, /* onCompletionAction= */ null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a {@link MediaSource} from the playlist and executes a custom action on completion.
|
||||
*
|
||||
* <p>Note: If you want to move the instance, it's preferable to use {@link #moveMediaSource(int,
|
||||
* int, Runnable)} instead.
|
||||
* int, Handler, Runnable)} instead.
|
||||
*
|
||||
* <p>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<MediaSourceHo
|
||||
* {@code toIndex} > {@link #getSize()}, {@code fromIndex} > {@code toIndex}
|
||||
*/
|
||||
public final synchronized void removeMediaSourceRange(int fromIndex, int toIndex) {
|
||||
removeMediaSourceRange(fromIndex, toIndex, null);
|
||||
removePublicMediaSources(
|
||||
fromIndex, toIndex, /* handler= */ null, /* onCompletionAction= */ null);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -310,27 +324,15 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
||||
* removed. This index must be in the range of 0 <= index <= {@link #getSize()}.
|
||||
* @param toIndex The final range index, pointing to the first media source that will be left
|
||||
* untouched. 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 range has been removed from the playlist.
|
||||
* @throws IllegalArgumentException When the range is malformed, i.e. {@code fromIndex} < 0,
|
||||
* {@code toIndex} > {@link #getSize()}, {@code fromIndex} > {@code toIndex}
|
||||
*/
|
||||
public final synchronized void removeMediaSourceRange(
|
||||
int fromIndex, int toIndex, @Nullable Runnable actionOnCompletion) {
|
||||
Util.removeRange(mediaSourcesPublic, fromIndex, toIndex);
|
||||
if (fromIndex == toIndex) {
|
||||
if (actionOnCompletion != null) {
|
||||
actionOnCompletion.run();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (playbackThreadHandler != null) {
|
||||
playbackThreadHandler
|
||||
.obtainMessage(MSG_REMOVE, new MessageData<>(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<MediaSourceHo
|
||||
* range of 0 <= index < {@link #getSize()}.
|
||||
*/
|
||||
public final synchronized void moveMediaSource(int currentIndex, int newIndex) {
|
||||
moveMediaSource(currentIndex, newIndex, null);
|
||||
movePublicMediaSource(
|
||||
currentIndex, newIndex, /* handler= */ null, /* onCompletionAction= */ null);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -353,40 +356,29 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
||||
* in the range of 0 <= index < {@link #getSize()}.
|
||||
* @param newIndex The target index of the media source in the playlist. 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 moved.
|
||||
*/
|
||||
public final synchronized void moveMediaSource(
|
||||
int currentIndex, int newIndex, @Nullable Runnable actionOnCompletion) {
|
||||
if (currentIndex == newIndex) {
|
||||
if (actionOnCompletion != null) {
|
||||
actionOnCompletion.run();
|
||||
}
|
||||
return;
|
||||
}
|
||||
mediaSourcesPublic.add(newIndex, mediaSourcesPublic.remove(currentIndex));
|
||||
if (playbackThreadHandler != null) {
|
||||
playbackThreadHandler
|
||||
.obtainMessage(MSG_MOVE, new MessageData<>(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<MediaSourceHo
|
||||
* @param shuffleOrder A {@link ShuffleOrder}.
|
||||
*/
|
||||
public final synchronized void setShuffleOrder(ShuffleOrder shuffleOrder) {
|
||||
setShuffleOrder(shuffleOrder, /* actionOnCompletion= */ null);
|
||||
setPublicShuffleOrder(shuffleOrder, /* handler= */ null, /* onCompletionAction= */ null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a new shuffle order to use when shuffling the child media sources.
|
||||
*
|
||||
* @param shuffleOrder A {@link ShuffleOrder}.
|
||||
* @param actionOnCompletion A {@link Runnable} which is executed immediately after the shuffle
|
||||
* @param handler The {@link Handler} to run {@code onCompletionAction}.
|
||||
* @param onCompletionAction A {@link Runnable} which is executed immediately after the shuffle
|
||||
* order has been changed.
|
||||
*/
|
||||
public final synchronized void setShuffleOrder(
|
||||
ShuffleOrder shuffleOrder, @Nullable Runnable actionOnCompletion) {
|
||||
Handler playbackThreadHandler = this.playbackThreadHandler;
|
||||
if (playbackThreadHandler != null) {
|
||||
int size = getSize();
|
||||
if (shuffleOrder.getLength() != size) {
|
||||
shuffleOrder =
|
||||
shuffleOrder
|
||||
.cloneAndClear()
|
||||
.cloneAndInsert(/* insertionIndex= */ 0, /* insertionCount= */ size);
|
||||
}
|
||||
playbackThreadHandler
|
||||
.obtainMessage(
|
||||
MSG_SET_SHUFFLE_ORDER,
|
||||
new MessageData<>(/* 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<MediaSourceHo
|
||||
@Nullable TransferListener mediaTransferListener) {
|
||||
super.prepareSourceInternal(player, isTopLevelSource, mediaTransferListener);
|
||||
playbackThreadHandler = new Handler(/* callback= */ this::handleMessage);
|
||||
applicationThreadHandler = new Handler(player.getApplicationLooper());
|
||||
if (mediaSourcesPublic.isEmpty()) {
|
||||
notifyListener();
|
||||
updateTimelineAndScheduleOnCompletionActions();
|
||||
} else {
|
||||
shuffleOrder = shuffleOrder.cloneAndInsert(0, mediaSourcesPublic.size());
|
||||
addMediaSourcesInternal(0, mediaSourcesPublic);
|
||||
scheduleListenerNotification(/* actionOnCompletion= */ null);
|
||||
scheduleTimelineUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
@ -509,15 +483,20 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void releaseSourceInternal() {
|
||||
public final synchronized void releaseSourceInternal() {
|
||||
super.releaseSourceInternal();
|
||||
mediaSourceHolders.clear();
|
||||
mediaSourceByUid.clear();
|
||||
playbackThreadHandler = null;
|
||||
applicationThreadHandler = null;
|
||||
shuffleOrder = shuffleOrder.cloneAndClear();
|
||||
windowCount = 0;
|
||||
periodCount = 0;
|
||||
if (playbackThreadHandler != null) {
|
||||
playbackThreadHandler.removeCallbacksAndMessages(null);
|
||||
playbackThreadHandler = null;
|
||||
}
|
||||
timelineUpdateScheduled = false;
|
||||
nextTimelineUpdateOnCompletionActions.clear();
|
||||
dispatchOnCompletionActions(pendingOnCompletionActions);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -550,19 +529,123 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
||||
return windowIndex + mediaSourceHolder.firstWindowIndexInChild;
|
||||
}
|
||||
|
||||
// Internal methods. Called from any thread.
|
||||
|
||||
@GuardedBy("this")
|
||||
private void addPublicMediaSources(
|
||||
int index,
|
||||
Collection<MediaSource> 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<MediaSourceHolder> 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<Collection<MediaSourceHolder>> addMessage =
|
||||
(MessageData<Collection<MediaSourceHolder>>) 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<Integer> removeMessage = (MessageData<Integer>) Util.castNonNull(msg.obj);
|
||||
@ -576,30 +659,27 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
||||
for (int index = toIndex - 1; index >= fromIndex; index--) {
|
||||
removeMediaSourceInternal(index);
|
||||
}
|
||||
scheduleListenerNotification(removeMessage.actionOnCompletion);
|
||||
scheduleTimelineUpdate(removeMessage.onCompletionAction);
|
||||
break;
|
||||
case MSG_MOVE:
|
||||
MessageData<Integer> moveMessage = (MessageData<Integer>) 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<ShuffleOrder> shuffleOrderMessage =
|
||||
(MessageData<ShuffleOrder>) 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<Runnable> actionsOnCompletion = (List<Runnable>) Util.castNonNull(msg.obj);
|
||||
Handler handler = Assertions.checkNotNull(applicationThreadHandler);
|
||||
for (int i = 0; i < actionsOnCompletion.size(); i++) {
|
||||
handler.post(actionsOnCompletion.get(i));
|
||||
}
|
||||
Set<HandlerAndRunnable> actions = (Set<HandlerAndRunnable>) Util.castNonNull(msg.obj);
|
||||
dispatchOnCompletionActions(actions);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException();
|
||||
@ -607,34 +687,46 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
||||
return true;
|
||||
}
|
||||
|
||||
private void scheduleListenerNotification(@Nullable Runnable actionOnCompletion) {
|
||||
if (!listenerNotificationScheduled) {
|
||||
Assertions.checkNotNull(playbackThreadHandler)
|
||||
.obtainMessage(MSG_NOTIFY_LISTENER)
|
||||
.sendToTarget();
|
||||
listenerNotificationScheduled = true;
|
||||
private void scheduleTimelineUpdate() {
|
||||
scheduleTimelineUpdate(/* onCompletionAction= */ null);
|
||||
}
|
||||
|
||||
private void scheduleTimelineUpdate(@Nullable HandlerAndRunnable onCompletionAction) {
|
||||
if (!timelineUpdateScheduled) {
|
||||
getPlaybackThreadHandlerOnPlaybackThread().obtainMessage(MSG_UPDATE_TIMELINE).sendToTarget();
|
||||
timelineUpdateScheduled = true;
|
||||
}
|
||||
if (actionOnCompletion != null) {
|
||||
pendingOnCompletionActions.add(actionOnCompletion);
|
||||
if (onCompletionAction != null) {
|
||||
nextTimelineUpdateOnCompletionActions.add(onCompletionAction);
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyListener() {
|
||||
listenerNotificationScheduled = false;
|
||||
List<Runnable> actionsOnCompletion =
|
||||
pendingOnCompletionActions.isEmpty()
|
||||
? Collections.emptyList()
|
||||
: new ArrayList<>(pendingOnCompletionActions);
|
||||
pendingOnCompletionActions.clear();
|
||||
private void updateTimelineAndScheduleOnCompletionActions() {
|
||||
timelineUpdateScheduled = false;
|
||||
Set<HandlerAndRunnable> 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<HandlerAndRunnable> onCompletionActions) {
|
||||
for (HandlerAndRunnable pendingAction : onCompletionActions) {
|
||||
pendingAction.dispatch();
|
||||
}
|
||||
pendingOnCompletionActions.removeAll(onCompletionActions);
|
||||
}
|
||||
|
||||
private void addMediaSourcesInternal(
|
||||
@ -733,7 +825,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
||||
}
|
||||
}
|
||||
mediaSourceHolder.isPrepared = true;
|
||||
scheduleListenerNotification(/* actionOnCompletion= */ null);
|
||||
scheduleTimelineUpdate();
|
||||
}
|
||||
|
||||
private void removeMediaSourceInternal(int index) {
|
||||
@ -846,12 +938,12 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
||||
|
||||
public final int index;
|
||||
public final T customData;
|
||||
public final @Nullable Runnable actionOnCompletion;
|
||||
@Nullable public final HandlerAndRunnable onCompletionAction;
|
||||
|
||||
public MessageData(int index, T customData, @Nullable Runnable actionOnCompletion) {
|
||||
public MessageData(int index, T customData, @Nullable HandlerAndRunnable onCompletionAction) {
|
||||
this.index = index;
|
||||
this.actionOnCompletion = actionOnCompletion;
|
||||
this.customData = customData;
|
||||
this.onCompletionAction = onCompletionAction;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1104,5 +1196,20 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
||||
// Do nothing.
|
||||
}
|
||||
}
|
||||
|
||||
private static final class HandlerAndRunnable {
|
||||
|
||||
private final Handler handler;
|
||||
private final Runnable runnable;
|
||||
|
||||
public HandlerAndRunnable(Handler handler, Runnable runnable) {
|
||||
this.handler = handler;
|
||||
this.runnable = runnable;
|
||||
}
|
||||
|
||||
public void dispatch() {
|
||||
handler.post(runnable);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -346,10 +346,9 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
||||
} else if (isPendingReset()) {
|
||||
return pendingResetPositionUs;
|
||||
}
|
||||
long largestQueuedTimestampUs = C.TIME_UNSET;
|
||||
long largestQueuedTimestampUs = Long.MAX_VALUE;
|
||||
if (haveAudioVideoTracks) {
|
||||
// Ignore non-AV tracks, which may be sparse or poorly interleaved.
|
||||
largestQueuedTimestampUs = Long.MAX_VALUE;
|
||||
int trackCount = sampleQueues.length;
|
||||
for (int i = 0; i < trackCount; i++) {
|
||||
if (trackIsAudioVideoFlags[i] && !sampleQueues[i].isLastSampleQueued()) {
|
||||
@ -358,7 +357,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (largestQueuedTimestampUs == C.TIME_UNSET) {
|
||||
if (largestQueuedTimestampUs == Long.MAX_VALUE) {
|
||||
largestQueuedTimestampUs = getLargestQueuedTimestampUs();
|
||||
}
|
||||
return largestQueuedTimestampUs == Long.MIN_VALUE ? lastSeekPositionUs
|
||||
|
@ -31,7 +31,7 @@ import java.util.Map;
|
||||
* Loops a {@link MediaSource} a specified number of times.
|
||||
*
|
||||
* <p>Note: 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<Void> {
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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 {
|
||||
|
@ -101,6 +101,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
|
||||
Pattern.compile("AVERAGE-BANDWIDTH=(\\d+)\\b");
|
||||
private static final Pattern REGEX_AUDIO = Pattern.compile("AUDIO=\"(.+?)\"");
|
||||
private static final Pattern REGEX_BANDWIDTH = Pattern.compile("[^-]BANDWIDTH=(\\d+)\\b");
|
||||
private static final Pattern REGEX_CHANNELS = Pattern.compile("CHANNELS=\"(.+?)\"");
|
||||
private static final Pattern REGEX_CODECS = Pattern.compile("CODECS=\"(.+?)\"");
|
||||
private static final Pattern REGEX_RESOLUTION = Pattern.compile("RESOLUTION=(\\d+x\\d+)");
|
||||
private static final Pattern REGEX_FRAME_RATE = Pattern.compile("FRAME-RATE=([\\d\\.]+)\\b");
|
||||
@ -346,6 +347,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
|
||||
switch (parseStringAttr(line, REGEX_TYPE, variableDefinitions)) {
|
||||
case TYPE_AUDIO:
|
||||
String codecs = audioGroupIdToCodecs.get(groupId);
|
||||
int channelCount = parseChannelsAttribute(line, variableDefinitions);
|
||||
String sampleMimeType = codecs != null ? MimeTypes.getMediaMimeType(codecs) : null;
|
||||
format =
|
||||
Format.createAudioContainerFormat(
|
||||
@ -355,7 +357,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
|
||||
sampleMimeType,
|
||||
codecs,
|
||||
/* bitrate= */ Format.NO_VALUE,
|
||||
/* channelCount= */ Format.NO_VALUE,
|
||||
channelCount,
|
||||
/* sampleRate= */ Format.NO_VALUE,
|
||||
/* initializationData= */ null,
|
||||
selectionFlags,
|
||||
@ -426,21 +428,6 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
|
||||
variableDefinitions);
|
||||
}
|
||||
|
||||
@C.SelectionFlags
|
||||
private static int parseSelectionFlags(String line) {
|
||||
int flags = 0;
|
||||
if (parseOptionalBooleanAttribute(line, REGEX_DEFAULT, false)) {
|
||||
flags |= C.SELECTION_FLAG_DEFAULT;
|
||||
}
|
||||
if (parseOptionalBooleanAttribute(line, REGEX_FORCED, false)) {
|
||||
flags |= C.SELECTION_FLAG_FORCED;
|
||||
}
|
||||
if (parseOptionalBooleanAttribute(line, REGEX_AUTOSELECT, false)) {
|
||||
flags |= C.SELECTION_FLAG_AUTOSELECT;
|
||||
}
|
||||
return flags;
|
||||
}
|
||||
|
||||
private static HlsMediaPlaylist parseMediaPlaylist(
|
||||
HlsMasterPlaylist masterPlaylist, LineIterator iterator, String baseUri) throws IOException {
|
||||
@HlsMediaPlaylist.PlaylistType int playlistType = HlsMediaPlaylist.PLAYLIST_TYPE_UNKNOWN;
|
||||
@ -661,6 +648,28 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
|
||||
segments);
|
||||
}
|
||||
|
||||
@C.SelectionFlags
|
||||
private static int parseSelectionFlags(String line) {
|
||||
int flags = 0;
|
||||
if (parseOptionalBooleanAttribute(line, REGEX_DEFAULT, false)) {
|
||||
flags |= C.SELECTION_FLAG_DEFAULT;
|
||||
}
|
||||
if (parseOptionalBooleanAttribute(line, REGEX_FORCED, false)) {
|
||||
flags |= C.SELECTION_FLAG_FORCED;
|
||||
}
|
||||
if (parseOptionalBooleanAttribute(line, REGEX_AUTOSELECT, false)) {
|
||||
flags |= C.SELECTION_FLAG_AUTOSELECT;
|
||||
}
|
||||
return flags;
|
||||
}
|
||||
|
||||
private static int parseChannelsAttribute(String line, Map<String, String> 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<String, String> variableDefinitions) throws ParserException {
|
||||
String keyFormatVersions =
|
||||
|
@ -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<HlsMasterPlaylist.HlsUrl> 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);
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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<FrameworkMediaCrypto> drmSessionManager, long allowedVideoJoiningTimeMs,
|
||||
Handler eventHandler, VideoRendererEventListener eventListener,
|
||||
@ExtensionRendererMode int extensionRendererMode, ArrayList<Renderer> 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<FrameworkMediaCrypto> drmSessionManager,
|
||||
boolean playClearSamplesWithoutKeys,
|
||||
Handler eventHandler,
|
||||
VideoRendererEventListener eventListener,
|
||||
long allowedVideoJoiningTimeMs,
|
||||
ArrayList<Renderer> 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<FrameworkMediaCrypto> drmSessionManager,
|
||||
Handler eventHandler, VideoRendererEventListener eventListener,
|
||||
public DebugMediaCodecVideoRenderer(
|
||||
Context context,
|
||||
MediaCodecSelector mediaCodecSelector,
|
||||
long allowedJoiningTimeMs,
|
||||
DrmSessionManager<FrameworkMediaCrypto> 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
|
||||
|
@ -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<PendingMessage> {
|
||||
|
Loading…
x
Reference in New Issue
Block a user