This is suitable for applications that do not play media with the screen off. + */ + public static final int WAKE_MODE_NONE = 0; + /** + * A wake mode that will cause the player to hold a {@link android.os.PowerManager.WakeLock} + * during playback. + * + *
This is suitable for applications that play media with the screen off and do not load media + * over wifi. + */ + public static final int WAKE_MODE_LOCAL = 1; + /** + * A wake mode that will cause the player to hold a {@link android.os.PowerManager.WakeLock} and a + * {@link android.net.wifi.WifiManager.WifiLock} during playback. + * + *
This is suitable for applications that play media with the screen off and may load media + * over wifi. + */ + public static final int WAKE_MODE_NETWORK = 2; + /** * Track role flags. Possible flag values are {@link #ROLE_FLAG_MAIN}, {@link * #ROLE_FLAG_ALTERNATE}, {@link #ROLE_FLAG_SUPPLEMENTARY}, {@link #ROLE_FLAG_COMMENTARY}, {@link diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index 240c6436c4..6fd23a93c5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -386,7 +386,7 @@ import java.util.concurrent.atomic.AtomicBoolean; playbackInfo = playbackInfo.copyWithPlaybackError(e); maybeNotifyPlaybackInfoChanged(); } catch (IOException e) { - Log.e(TAG, "Source error.", e); + Log.e(TAG, "Source error", e); stopInternal( /* forceResetRenderers= */ false, /* resetPositionAndState= */ false, @@ -394,7 +394,7 @@ import java.util.concurrent.atomic.AtomicBoolean; playbackInfo = playbackInfo.copyWithPlaybackError(ExoPlaybackException.createForSource(e)); maybeNotifyPlaybackInfoChanged(); } catch (RuntimeException | OutOfMemoryError e) { - Log.e(TAG, "Internal runtime error.", e); + Log.e(TAG, "Internal runtime error", e); ExoPlaybackException error = e instanceof OutOfMemoryError ? ExoPlaybackException.createForOutOfMemoryError((OutOfMemoryError) e) 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 51d077270a..06743732e7 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.11.3"; + public static final String VERSION = "2.11.4"; /** 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.11.3"; + public static final String VERSION_SLASHY = "ExoPlayerLib/2.11.4"; /** * 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 = 2011003; + public static final int VERSION_INT = 2011004; /** * 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/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index 52b686000a..839c65b124 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -325,6 +325,7 @@ public class SimpleExoPlayer extends BasePlayer private final AudioBecomingNoisyManager audioBecomingNoisyManager; private final AudioFocusManager audioFocusManager; private final WakeLockManager wakeLockManager; + private final WifiLockManager wifiLockManager; @Nullable private Format videoFormat; @Nullable private Format audioFormat; @@ -445,8 +446,8 @@ public class SimpleExoPlayer extends BasePlayer player = new ExoPlayerImpl(renderers, trackSelector, loadControl, bandwidthMeter, clock, looper); analyticsCollector.setPlayer(player); - addListener(analyticsCollector); - addListener(componentListener); + player.addListener(analyticsCollector); + player.addListener(componentListener); videoDebugListeners.add(analyticsCollector); videoListeners.add(analyticsCollector); audioDebugListeners.add(analyticsCollector); @@ -460,6 +461,7 @@ public class SimpleExoPlayer extends BasePlayer new AudioBecomingNoisyManager(context, eventHandler, componentListener); audioFocusManager = new AudioFocusManager(context, eventHandler, componentListener); wakeLockManager = new WakeLockManager(context); + wifiLockManager = new WifiLockManager(context); } @Override @@ -684,11 +686,11 @@ public class SimpleExoPlayer extends BasePlayer } } + audioFocusManager.setAudioAttributes(handleAudioFocus ? audioAttributes : null); + boolean playWhenReady = getPlayWhenReady(); @AudioFocusManager.PlayerCommand - int playerCommand = - audioFocusManager.setAudioAttributes( - handleAudioFocus ? audioAttributes : null, getPlayWhenReady(), getPlaybackState()); - updatePlayWhenReady(getPlayWhenReady(), playerCommand); + int playerCommand = audioFocusManager.updateAudioFocus(playWhenReady, getPlaybackState()); + updatePlayWhenReady(playWhenReady, playerCommand); } @Override @@ -1187,9 +1189,10 @@ public class SimpleExoPlayer extends BasePlayer } this.mediaSource = mediaSource; mediaSource.addEventListener(eventHandler, analyticsCollector); + boolean playWhenReady = getPlayWhenReady(); @AudioFocusManager.PlayerCommand - int playerCommand = audioFocusManager.handlePrepare(getPlayWhenReady()); - updatePlayWhenReady(getPlayWhenReady(), playerCommand); + int playerCommand = audioFocusManager.updateAudioFocus(playWhenReady, Player.STATE_BUFFERING); + updatePlayWhenReady(playWhenReady, playerCommand); player.prepare(mediaSource, resetPosition, resetState); } @@ -1197,7 +1200,7 @@ public class SimpleExoPlayer extends BasePlayer public void setPlayWhenReady(boolean playWhenReady) { verifyApplicationThread(); @AudioFocusManager.PlayerCommand - int playerCommand = audioFocusManager.handleSetPlayWhenReady(playWhenReady, getPlaybackState()); + int playerCommand = audioFocusManager.updateAudioFocus(playWhenReady, getPlaybackState()); updatePlayWhenReady(playWhenReady, playerCommand); } @@ -1276,6 +1279,7 @@ public class SimpleExoPlayer extends BasePlayer @Override public void stop(boolean reset) { verifyApplicationThread(); + audioFocusManager.updateAudioFocus(getPlayWhenReady(), Player.STATE_IDLE); player.stop(reset); if (mediaSource != null) { mediaSource.removeEventListener(analyticsCollector); @@ -1284,7 +1288,6 @@ public class SimpleExoPlayer extends BasePlayer mediaSource = null; } } - audioFocusManager.handleStop(); currentCues = Collections.emptyList(); } @@ -1292,8 +1295,9 @@ public class SimpleExoPlayer extends BasePlayer public void release() { verifyApplicationThread(); audioBecomingNoisyManager.setEnabled(false); - audioFocusManager.handleStop(); wakeLockManager.setStayAwake(false); + wifiLockManager.setStayAwake(false); + audioFocusManager.release(); player.release(); removeSurfaceCallbacks(); if (surface != null) { @@ -1432,9 +1436,45 @@ public class SimpleExoPlayer extends BasePlayer * * @param handleWakeLock Whether the player should use a {@link android.os.PowerManager.WakeLock} * to ensure the device stays awake for playback, even when the screen is off. + * @deprecated Use {@link #setWakeMode(int)} instead. */ + @Deprecated public void setHandleWakeLock(boolean handleWakeLock) { - wakeLockManager.setEnabled(handleWakeLock); + setWakeMode(handleWakeLock ? C.WAKE_MODE_LOCAL : C.WAKE_MODE_NONE); + } + + /** + * Sets how the player should keep the device awake for playback when the screen is off. + * + *
Enabling this feature requires the {@link android.Manifest.permission#WAKE_LOCK} permission. + * It should be used together with a foreground {@link android.app.Service} for use cases where + * playback occurs and the screen is off (e.g. background audio playback). It is not useful when + * the screen will be kept on during playback (e.g. foreground video playback). + * + *
When enabled, the locks ({@link android.os.PowerManager.WakeLock} / {@link + * android.net.wifi.WifiManager.WifiLock}) will be held whenever the player is in the {@link + * #STATE_READY} or {@link #STATE_BUFFERING} states with {@code playWhenReady = true}. The locks + * held depends on the specified {@link C.WakeMode}. + * + * @param wakeMode The {@link C.WakeMode} option to keep the device awake during playback. + */ + public void setWakeMode(@C.WakeMode int wakeMode) { + switch (wakeMode) { + case C.WAKE_MODE_NONE: + wakeLockManager.setEnabled(false); + wifiLockManager.setEnabled(false); + break; + case C.WAKE_MODE_LOCAL: + wakeLockManager.setEnabled(true); + wifiLockManager.setEnabled(false); + break; + case C.WAKE_MODE_NETWORK: + wakeLockManager.setEnabled(true); + wifiLockManager.setEnabled(true); + break; + default: + break; + } } // Internal methods. @@ -1537,6 +1577,24 @@ public class SimpleExoPlayer extends BasePlayer } } + private void updateWakeAndWifiLock() { + @State int playbackState = getPlaybackState(); + switch (playbackState) { + case Player.STATE_READY: + case Player.STATE_BUFFERING: + wakeLockManager.setStayAwake(getPlayWhenReady()); + wifiLockManager.setStayAwake(getPlayWhenReady()); + break; + case Player.STATE_ENDED: + case Player.STATE_IDLE: + wakeLockManager.setStayAwake(false); + wifiLockManager.setStayAwake(false); + break; + default: + throw new IllegalStateException(); + } + } + private final class ComponentListener implements VideoRendererEventListener, AudioRendererEventListener, @@ -1781,16 +1839,7 @@ public class SimpleExoPlayer extends BasePlayer @Override public void onPlayerStateChanged(boolean playWhenReady, @State int playbackState) { - switch (playbackState) { - case Player.STATE_READY: - case Player.STATE_BUFFERING: - wakeLockManager.setStayAwake(playWhenReady); - break; - case Player.STATE_ENDED: - case Player.STATE_IDLE: - wakeLockManager.setStayAwake(false); - break; - } + updateWakeAndWifiLock(); } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/WakeLockManager.java b/library/core/src/main/java/com/google/android/exoplayer2/WakeLockManager.java index f498eea6f4..6de302d62d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/WakeLockManager.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/WakeLockManager.java @@ -39,7 +39,8 @@ import com.google.android.exoplayer2.util.Log; private boolean stayAwake; public WakeLockManager(Context context) { - powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + powerManager = + (PowerManager) context.getApplicationContext().getSystemService(Context.POWER_SERVICE); } /** @@ -48,18 +49,19 @@ import com.google.android.exoplayer2.util.Log; *
By default, wake lock handling is not enabled. Enabling this will acquire the wake lock if * necessary. Disabling this will release the wake lock if it is held. * - * @param enabled True if the player should handle a {@link WakeLock}, false otherwise. Please - * note that enabling this requires the {@link android.Manifest.permission#WAKE_LOCK} - * permission. + *
Enabling {@link WakeLock} requires the {@link android.Manifest.permission#WAKE_LOCK}. + * + * @param enabled True if the player should handle a {@link WakeLock}, false otherwise. */ public void setEnabled(boolean enabled) { if (enabled) { if (wakeLock == null) { if (powerManager == null) { - Log.w(TAG, "PowerManager was null, therefore the WakeLock was not created."); + Log.w(TAG, "PowerManager is null, therefore not creating the WakeLock."); return; } wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKE_LOCK_TAG); + wakeLock.setReferenceCounted(false); } } @@ -86,17 +88,14 @@ import com.google.android.exoplayer2.util.Log; // reasonable timeout that would not affect the user. @SuppressLint("WakelockTimeout") private void updateWakeLock() { - // Needed for the library nullness check. If enabled is true, the wakelock will not be null. - if (wakeLock != null) { - if (enabled) { - if (stayAwake && !wakeLock.isHeld()) { - wakeLock.acquire(); - } else if (!stayAwake && wakeLock.isHeld()) { - wakeLock.release(); - } - } else if (wakeLock.isHeld()) { - wakeLock.release(); - } + if (wakeLock == null) { + return; + } + + if (enabled && stayAwake) { + wakeLock.acquire(); + } else { + wakeLock.release(); } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/WifiLockManager.java b/library/core/src/main/java/com/google/android/exoplayer2/WifiLockManager.java new file mode 100644 index 0000000000..d3700a646a --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/WifiLockManager.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2; + +import android.content.Context; +import android.net.wifi.WifiManager; +import android.net.wifi.WifiManager.WifiLock; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.util.Log; + +/** + * Handles a {@link WifiLock} + * + *
The handling of wifi locks requires the {@link android.Manifest.permission#WAKE_LOCK} + * permission. + */ +/* package */ final class WifiLockManager { + + private static final String TAG = "WifiLockManager"; + private static final String WIFI_LOCK_TAG = "ExoPlayer:WifiLockManager"; + + @Nullable private final WifiManager wifiManager; + @Nullable private WifiLock wifiLock; + private boolean enabled; + private boolean stayAwake; + + public WifiLockManager(Context context) { + wifiManager = + (WifiManager) context.getApplicationContext().getSystemService(Context.WIFI_SERVICE); + } + + /** + * Sets whether to enable the usage of a {@link WifiLock}. + * + *
By default, wifi lock handling is not enabled. Enabling will acquire the wifi lock if + * necessary. Disabling will release the wifi lock if held. + * + *
Enabling {@link WifiLock} requires the {@link android.Manifest.permission#WAKE_LOCK}. + * + * @param enabled True if the player should handle a {@link WifiLock}. + */ + public void setEnabled(boolean enabled) { + if (enabled && wifiLock == null) { + if (wifiManager == null) { + Log.w(TAG, "WifiManager is null, therefore not creating the WifiLock."); + return; + } + wifiLock = wifiManager.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, WIFI_LOCK_TAG); + wifiLock.setReferenceCounted(false); + } + + this.enabled = enabled; + updateWifiLock(); + } + + /** + * Sets whether to acquire or release the {@link WifiLock}. + * + *
The wifi lock will not be acquired unless handling has been enabled through {@link + * #setEnabled(boolean)}. + * + * @param stayAwake True if the player should acquire the {@link WifiLock}. False if it should + * release. + */ + public void setStayAwake(boolean stayAwake) { + this.stayAwake = stayAwake; + updateWifiLock(); + } + + private void updateWifiLock() { + if (wifiLock == null) { + return; + } + + if (enabled && stayAwake) { + wifiLock.acquire(); + } else { + wifiLock.release(); + } + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStatsListener.java b/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStatsListener.java index 3430bfc1dd..a9fd9d8641 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStatsListener.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStatsListener.java @@ -50,7 +50,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; *
For accurate measurements, the listener should be added to the player before loading media, * i.e., {@link Player#getPlaybackState()} should be {@link Player#STATE_IDLE}. * - *
Playback stats are gathered separately for all playback session, i.e. each window in the + *
Playback stats are gathered separately for each playback session, i.e. each window in the * {@link Timeline} and each single ad. */ public final class PlaybackStatsListener @@ -931,6 +931,9 @@ public final class PlaybackStatsListener } private void maybeUpdateMediaTimeHistory(long realtimeMs, long mediaTimeMs) { + if (!keepHistory) { + return; + } if (currentPlaybackState != PlaybackStats.PLAYBACK_STATE_PLAYING) { if (mediaTimeMs == C.TIME_UNSET) { return; 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 27abf486fa..ba31c118e7 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 @@ -238,7 +238,7 @@ public final class DefaultAudioSink implements AudioSink { @Nullable private final AudioCapabilities audioCapabilities; private final AudioProcessorChain audioProcessorChain; - private final boolean enableConvertHighResIntPcmToFloat; + private final boolean enableFloatOutput; private final ChannelMappingAudioProcessor channelMappingAudioProcessor; private final TrimmingAudioProcessor trimmingAudioProcessor; private final AudioProcessor[] toIntPcmAvailableAudioProcessors; @@ -299,7 +299,7 @@ public final class DefaultAudioSink implements AudioSink { */ public DefaultAudioSink( @Nullable AudioCapabilities audioCapabilities, AudioProcessor[] audioProcessors) { - this(audioCapabilities, audioProcessors, /* enableConvertHighResIntPcmToFloat= */ false); + this(audioCapabilities, audioProcessors, /* enableFloatOutput= */ false); } /** @@ -309,19 +309,16 @@ public final class DefaultAudioSink implements AudioSink { * default capabilities (no encoded audio passthrough support) should be assumed. * @param audioProcessors An array of {@link AudioProcessor}s that will process PCM audio before * output. May be empty. - * @param enableConvertHighResIntPcmToFloat Whether to enable conversion of high resolution - * integer PCM to 32-bit float for output, if possible. Functionality that uses 16-bit integer - * audio processing (for example, speed and pitch adjustment) will not be available when float - * output is in use. + * @param enableFloatOutput Whether to enable 32-bit float output. Where possible, 32-bit float + * output will be used if the input is 32-bit float, and also if the input is high resolution + * (24-bit or 32-bit) integer PCM. Audio processing (for example, speed adjustment) will not + * be available when float output is in use. */ public DefaultAudioSink( @Nullable AudioCapabilities audioCapabilities, AudioProcessor[] audioProcessors, - boolean enableConvertHighResIntPcmToFloat) { - this( - audioCapabilities, - new DefaultAudioProcessorChain(audioProcessors), - enableConvertHighResIntPcmToFloat); + boolean enableFloatOutput) { + this(audioCapabilities, new DefaultAudioProcessorChain(audioProcessors), enableFloatOutput); } /** @@ -332,18 +329,18 @@ public final class DefaultAudioSink implements AudioSink { * default capabilities (no encoded audio passthrough support) should be assumed. * @param audioProcessorChain An {@link AudioProcessorChain} which is used to apply playback * parameters adjustments. The instance passed in must not be reused in other sinks. - * @param enableConvertHighResIntPcmToFloat Whether to enable conversion of high resolution - * integer PCM to 32-bit float for output, if possible. Functionality that uses 16-bit integer - * audio processing (for example, speed and pitch adjustment) will not be available when float - * output is in use. + * @param enableFloatOutput Whether to enable 32-bit float output. Where possible, 32-bit float + * output will be used if the input is 32-bit float, and also if the input is high resolution + * (24-bit or 32-bit) integer PCM. Audio processing (for example, speed adjustment) will not + * be available when float output is in use. */ public DefaultAudioSink( @Nullable AudioCapabilities audioCapabilities, AudioProcessorChain audioProcessorChain, - boolean enableConvertHighResIntPcmToFloat) { + boolean enableFloatOutput) { this.audioCapabilities = audioCapabilities; this.audioProcessorChain = Assertions.checkNotNull(audioProcessorChain); - this.enableConvertHighResIntPcmToFloat = enableConvertHighResIntPcmToFloat; + this.enableFloatOutput = enableFloatOutput; releasingConditionVariable = new ConditionVariable(true); audioTrackPositionTracker = new AudioTrackPositionTracker(new PositionTrackerListener()); channelMappingAudioProcessor = new ChannelMappingAudioProcessor(); @@ -422,37 +419,34 @@ public final class DefaultAudioSink implements AudioSink { } boolean isInputPcm = Util.isEncodingLinearPcm(inputEncoding); - boolean processingEnabled = isInputPcm && inputEncoding != C.ENCODING_PCM_FLOAT; + boolean processingEnabled = isInputPcm; int sampleRate = inputSampleRate; int channelCount = inputChannelCount; @C.Encoding int encoding = inputEncoding; - boolean shouldConvertHighResIntPcmToFloat = - enableConvertHighResIntPcmToFloat + boolean useFloatOutput = + enableFloatOutput && supportsOutput(inputChannelCount, C.ENCODING_PCM_FLOAT) - && Util.isEncodingHighResolutionIntegerPcm(inputEncoding); + && Util.isEncodingHighResolutionPcm(inputEncoding); AudioProcessor[] availableAudioProcessors = - shouldConvertHighResIntPcmToFloat - ? toFloatPcmAvailableAudioProcessors - : toIntPcmAvailableAudioProcessors; + useFloatOutput ? toFloatPcmAvailableAudioProcessors : toIntPcmAvailableAudioProcessors; if (processingEnabled) { trimmingAudioProcessor.setTrimFrameCount(trimStartFrames, trimEndFrames); channelMappingAudioProcessor.setChannelMap(outputChannels); - AudioProcessor.AudioFormat inputAudioFormat = + AudioProcessor.AudioFormat outputFormat = new AudioProcessor.AudioFormat(sampleRate, channelCount, encoding); - AudioProcessor.AudioFormat outputAudioFormat = inputAudioFormat; for (AudioProcessor audioProcessor : availableAudioProcessors) { try { - outputAudioFormat = audioProcessor.configure(inputAudioFormat); + AudioProcessor.AudioFormat nextFormat = audioProcessor.configure(outputFormat); + if (audioProcessor.isActive()) { + outputFormat = nextFormat; + } } catch (UnhandledAudioFormatException e) { throw new ConfigurationException(e); } - if (audioProcessor.isActive()) { - inputAudioFormat = outputAudioFormat; - } } - sampleRate = outputAudioFormat.sampleRate; - channelCount = outputAudioFormat.channelCount; - encoding = outputAudioFormat.encoding; + sampleRate = outputFormat.sampleRate; + channelCount = outputFormat.channelCount; + encoding = outputFormat.encoding; } int outputChannelConfig = getChannelConfig(channelCount, isInputPcm); @@ -464,7 +458,7 @@ public final class DefaultAudioSink implements AudioSink { isInputPcm ? Util.getPcmFrameSize(inputEncoding, inputChannelCount) : C.LENGTH_UNSET; int outputPcmFrameSize = isInputPcm ? Util.getPcmFrameSize(encoding, channelCount) : C.LENGTH_UNSET; - boolean canApplyPlaybackParameters = processingEnabled && !shouldConvertHighResIntPcmToFloat; + boolean canApplyPlaybackParameters = processingEnabled && !useFloatOutput; Configuration pendingConfiguration = new Configuration( isInputPcm, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/FloatResamplingAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/FloatResamplingAudioProcessor.java index a75e675e6e..ca6b4f3f13 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/FloatResamplingAudioProcessor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/FloatResamplingAudioProcessor.java @@ -16,13 +16,19 @@ package com.google.android.exoplayer2.audio; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.util.Util; import java.nio.ByteBuffer; /** - * An {@link AudioProcessor} that converts 24-bit and 32-bit integer PCM audio to 32-bit float PCM - * audio. + * An {@link AudioProcessor} that converts high resolution PCM audio to 32-bit float. The following + * encodings are supported as input: + * + *
SYNC word pattern can occur within AAC data, so we perform a few checks to make sure this is - * really a SYNC word. This includes: + * Checks whether a candidate SYNC word position is likely to be the position of a real SYNC word. + * The caller must check that the first byte of the SYNC word is 0xFF before calling this method. + * This method performs the following checks: * *
If true is returned, the renderer will work around the issue by instantiating a new decoder + * when this case occurs. + * + *
See [Internal: b/141097367]. + * + * @param name The name of the decoder. + * @return True if the decoder is known to behave incorrectly if flushed prior to having output a + * {@link MediaFormat}. False otherwise. + */ + private static boolean codecNeedsSosFlushWorkaround(String name) { + return Util.SDK_INT == 29 && "c2.android.aac.decoder".equals(name); + } } 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 bb6c52d253..2b8dcc0a55 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 @@ -25,6 +25,7 @@ import android.util.Pair; import android.util.SparseIntArray; import androidx.annotation.CheckResult; import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.util.Log; @@ -289,9 +290,16 @@ public final class MediaCodecUtil { // Note: MediaCodecList is sorted by the framework such that the best decoders come first. for (int i = 0; i < numberOfCodecs; i++) { android.media.MediaCodecInfo codecInfo = mediaCodecList.getCodecInfoAt(i); + if (isAlias(codecInfo)) { + // Skip aliases of other codecs, since they will also be listed under their canonical + // names. + continue; + } String name = codecInfo.getName(); - @Nullable - String codecMimeType = getCodecMimeType(codecInfo, name, secureDecodersExplicit, mimeType); + if (!isCodecUsableDecoder(codecInfo, name, secureDecodersExplicit, mimeType)) { + continue; + } + @Nullable String codecMimeType = getCodecMimeType(codecInfo, name, mimeType); if (codecMimeType == null) { continue; } @@ -373,7 +381,6 @@ public final class MediaCodecUtil { * * @param info The codec information. * @param name The name of the codec - * @param secureDecodersExplicit Whether secure decoders were explicitly listed, if present. * @param mimeType The MIME type. * @return The codec's supported MIME type for media of type {@code mimeType}, or {@code null} if * the codec can't be used. If non-null, the returned type will be equal to {@code mimeType} @@ -383,12 +390,7 @@ public final class MediaCodecUtil { private static String getCodecMimeType( android.media.MediaCodecInfo info, String name, - boolean secureDecodersExplicit, String mimeType) { - if (!isCodecUsableDecoder(info, name, secureDecodersExplicit, mimeType)) { - return null; - } - String[] supportedTypes = info.getSupportedTypes(); for (String supportedType : supportedTypes) { if (supportedType.equalsIgnoreCase(mimeType)) { @@ -591,6 +593,15 @@ public final class MediaCodecUtil { } } + private static boolean isAlias(android.media.MediaCodecInfo info) { + return Util.SDK_INT >= 29 && isAliasV29(info); + } + + @RequiresApi(29) + private static boolean isAliasV29(android.media.MediaCodecInfo info) { + return info.isAlias(); + } + /** * The result of {@link android.media.MediaCodecInfo#isHardwareAccelerated()} for API levels 29+, * or a best-effort approximation for lower levels. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java index d176b1905c..6707c1e496 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java @@ -245,8 +245,8 @@ public final class DownloadHelper { * @param dataSourceFactory A {@link DataSource.Factory} used to load the manifest. * @param renderersFactory A {@link RenderersFactory} creating the renderers for which tracks are * selected. - * @param drmSessionManager An optional {@link DrmSessionManager} used by the renderers created by - * {@code renderersFactory}. + * @param drmSessionManager An optional {@link DrmSessionManager}. Used to help determine which + * tracks can be selected. * @param trackSelectorParameters {@link DefaultTrackSelector.Parameters} for selecting tracks for * downloading. * @return A {@link DownloadHelper} for DASH streams. @@ -315,8 +315,8 @@ public final class DownloadHelper { * @param dataSourceFactory A {@link DataSource.Factory} used to load the playlist. * @param renderersFactory A {@link RenderersFactory} creating the renderers for which tracks are * selected. - * @param drmSessionManager An optional {@link DrmSessionManager} used by the renderers created by - * {@code renderersFactory}. + * @param drmSessionManager An optional {@link DrmSessionManager}. Used to help determine which + * tracks can be selected. * @param trackSelectorParameters {@link DefaultTrackSelector.Parameters} for selecting tracks for * downloading. * @return A {@link DownloadHelper} for HLS streams. @@ -385,8 +385,8 @@ public final class DownloadHelper { * @param dataSourceFactory A {@link DataSource.Factory} used to load the manifest. * @param renderersFactory A {@link RenderersFactory} creating the renderers for which tracks are * selected. - * @param drmSessionManager An optional {@link DrmSessionManager} used by the renderers created by - * {@code renderersFactory}. + * @param drmSessionManager An optional {@link DrmSessionManager}. Used to help determine which + * tracks can be selected. * @param trackSelectorParameters {@link DefaultTrackSelector.Parameters} for selecting tracks for * downloading. * @return A {@link DownloadHelper} for SmoothStreaming streams. @@ -414,27 +414,27 @@ public final class DownloadHelper { /** * Equivalent to {@link #createMediaSource(DownloadRequest, Factory, DrmSessionManager) - * createMediaSource(downloadRequest, dataSourceFactory, - * DrmSessionManager.getDummyDrmSessionManager())}. + * createMediaSource(downloadRequest, dataSourceFactory, null)}. */ public static MediaSource createMediaSource( DownloadRequest downloadRequest, DataSource.Factory dataSourceFactory) { - return createMediaSource( - downloadRequest, dataSourceFactory, DrmSessionManager.getDummyDrmSessionManager()); + return createMediaSource(downloadRequest, dataSourceFactory, /* drmSessionManager= */ null); } /** - * Utility method to create a MediaSource which only contains the tracks defined in {@code + * Utility method to create a {@link MediaSource} that only exposes the tracks defined in {@code * downloadRequest}. * * @param downloadRequest A {@link DownloadRequest}. * @param dataSourceFactory A factory for {@link DataSource}s to read the media. - * @return A MediaSource which only contains the tracks defined in {@code downloadRequest}. + * @param drmSessionManager An optional {@link DrmSessionManager} to be passed to the {@link + * MediaSource}. + * @return A {@link MediaSource} that only exposes the tracks defined in {@code downloadRequest}. */ public static MediaSource createMediaSource( DownloadRequest downloadRequest, DataSource.Factory dataSourceFactory, - DrmSessionManager> drmSessionManager) { + @Nullable DrmSessionManager> drmSessionManager) { @Nullable Constructor extends MediaSourceFactory> constructor; switch (downloadRequest.type) { case DownloadRequest.TYPE_DASH: diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java index b1ab5ac7c6..819478b80e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java @@ -595,7 +595,7 @@ public abstract class DownloadService extends Service { } @Override - public int onStartCommand(Intent intent, int flags, int startId) { + public int onStartCommand(@Nullable Intent intent, int flags, int startId) { lastStartId = startId; taskRemoved = false; @Nullable String intentAction = null; @@ -617,7 +617,9 @@ public abstract class DownloadService extends Service { // Do nothing. break; case ACTION_ADD_DOWNLOAD: - @Nullable DownloadRequest downloadRequest = intent.getParcelableExtra(KEY_DOWNLOAD_REQUEST); + @Nullable + DownloadRequest downloadRequest = + Assertions.checkNotNull(intent).getParcelableExtra(KEY_DOWNLOAD_REQUEST); if (downloadRequest == null) { Log.e(TAG, "Ignored ADD_DOWNLOAD: Missing " + KEY_DOWNLOAD_REQUEST + " extra"); } else { @@ -642,7 +644,7 @@ public abstract class DownloadService extends Service { downloadManager.pauseDownloads(); break; case ACTION_SET_STOP_REASON: - if (!intent.hasExtra(KEY_STOP_REASON)) { + if (!Assertions.checkNotNull(intent).hasExtra(KEY_STOP_REASON)) { Log.e(TAG, "Ignored SET_STOP_REASON: Missing " + KEY_STOP_REASON + " extra"); } else { int stopReason = intent.getIntExtra(KEY_STOP_REASON, /* defaultValue= */ 0); @@ -650,7 +652,9 @@ public abstract class DownloadService extends Service { } break; case ACTION_SET_REQUIREMENTS: - @Nullable Requirements requirements = intent.getParcelableExtra(KEY_REQUIREMENTS); + @Nullable + Requirements requirements = + Assertions.checkNotNull(intent).getParcelableExtra(KEY_REQUIREMENTS); if (requirements == null) { Log.e(TAG, "Ignored SET_REQUIREMENTS: Missing " + KEY_REQUIREMENTS + " extra"); } else { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java index 8aafb9a0e5..4385a41ff3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java @@ -324,7 +324,8 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb if (endUs != C.TIME_END_OF_SOURCE && ((result == C.RESULT_BUFFER_READ && buffer.timeUs >= endUs) || (result == C.RESULT_NOTHING_READ - && getBufferedPositionUs() == C.TIME_END_OF_SOURCE))) { + && getBufferedPositionUs() == C.TIME_END_OF_SOURCE + && !buffer.waitingForKeys))) { buffer.clear(); buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM); sentEos = true; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java index d4e447bc61..b5cfe6ed72 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java @@ -539,7 +539,7 @@ public class SampleQueue implements TrackOutput { boolean loadingFinished, long decodeOnlyUntilUs, SampleExtrasHolder extrasHolder) { - + buffer.waitingForKeys = false; // This is a temporary fix for https://github.com/google/ExoPlayer/issues/6155. // TODO: Remove it and replace it with a fix that discards samples when writing to the queue. boolean hasNextSample; @@ -573,6 +573,7 @@ public class SampleQueue implements TrackOutput { } if (!mayReadSample(relativeReadIndex)) { + buffer.waitingForKeys = true; return C.RESULT_NOTHING_READ; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java index 058b1c4526..46c26db122 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java @@ -23,11 +23,11 @@ import androidx.annotation.IntDef; import androidx.annotation.Nullable; import com.google.android.exoplayer2.BaseRenderer; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; import java.lang.annotation.Documented; @@ -45,6 +45,8 @@ import java.util.List; */ public final class TextRenderer extends BaseRenderer implements Callback { + private static final String TAG = "TextRenderer"; + @Documented @Retention(RetentionPolicy.SOURCE) @IntDef({ @@ -143,19 +145,13 @@ public final class TextRenderer extends BaseRenderer implements Callback { @Override protected void onPositionReset(long positionUs, boolean joining) { - clearOutput(); inputStreamEnded = false; outputStreamEnded = false; - if (decoderReplacementState != REPLACEMENT_STATE_NONE) { - replaceDecoder(); - } else { - releaseBuffers(); - decoder.flush(); - } + resetOutputAndDecoder(); } @Override - public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { + public void render(long positionUs, long elapsedRealtimeUs) { if (outputStreamEnded) { return; } @@ -165,7 +161,8 @@ public final class TextRenderer extends BaseRenderer implements Callback { try { nextSubtitle = decoder.dequeueOutputBuffer(); } catch (SubtitleDecoderException e) { - throw createRendererException(e, streamFormat); + handleDecoderError(e); + return; } } @@ -247,7 +244,8 @@ public final class TextRenderer extends BaseRenderer implements Callback { } } } catch (SubtitleDecoderException e) { - throw createRendererException(e, streamFormat); + handleDecoderError(e); + return; } } @@ -329,4 +327,24 @@ public final class TextRenderer extends BaseRenderer implements Callback { output.onCues(cues); } + /** + * Called when {@link #decoder} throws an exception, so it can be logged and playback can + * continue. + * + *
Logs {@code e} and resets state to allow decoding the next sample. + */ + private void handleDecoderError(SubtitleDecoderException e) { + Log.e(TAG, "Subtitle decoding failed. streamFormat=" + streamFormat, e); + resetOutputAndDecoder(); + } + + private void resetOutputAndDecoder() { + clearOutput(); + if (decoderReplacementState != REPLACEMENT_STATE_NONE) { + replaceDecoder(); + } else { + releaseBuffers(); + decoder.flush(); + } + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripDecoder.java index 0c402ac018..cef7e3f53f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripDecoder.java @@ -41,10 +41,12 @@ public final class SubripDecoder extends SimpleSubtitleDecoder { private static final String TAG = "SubripDecoder"; - private static final String SUBRIP_TIMECODE = "(?:(\\d+):)?(\\d+):(\\d+),(\\d+)"; + // Some SRT files don't include hours or milliseconds in the timecode, so we use optional groups. + private static final String SUBRIP_TIMECODE = "(?:(\\d+):)?(\\d+):(\\d+)(?:,(\\d+))?"; private static final Pattern SUBRIP_TIMING_LINE = Pattern.compile("\\s*(" + SUBRIP_TIMECODE + ")\\s*-->\\s*(" + SUBRIP_TIMECODE + ")\\s*"); + // NOTE: Android Studio's suggestion to simplify '\\}' is incorrect [internal: b/144480183]. private static final Pattern SUBRIP_TAG_PATTERN = Pattern.compile("\\{\\\\.*?\\}"); private static final String SUBRIP_ALIGNMENT_TAG = "\\{\\\\an[1-9]\\}"; @@ -229,10 +231,14 @@ public final class SubripDecoder extends SimpleSubtitleDecoder { } private static long parseTimecode(Matcher matcher, int groupOffset) { - long timestampMs = Long.parseLong(matcher.group(groupOffset + 1)) * 60 * 60 * 1000; + @Nullable String hours = matcher.group(groupOffset + 1); + long timestampMs = hours != null ? Long.parseLong(hours) * 60 * 60 * 1000 : 0; timestampMs += Long.parseLong(matcher.group(groupOffset + 2)) * 60 * 1000; timestampMs += Long.parseLong(matcher.group(groupOffset + 3)) * 1000; - timestampMs += Long.parseLong(matcher.group(groupOffset + 4)); + @Nullable String millis = matcher.group(groupOffset + 4); + if (millis != null) { + timestampMs += Long.parseLong(millis); + } return timestampMs * 1000; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java index ae115ab58c..ec11ad2348 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java @@ -279,8 +279,8 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou try { connection = makeConnection(dataSpec); } catch (IOException e) { - throw new HttpDataSourceException("Unable to connect to " + dataSpec.uri.toString(), e, - dataSpec, HttpDataSourceException.TYPE_OPEN); + throw new HttpDataSourceException( + "Unable to connect", e, dataSpec, HttpDataSourceException.TYPE_OPEN); } String responseMessage; @@ -289,8 +289,8 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou responseMessage = connection.getResponseMessage(); } catch (IOException e) { closeConnectionQuietly(); - throw new HttpDataSourceException("Unable to connect to " + dataSpec.uri.toString(), e, - dataSpec, HttpDataSourceException.TYPE_OPEN); + throw new HttpDataSourceException( + "Unable to connect", e, dataSpec, HttpDataSourceException.TYPE_OPEN); } // Check for a valid response code. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/EventLogger.java b/library/core/src/main/java/com/google/android/exoplayer2/util/EventLogger.java index 0a303c1df7..9d145caee5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/EventLogger.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/EventLogger.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.util; import android.os.SystemClock; +import android.text.TextUtils; import android.view.Surface; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; @@ -198,7 +199,7 @@ public class EventLogger implements AnalyticsListener { logd(eventTime, "tracks", "[]"); return; } - logd("tracks [" + getEventTimeString(eventTime) + ", "); + logd("tracks [" + getEventTimeString(eventTime)); // Log tracks associated to renderers. int rendererCount = mappedTrackInfo.getRendererCount(); for (int rendererIndex = 0; rendererIndex < rendererCount; rendererIndex++) { @@ -282,7 +283,7 @@ public class EventLogger implements AnalyticsListener { @Override public void onMetadata(EventTime eventTime, Metadata metadata) { - logd("metadata [" + getEventTimeString(eventTime) + ", "); + logd("metadata [" + getEventTimeString(eventTime)); printMetadata(metadata, " "); logd("]"); } @@ -469,27 +470,26 @@ public class EventLogger implements AnalyticsListener { } /** - * Logs an error message and exception. + * Logs an error message. * * @param msg The message to log. - * @param tr The exception to log. */ - protected void loge(String msg, @Nullable Throwable tr) { - Log.e(tag, msg, tr); + protected void loge(String msg) { + Log.e(tag, msg); } // Internal methods private void logd(EventTime eventTime, String eventName) { - logd(getEventString(eventTime, eventName)); + logd(getEventString(eventTime, eventName, /* eventDescription= */ null, /* throwable= */ null)); } private void logd(EventTime eventTime, String eventName, String eventDescription) { - logd(getEventString(eventTime, eventName, eventDescription)); + logd(getEventString(eventTime, eventName, eventDescription, /* throwable= */ null)); } private void loge(EventTime eventTime, String eventName, @Nullable Throwable throwable) { - loge(getEventString(eventTime, eventName), throwable); + loge(getEventString(eventTime, eventName, /* eventDescription= */ null, throwable)); } private void loge( @@ -497,7 +497,7 @@ public class EventLogger implements AnalyticsListener { String eventName, String eventDescription, @Nullable Throwable throwable) { - loge(getEventString(eventTime, eventName, eventDescription), throwable); + loge(getEventString(eventTime, eventName, eventDescription, throwable)); } private void printInternalError(EventTime eventTime, String type, Exception e) { @@ -510,12 +510,21 @@ public class EventLogger implements AnalyticsListener { } } - private String getEventString(EventTime eventTime, String eventName) { - return eventName + " [" + getEventTimeString(eventTime) + "]"; - } - - private String getEventString(EventTime eventTime, String eventName, String eventDescription) { - return eventName + " [" + getEventTimeString(eventTime) + ", " + eventDescription + "]"; + private String getEventString( + EventTime eventTime, + String eventName, + @Nullable String eventDescription, + @Nullable Throwable throwable) { + String eventString = eventName + " [" + getEventTimeString(eventTime); + if (eventDescription != null) { + eventString += ", " + eventDescription; + } + @Nullable String throwableString = Log.getThrowableString(throwable); + if (!TextUtils.isEmpty(throwableString)) { + eventString += "\n " + throwableString.replace("\n", "\n ") + '\n'; + } + eventString += "]"; + return eventString; } private String getEventTimeString(EventTime eventTime) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/Log.java b/library/core/src/main/java/com/google/android/exoplayer2/util/Log.java index a29460b84c..e5e6f88d4d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/Log.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/Log.java @@ -21,6 +21,7 @@ import androidx.annotation.Nullable; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.net.UnknownHostException; /** Wrapper around {@link android.util.Log} which allows to set the log level. */ public final class Log { @@ -69,7 +70,8 @@ public final class Log { } /** - * Sets whether stack traces of {@link Throwable}s will be logged to logcat. + * Sets whether stack traces of {@link Throwable}s will be logged to logcat. Stack trace logging + * is enabled by default. * * @param logStackTraces Whether stack traces will be logged. */ @@ -86,11 +88,7 @@ public final class Log { /** @see android.util.Log#d(String, String, Throwable) */ public static void d(String tag, String message, @Nullable Throwable throwable) { - if (!logStackTraces) { - d(tag, appendThrowableMessage(message, throwable)); - } else if (logLevel == LOG_LEVEL_ALL) { - android.util.Log.d(tag, message, throwable); - } + d(tag, appendThrowableString(message, throwable)); } /** @see android.util.Log#i(String, String) */ @@ -102,11 +100,7 @@ public final class Log { /** @see android.util.Log#i(String, String, Throwable) */ public static void i(String tag, String message, @Nullable Throwable throwable) { - if (!logStackTraces) { - i(tag, appendThrowableMessage(message, throwable)); - } else if (logLevel <= LOG_LEVEL_INFO) { - android.util.Log.i(tag, message, throwable); - } + i(tag, appendThrowableString(message, throwable)); } /** @see android.util.Log#w(String, String) */ @@ -118,11 +112,7 @@ public final class Log { /** @see android.util.Log#w(String, String, Throwable) */ public static void w(String tag, String message, @Nullable Throwable throwable) { - if (!logStackTraces) { - w(tag, appendThrowableMessage(message, throwable)); - } else if (logLevel <= LOG_LEVEL_WARNING) { - android.util.Log.w(tag, message, throwable); - } + w(tag, appendThrowableString(message, throwable)); } /** @see android.util.Log#e(String, String) */ @@ -134,18 +124,54 @@ public final class Log { /** @see android.util.Log#e(String, String, Throwable) */ public static void e(String tag, String message, @Nullable Throwable throwable) { - if (!logStackTraces) { - e(tag, appendThrowableMessage(message, throwable)); - } else if (logLevel <= LOG_LEVEL_ERROR) { - android.util.Log.e(tag, message, throwable); + e(tag, appendThrowableString(message, throwable)); + } + + /** + * Returns a string representation of a {@link Throwable} suitable for logging, taking into + * account whether {@link #setLogStackTraces(boolean)} stack trace logging} is enabled. + * + *
Stack trace logging may be unconditionally suppressed for some expected failure modes (e.g.,
+ * {@link Throwable Throwables} that are expected if the device doesn't have network connectivity)
+ * to avoid log spam.
+ *
+ * @param throwable The {@link Throwable}.
+ * @return The string representation of the {@link Throwable}.
+ */
+ @Nullable
+ public static String getThrowableString(@Nullable Throwable throwable) {
+ if (throwable == null) {
+ return null;
+ } else if (isCausedByUnknownHostException(throwable)) {
+ // UnknownHostException implies the device doesn't have network connectivity.
+ // UnknownHostException.getMessage() may return a string that's more verbose than desired for
+ // logging an expected failure mode. Conversely, android.util.Log.getStackTraceString has
+ // special handling to return the empty string, which can result in logging that doesn't
+ // indicate the failure mode at all. Hence we special case this exception to always return a
+ // concise but useful message.
+ return "UnknownHostException (no network)";
+ } else if (!logStackTraces) {
+ return throwable.getMessage();
+ } else {
+ return android.util.Log.getStackTraceString(throwable).trim().replace("\t", " ");
}
}
- private static String appendThrowableMessage(String message, @Nullable Throwable throwable) {
- if (throwable == null) {
- return message;
+ private static String appendThrowableString(String message, @Nullable Throwable throwable) {
+ @Nullable String throwableString = getThrowableString(throwable);
+ if (!TextUtils.isEmpty(throwableString)) {
+ message += "\n " + throwableString.replace("\n", "\n ") + '\n';
}
- String throwableMessage = throwable.getMessage();
- return TextUtils.isEmpty(throwableMessage) ? message : message + " - " + throwableMessage;
+ return message;
+ }
+
+ private static boolean isCausedByUnknownHostException(@Nullable Throwable throwable) {
+ while (throwable != null) {
+ if (throwable instanceof UnknownHostException) {
+ return true;
+ }
+ throwable = throwable.getCause();
+ }
+ return false;
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java b/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java
index e61ab83777..c3ca9257d6 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java
@@ -122,22 +122,22 @@ public final class MimeTypes {
customMimeTypes.add(customMimeType);
}
- /** Returns whether the given string is an audio mime type. */
+ /** Returns whether the given string is an audio MIME type. */
public static boolean isAudio(@Nullable String mimeType) {
return BASE_TYPE_AUDIO.equals(getTopLevelType(mimeType));
}
- /** Returns whether the given string is a video mime type. */
+ /** Returns whether the given string is a video MIME type. */
public static boolean isVideo(@Nullable String mimeType) {
return BASE_TYPE_VIDEO.equals(getTopLevelType(mimeType));
}
- /** Returns whether the given string is a text mime type. */
+ /** Returns whether the given string is a text MIME type. */
public static boolean isText(@Nullable String mimeType) {
return BASE_TYPE_TEXT.equals(getTopLevelType(mimeType));
}
- /** Returns whether the given string is an application mime type. */
+ /** Returns whether the given string is an application MIME type. */
public static boolean isApplication(@Nullable String mimeType) {
return BASE_TYPE_APPLICATION.equals(getTopLevelType(mimeType));
}
@@ -173,13 +173,14 @@ public final class MimeTypes {
* @param codecs The codecs attribute.
* @return The derived video mimeType, or null if it could not be derived.
*/
- public static @Nullable String getVideoMediaMimeType(@Nullable String codecs) {
+ @Nullable
+ public static String getVideoMediaMimeType(@Nullable String codecs) {
if (codecs == null) {
return null;
}
String[] codecList = Util.splitCodecs(codecs);
for (String codec : codecList) {
- String mimeType = getMediaMimeType(codec);
+ @Nullable String mimeType = getMediaMimeType(codec);
if (mimeType != null && isVideo(mimeType)) {
return mimeType;
}
@@ -193,13 +194,14 @@ public final class MimeTypes {
* @param codecs The codecs attribute.
* @return The derived audio mimeType, or null if it could not be derived.
*/
- public static @Nullable String getAudioMediaMimeType(@Nullable String codecs) {
+ @Nullable
+ public static String getAudioMediaMimeType(@Nullable String codecs) {
if (codecs == null) {
return null;
}
String[] codecList = Util.splitCodecs(codecs);
for (String codec : codecList) {
- String mimeType = getMediaMimeType(codec);
+ @Nullable String mimeType = getMediaMimeType(codec);
if (mimeType != null && isAudio(mimeType)) {
return mimeType;
}
@@ -213,7 +215,8 @@ public final class MimeTypes {
* @param codec The codec identifier to derive.
* @return The mimeType, or null if it could not be derived.
*/
- public static @Nullable String getMediaMimeType(@Nullable String codec) {
+ @Nullable
+ public static String getMediaMimeType(@Nullable String codec) {
if (codec == null) {
return null;
}
@@ -234,7 +237,7 @@ public final class MimeTypes {
} else if (codec.startsWith("vp8") || codec.startsWith("vp08")) {
return MimeTypes.VIDEO_VP8;
} else if (codec.startsWith("mp4a")) {
- String mimeType = null;
+ @Nullable String mimeType = null;
if (codec.startsWith("mp4a.")) {
String objectTypeString = codec.substring(5); // remove the 'mp4a.' prefix
if (objectTypeString.length() >= 2) {
@@ -243,7 +246,7 @@ public final class MimeTypes {
int objectTypeInt = Integer.parseInt(objectTypeHexString, 16);
mimeType = getMimeTypeFromMp4ObjectType(objectTypeInt);
} catch (NumberFormatException ignored) {
- // ignored
+ // Ignored.
}
}
}
@@ -266,6 +269,10 @@ public final class MimeTypes {
return MimeTypes.AUDIO_VORBIS;
} else if (codec.startsWith("flac")) {
return MimeTypes.AUDIO_FLAC;
+ } else if (codec.startsWith("stpp")) {
+ return MimeTypes.APPLICATION_TTML;
+ } else if (codec.startsWith("wvtt")) {
+ return MimeTypes.TEXT_VTT;
} else {
return getCustomMimeTypeForCodec(codec);
}
@@ -405,7 +412,8 @@ public final class MimeTypes {
* Returns the top-level type of {@code mimeType}, or null if {@code mimeType} is null or does not
* contain a forward slash character ({@code '/'}).
*/
- private static @Nullable String getTopLevelType(@Nullable String mimeType) {
+ @Nullable
+ private static String getTopLevelType(@Nullable String mimeType) {
if (mimeType == null) {
return null;
}
@@ -416,7 +424,8 @@ public final class MimeTypes {
return mimeType.substring(0, indexOfSlash);
}
- private static @Nullable String getCustomMimeTypeForCodec(String codec) {
+ @Nullable
+ private static String getCustomMimeTypeForCodec(String codec) {
int customMimeTypeCount = customMimeTypes.size();
for (int i = 0; i < customMimeTypeCount; i++) {
CustomMimeType customMimeType = customMimeTypes.get(i);
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java
index 5518eeaf36..ea43ee7bb3 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java
@@ -1360,13 +1360,15 @@ public final class Util {
}
/**
- * Returns whether {@code encoding} is high resolution (> 16-bit) integer PCM.
+ * Returns whether {@code encoding} is high resolution (> 16-bit) PCM.
*
* @param encoding The encoding of the audio data.
- * @return Whether the encoding is high resolution integer PCM.
+ * @return Whether the encoding is high resolution PCM.
*/
- public static boolean isEncodingHighResolutionIntegerPcm(@C.PcmEncoding int encoding) {
- return encoding == C.ENCODING_PCM_24BIT || encoding == C.ENCODING_PCM_32BIT;
+ public static boolean isEncodingHighResolutionPcm(@C.PcmEncoding int encoding) {
+ return encoding == C.ENCODING_PCM_24BIT
+ || encoding == C.ENCODING_PCM_32BIT
+ || encoding == C.ENCODING_PCM_FLOAT;
}
/**
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 4e72a1b3d7..25dc09be81 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
@@ -1000,6 +1000,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
processOutputFormat(getCodec(), format.width, format.height);
}
maybeNotifyVideoSizeChanged();
+ decoderCounters.renderedOutputBufferCount++;
maybeNotifyRenderedFirstFrame();
onProcessedOutputBuffer(presentationTimeUs);
}
diff --git a/library/core/src/test/assets/subrip/typical b/library/core/src/test/assets/subrip/typical
index 1331f75651..cc9a3da871 100644
--- a/library/core/src/test/assets/subrip/typical
+++ b/library/core/src/test/assets/subrip/typical
@@ -8,5 +8,5 @@ This is the second subtitle.
Second subtitle with second line.
3
-00:00:04,567 --> 00:00:08,901
+02:00:04,567 --> 02:00:08,901
This is the third subtitle.
diff --git a/library/core/src/test/assets/subrip/typical_extra_blank_line b/library/core/src/test/assets/subrip/typical_extra_blank_line
index f5882a1d68..392cb7e91c 100644
--- a/library/core/src/test/assets/subrip/typical_extra_blank_line
+++ b/library/core/src/test/assets/subrip/typical_extra_blank_line
@@ -9,5 +9,5 @@ This is the second subtitle.
Second subtitle with second line.
3
-00:00:04,567 --> 00:00:08,901
+02:00:04,567 --> 02:00:08,901
This is the third subtitle.
diff --git a/library/core/src/test/assets/subrip/typical_missing_sequence b/library/core/src/test/assets/subrip/typical_missing_sequence
index 56d49ac63c..e75711d7a8 100644
--- a/library/core/src/test/assets/subrip/typical_missing_sequence
+++ b/library/core/src/test/assets/subrip/typical_missing_sequence
@@ -7,5 +7,5 @@ This is the second subtitle.
Second subtitle with second line.
3
-00:00:04,567 --> 00:00:08,901
+02:00:04,567 --> 02:00:08,901
This is the third subtitle.
diff --git a/library/core/src/test/assets/subrip/typical_missing_timecode b/library/core/src/test/assets/subrip/typical_missing_timecode
index cd25ffca3b..bce61a77f9 100644
--- a/library/core/src/test/assets/subrip/typical_missing_timecode
+++ b/library/core/src/test/assets/subrip/typical_missing_timecode
@@ -7,13 +7,13 @@ This is the second subtitle.
Second subtitle with second line.
3
-00:00:04,567 --> 00:00:08,901
+02:00:04,567 --> 02:00:08,901
This is the third subtitle.
4
- --> 00:00:10,901
+ --> 02:00:10,901
This is the fourth subtitle.
5
-00:00:12,901 -->
+02:00:12,901 -->
This is the fifth subtitle.
diff --git a/library/core/src/test/assets/subrip/typical_negative_timestamps b/library/core/src/test/assets/subrip/typical_negative_timestamps
index 2a47c0993b..1df7bf68e3 100644
--- a/library/core/src/test/assets/subrip/typical_negative_timestamps
+++ b/library/core/src/test/assets/subrip/typical_negative_timestamps
@@ -8,5 +8,5 @@ This is the second subtitle.
Second subtitle with second line.
3
-00:00:04,567 --> 00:00:08,901
+02:00:04,567 --> 02:00:08,901
This is the third subtitle.
diff --git a/library/core/src/test/assets/subrip/typical_no_hours_and_millis b/library/core/src/test/assets/subrip/typical_no_hours_and_millis
new file mode 100644
index 0000000000..5340fc72c2
--- /dev/null
+++ b/library/core/src/test/assets/subrip/typical_no_hours_and_millis
@@ -0,0 +1,12 @@
+1
+00:00,000 --> 00:01,234
+This is the first subtitle.
+
+2
+00:00:02 --> 00:00:03
+This is the second subtitle.
+Second subtitle with second line.
+
+3
+02:00:04,567 --> 02:00:08,901
+This is the third subtitle.
diff --git a/library/core/src/test/assets/subrip/typical_with_byte_order_mark b/library/core/src/test/assets/subrip/typical_with_byte_order_mark
index 4f5b32f4d7..050e1c02a6 100644
--- a/library/core/src/test/assets/subrip/typical_with_byte_order_mark
+++ b/library/core/src/test/assets/subrip/typical_with_byte_order_mark
@@ -8,5 +8,5 @@ This is the second subtitle.
Second subtitle with second line.
3
-00:00:04,567 --> 00:00:08,901
+02:00:04,567 --> 02:00:08,901
This is the third subtitle.
diff --git a/library/core/src/test/assets/ts/sample_with_id3.adts b/library/core/src/test/assets/ts/sample_with_id3.adts
new file mode 100644
index 0000000000..690fe90bd0
Binary files /dev/null and b/library/core/src/test/assets/ts/sample_with_id3.adts differ
diff --git a/library/core/src/test/assets/ts/sample_with_id3.adts.0.dump b/library/core/src/test/assets/ts/sample_with_id3.adts.0.dump
new file mode 100644
index 0000000000..b9cde05d69
--- /dev/null
+++ b/library/core/src/test/assets/ts/sample_with_id3.adts.0.dump
@@ -0,0 +1,641 @@
+seekMap:
+ isSeekable = false
+ duration = UNSET TIME
+ getPosition(0) = [[timeUs=0, position=0]]
+numberOfTracks = 2
+track 0:
+ format:
+ bitrate = -1
+ id = 0
+ containerMimeType = null
+ sampleMimeType = audio/mp4a-latm
+ maxInputSize = -1
+ width = -1
+ height = -1
+ frameRate = -1.0
+ rotationDegrees = 0
+ pixelWidthHeightRatio = 1.0
+ channelCount = 1
+ sampleRate = 44100
+ pcmEncoding = -1
+ encoderDelay = 0
+ encoderPadding = 0
+ subsampleOffsetUs = 9223372036854775807
+ selectionFlags = 0
+ language = null
+ drmInitData = -
+ metadata = null
+ initializationData:
+ data = length 2, hash 5F7
+ total output bytes = 30797
+ sample count = 144
+ sample 0:
+ time = 0
+ flags = 1
+ data = length 23, hash 47DE9131
+ sample 1:
+ time = 23219
+ flags = 1
+ data = length 6, hash 31CF3A46
+ sample 2:
+ time = 46438
+ flags = 1
+ data = length 6, hash 31CF3A46
+ sample 3:
+ time = 69657
+ flags = 1
+ data = length 6, hash 31CF3A46
+ sample 4:
+ time = 92876
+ flags = 1
+ data = length 6, hash 31EC5206
+ sample 5:
+ time = 116095
+ flags = 1
+ data = length 171, hash 4F6478F6
+ sample 6:
+ time = 139314
+ flags = 1
+ data = length 202, hash AF4068A3
+ sample 7:
+ time = 162533
+ flags = 1
+ data = length 210, hash E4C10618
+ sample 8:
+ time = 185752
+ flags = 1
+ data = length 217, hash 9ECCD0D9
+ sample 9:
+ time = 208971
+ flags = 1
+ data = length 212, hash 6BAC2CD9
+ sample 10:
+ time = 232190
+ flags = 1
+ data = length 223, hash 188B6010
+ sample 11:
+ time = 255409
+ flags = 1
+ data = length 222, hash C1A04D0C
+ sample 12:
+ time = 278628
+ flags = 1
+ data = length 220, hash D65F9768
+ sample 13:
+ time = 301847
+ flags = 1
+ data = length 227, hash B96C9E14
+ sample 14:
+ time = 325066
+ flags = 1
+ data = length 229, hash 9FB09972
+ sample 15:
+ time = 348285
+ flags = 1
+ data = length 220, hash 2271F053
+ sample 16:
+ time = 371504
+ flags = 1
+ data = length 226, hash 5EDD2F4F
+ sample 17:
+ time = 394723
+ flags = 1
+ data = length 239, hash 957510E0
+ sample 18:
+ time = 417942
+ flags = 1
+ data = length 224, hash 718A8F47
+ sample 19:
+ time = 441161
+ flags = 1
+ data = length 225, hash 5E11E293
+ sample 20:
+ time = 464380
+ flags = 1
+ data = length 227, hash FCE50D27
+ sample 21:
+ time = 487599
+ flags = 1
+ data = length 212, hash 77908C40
+ sample 22:
+ time = 510818
+ flags = 1
+ data = length 227, hash 34C4EB32
+ sample 23:
+ time = 534037
+ flags = 1
+ data = length 231, hash 95488307
+ sample 24:
+ time = 557256
+ flags = 1
+ data = length 226, hash 97F12D6F
+ sample 25:
+ time = 580475
+ flags = 1
+ data = length 236, hash 91A9D9A2
+ sample 26:
+ time = 603694
+ flags = 1
+ data = length 227, hash 27A608F9
+ sample 27:
+ time = 626913
+ flags = 1
+ data = length 229, hash 57DAAE4
+ sample 28:
+ time = 650132
+ flags = 1
+ data = length 235, hash ED30AC34
+ sample 29:
+ time = 673351
+ flags = 1
+ data = length 227, hash BD3D6280
+ sample 30:
+ time = 696570
+ flags = 1
+ data = length 233, hash 694B1087
+ sample 31:
+ time = 719789
+ flags = 1
+ data = length 232, hash 1EDFE047
+ sample 32:
+ time = 743008
+ flags = 1
+ data = length 228, hash E2A831F4
+ sample 33:
+ time = 766227
+ flags = 1
+ data = length 231, hash 757E6012
+ sample 34:
+ time = 789446
+ flags = 1
+ data = length 223, hash 4003D791
+ sample 35:
+ time = 812665
+ flags = 1
+ data = length 232, hash 3CF9A07C
+ sample 36:
+ time = 835884
+ flags = 1
+ data = length 228, hash 25AC3FF7
+ sample 37:
+ time = 859103
+ flags = 1
+ data = length 220, hash 2C1824CE
+ sample 38:
+ time = 882322
+ flags = 1
+ data = length 229, hash 46FDD8FB
+ sample 39:
+ time = 905541
+ flags = 1
+ data = length 237, hash F6988018
+ sample 40:
+ time = 928760
+ flags = 1
+ data = length 242, hash 60436B6B
+ sample 41:
+ time = 951979
+ flags = 1
+ data = length 275, hash 90EDFA8E
+ sample 42:
+ time = 975198
+ flags = 1
+ data = length 242, hash 5C86EFCB
+ sample 43:
+ time = 998417
+ flags = 1
+ data = length 233, hash E0A51B82
+ sample 44:
+ time = 1021636
+ flags = 1
+ data = length 235, hash 590DF14F
+ sample 45:
+ time = 1044855
+ flags = 1
+ data = length 238, hash 69AF4E6E
+ sample 46:
+ time = 1068074
+ flags = 1
+ data = length 235, hash E745AE8D
+ sample 47:
+ time = 1091293
+ flags = 1
+ data = length 223, hash 295F2A13
+ sample 48:
+ time = 1114512
+ flags = 1
+ data = length 228, hash E2F47B21
+ sample 49:
+ time = 1137731
+ flags = 1
+ data = length 229, hash 262C3CFE
+ sample 50:
+ time = 1160950
+ flags = 1
+ data = length 232, hash 4B5BF5E8
+ sample 51:
+ time = 1184169
+ flags = 1
+ data = length 233, hash F3D80836
+ sample 52:
+ time = 1207388
+ flags = 1
+ data = length 237, hash 32E0A11E
+ sample 53:
+ time = 1230607
+ flags = 1
+ data = length 228, hash E1B89F13
+ sample 54:
+ time = 1253826
+ flags = 1
+ data = length 237, hash 8BDD9E38
+ sample 55:
+ time = 1277045
+ flags = 1
+ data = length 235, hash 3C84161F
+ sample 56:
+ time = 1300264
+ flags = 1
+ data = length 227, hash A47E1789
+ sample 57:
+ time = 1323483
+ flags = 1
+ data = length 228, hash 869FDFD3
+ sample 58:
+ time = 1346702
+ flags = 1
+ data = length 233, hash 272ECE2
+ sample 59:
+ time = 1369921
+ flags = 1
+ data = length 227, hash DB6B9618
+ sample 60:
+ time = 1393140
+ flags = 1
+ data = length 212, hash 63214325
+ sample 61:
+ time = 1416359
+ flags = 1
+ data = length 221, hash 9BA588A1
+ sample 62:
+ time = 1439578
+ flags = 1
+ data = length 225, hash 21EFD50C
+ sample 63:
+ time = 1462797
+ flags = 1
+ data = length 231, hash F3AD0BF
+ sample 64:
+ time = 1486016
+ flags = 1
+ data = length 224, hash 822C9210
+ sample 65:
+ time = 1509235
+ flags = 1
+ data = length 195, hash D4EF53EE
+ sample 66:
+ time = 1532454
+ flags = 1
+ data = length 195, hash A816647A
+ sample 67:
+ time = 1555673
+ flags = 1
+ data = length 184, hash 9A2B7E6
+ sample 68:
+ time = 1578892
+ flags = 1
+ data = length 210, hash 956E3600
+ sample 69:
+ time = 1602111
+ flags = 1
+ data = length 234, hash 35CFDA0A
+ sample 70:
+ time = 1625330
+ flags = 1
+ data = length 239, hash 9E15AC1E
+ sample 71:
+ time = 1648549
+ flags = 1
+ data = length 228, hash F3B70641
+ sample 72:
+ time = 1671768
+ flags = 1
+ data = length 237, hash 124E3194
+ sample 73:
+ time = 1694987
+ flags = 1
+ data = length 231, hash 950CD7C8
+ sample 74:
+ time = 1718206
+ flags = 1
+ data = length 236, hash A12E49AF
+ sample 75:
+ time = 1741425
+ flags = 1
+ data = length 242, hash 43BC9C24
+ sample 76:
+ time = 1764644
+ flags = 1
+ data = length 241, hash DCF0B17
+ sample 77:
+ time = 1787863
+ flags = 1
+ data = length 251, hash C0B99968
+ sample 78:
+ time = 1811082
+ flags = 1
+ data = length 245, hash 9B38ED1C
+ sample 79:
+ time = 1834301
+ flags = 1
+ data = length 238, hash 1BA69079
+ sample 80:
+ time = 1857520
+ flags = 1
+ data = length 233, hash 44C8C6BF
+ sample 81:
+ time = 1880739
+ flags = 1
+ data = length 231, hash EABBEE02
+ sample 82:
+ time = 1903958
+ flags = 1
+ data = length 226, hash D09C44FB
+ sample 83:
+ time = 1927177
+ flags = 1
+ data = length 235, hash BE6A6608
+ sample 84:
+ time = 1950396
+ flags = 1
+ data = length 235, hash 2735F454
+ sample 85:
+ time = 1973615
+ flags = 1
+ data = length 238, hash B160DFE7
+ sample 86:
+ time = 1996834
+ flags = 1
+ data = length 232, hash 1B217D2E
+ sample 87:
+ time = 2020053
+ flags = 1
+ data = length 251, hash D1C14CEA
+ sample 88:
+ time = 2043272
+ flags = 1
+ data = length 256, hash 97C87F08
+ sample 89:
+ time = 2066491
+ flags = 1
+ data = length 237, hash 6645DB3
+ sample 90:
+ time = 2089710
+ flags = 1
+ data = length 235, hash 727A1C82
+ sample 91:
+ time = 2112929
+ flags = 1
+ data = length 234, hash 5015F8B5
+ sample 92:
+ time = 2136148
+ flags = 1
+ data = length 241, hash 9102144B
+ sample 93:
+ time = 2159367
+ flags = 1
+ data = length 224, hash 64E0D807
+ sample 94:
+ time = 2182586
+ flags = 1
+ data = length 228, hash 1922B852
+ sample 95:
+ time = 2205805
+ flags = 1
+ data = length 224, hash 953502D8
+ sample 96:
+ time = 2229024
+ flags = 1
+ data = length 214, hash 92B87FE7
+ sample 97:
+ time = 2252243
+ flags = 1
+ data = length 213, hash BB0C8D86
+ sample 98:
+ time = 2275462
+ flags = 1
+ data = length 206, hash 9AD21017
+ sample 99:
+ time = 2298681
+ flags = 1
+ data = length 209, hash C479FE94
+ sample 100:
+ time = 2321900
+ flags = 1
+ data = length 220, hash 3033DCE1
+ sample 101:
+ time = 2345119
+ flags = 1
+ data = length 217, hash 7D589C94
+ sample 102:
+ time = 2368338
+ flags = 1
+ data = length 216, hash AAF6C183
+ sample 103:
+ time = 2391557
+ flags = 1
+ data = length 206, hash 1EE1207F
+ sample 104:
+ time = 2414776
+ flags = 1
+ data = length 204, hash 4BEB1210
+ sample 105:
+ time = 2437995
+ flags = 1
+ data = length 213, hash 21A841C9
+ sample 106:
+ time = 2461214
+ flags = 1
+ data = length 207, hash B80B0424
+ sample 107:
+ time = 2484433
+ flags = 1
+ data = length 212, hash 4785A1C3
+ sample 108:
+ time = 2507652
+ flags = 1
+ data = length 205, hash 59BF7229
+ sample 109:
+ time = 2530871
+ flags = 1
+ data = length 208, hash FA313DDE
+ sample 110:
+ time = 2554090
+ flags = 1
+ data = length 211, hash 190D85FD
+ sample 111:
+ time = 2577309
+ flags = 1
+ data = length 211, hash BA050052
+ sample 112:
+ time = 2600528
+ flags = 1
+ data = length 211, hash F3080F10
+ sample 113:
+ time = 2623747
+ flags = 1
+ data = length 210, hash F41B7BE7
+ sample 114:
+ time = 2646966
+ flags = 1
+ data = length 207, hash 2176C97E
+ sample 115:
+ time = 2670185
+ flags = 1
+ data = length 220, hash 32087455
+ sample 116:
+ time = 2693404
+ flags = 1
+ data = length 213, hash 4E5649A8
+ sample 117:
+ time = 2716623
+ flags = 1
+ data = length 213, hash 5F12FDCF
+ sample 118:
+ time = 2739842
+ flags = 1
+ data = length 204, hash 1E895C2A
+ sample 119:
+ time = 2763061
+ flags = 1
+ data = length 219, hash 45382270
+ sample 120:
+ time = 2786280
+ flags = 1
+ data = length 205, hash D66C6A1D
+ sample 121:
+ time = 2809499
+ flags = 1
+ data = length 204, hash 467AD01F
+ sample 122:
+ time = 2832718
+ flags = 1
+ data = length 211, hash F0435574
+ sample 123:
+ time = 2855937
+ flags = 1
+ data = length 206, hash 8C96B75F
+ sample 124:
+ time = 2879156
+ flags = 1
+ data = length 200, hash 82553248
+ sample 125:
+ time = 2902375
+ flags = 1
+ data = length 180, hash 1E51E6CE
+ sample 126:
+ time = 2925594
+ flags = 1
+ data = length 196, hash 33151DC4
+ sample 127:
+ time = 2948813
+ flags = 1
+ data = length 197, hash 1E62A7D6
+ sample 128:
+ time = 2972032
+ flags = 1
+ data = length 206, hash 6A6C4CC9
+ sample 129:
+ time = 2995251
+ flags = 1
+ data = length 209, hash A72FABAA
+ sample 130:
+ time = 3018470
+ flags = 1
+ data = length 217, hash BA33B985
+ sample 131:
+ time = 3041689
+ flags = 1
+ data = length 235, hash 9919CFD9
+ sample 132:
+ time = 3064908
+ flags = 1
+ data = length 236, hash A22C7267
+ sample 133:
+ time = 3088127
+ flags = 1
+ data = length 213, hash 3D57C901
+ sample 134:
+ time = 3111346
+ flags = 1
+ data = length 205, hash 47F68FDE
+ sample 135:
+ time = 3134565
+ flags = 1
+ data = length 210, hash 9A756E9C
+ sample 136:
+ time = 3157784
+ flags = 1
+ data = length 210, hash BD45C31F
+ sample 137:
+ time = 3181003
+ flags = 1
+ data = length 207, hash 8774FF7B
+ sample 138:
+ time = 3204222
+ flags = 1
+ data = length 149, hash 4678C0E5
+ sample 139:
+ time = 3227441
+ flags = 1
+ data = length 161, hash E991035D
+ sample 140:
+ time = 3250660
+ flags = 1
+ data = length 197, hash C3013689
+ sample 141:
+ time = 3273879
+ flags = 1
+ data = length 208, hash E6C0237
+ sample 142:
+ time = 3297098
+ flags = 1
+ data = length 232, hash A330F188
+ sample 143:
+ time = 3320317
+ flags = 1
+ data = length 174, hash 2B69C34E
+track 1:
+ format:
+ bitrate = -1
+ id = 1
+ containerMimeType = null
+ sampleMimeType = application/id3
+ maxInputSize = -1
+ width = -1
+ height = -1
+ frameRate = -1.0
+ rotationDegrees = 0
+ pixelWidthHeightRatio = 1.0
+ channelCount = -1
+ sampleRate = -1
+ pcmEncoding = -1
+ encoderDelay = 0
+ encoderPadding = 0
+ subsampleOffsetUs = 9223372036854775807
+ selectionFlags = 0
+ language = null
+ drmInitData = -
+ metadata = null
+ initializationData:
+ total output bytes = 141
+ sample count = 2
+ sample 0:
+ time = 0
+ flags = 1
+ data = length 55, hash A7EB51A0
+ sample 1:
+ time = 23219
+ flags = 1
+ data = length 86, hash 3FA72D40
+tracksEnded = true
diff --git a/library/core/src/test/assets/ts/sample_with_id3.adts.unknown_length.dump b/library/core/src/test/assets/ts/sample_with_id3.adts.unknown_length.dump
new file mode 100644
index 0000000000..b9cde05d69
--- /dev/null
+++ b/library/core/src/test/assets/ts/sample_with_id3.adts.unknown_length.dump
@@ -0,0 +1,641 @@
+seekMap:
+ isSeekable = false
+ duration = UNSET TIME
+ getPosition(0) = [[timeUs=0, position=0]]
+numberOfTracks = 2
+track 0:
+ format:
+ bitrate = -1
+ id = 0
+ containerMimeType = null
+ sampleMimeType = audio/mp4a-latm
+ maxInputSize = -1
+ width = -1
+ height = -1
+ frameRate = -1.0
+ rotationDegrees = 0
+ pixelWidthHeightRatio = 1.0
+ channelCount = 1
+ sampleRate = 44100
+ pcmEncoding = -1
+ encoderDelay = 0
+ encoderPadding = 0
+ subsampleOffsetUs = 9223372036854775807
+ selectionFlags = 0
+ language = null
+ drmInitData = -
+ metadata = null
+ initializationData:
+ data = length 2, hash 5F7
+ total output bytes = 30797
+ sample count = 144
+ sample 0:
+ time = 0
+ flags = 1
+ data = length 23, hash 47DE9131
+ sample 1:
+ time = 23219
+ flags = 1
+ data = length 6, hash 31CF3A46
+ sample 2:
+ time = 46438
+ flags = 1
+ data = length 6, hash 31CF3A46
+ sample 3:
+ time = 69657
+ flags = 1
+ data = length 6, hash 31CF3A46
+ sample 4:
+ time = 92876
+ flags = 1
+ data = length 6, hash 31EC5206
+ sample 5:
+ time = 116095
+ flags = 1
+ data = length 171, hash 4F6478F6
+ sample 6:
+ time = 139314
+ flags = 1
+ data = length 202, hash AF4068A3
+ sample 7:
+ time = 162533
+ flags = 1
+ data = length 210, hash E4C10618
+ sample 8:
+ time = 185752
+ flags = 1
+ data = length 217, hash 9ECCD0D9
+ sample 9:
+ time = 208971
+ flags = 1
+ data = length 212, hash 6BAC2CD9
+ sample 10:
+ time = 232190
+ flags = 1
+ data = length 223, hash 188B6010
+ sample 11:
+ time = 255409
+ flags = 1
+ data = length 222, hash C1A04D0C
+ sample 12:
+ time = 278628
+ flags = 1
+ data = length 220, hash D65F9768
+ sample 13:
+ time = 301847
+ flags = 1
+ data = length 227, hash B96C9E14
+ sample 14:
+ time = 325066
+ flags = 1
+ data = length 229, hash 9FB09972
+ sample 15:
+ time = 348285
+ flags = 1
+ data = length 220, hash 2271F053
+ sample 16:
+ time = 371504
+ flags = 1
+ data = length 226, hash 5EDD2F4F
+ sample 17:
+ time = 394723
+ flags = 1
+ data = length 239, hash 957510E0
+ sample 18:
+ time = 417942
+ flags = 1
+ data = length 224, hash 718A8F47
+ sample 19:
+ time = 441161
+ flags = 1
+ data = length 225, hash 5E11E293
+ sample 20:
+ time = 464380
+ flags = 1
+ data = length 227, hash FCE50D27
+ sample 21:
+ time = 487599
+ flags = 1
+ data = length 212, hash 77908C40
+ sample 22:
+ time = 510818
+ flags = 1
+ data = length 227, hash 34C4EB32
+ sample 23:
+ time = 534037
+ flags = 1
+ data = length 231, hash 95488307
+ sample 24:
+ time = 557256
+ flags = 1
+ data = length 226, hash 97F12D6F
+ sample 25:
+ time = 580475
+ flags = 1
+ data = length 236, hash 91A9D9A2
+ sample 26:
+ time = 603694
+ flags = 1
+ data = length 227, hash 27A608F9
+ sample 27:
+ time = 626913
+ flags = 1
+ data = length 229, hash 57DAAE4
+ sample 28:
+ time = 650132
+ flags = 1
+ data = length 235, hash ED30AC34
+ sample 29:
+ time = 673351
+ flags = 1
+ data = length 227, hash BD3D6280
+ sample 30:
+ time = 696570
+ flags = 1
+ data = length 233, hash 694B1087
+ sample 31:
+ time = 719789
+ flags = 1
+ data = length 232, hash 1EDFE047
+ sample 32:
+ time = 743008
+ flags = 1
+ data = length 228, hash E2A831F4
+ sample 33:
+ time = 766227
+ flags = 1
+ data = length 231, hash 757E6012
+ sample 34:
+ time = 789446
+ flags = 1
+ data = length 223, hash 4003D791
+ sample 35:
+ time = 812665
+ flags = 1
+ data = length 232, hash 3CF9A07C
+ sample 36:
+ time = 835884
+ flags = 1
+ data = length 228, hash 25AC3FF7
+ sample 37:
+ time = 859103
+ flags = 1
+ data = length 220, hash 2C1824CE
+ sample 38:
+ time = 882322
+ flags = 1
+ data = length 229, hash 46FDD8FB
+ sample 39:
+ time = 905541
+ flags = 1
+ data = length 237, hash F6988018
+ sample 40:
+ time = 928760
+ flags = 1
+ data = length 242, hash 60436B6B
+ sample 41:
+ time = 951979
+ flags = 1
+ data = length 275, hash 90EDFA8E
+ sample 42:
+ time = 975198
+ flags = 1
+ data = length 242, hash 5C86EFCB
+ sample 43:
+ time = 998417
+ flags = 1
+ data = length 233, hash E0A51B82
+ sample 44:
+ time = 1021636
+ flags = 1
+ data = length 235, hash 590DF14F
+ sample 45:
+ time = 1044855
+ flags = 1
+ data = length 238, hash 69AF4E6E
+ sample 46:
+ time = 1068074
+ flags = 1
+ data = length 235, hash E745AE8D
+ sample 47:
+ time = 1091293
+ flags = 1
+ data = length 223, hash 295F2A13
+ sample 48:
+ time = 1114512
+ flags = 1
+ data = length 228, hash E2F47B21
+ sample 49:
+ time = 1137731
+ flags = 1
+ data = length 229, hash 262C3CFE
+ sample 50:
+ time = 1160950
+ flags = 1
+ data = length 232, hash 4B5BF5E8
+ sample 51:
+ time = 1184169
+ flags = 1
+ data = length 233, hash F3D80836
+ sample 52:
+ time = 1207388
+ flags = 1
+ data = length 237, hash 32E0A11E
+ sample 53:
+ time = 1230607
+ flags = 1
+ data = length 228, hash E1B89F13
+ sample 54:
+ time = 1253826
+ flags = 1
+ data = length 237, hash 8BDD9E38
+ sample 55:
+ time = 1277045
+ flags = 1
+ data = length 235, hash 3C84161F
+ sample 56:
+ time = 1300264
+ flags = 1
+ data = length 227, hash A47E1789
+ sample 57:
+ time = 1323483
+ flags = 1
+ data = length 228, hash 869FDFD3
+ sample 58:
+ time = 1346702
+ flags = 1
+ data = length 233, hash 272ECE2
+ sample 59:
+ time = 1369921
+ flags = 1
+ data = length 227, hash DB6B9618
+ sample 60:
+ time = 1393140
+ flags = 1
+ data = length 212, hash 63214325
+ sample 61:
+ time = 1416359
+ flags = 1
+ data = length 221, hash 9BA588A1
+ sample 62:
+ time = 1439578
+ flags = 1
+ data = length 225, hash 21EFD50C
+ sample 63:
+ time = 1462797
+ flags = 1
+ data = length 231, hash F3AD0BF
+ sample 64:
+ time = 1486016
+ flags = 1
+ data = length 224, hash 822C9210
+ sample 65:
+ time = 1509235
+ flags = 1
+ data = length 195, hash D4EF53EE
+ sample 66:
+ time = 1532454
+ flags = 1
+ data = length 195, hash A816647A
+ sample 67:
+ time = 1555673
+ flags = 1
+ data = length 184, hash 9A2B7E6
+ sample 68:
+ time = 1578892
+ flags = 1
+ data = length 210, hash 956E3600
+ sample 69:
+ time = 1602111
+ flags = 1
+ data = length 234, hash 35CFDA0A
+ sample 70:
+ time = 1625330
+ flags = 1
+ data = length 239, hash 9E15AC1E
+ sample 71:
+ time = 1648549
+ flags = 1
+ data = length 228, hash F3B70641
+ sample 72:
+ time = 1671768
+ flags = 1
+ data = length 237, hash 124E3194
+ sample 73:
+ time = 1694987
+ flags = 1
+ data = length 231, hash 950CD7C8
+ sample 74:
+ time = 1718206
+ flags = 1
+ data = length 236, hash A12E49AF
+ sample 75:
+ time = 1741425
+ flags = 1
+ data = length 242, hash 43BC9C24
+ sample 76:
+ time = 1764644
+ flags = 1
+ data = length 241, hash DCF0B17
+ sample 77:
+ time = 1787863
+ flags = 1
+ data = length 251, hash C0B99968
+ sample 78:
+ time = 1811082
+ flags = 1
+ data = length 245, hash 9B38ED1C
+ sample 79:
+ time = 1834301
+ flags = 1
+ data = length 238, hash 1BA69079
+ sample 80:
+ time = 1857520
+ flags = 1
+ data = length 233, hash 44C8C6BF
+ sample 81:
+ time = 1880739
+ flags = 1
+ data = length 231, hash EABBEE02
+ sample 82:
+ time = 1903958
+ flags = 1
+ data = length 226, hash D09C44FB
+ sample 83:
+ time = 1927177
+ flags = 1
+ data = length 235, hash BE6A6608
+ sample 84:
+ time = 1950396
+ flags = 1
+ data = length 235, hash 2735F454
+ sample 85:
+ time = 1973615
+ flags = 1
+ data = length 238, hash B160DFE7
+ sample 86:
+ time = 1996834
+ flags = 1
+ data = length 232, hash 1B217D2E
+ sample 87:
+ time = 2020053
+ flags = 1
+ data = length 251, hash D1C14CEA
+ sample 88:
+ time = 2043272
+ flags = 1
+ data = length 256, hash 97C87F08
+ sample 89:
+ time = 2066491
+ flags = 1
+ data = length 237, hash 6645DB3
+ sample 90:
+ time = 2089710
+ flags = 1
+ data = length 235, hash 727A1C82
+ sample 91:
+ time = 2112929
+ flags = 1
+ data = length 234, hash 5015F8B5
+ sample 92:
+ time = 2136148
+ flags = 1
+ data = length 241, hash 9102144B
+ sample 93:
+ time = 2159367
+ flags = 1
+ data = length 224, hash 64E0D807
+ sample 94:
+ time = 2182586
+ flags = 1
+ data = length 228, hash 1922B852
+ sample 95:
+ time = 2205805
+ flags = 1
+ data = length 224, hash 953502D8
+ sample 96:
+ time = 2229024
+ flags = 1
+ data = length 214, hash 92B87FE7
+ sample 97:
+ time = 2252243
+ flags = 1
+ data = length 213, hash BB0C8D86
+ sample 98:
+ time = 2275462
+ flags = 1
+ data = length 206, hash 9AD21017
+ sample 99:
+ time = 2298681
+ flags = 1
+ data = length 209, hash C479FE94
+ sample 100:
+ time = 2321900
+ flags = 1
+ data = length 220, hash 3033DCE1
+ sample 101:
+ time = 2345119
+ flags = 1
+ data = length 217, hash 7D589C94
+ sample 102:
+ time = 2368338
+ flags = 1
+ data = length 216, hash AAF6C183
+ sample 103:
+ time = 2391557
+ flags = 1
+ data = length 206, hash 1EE1207F
+ sample 104:
+ time = 2414776
+ flags = 1
+ data = length 204, hash 4BEB1210
+ sample 105:
+ time = 2437995
+ flags = 1
+ data = length 213, hash 21A841C9
+ sample 106:
+ time = 2461214
+ flags = 1
+ data = length 207, hash B80B0424
+ sample 107:
+ time = 2484433
+ flags = 1
+ data = length 212, hash 4785A1C3
+ sample 108:
+ time = 2507652
+ flags = 1
+ data = length 205, hash 59BF7229
+ sample 109:
+ time = 2530871
+ flags = 1
+ data = length 208, hash FA313DDE
+ sample 110:
+ time = 2554090
+ flags = 1
+ data = length 211, hash 190D85FD
+ sample 111:
+ time = 2577309
+ flags = 1
+ data = length 211, hash BA050052
+ sample 112:
+ time = 2600528
+ flags = 1
+ data = length 211, hash F3080F10
+ sample 113:
+ time = 2623747
+ flags = 1
+ data = length 210, hash F41B7BE7
+ sample 114:
+ time = 2646966
+ flags = 1
+ data = length 207, hash 2176C97E
+ sample 115:
+ time = 2670185
+ flags = 1
+ data = length 220, hash 32087455
+ sample 116:
+ time = 2693404
+ flags = 1
+ data = length 213, hash 4E5649A8
+ sample 117:
+ time = 2716623
+ flags = 1
+ data = length 213, hash 5F12FDCF
+ sample 118:
+ time = 2739842
+ flags = 1
+ data = length 204, hash 1E895C2A
+ sample 119:
+ time = 2763061
+ flags = 1
+ data = length 219, hash 45382270
+ sample 120:
+ time = 2786280
+ flags = 1
+ data = length 205, hash D66C6A1D
+ sample 121:
+ time = 2809499
+ flags = 1
+ data = length 204, hash 467AD01F
+ sample 122:
+ time = 2832718
+ flags = 1
+ data = length 211, hash F0435574
+ sample 123:
+ time = 2855937
+ flags = 1
+ data = length 206, hash 8C96B75F
+ sample 124:
+ time = 2879156
+ flags = 1
+ data = length 200, hash 82553248
+ sample 125:
+ time = 2902375
+ flags = 1
+ data = length 180, hash 1E51E6CE
+ sample 126:
+ time = 2925594
+ flags = 1
+ data = length 196, hash 33151DC4
+ sample 127:
+ time = 2948813
+ flags = 1
+ data = length 197, hash 1E62A7D6
+ sample 128:
+ time = 2972032
+ flags = 1
+ data = length 206, hash 6A6C4CC9
+ sample 129:
+ time = 2995251
+ flags = 1
+ data = length 209, hash A72FABAA
+ sample 130:
+ time = 3018470
+ flags = 1
+ data = length 217, hash BA33B985
+ sample 131:
+ time = 3041689
+ flags = 1
+ data = length 235, hash 9919CFD9
+ sample 132:
+ time = 3064908
+ flags = 1
+ data = length 236, hash A22C7267
+ sample 133:
+ time = 3088127
+ flags = 1
+ data = length 213, hash 3D57C901
+ sample 134:
+ time = 3111346
+ flags = 1
+ data = length 205, hash 47F68FDE
+ sample 135:
+ time = 3134565
+ flags = 1
+ data = length 210, hash 9A756E9C
+ sample 136:
+ time = 3157784
+ flags = 1
+ data = length 210, hash BD45C31F
+ sample 137:
+ time = 3181003
+ flags = 1
+ data = length 207, hash 8774FF7B
+ sample 138:
+ time = 3204222
+ flags = 1
+ data = length 149, hash 4678C0E5
+ sample 139:
+ time = 3227441
+ flags = 1
+ data = length 161, hash E991035D
+ sample 140:
+ time = 3250660
+ flags = 1
+ data = length 197, hash C3013689
+ sample 141:
+ time = 3273879
+ flags = 1
+ data = length 208, hash E6C0237
+ sample 142:
+ time = 3297098
+ flags = 1
+ data = length 232, hash A330F188
+ sample 143:
+ time = 3320317
+ flags = 1
+ data = length 174, hash 2B69C34E
+track 1:
+ format:
+ bitrate = -1
+ id = 1
+ containerMimeType = null
+ sampleMimeType = application/id3
+ maxInputSize = -1
+ width = -1
+ height = -1
+ frameRate = -1.0
+ rotationDegrees = 0
+ pixelWidthHeightRatio = 1.0
+ channelCount = -1
+ sampleRate = -1
+ pcmEncoding = -1
+ encoderDelay = 0
+ encoderPadding = 0
+ subsampleOffsetUs = 9223372036854775807
+ selectionFlags = 0
+ language = null
+ drmInitData = -
+ metadata = null
+ initializationData:
+ total output bytes = 141
+ sample count = 2
+ sample 0:
+ time = 0
+ flags = 1
+ data = length 55, hash A7EB51A0
+ sample 1:
+ time = 23219
+ flags = 1
+ data = length 86, hash 3FA72D40
+tracksEnded = true
diff --git a/library/core/src/test/assets/wav/sample_with_trailing_bytes.wav b/library/core/src/test/assets/wav/sample_with_trailing_bytes.wav
new file mode 100644
index 0000000000..0b06efcf09
Binary files /dev/null and b/library/core/src/test/assets/wav/sample_with_trailing_bytes.wav differ
diff --git a/library/core/src/test/java/com/google/android/exoplayer2/AudioFocusManagerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/AudioFocusManagerTest.java
index 9a44d6def6..b0f258c0c2 100644
--- a/library/core/src/test/java/com/google/android/exoplayer2/AudioFocusManagerTest.java
+++ b/library/core/src/test/java/com/google/android/exoplayer2/AudioFocusManagerTest.java
@@ -65,13 +65,11 @@ public class AudioFocusManagerTest {
@Test
public void setAudioAttributes_withNullUsage_doesNotManageAudioFocus() {
// Ensure that NULL audio attributes -> don't manage audio focus
- assertThat(
- audioFocusManager.setAudioAttributes(
- /* audioAttributes= */ null, /* playWhenReady= */ false, Player.STATE_IDLE))
+ audioFocusManager.setAudioAttributes(/* audioAttributes= */ null);
+
+ assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ false, Player.STATE_IDLE))
.isEqualTo(PLAYER_COMMAND_DO_NOT_PLAY);
- assertThat(
- audioFocusManager.setAudioAttributes(
- /* audioAttributes= */ null, /* playWhenReady= */ true, Player.STATE_READY))
+ assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_READY))
.isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY);
ShadowAudioManager.AudioFocusRequest request =
Shadows.shadowOf(audioManager).getLastAudioFocusRequest();
@@ -85,18 +83,17 @@ public class AudioFocusManagerTest {
AudioAttributes media = new AudioAttributes.Builder().setUsage(C.USAGE_MEDIA).build();
Shadows.shadowOf(audioManager)
.setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED);
- assertThat(
- audioFocusManager.setAudioAttributes(
- media, /* playWhenReady= */ true, Player.STATE_READY))
+ audioFocusManager.setAudioAttributes(media);
+ assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_READY))
.isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY);
ShadowAudioManager.AudioFocusRequest request =
Shadows.shadowOf(audioManager).getLastAudioFocusRequest();
assertThat(request.durationHint).isEqualTo(AudioManager.AUDIOFOCUS_GAIN);
// Ensure that setting null audio attributes with audio focus releases audio focus.
- assertThat(
- audioFocusManager.setAudioAttributes(
- /* audioAttributes= */ null, /* playWhenReady= */ true, Player.STATE_READY))
+ audioFocusManager.setAudioAttributes(/* audioAttributes= */ null);
+
+ assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_READY))
.isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY);
AudioManager.OnAudioFocusChangeListener lastRequest =
Shadows.shadowOf(audioManager).getLastAbandonedAudioFocusListener();
@@ -110,18 +107,16 @@ public class AudioFocusManagerTest {
AudioAttributes media = new AudioAttributes.Builder().setUsage(C.USAGE_MEDIA).build();
Shadows.shadowOf(audioManager)
.setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED);
- assertThat(
- audioFocusManager.setAudioAttributes(
- media, /* playWhenReady= */ true, Player.STATE_READY))
+ audioFocusManager.setAudioAttributes(media);
+ assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_READY))
.isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY);
ShadowAudioManager.AudioFocusRequest request =
Shadows.shadowOf(audioManager).getLastAudioFocusRequest();
assertThat(getAudioFocusGainFromRequest(request)).isEqualTo(AudioManager.AUDIOFOCUS_GAIN);
// Ensure that setting null audio attributes with audio focus releases audio focus.
- assertThat(
- audioFocusManager.setAudioAttributes(
- /* audioAttributes= */ null, /* playWhenReady= */ true, Player.STATE_READY))
+ audioFocusManager.setAudioAttributes(/* audioAttributes= */ null);
+ assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_READY))
.isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY);
AudioFocusRequest lastRequest =
Shadows.shadowOf(audioManager).getLastAbandonedAudioFocusRequest();
@@ -130,10 +125,10 @@ public class AudioFocusManagerTest {
@Test
public void setAudioAttributes_withUsageAlarm_throwsIllegalArgumentException() {
- // Ensure that audio attributes that map to AUDIOFOCUS_GAIN_TRANSIENT* throw
+ // Ensure that audio attributes that map to AUDIOFOCUS_GAIN_TRANSIENT* throw.
AudioAttributes alarm = new AudioAttributes.Builder().setUsage(C.USAGE_ALARM).build();
try {
- audioFocusManager.setAudioAttributes(alarm, /* playWhenReady= */ false, Player.STATE_IDLE);
+ audioFocusManager.setAudioAttributes(alarm);
fail();
} catch (IllegalArgumentException e) {
// Expected
@@ -147,9 +142,9 @@ public class AudioFocusManagerTest {
Shadows.shadowOf(audioManager)
.setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED);
- assertThat(
- audioFocusManager.setAudioAttributes(
- media, /* playWhenReady= */ true, Player.STATE_READY))
+ audioFocusManager.setAudioAttributes(media);
+
+ assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_READY))
.isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY);
ShadowAudioManager.AudioFocusRequest request =
Shadows.shadowOf(audioManager).getLastAudioFocusRequest();
@@ -163,9 +158,9 @@ public class AudioFocusManagerTest {
Shadows.shadowOf(audioManager)
.setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED);
- assertThat(
- audioFocusManager.setAudioAttributes(
- media, /* playWhenReady= */ true, Player.STATE_ENDED))
+ audioFocusManager.setAudioAttributes(media);
+
+ assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_ENDED))
.isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY);
ShadowAudioManager.AudioFocusRequest request =
Shadows.shadowOf(audioManager).getLastAudioFocusRequest();
@@ -173,42 +168,221 @@ public class AudioFocusManagerTest {
}
@Test
- public void handlePrepare_afterSetAudioAttributes_setsPlayerCommandPlayWhenReady() {
+ public void updateAudioFocus_idleToBuffering_setsPlayerCommandPlayWhenReady() {
// Ensure that when playWhenReady is true while the player is IDLE, audio focus is only
- // requested after calling handlePrepare.
+ // requested after calling prepare (= changing the state to BUFFERING).
AudioAttributes media = new AudioAttributes.Builder().setUsage(C.USAGE_MEDIA).build();
-
Shadows.shadowOf(audioManager)
.setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED);
+ audioFocusManager.setAudioAttributes(media);
- assertThat(
- audioFocusManager.setAudioAttributes(
- media, /* playWhenReady= */ true, Player.STATE_IDLE))
+ assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_IDLE))
.isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY);
assertThat(Shadows.shadowOf(audioManager).getLastAudioFocusRequest()).isNull();
- assertThat(audioFocusManager.handlePrepare(/* playWhenReady= */ true))
+ assertThat(
+ audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_BUFFERING))
+ .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY);
+ ShadowAudioManager.AudioFocusRequest request =
+ Shadows.shadowOf(audioManager).getLastAudioFocusRequest();
+ assertThat(getAudioFocusGainFromRequest(request)).isEqualTo(AudioManager.AUDIOFOCUS_GAIN);
+ }
+
+ @Test
+ public void updateAudioFocus_pausedToPlaying_setsPlayerCommandPlayWhenReady() {
+ AudioAttributes media = new AudioAttributes.Builder().setUsage(C.USAGE_MEDIA).build();
+ Shadows.shadowOf(audioManager)
+ .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED);
+ audioFocusManager.setAudioAttributes(media);
+
+ // Audio focus should not be requested yet, because playWhenReady=false.
+ assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ false, Player.STATE_READY))
+ .isEqualTo(PLAYER_COMMAND_DO_NOT_PLAY);
+ assertThat(Shadows.shadowOf(audioManager).getLastAudioFocusRequest()).isNull();
+
+ // Audio focus should be requested now that playWhenReady=true.
+ assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_READY))
+ .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY);
+ ShadowAudioManager.AudioFocusRequest request =
+ Shadows.shadowOf(audioManager).getLastAudioFocusRequest();
+ assertThat(getAudioFocusGainFromRequest(request)).isEqualTo(AudioManager.AUDIOFOCUS_GAIN);
+ }
+
+ // See https://github.com/google/ExoPlayer/issues/7182 for context.
+ @Test
+ public void updateAudioFocus_pausedToPlaying_withTransientLoss_setsPlayerCommandPlayWhenReady() {
+ AudioAttributes media = new AudioAttributes.Builder().setUsage(C.USAGE_MEDIA).build();
+ Shadows.shadowOf(audioManager)
+ .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED);
+ audioFocusManager.setAudioAttributes(media);
+
+ assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_READY))
+ .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY);
+
+ // Simulate transient focus loss.
+ audioFocusManager.getFocusListener().onAudioFocusChange(AudioManager.AUDIOFOCUS_LOSS_TRANSIENT);
+
+ // Focus should be re-requested, rather than staying in a state of transient focus loss.
+ assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_READY))
.isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY);
}
@Test
- public void handleSetPlayWhenReady_afterSetAudioAttributes_setsPlayerCommandPlayWhenReady() {
- // Ensure that audio focus is not requested until playWhenReady is true.
+ public void updateAudioFocus_pausedToPlaying_withTransientDuck_setsPlayerCommandPlayWhenReady() {
AudioAttributes media = new AudioAttributes.Builder().setUsage(C.USAGE_MEDIA).build();
-
Shadows.shadowOf(audioManager)
.setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED);
+ audioFocusManager.setAudioAttributes(media);
- assertThat(audioFocusManager.handlePrepare(/* playWhenReady= */ false))
- .isEqualTo(PLAYER_COMMAND_DO_NOT_PLAY);
- assertThat(Shadows.shadowOf(audioManager).getLastAudioFocusRequest()).isNull();
- assertThat(
- audioFocusManager.setAudioAttributes(
- media, /* playWhenReady= */ false, Player.STATE_READY))
- .isEqualTo(PLAYER_COMMAND_DO_NOT_PLAY);
- assertThat(Shadows.shadowOf(audioManager).getLastAudioFocusRequest()).isNull();
- assertThat(
- audioFocusManager.handleSetPlayWhenReady(/* playWhenReady= */ true, Player.STATE_READY))
+ assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_READY))
.isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY);
+
+ // Simulate transient ducking.
+ audioFocusManager
+ .getFocusListener()
+ .onAudioFocusChange(AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK);
+ assertThat(testPlayerControl.lastVolumeMultiplier).isLessThan(1.0f);
+
+ // Focus should be re-requested, rather than staying in a state of transient ducking. This
+ // should restore the volume to 1.0.
+ assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_READY))
+ .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY);
+ assertThat(testPlayerControl.lastVolumeMultiplier).isEqualTo(1.0f);
+ }
+
+ @Test
+ public void updateAudioFocus_abandonFocusWhenDucked_restoresFullVolume() {
+ AudioAttributes media = new AudioAttributes.Builder().setUsage(C.USAGE_MEDIA).build();
+ Shadows.shadowOf(audioManager)
+ .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED);
+ audioFocusManager.setAudioAttributes(media);
+
+ assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_READY))
+ .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY);
+
+ // Simulate transient ducking.
+ audioFocusManager
+ .getFocusListener()
+ .onAudioFocusChange(AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK);
+ assertThat(testPlayerControl.lastVolumeMultiplier).isLessThan(1.0f);
+
+ // Configure the manager to no longer handle audio focus.
+ audioFocusManager.setAudioAttributes(null);
+
+ // Focus should be abandoned, which should restore the volume to 1.0.
+ assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_READY))
+ .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY);
+ assertThat(testPlayerControl.lastVolumeMultiplier).isEqualTo(1.0f);
+ }
+
+ @Test
+ @Config(maxSdk = 25)
+ public void updateAudioFocus_readyToIdle_abandonsAudioFocus() {
+ // Ensure that stopping the player (=changing state to idle) abandons audio focus.
+ AudioAttributes media =
+ new AudioAttributes.Builder()
+ .setUsage(C.USAGE_MEDIA)
+ .setContentType(C.CONTENT_TYPE_SPEECH)
+ .build();
+ Shadows.shadowOf(audioManager)
+ .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED);
+ audioFocusManager.setAudioAttributes(media);
+ assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_READY))
+ .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY);
+ assertThat(Shadows.shadowOf(audioManager).getLastAbandonedAudioFocusListener()).isNull();
+
+ ShadowAudioManager.AudioFocusRequest request =
+ Shadows.shadowOf(audioManager).getLastAudioFocusRequest();
+ assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_IDLE))
+ .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY);
+ assertThat(Shadows.shadowOf(audioManager).getLastAbandonedAudioFocusListener())
+ .isEqualTo(request.listener);
+ }
+
+ @Test
+ @Config(minSdk = 26, maxSdk = TARGET_SDK)
+ public void updateAudioFocus_readyToIdle_abandonsAudioFocus_v26() {
+ // Ensure that stopping the player (=changing state to idle) abandons audio focus.
+ AudioAttributes media =
+ new AudioAttributes.Builder()
+ .setUsage(C.USAGE_MEDIA)
+ .setContentType(C.CONTENT_TYPE_SPEECH)
+ .build();
+ Shadows.shadowOf(audioManager)
+ .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED);
+ audioFocusManager.setAudioAttributes(media);
+
+ assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_READY))
+ .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY);
+ assertThat(Shadows.shadowOf(audioManager).getLastAbandonedAudioFocusRequest()).isNull();
+
+ ShadowAudioManager.AudioFocusRequest request =
+ Shadows.shadowOf(audioManager).getLastAudioFocusRequest();
+ assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_IDLE))
+ .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY);
+ assertThat(Shadows.shadowOf(audioManager).getLastAbandonedAudioFocusRequest())
+ .isEqualTo(request.audioFocusRequest);
+ }
+
+ @Test
+ @Config(maxSdk = 25)
+ public void updateAudioFocus_readyToIdle_withoutHandlingAudioFocus_isNoOp() {
+ // Ensure that changing state to idle is a no-op if audio focus isn't handled.
+ Shadows.shadowOf(audioManager)
+ .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED);
+ audioFocusManager.setAudioAttributes(null);
+
+ assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ false, Player.STATE_READY))
+ .isEqualTo(PLAYER_COMMAND_DO_NOT_PLAY);
+ assertThat(Shadows.shadowOf(audioManager).getLastAbandonedAudioFocusListener()).isNull();
+ ShadowAudioManager.AudioFocusRequest request =
+ Shadows.shadowOf(audioManager).getLastAudioFocusRequest();
+ assertThat(request).isNull();
+
+ assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ false, Player.STATE_IDLE))
+ .isEqualTo(PLAYER_COMMAND_DO_NOT_PLAY);
+ assertThat(Shadows.shadowOf(audioManager).getLastAbandonedAudioFocusListener()).isNull();
+ }
+
+ @Test
+ @Config(minSdk = 26, maxSdk = TARGET_SDK)
+ public void updateAudioFocus_readyToIdle_withoutHandlingAudioFocus_isNoOp_v26() {
+ // Ensure that changing state to idle is a no-op if audio focus isn't handled.
+ Shadows.shadowOf(audioManager)
+ .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED);
+ audioFocusManager.setAudioAttributes(null);
+
+ assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ false, Player.STATE_READY))
+ .isEqualTo(PLAYER_COMMAND_DO_NOT_PLAY);
+ assertThat(Shadows.shadowOf(audioManager).getLastAbandonedAudioFocusRequest()).isNull();
+ ShadowAudioManager.AudioFocusRequest request =
+ Shadows.shadowOf(audioManager).getLastAudioFocusRequest();
+ assertThat(request).isNull();
+
+ assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ false, Player.STATE_IDLE))
+ .isEqualTo(PLAYER_COMMAND_DO_NOT_PLAY);
+ assertThat(Shadows.shadowOf(audioManager).getLastAbandonedAudioFocusRequest()).isNull();
+ }
+
+ @Test
+ public void release_doesNotCallPlayerControlToRestoreVolume() {
+ AudioAttributes media = new AudioAttributes.Builder().setUsage(C.USAGE_MEDIA).build();
+ Shadows.shadowOf(audioManager)
+ .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED);
+ audioFocusManager.setAudioAttributes(media);
+
+ assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_READY))
+ .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY);
+
+ // Simulate transient ducking.
+ audioFocusManager
+ .getFocusListener()
+ .onAudioFocusChange(AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK);
+ assertThat(testPlayerControl.lastVolumeMultiplier).isLessThan(1.0f);
+
+ audioFocusManager.release();
+
+ // PlaybackController.setVolumeMultiplier should not have been called to restore the volume.
+ assertThat(testPlayerControl.lastVolumeMultiplier).isLessThan(1.0f);
}
@Test
@@ -217,17 +391,17 @@ public class AudioFocusManagerTest {
// AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK, and returns to the default value after focus is
// regained.
AudioAttributes media = new AudioAttributes.Builder().setUsage(C.USAGE_MEDIA).build();
-
Shadows.shadowOf(audioManager)
.setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED);
- assertThat(
- audioFocusManager.setAudioAttributes(
- media, /* playWhenReady= */ true, Player.STATE_READY))
+ audioFocusManager.setAudioAttributes(media);
+
+ assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_READY))
.isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY);
audioFocusManager
.getFocusListener()
.onAudioFocusChange(AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK);
+
assertThat(testPlayerControl.lastVolumeMultiplier).isLessThan(1.0f);
assertThat(testPlayerControl.lastPlayerCommand).isEqualTo(NO_COMMAND_RECEIVED);
audioFocusManager.getFocusListener().onAudioFocusChange(AudioManager.AUDIOFOCUS_GAIN);
@@ -243,12 +417,11 @@ public class AudioFocusManagerTest {
.setUsage(C.USAGE_MEDIA)
.setContentType(C.CONTENT_TYPE_SPEECH)
.build();
-
Shadows.shadowOf(audioManager)
.setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED);
- assertThat(
- audioFocusManager.setAudioAttributes(
- media, /* playWhenReady= */ true, Player.STATE_READY))
+ audioFocusManager.setAudioAttributes(media);
+
+ assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_READY))
.isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY);
audioFocusManager
@@ -261,16 +434,15 @@ public class AudioFocusManagerTest {
}
@Test
- public void onAudioFocusChange_withTransientLost_sendsCommandWaitForCallback() {
+ public void onAudioFocusChange_withTransientLoss_sendsCommandWaitForCallback() {
// Ensure that the player is commanded to pause when audio focus is lost with
// AUDIOFOCUS_LOSS_TRANSIENT.
AudioAttributes media = new AudioAttributes.Builder().setUsage(C.USAGE_MEDIA).build();
-
Shadows.shadowOf(audioManager)
.setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED);
- assertThat(
- audioFocusManager.setAudioAttributes(
- media, /* playWhenReady= */ true, Player.STATE_READY))
+ audioFocusManager.setAudioAttributes(media);
+
+ assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_READY))
.isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY);
audioFocusManager.getFocusListener().onAudioFocusChange(AudioManager.AUDIOFOCUS_LOSS_TRANSIENT);
@@ -280,7 +452,7 @@ public class AudioFocusManagerTest {
@Test
@Config(maxSdk = 25)
- public void onAudioFocusChange_withAudioFocusLost_sendsDoNotPlayAndAbandondsFocus() {
+ public void onAudioFocusChange_withFocusLoss_sendsDoNotPlayAndAbandonsFocus() {
// Ensure that AUDIOFOCUS_LOSS causes AudioFocusManager to pause playback and abandon audio
// focus.
AudioAttributes media =
@@ -288,12 +460,11 @@ public class AudioFocusManagerTest {
.setUsage(C.USAGE_MEDIA)
.setContentType(C.CONTENT_TYPE_SPEECH)
.build();
-
Shadows.shadowOf(audioManager)
.setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED);
- assertThat(
- audioFocusManager.setAudioAttributes(
- media, /* playWhenReady= */ true, Player.STATE_READY))
+ audioFocusManager.setAudioAttributes(media);
+
+ assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_READY))
.isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY);
assertThat(Shadows.shadowOf(audioManager).getLastAbandonedAudioFocusListener()).isNull();
@@ -307,7 +478,7 @@ public class AudioFocusManagerTest {
@Test
@Config(minSdk = 26, maxSdk = TARGET_SDK)
- public void onAudioFocusChange_withAudioFocusLost_sendsDoNotPlayAndAbandondsFocus_v26() {
+ public void onAudioFocusChange_withFocusLoss_sendsDoNotPlayAndAbandonsFocus_v26() {
// Ensure that AUDIOFOCUS_LOSS causes AudioFocusManager to pause playback and abandon audio
// focus.
AudioAttributes media =
@@ -315,12 +486,11 @@ public class AudioFocusManagerTest {
.setUsage(C.USAGE_MEDIA)
.setContentType(C.CONTENT_TYPE_SPEECH)
.build();
-
Shadows.shadowOf(audioManager)
.setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED);
- assertThat(
- audioFocusManager.setAudioAttributes(
- media, /* playWhenReady= */ true, Player.STATE_READY))
+ audioFocusManager.setAudioAttributes(media);
+
+ assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_READY))
.isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY);
assertThat(Shadows.shadowOf(audioManager).getLastAbandonedAudioFocusRequest()).isNull();
@@ -330,120 +500,6 @@ public class AudioFocusManagerTest {
.isEqualTo(Shadows.shadowOf(audioManager).getLastAudioFocusRequest().audioFocusRequest);
}
- @Test
- @Config(maxSdk = 25)
- public void handleStop_withAudioFocus_abandonsAudioFocus() {
- // Ensure that handleStop causes AudioFocusManager to abandon audio focus.
- AudioAttributes media =
- new AudioAttributes.Builder()
- .setUsage(C.USAGE_MEDIA)
- .setContentType(C.CONTENT_TYPE_SPEECH)
- .build();
-
- Shadows.shadowOf(audioManager)
- .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED);
- assertThat(
- audioFocusManager.setAudioAttributes(
- media, /* playWhenReady= */ true, Player.STATE_READY))
- .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY);
- assertThat(Shadows.shadowOf(audioManager).getLastAbandonedAudioFocusListener()).isNull();
-
- ShadowAudioManager.AudioFocusRequest request =
- Shadows.shadowOf(audioManager).getLastAudioFocusRequest();
- audioFocusManager.handleStop();
- assertThat(Shadows.shadowOf(audioManager).getLastAbandonedAudioFocusListener())
- .isEqualTo(request.listener);
- }
-
- @Test
- @Config(minSdk = 26, maxSdk = TARGET_SDK)
- public void handleStop_withAudioFocus_abandonsAudioFocus_v26() {
- // Ensure that handleStop causes AudioFocusManager to abandon audio focus.
- AudioAttributes media =
- new AudioAttributes.Builder()
- .setUsage(C.USAGE_MEDIA)
- .setContentType(C.CONTENT_TYPE_SPEECH)
- .build();
-
- Shadows.shadowOf(audioManager)
- .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED);
- assertThat(
- audioFocusManager.setAudioAttributes(
- media, /* playWhenReady= */ true, Player.STATE_READY))
- .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY);
- assertThat(Shadows.shadowOf(audioManager).getLastAbandonedAudioFocusRequest()).isNull();
-
- ShadowAudioManager.AudioFocusRequest request =
- Shadows.shadowOf(audioManager).getLastAudioFocusRequest();
- audioFocusManager.handleStop();
- assertThat(Shadows.shadowOf(audioManager).getLastAbandonedAudioFocusRequest())
- .isEqualTo(request.audioFocusRequest);
- }
-
- @Test
- @Config(maxSdk = 25)
- public void handleStop_withoutAudioFocus_stillAbandonsFocus() {
- // Ensure that handleStop causes AudioFocusManager to call through to abandon audio focus
- // even if focus wasn't requested.
- AudioAttributes media =
- new AudioAttributes.Builder()
- .setUsage(C.USAGE_MEDIA)
- .setContentType(C.CONTENT_TYPE_SPEECH)
- .build();
-
- Shadows.shadowOf(audioManager)
- .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED);
- assertThat(
- audioFocusManager.setAudioAttributes(
- media, /* playWhenReady= */ false, Player.STATE_READY))
- .isEqualTo(PLAYER_COMMAND_DO_NOT_PLAY);
- assertThat(Shadows.shadowOf(audioManager).getLastAbandonedAudioFocusListener()).isNull();
- ShadowAudioManager.AudioFocusRequest request =
- Shadows.shadowOf(audioManager).getLastAudioFocusRequest();
- assertThat(request).isNull();
-
- audioFocusManager.handleStop();
- assertThat(Shadows.shadowOf(audioManager).getLastAbandonedAudioFocusListener()).isNotNull();
- }
-
- @Test
- @Config(maxSdk = 25)
- public void handleStop_withoutHandlingAudioFocus_isNoOp() {
- // Ensure that handleStop is a no-op if audio focus isn't handled.
- Shadows.shadowOf(audioManager)
- .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED);
- assertThat(
- audioFocusManager.setAudioAttributes(
- /* audioAttributes= */ null, /* playWhenReady= */ false, Player.STATE_READY))
- .isEqualTo(PLAYER_COMMAND_DO_NOT_PLAY);
- assertThat(Shadows.shadowOf(audioManager).getLastAbandonedAudioFocusListener()).isNull();
- ShadowAudioManager.AudioFocusRequest request =
- Shadows.shadowOf(audioManager).getLastAudioFocusRequest();
- assertThat(request).isNull();
-
- audioFocusManager.handleStop();
- assertThat(Shadows.shadowOf(audioManager).getLastAbandonedAudioFocusListener()).isNull();
- }
-
- @Test
- @Config(minSdk = 26, maxSdk = TARGET_SDK)
- public void handleStop_withoutHandlingAudioFocus_isNoOp_v26() {
- // Ensure that handleStop is a no-op if audio focus isn't handled.
- Shadows.shadowOf(audioManager)
- .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED);
- assertThat(
- audioFocusManager.setAudioAttributes(
- /* audioAttributes= */ null, /* playWhenReady= */ false, Player.STATE_READY))
- .isEqualTo(PLAYER_COMMAND_DO_NOT_PLAY);
- assertThat(Shadows.shadowOf(audioManager).getLastAbandonedAudioFocusRequest()).isNull();
- ShadowAudioManager.AudioFocusRequest request =
- Shadows.shadowOf(audioManager).getLastAudioFocusRequest();
- assertThat(request).isNull();
-
- audioFocusManager.handleStop();
- assertThat(Shadows.shadowOf(audioManager).getLastAbandonedAudioFocusRequest()).isNull();
- }
-
private int getAudioFocusGainFromRequest(ShadowAudioManager.AudioFocusRequest audioFocusRequest) {
return Util.SDK_INT >= 26
? audioFocusRequest.audioFocusRequest.getFocusGain()
diff --git a/library/core/src/test/java/com/google/android/exoplayer2/analytics/PlaybackStatsListenerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/analytics/PlaybackStatsListenerTest.java
new file mode 100644
index 0000000000..10122d36ec
--- /dev/null
+++ b/library/core/src/test/java/com/google/android/exoplayer2/analytics/PlaybackStatsListenerTest.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.analytics;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.annotation.Nullable;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import com.google.android.exoplayer2.Player;
+import com.google.android.exoplayer2.Timeline;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Unit test for {@link PlaybackStatsListener}. */
+@RunWith(AndroidJUnit4.class)
+public final class PlaybackStatsListenerTest {
+
+ private static final AnalyticsListener.EventTime TEST_EVENT_TIME =
+ new AnalyticsListener.EventTime(
+ /* realtimeMs= */ 500,
+ Timeline.EMPTY,
+ /* windowIndex= */ 0,
+ /* mediaPeriodId= */ null,
+ /* eventPlaybackPositionMs= */ 0,
+ /* currentPlaybackPositionMs= */ 0,
+ /* totalBufferedDurationMs= */ 0);
+
+ @Test
+ public void playback_withKeepHistory_updatesStats() {
+ PlaybackStatsListener playbackStatsListener =
+ new PlaybackStatsListener(/* keepHistory= */ true, /* callback= */ null);
+
+ playbackStatsListener.onPlayerStateChanged(
+ TEST_EVENT_TIME, /* playWhenReady= */ true, Player.STATE_BUFFERING);
+ playbackStatsListener.onPlayerStateChanged(
+ TEST_EVENT_TIME, /* playWhenReady= */ true, Player.STATE_READY);
+ playbackStatsListener.onPlayerStateChanged(
+ TEST_EVENT_TIME, /* playWhenReady= */ true, Player.STATE_ENDED);
+
+ @Nullable PlaybackStats playbackStats = playbackStatsListener.getPlaybackStats();
+ assertThat(playbackStats).isNotNull();
+ assertThat(playbackStats.endedCount).isEqualTo(1);
+ }
+
+ @Test
+ public void playback_withoutKeepHistory_updatesStats() {
+ PlaybackStatsListener playbackStatsListener =
+ new PlaybackStatsListener(/* keepHistory= */ false, /* callback= */ null);
+
+ playbackStatsListener.onPlayerStateChanged(
+ TEST_EVENT_TIME, /* playWhenReady= */ true, Player.STATE_BUFFERING);
+ playbackStatsListener.onPlayerStateChanged(
+ TEST_EVENT_TIME, /* playWhenReady= */ true, Player.STATE_READY);
+ playbackStatsListener.onPlayerStateChanged(
+ TEST_EVENT_TIME, /* playWhenReady= */ true, Player.STATE_ENDED);
+
+ @Nullable PlaybackStats playbackStats = playbackStatsListener.getPlaybackStats();
+ assertThat(playbackStats).isNotNull();
+ assertThat(playbackStats.endedCount).isEqualTo(1);
+ }
+}
diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorTest.java
index 56776aea09..3ef454d716 100644
--- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorTest.java
+++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorTest.java
@@ -29,6 +29,11 @@ public final class AdtsExtractorTest {
ExtractorAsserts.assertBehavior(AdtsExtractor::new, "ts/sample.adts");
}
+ @Test
+ public void testSample_with_id3() throws Exception {
+ ExtractorAsserts.assertBehavior(AdtsExtractor::new, "ts/sample_with_id3.adts");
+ }
+
@Test
public void testSample_withSeeking() throws Exception {
ExtractorAsserts.assertBehavior(
diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/wav/WavExtractorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/wav/WavExtractorTest.java
index 7f9549ea75..0f4220c7f3 100644
--- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/wav/WavExtractorTest.java
+++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/wav/WavExtractorTest.java
@@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.extractor.wav;
+import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.testutil.ExtractorAsserts;
import org.junit.Test;
@@ -30,7 +31,7 @@ public final class WavExtractorTest {
}
@Test
- public void testSampleImaAdpcm() throws Exception {
+ public void sample_imaAdpcm() throws Exception {
ExtractorAsserts.assertBehavior(WavExtractor::new, "wav/sample_ima_adpcm.wav");
}
}
diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/SampleQueueTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/SampleQueueTest.java
index 427e81d29f..a34488d2e7 100644
--- a/library/core/src/test/java/com/google/android/exoplayer2/source/SampleQueueTest.java
+++ b/library/core/src/test/java/com/google/android/exoplayer2/source/SampleQueueTest.java
@@ -372,8 +372,10 @@ public final class SampleQueueTest {
assertReadFormat(/* formatRequired= */ false, FORMAT_ENCRYPTED);
assertReadNothing(/* formatRequired= */ false);
+ assertThat(inputBuffer.waitingForKeys).isTrue();
when(mockDrmSession.getState()).thenReturn(DrmSession.STATE_OPENED_WITH_KEYS);
assertReadEncryptedSample(/* sampleIndex= */ 0);
+ assertThat(inputBuffer.waitingForKeys).isFalse();
}
@Test
diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/subrip/SubripDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/subrip/SubripDecoderTest.java
index 9f66f65a56..dec7a25523 100644
--- a/library/core/src/test/java/com/google/android/exoplayer2/text/subrip/SubripDecoderTest.java
+++ b/library/core/src/test/java/com/google/android/exoplayer2/text/subrip/SubripDecoderTest.java
@@ -39,6 +39,7 @@ public final class SubripDecoderTest {
private static final String TYPICAL_NEGATIVE_TIMESTAMPS = "subrip/typical_negative_timestamps";
private static final String TYPICAL_UNEXPECTED_END = "subrip/typical_unexpected_end";
private static final String TYPICAL_WITH_TAGS = "subrip/typical_with_tags";
+ private static final String TYPICAL_NO_HOURS_AND_MILLIS = "subrip/typical_no_hours_and_millis";
@Test
public void testDecodeEmpty() throws IOException {
@@ -151,9 +152,14 @@ public final class SubripDecoderTest {
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TYPICAL_WITH_TAGS);
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
- assertTypicalCue1(subtitle, 0);
- assertTypicalCue2(subtitle, 2);
- assertTypicalCue3(subtitle, 4);
+ assertThat(subtitle.getCues(subtitle.getEventTime(0)).get(0).text.toString())
+ .isEqualTo("This is the first subtitle.");
+
+ assertThat(subtitle.getCues(subtitle.getEventTime(2)).get(0).text.toString())
+ .isEqualTo("This is the second subtitle.\nSecond subtitle with second line.");
+
+ assertThat(subtitle.getCues(subtitle.getEventTime(4)).get(0).text.toString())
+ .isEqualTo("This is the third subtitle.");
assertThat(subtitle.getCues(subtitle.getEventTime(6)).get(0).text.toString())
.isEqualTo("This { \\an2} is not a valid tag due to the space after the opening bracket.");
@@ -172,6 +178,21 @@ public final class SubripDecoderTest {
assertAlignmentCue(subtitle, 26, Cue.ANCHOR_TYPE_START, Cue.ANCHOR_TYPE_END); // {/an9}
}
+ @Test
+ public void decodeTypicalNoHoursAndMillis() throws IOException {
+ SubripDecoder decoder = new SubripDecoder();
+ byte[] bytes =
+ TestUtil.getByteArray(
+ ApplicationProvider.getApplicationContext(), TYPICAL_NO_HOURS_AND_MILLIS);
+ Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
+
+ assertThat(subtitle.getEventTimeCount()).isEqualTo(6);
+ assertTypicalCue1(subtitle, 0);
+ assertThat(subtitle.getEventTime(2)).isEqualTo(2_000_000);
+ assertThat(subtitle.getEventTime(3)).isEqualTo(3_000_000);
+ assertTypicalCue3(subtitle, 4);
+ }
+
private static void assertTypicalCue1(Subtitle subtitle, int eventIndex) {
assertThat(subtitle.getEventTime(eventIndex)).isEqualTo(0);
assertThat(subtitle.getCues(subtitle.getEventTime(eventIndex)).get(0).text.toString())
@@ -187,10 +208,12 @@ public final class SubripDecoderTest {
}
private static void assertTypicalCue3(Subtitle subtitle, int eventIndex) {
- assertThat(subtitle.getEventTime(eventIndex)).isEqualTo(4567000);
+ long expectedStartTimeUs = (((2L * 60L * 60L) + 4L) * 1000L + 567L) * 1000L;
+ assertThat(subtitle.getEventTime(eventIndex)).isEqualTo(expectedStartTimeUs);
assertThat(subtitle.getCues(subtitle.getEventTime(eventIndex)).get(0).text.toString())
.isEqualTo("This is the third subtitle.");
- assertThat(subtitle.getEventTime(eventIndex + 1)).isEqualTo(8901000);
+ long expectedEndTimeUs = (((2L * 60L * 60L) + 8L) * 1000L + 901L) * 1000L;
+ assertThat(subtitle.getEventTime(eventIndex + 1)).isEqualTo(expectedEndTimeUs);
}
private static void assertAlignmentCue(
diff --git a/library/core/src/test/java/com/google/android/exoplayer2/util/MimeTypesTest.java b/library/core/src/test/java/com/google/android/exoplayer2/util/MimeTypesTest.java
index 288ad918b2..e397383490 100644
--- a/library/core/src/test/java/com/google/android/exoplayer2/util/MimeTypesTest.java
+++ b/library/core/src/test/java/com/google/android/exoplayer2/util/MimeTypesTest.java
@@ -73,6 +73,10 @@ public final class MimeTypesTest {
assertThat(MimeTypes.getMediaMimeType("mp4a.AA")).isEqualTo(MimeTypes.AUDIO_DTS_HD);
assertThat(MimeTypes.getMediaMimeType("mp4a.AB")).isEqualTo(MimeTypes.AUDIO_DTS_HD);
assertThat(MimeTypes.getMediaMimeType("mp4a.AD")).isEqualTo(MimeTypes.AUDIO_OPUS);
+
+ assertThat(MimeTypes.getMediaMimeType("wvtt")).isEqualTo(MimeTypes.TEXT_VTT);
+ assertThat(MimeTypes.getMediaMimeType("stpp.")).isEqualTo(MimeTypes.APPLICATION_TTML);
+ assertThat(MimeTypes.getMediaMimeType("stpp.ttml.im1t")).isEqualTo(MimeTypes.APPLICATION_TTML);
}
@Test
diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java
index dfcd62b8b1..dcd4b15cae 100644
--- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java
+++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java
@@ -807,15 +807,18 @@ public final class DashMediaSource extends BaseMediaSource {
manifestLoadPending &= manifest.dynamic;
manifestLoadStartTimestampMs = elapsedRealtimeMs - loadDurationMs;
manifestLoadEndTimestampMs = elapsedRealtimeMs;
- if (manifest.location != null) {
- synchronized (manifestUriLock) {
- // This condition checks that replaceManifestUri wasn't called between the start and end of
- // this load. If it was, we ignore the manifest location and prefer the manual replacement.
- @SuppressWarnings("ReferenceEquality")
- boolean isSameUriInstance = loadable.dataSpec.uri == manifestUri;
- if (isSameUriInstance) {
- manifestUri = manifest.location;
- }
+
+ synchronized (manifestUriLock) {
+ // Checks whether replaceManifestUri(Uri) was called to manually replace the URI between the
+ // start and end of this load. If it was then isSameUriInstance evaluates to false, and we
+ // prefer the manual replacement to one derived from the previous request.
+ @SuppressWarnings("ReferenceEquality")
+ boolean isSameUriInstance = loadable.dataSpec.uri == manifestUri;
+ if (isSameUriInstance) {
+ // Replace the manifest URI with one specified by a manifest Location element (if present),
+ // or with the final (possibly redirected) URI. This follows the recommendation in
+ // DASH-IF-IOP 4.3, section 3.2.15.3. See: https://dashif.org/docs/DASH-IF-IOP-v4.3.pdf.
+ manifestUri = manifest.location != null ? manifest.location : loadable.getUri();
}
}
diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java
index b107be4794..95129d68c4 100644
--- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java
+++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java
@@ -222,10 +222,11 @@ public class DashManifestParser extends DefaultHandler
protected Pair
+ *
*
@@ -308,6 +314,7 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider
@Nullable private Drawable defaultArtwork;
private @ShowBuffering int showBuffering;
private boolean keepContentOnPlayerReset;
+ private boolean useSensorRotation;
@Nullable private ErrorMessageProvider super ExoPlaybackException> errorMessageProvider;
@Nullable private CharSequence customErrorMessage;
private int controllerShowTimeoutMs;
@@ -367,6 +374,7 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider
boolean controllerAutoShow = true;
boolean controllerHideDuringAds = true;
int showBuffering = SHOW_BUFFERING_NEVER;
+ useSensorRotation = true;
if (attrs != null) {
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.PlayerView, 0, 0);
try {
@@ -390,6 +398,8 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider
R.styleable.PlayerView_keep_content_on_player_reset, keepContentOnPlayerReset);
controllerHideDuringAds =
a.getBoolean(R.styleable.PlayerView_hide_during_ads, controllerHideDuringAds);
+ useSensorRotation =
+ a.getBoolean(R.styleable.PlayerView_use_sensor_rotation, useSensorRotation);
} finally {
a.recycle();
}
@@ -422,6 +432,7 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider
case SURFACE_TYPE_SPHERICAL_GL_SURFACE_VIEW:
SphericalGLSurfaceView sphericalGLSurfaceView = new SphericalGLSurfaceView(context);
sphericalGLSurfaceView.setSingleTapListener(componentListener);
+ sphericalGLSurfaceView.setUseSensorRotation(useSensorRotation);
surfaceView = sphericalGLSurfaceView;
break;
case SURFACE_TYPE_VIDEO_DECODER_GL_SURFACE_VIEW:
@@ -746,6 +757,22 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider
}
}
+ /**
+ * Sets whether to use the orientation sensor for rotation during spherical playbacks (if
+ * available)
+ *
+ * @param useSensorRotation Whether to use the orientation sensor for rotation during spherical
+ * playbacks.
+ */
+ public void setUseSensorRotation(boolean useSensorRotation) {
+ if (this.useSensorRotation != useSensorRotation) {
+ this.useSensorRotation = useSensorRotation;
+ if (surfaceView instanceof SphericalGLSurfaceView) {
+ ((SphericalGLSurfaceView) surfaceView).setUseSensorRotation(useSensorRotation);
+ }
+ }
+ }
+
/**
* Sets whether a buffering spinner is displayed when the player is in the buffering state. The
* buffering spinner is not displayed by default.
diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/SphericalGLSurfaceView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/SphericalGLSurfaceView.java
index c01fccf54b..1c96f41df5 100644
--- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/SphericalGLSurfaceView.java
+++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/SphericalGLSurfaceView.java
@@ -72,6 +72,9 @@ public final class SphericalGLSurfaceView extends GLSurfaceView {
@Nullable private SurfaceTexture surfaceTexture;
@Nullable private Surface surface;
@Nullable private Player.VideoComponent videoComponent;
+ private boolean useSensorRotation;
+ private boolean isStarted;
+ private boolean isOrientationListenerRegistered;
public SphericalGLSurfaceView(Context context) {
this(context, null);
@@ -104,6 +107,7 @@ public final class SphericalGLSurfaceView extends GLSurfaceView {
WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
Display display = Assertions.checkNotNull(windowManager).getDefaultDisplay();
orientationListener = new OrientationListener(display, touchTracker, renderer);
+ useSensorRotation = true;
setEGLContextClientVersion(2);
setRenderer(renderer);
@@ -145,20 +149,23 @@ public final class SphericalGLSurfaceView extends GLSurfaceView {
touchTracker.setSingleTapListener(listener);
}
+ /** Sets whether to use the orientation sensor for rotation (if available). */
+ public void setUseSensorRotation(boolean useSensorRotation) {
+ this.useSensorRotation = useSensorRotation;
+ updateOrientationListenerRegistration();
+ }
+
@Override
public void onResume() {
super.onResume();
- if (orientationSensor != null) {
- sensorManager.registerListener(
- orientationListener, orientationSensor, SensorManager.SENSOR_DELAY_FASTEST);
- }
+ isStarted = true;
+ updateOrientationListenerRegistration();
}
@Override
public void onPause() {
- if (orientationSensor != null) {
- sensorManager.unregisterListener(orientationListener);
- }
+ isStarted = false;
+ updateOrientationListenerRegistration();
super.onPause();
}
@@ -181,6 +188,20 @@ public final class SphericalGLSurfaceView extends GLSurfaceView {
});
}
+ private void updateOrientationListenerRegistration() {
+ boolean enabled = useSensorRotation && isStarted;
+ if (orientationSensor == null || enabled == isOrientationListenerRegistered) {
+ return;
+ }
+ if (enabled) {
+ sensorManager.registerListener(
+ orientationListener, orientationSensor, SensorManager.SENSOR_DELAY_FASTEST);
+ } else {
+ sensorManager.unregisterListener(orientationListener);
+ }
+ isOrientationListenerRegistered = enabled;
+ }
+
// Called on GL thread.
private void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture) {
mainHandler.post(
diff --git a/library/ui/src/main/res/values/attrs.xml b/library/ui/src/main/res/values/attrs.xml
index 535bf320fb..e0a6b7faf4 100644
--- a/library/ui/src/main/res/values/attrs.xml
+++ b/library/ui/src/main/res/values/attrs.xml
@@ -77,8 +77,8 @@