diff --git a/.gitignore b/.gitignore index cb4cfaada1..3ab16a94fd 100644 --- a/.gitignore +++ b/.gitignore @@ -47,6 +47,7 @@ bazel-testlogs .DS_Store cmake-build-debug dist +jacoco.exec tmp # External native builds diff --git a/RELEASENOTES.md b/RELEASENOTES.md index b2ac9f0197..1b62db9fd4 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,5 +1,38 @@ # Release notes +### 2.13.2 (2021-02-25) + +* Extractors: + * Add support for MP4 and QuickTime meta atoms that are not full atoms. +* UI: + * Make conditions to enable UI actions consistent in + `DefaultControlDispatcher`, `PlayerControlView`, + `StyledPlayerControlView`, `PlayerNotificationManager` and + `TimelineQueueNavigator`. + * Fix conditions to enable seeking to next/previous media item to handle + the case where a live stream has ended. +* Audio: + * Fix `SimpleExoPlayer` reporting audio session ID as 0 in some cases + ([#8585](https://github.com/google/ExoPlayer/issues/8585)). +* IMA extension: + * Fix a bug where playback could get stuck when seeking into a playlist + item with ads, if the preroll ad had preloaded but the window position + of the seek should instead trigger playback of a midroll. + * Fix a bug with playback of ads in playlists, where the incorrect period + index was used when deciding whether to trigger playback of an ad after + a seek. +* Text: + * Parse SSA/ASS font size in `Style:` lines + ([#8435](https://github.com/google/ExoPlayer/issues/8435)). +* VP9 extension: Update to use NDK r21 + ([#8581](https://github.com/google/ExoPlayer/issues/8581)). +* FLAC extension: Update to use NDK r21 + ([#8581](https://github.com/google/ExoPlayer/issues/8581)). +* Opus extension: Update to use NDK r21 + ([#8581](https://github.com/google/ExoPlayer/issues/8581)). +* FFmpeg extension: Update to use NDK r21 + ([#8581](https://github.com/google/ExoPlayer/issues/8581)). + ### 2.13.1 (2021-02-12) * Live streaming: diff --git a/constants.gradle b/constants.gradle index 7b73235144..6c7eefe76f 100644 --- a/constants.gradle +++ b/constants.gradle @@ -13,8 +13,8 @@ // limitations under the License. project.ext { // ExoPlayer version and version code. - releaseVersion = '2.13.1' - releaseVersionCode = 2013001 + releaseVersion = '2.13.2' + releaseVersionCode = 2013002 minSdkVersion = 16 appTargetSdkVersion = 29 targetSdkVersion = 28 // TODO: Bump once b/143232359 is resolved. Also fix TODOs in UtilTest. diff --git a/extensions/ffmpeg/README.md b/extensions/ffmpeg/README.md index 639d1f6d6c..da2fa40141 100644 --- a/extensions/ffmpeg/README.md +++ b/extensions/ffmpeg/README.md @@ -30,7 +30,7 @@ FFMPEG_EXT_PATH="${EXOPLAYER_ROOT}/extensions/ffmpeg/src/main" ``` * Download the [Android NDK][] and set its location in a shell variable. - This build configuration has been tested on NDK r20. + This build configuration has been tested on NDK r21. ``` NDK_PATH="" diff --git a/extensions/flac/README.md b/extensions/flac/README.md index 47c74d1148..074daca71e 100644 --- a/extensions/flac/README.md +++ b/extensions/flac/README.md @@ -29,7 +29,7 @@ FLAC_EXT_PATH="${EXOPLAYER_ROOT}/extensions/flac/src/main" ``` * Download the [Android NDK][] and set its location in an environment variable. - This build configuration has been tested on NDK r20. + This build configuration has been tested on NDK r21. ``` NDK_PATH="" diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/AdTagLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/AdTagLoader.java index 9908e4940c..39f9f36fd4 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/AdTagLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/AdTagLoader.java @@ -341,11 +341,25 @@ import java.util.Map; boolean playWhenReady = player.getPlayWhenReady(); onTimelineChanged(player.getCurrentTimeline(), Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); - if (!AdPlaybackState.NONE.equals(adPlaybackState) - && adsManager != null - && imaPausedContent - && playWhenReady) { - adsManager.resume(); + @Nullable AdsManager adsManager = this.adsManager; + if (!AdPlaybackState.NONE.equals(adPlaybackState) && adsManager != null && imaPausedContent) { + // Check whether the current ad break matches the expected ad break based on the current + // position. If not, discard the current ad break so that the correct ad break can load. + long contentPositionMs = getContentPeriodPositionMs(player, timeline, period); + int adGroupForPositionIndex = + adPlaybackState.getAdGroupIndexForPositionUs( + C.msToUs(contentPositionMs), C.msToUs(contentDurationMs)); + if (adGroupForPositionIndex != C.INDEX_UNSET + && imaAdInfo != null + && imaAdInfo.adGroupIndex != adGroupForPositionIndex) { + if (configuration.debugModeEnabled) { + Log.d(TAG, "Discarding preloaded ad " + imaAdInfo); + } + adsManager.discardAdBreak(); + } + if (playWhenReady) { + adsManager.resume(); + } } } @@ -826,7 +840,7 @@ import java.util.Map; ensureSentContentCompleteIfAtEndOfStream(); if (!sentContentComplete && !timeline.isEmpty()) { long positionMs = getContentPeriodPositionMs(player, timeline, period); - timeline.getPeriod(/* periodIndex= */ 0, period); + timeline.getPeriod(player.getCurrentPeriodIndex(), period); int newAdGroupIndex = period.getAdGroupIndexForPositionUs(C.msToUs(positionMs)); if (newAdGroupIndex != C.INDEX_UNSET) { sentPendingContentPositionMs = false; diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java index 203479a7ed..bc86da4a86 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java @@ -98,8 +98,8 @@ public abstract class TimelineQueueNavigator implements MediaSessionConnector.Qu if (!timeline.isEmpty() && !player.isPlayingAd()) { timeline.getWindow(player.getCurrentWindowIndex(), window); enableSkipTo = timeline.getWindowCount() > 1; - enablePrevious = window.isSeekable || !window.isDynamic || player.hasPrevious(); - enableNext = window.isDynamic || player.hasNext(); + enablePrevious = window.isSeekable || !window.isLive() || player.hasPrevious(); + enableNext = (window.isLive() && window.isDynamic) || player.hasNext(); } long actions = 0; diff --git a/extensions/opus/README.md b/extensions/opus/README.md index b683dae0bf..4daff54abf 100644 --- a/extensions/opus/README.md +++ b/extensions/opus/README.md @@ -29,7 +29,7 @@ OPUS_EXT_PATH="${EXOPLAYER_ROOT}/extensions/opus/src/main" ``` * Download the [Android NDK][] and set its location in an environment variable. - This build configuration has been tested on NDK r20. + This build configuration has been tested on NDK r21. ``` NDK_PATH="" diff --git a/extensions/vp9/README.md b/extensions/vp9/README.md index 1c1d91eb03..30b7a252d0 100644 --- a/extensions/vp9/README.md +++ b/extensions/vp9/README.md @@ -29,7 +29,7 @@ VP9_EXT_PATH="${EXOPLAYER_ROOT}/extensions/vp9/src/main" ``` * Download the [Android NDK][] and set its location in an environment variable. - This build configuration has been tested on NDK r20. + This build configuration has been tested on NDK r21. ``` NDK_PATH="" diff --git a/library/common/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java b/library/common/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java index 0315cfc9cd..24c8505d98 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java @@ -30,11 +30,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.13.1"; + public static final String VERSION = "2.13.2"; /** 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.13.1"; + public static final String VERSION_SLASHY = "ExoPlayerLib/2.13.2"; /** * The version of the library expressed as an integer, for example 1002003. @@ -44,7 +44,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 = 2013001; + public static final int VERSION_INT = 2013002; /** * The default user agent for requests made by the library. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/DefaultControlDispatcher.java b/library/core/src/main/java/com/google/android/exoplayer2/DefaultControlDispatcher.java index fe23f28db7..03f03d5903 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/DefaultControlDispatcher.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/DefaultControlDispatcher.java @@ -79,11 +79,12 @@ public class DefaultControlDispatcher implements ControlDispatcher { int windowIndex = player.getCurrentWindowIndex(); timeline.getWindow(windowIndex, window); int previousWindowIndex = player.getPreviousWindowIndex(); + boolean isUnseekableLiveStream = window.isLive() && !window.isSeekable; if (previousWindowIndex != C.INDEX_UNSET && (player.getCurrentPosition() <= MAX_POSITION_FOR_SEEK_TO_PREVIOUS - || (window.isDynamic && !window.isSeekable))) { + || isUnseekableLiveStream)) { player.seekTo(previousWindowIndex, C.TIME_UNSET); - } else { + } else if (!isUnseekableLiveStream) { player.seekTo(windowIndex, /* positionMs= */ 0); } return true; @@ -96,10 +97,11 @@ public class DefaultControlDispatcher implements ControlDispatcher { return true; } int windowIndex = player.getCurrentWindowIndex(); + timeline.getWindow(windowIndex, window); int nextWindowIndex = player.getNextWindowIndex(); if (nextWindowIndex != C.INDEX_UNSET) { player.seekTo(nextWindowIndex, C.TIME_UNSET); - } else if (timeline.getWindow(windowIndex, window).isLive()) { + } else if (window.isLive() && window.isDynamic) { player.seekTo(windowIndex, C.TIME_UNSET); } return true; 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 e89e05eb64..a98e3475af 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 @@ -2140,7 +2140,6 @@ public class SimpleExoPlayer extends BasePlayer analyticsCollector.onAudioDisabled(counters); audioFormat = null; audioDecoderCounters = null; - audioSessionId = C.AUDIO_SESSION_ID_UNSET; } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SilenceMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SilenceMediaSource.java index b802717ee2..447a3e3ad5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SilenceMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SilenceMediaSource.java @@ -305,11 +305,15 @@ public final class SilenceMediaSource extends BaseMediaSource { return C.RESULT_BUFFER_READ; } + buffer.timeUs = getAudioPositionUs(positionBytes); + buffer.addFlag(C.BUFFER_FLAG_KEY_FRAME); + if (buffer.isFlagsOnly()) { + return C.RESULT_BUFFER_READ; + } + int bytesToWrite = (int) min(SILENCE_SAMPLE.length, bytesRemaining); buffer.ensureSpaceForWrite(bytesToWrite); buffer.data.put(SILENCE_SAMPLE, /* offset= */ 0, bytesToWrite); - buffer.timeUs = getAudioPositionUs(positionBytes); - buffer.addFlag(C.BUFFER_FLAG_KEY_FRAME); positionBytes += bytesToWrite; return C.RESULT_BUFFER_READ; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java index be1ab81cd3..dbe54168e8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java @@ -51,9 +51,9 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** - * A {@link MediaSource} that inserts ads linearly with a provided content media source. This source - * cannot be used as a child source in a composition. It must be the top-level source used to - * prepare the player. + * A {@link MediaSource} that inserts ads linearly into a provided content media source. + * + *

The wrapped content media source must contain a single {@link Timeline.Period}. */ public final class AdsMediaSource extends CompositeMediaSource { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java index b8e047dbcb..3f7a7c1dd6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java @@ -314,6 +314,10 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { /* end= */ spannableText.length(), SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE); } + if (style.fontSize != Cue.DIMEN_UNSET && screenHeight != Cue.DIMEN_UNSET) { + cue.setTextSize( + style.fontSize / screenHeight, Cue.TEXT_SIZE_TYPE_FRACTIONAL_IGNORE_PADDING); + } } @SsaStyle.SsaAlignment int alignment; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaStyle.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaStyle.java index bd378cccec..6379237387 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaStyle.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaStyle.java @@ -27,6 +27,7 @@ import androidx.annotation.ColorInt; import androidx.annotation.IntDef; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Util; @@ -90,12 +91,17 @@ import java.util.regex.Pattern; public final String name; @SsaAlignment public final int alignment; @Nullable @ColorInt public final Integer primaryColor; + public final float fontSize; private SsaStyle( - String name, @SsaAlignment int alignment, @Nullable @ColorInt Integer primaryColor) { + String name, + @SsaAlignment int alignment, + @Nullable @ColorInt Integer primaryColor, + float fontSize) { this.name = name; this.alignment = alignment; this.primaryColor = primaryColor; + this.fontSize = fontSize; } @Nullable @@ -114,7 +120,8 @@ import java.util.regex.Pattern; return new SsaStyle( styleValues[format.nameIndex].trim(), parseAlignment(styleValues[format.alignmentIndex].trim()), - parseColor(styleValues[format.primaryColorIndex].trim())); + parseColor(styleValues[format.primaryColorIndex].trim()), + parseFontSize(styleValues[format.fontSizeIndex].trim())); } catch (RuntimeException e) { Log.w(TAG, "Skipping malformed 'Style:' line: '" + styleLine + "'", e); return null; @@ -191,6 +198,15 @@ import java.util.regex.Pattern; return Color.argb(a, r, g, b); } + private static float parseFontSize(String fontSize) { + try { + return Float.parseFloat(fontSize); + } catch (NumberFormatException e) { + Log.w(TAG, "Failed to parse font size: '" + fontSize + "'", e); + return Cue.DIMEN_UNSET; + } + } + /** * Represents a {@code Format:} line from the {@code [V4+ Styles]} section * @@ -202,12 +218,15 @@ import java.util.regex.Pattern; public final int nameIndex; public final int alignmentIndex; public final int primaryColorIndex; + public final int fontSizeIndex; public final int length; - private Format(int nameIndex, int alignmentIndex, int primaryColorIndex, int length) { + private Format( + int nameIndex, int alignmentIndex, int primaryColorIndex, int fontSizeIndex, int length) { this.nameIndex = nameIndex; this.alignmentIndex = alignmentIndex; this.primaryColorIndex = primaryColorIndex; + this.fontSizeIndex = fontSizeIndex; this.length = length; } @@ -221,6 +240,7 @@ import java.util.regex.Pattern; int nameIndex = C.INDEX_UNSET; int alignmentIndex = C.INDEX_UNSET; int primaryColorIndex = C.INDEX_UNSET; + int fontSizeIndex = C.INDEX_UNSET; String[] keys = TextUtils.split(styleFormatLine.substring(SsaDecoder.FORMAT_LINE_PREFIX.length()), ","); for (int i = 0; i < keys.length; i++) { @@ -234,10 +254,13 @@ import java.util.regex.Pattern; case "primarycolour": primaryColorIndex = i; break; + case "fontsize": + fontSizeIndex = i; + break; } } return nameIndex != C.INDEX_UNSET - ? new Format(nameIndex, alignmentIndex, primaryColorIndex, keys.length) + ? new Format(nameIndex, alignmentIndex, primaryColorIndex, fontSizeIndex, keys.length) : null; } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/ssa/SsaDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/ssa/SsaDecoderTest.java index a734019d09..24a9188022 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/text/ssa/SsaDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/ssa/SsaDecoderTest.java @@ -47,7 +47,8 @@ public final class SsaDecoderTest { private static final String INVALID_TIMECODES = "media/ssa/invalid_timecodes"; private static final String INVALID_POSITIONS = "media/ssa/invalid_positioning"; private static final String POSITIONS_WITHOUT_PLAYRES = "media/ssa/positioning_without_playres"; - private static final String COLORS = "media/ssa/colors"; + private static final String STYLE_COLORS = "media/ssa/style_colors"; + private static final String STYLE_FONT_SIZE = "media/ssa/style_font_size"; @Test public void decodeEmpty() throws IOException { @@ -274,7 +275,7 @@ public final class SsaDecoderTest { @Test public void decodeColors() throws IOException { SsaDecoder decoder = new SsaDecoder(); - byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), COLORS); + byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), STYLE_COLORS); Subtitle subtitle = decoder.decode(bytes, bytes.length, false); assertThat(subtitle.getEventTimeCount()).isEqualTo(14); // &H000000FF (AABBGGRR) -> #FFFF0000 (AARRGGBB) @@ -319,6 +320,22 @@ public final class SsaDecoderTest { .hasNoForegroundColorSpanBetween(0, seventhCueText.length()); } + @Test + public void decodeFontSize() throws IOException { + SsaDecoder decoder = new SsaDecoder(); + byte[] bytes = + TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), STYLE_FONT_SIZE); + Subtitle subtitle = decoder.decode(bytes, bytes.length, false); + assertThat(subtitle.getEventTimeCount()).isEqualTo(4); + + Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0))); + assertThat(firstCue.textSize).isWithin(1.0e-8f).of(30f / 720f); + assertThat(firstCue.textSizeType).isEqualTo(Cue.TEXT_SIZE_TYPE_FRACTIONAL_IGNORE_PADDING); + Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2))); + assertThat(secondCue.textSize).isWithin(1.0e-8f).of(72.2f / 720f); + assertThat(secondCue.textSizeType).isEqualTo(Cue.TEXT_SIZE_TYPE_FRACTIONAL_IGNORE_PADDING); + } + private static void assertTypicalCue1(Subtitle subtitle, int eventIndex) { assertThat(subtitle.getEventTime(eventIndex)).isEqualTo(0); assertThat(subtitle.getCues(subtitle.getEventTime(eventIndex)).get(0).text.toString()) diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheWriterTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheWriterTest.java index 6064783e08..4c2fd3f765 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheWriterTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheWriterTest.java @@ -17,7 +17,6 @@ package com.google.android.exoplayer2.upstream.cache; import static com.google.android.exoplayer2.testutil.CacheAsserts.assertCachedData; import static com.google.common.truth.Truth.assertThat; -import static java.lang.Math.min; import static org.junit.Assert.assertThrows; import android.net.Uri; @@ -36,65 +35,18 @@ import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.Answers; -import org.mockito.Mock; import org.mockito.MockitoAnnotations; /** Unit tests for {@link CacheWriter}. */ @RunWith(AndroidJUnit4.class) public final class CacheWriterTest { - /** - * Abstract fake Cache implementation used by the test. This class must be public so Mockito can - * create a proxy for it. - */ - public abstract static class AbstractFakeCache implements Cache { - - // This array is set to alternating length of cached and not cached regions in tests: - // spansAndGaps = {, , - // , , ... } - // Ideally it should end with a cached region but it shouldn't matter for any code. - private int[] spansAndGaps; - private long contentLength; - - private void init() { - spansAndGaps = new int[] {}; - contentLength = C.LENGTH_UNSET; - } - - @Override - public long getCachedLength(String key, long position, long length) { - if (length == C.LENGTH_UNSET) { - length = Long.MAX_VALUE; - } - for (int i = 0; i < spansAndGaps.length; i++) { - int spanOrGap = spansAndGaps[i]; - if (position < spanOrGap) { - long left = min(spanOrGap - position, length); - return (i & 1) == 1 ? -left : left; - } - position -= spanOrGap; - } - return -length; - } - - @Override - public ContentMetadata getContentMetadata(String key) { - DefaultContentMetadata metadata = new DefaultContentMetadata(); - ContentMetadataMutations mutations = new ContentMetadataMutations(); - ContentMetadataMutations.setContentLength(mutations, contentLength); - return metadata.copyWithMutationsApplied(mutations); - } - } - - @Mock(answer = Answers.CALLS_REAL_METHODS) private AbstractFakeCache mockCache; private File tempFolder; private SimpleCache cache; @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); - mockCache.init(); tempFolder = Util.createTempDirectory(ApplicationProvider.getApplicationContext(), "ExoPlayerTest"); cache = diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java index 162294f1a0..dc098994df 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java @@ -145,12 +145,11 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; * Parses a udta atom. * * @param udtaAtom The udta (user data) atom to decode. - * @param isQuickTime True for QuickTime media. False otherwise. * @return A {@link Pair} containing the metadata from the meta child atom as first value (if * any), and the metadata from the smta child atom as second value (if any). */ public static Pair<@NullableType Metadata, @NullableType Metadata> parseUdta( - Atom.LeafAtom udtaAtom, boolean isQuickTime) { + Atom.LeafAtom udtaAtom) { ParsableByteArray udtaData = udtaAtom.data; udtaData.setPosition(Atom.HEADER_SIZE); @Nullable Metadata metaMetadata = null; @@ -159,8 +158,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; int atomPosition = udtaData.getPosition(); int atomSize = udtaData.readInt(); int atomType = udtaData.readInt(); - // Meta boxes are regular boxes rather than full boxes in QuickTime. Ignore them for now. - if (atomType == Atom.TYPE_meta && !isQuickTime) { + if (atomType == Atom.TYPE_meta) { udtaData.setPosition(atomPosition); metaMetadata = parseUdtaMeta(udtaData, atomPosition + atomSize); } else if (atomType == Atom.TYPE_smta) { @@ -227,6 +225,30 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; return entries.isEmpty() ? null : new Metadata(entries); } + /** + * Possibly skips the version and flags fields (1+3 byte) of a full meta atom. + * + *

Atoms of type {@link Atom#TYPE_meta} are defined to be full atoms which have four additional + * bytes for a version and a flags field (see 4.2 'Object Structure' in ISO/IEC 14496-12:2005). + * QuickTime do not have such a full box structure. Since some of these files are encoded wrongly, + * we can't rely on the file type though. Instead we must check the 8 bytes after the common + * header bytes ourselves. + * + * @param meta The 8 or more bytes following the meta atom size and type. + */ + public static void maybeSkipRemainingMetaAtomHeaderBytes(ParsableByteArray meta) { + int endPosition = meta.getPosition(); + // The next 8 bytes can be either: + // (iso) [1 byte version + 3 bytes flags][4 byte size of next atom] + // (qt) [4 byte size of next atom ][4 byte hdlr atom type ] + // In case of (iso) we need to skip the next 4 bytes. + meta.skipBytes(4); + if (meta.readInt() != Atom.TYPE_hdlr) { + endPosition += 4; + } + meta.setPosition(endPosition); + } + /** * Parses a trak atom (defined in ISO/IEC 14496-12). * @@ -677,7 +699,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; @Nullable private static Metadata parseUdtaMeta(ParsableByteArray meta, int limit) { - meta.skipBytes(Atom.FULL_HEADER_SIZE); + meta.skipBytes(Atom.HEADER_SIZE); + maybeSkipRemainingMetaAtomHeaderBytes(meta); while (meta.getPosition() < limit) { int atomPosition = meta.getPosition(); int atomSize = meta.readInt(); diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java index 823fd77fe0..08c399baff 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java @@ -470,7 +470,7 @@ public final class Mp4Extractor implements Extractor, SeekMap { @Nullable Atom.LeafAtom udta = moov.getLeafAtomOfType(Atom.TYPE_udta); if (udta != null) { Pair<@NullableType Metadata, @NullableType Metadata> udtaMetadata = - AtomParsers.parseUdta(udta, isQuickTime); + AtomParsers.parseUdta(udta); udtaMetaMetadata = udtaMetadata.first; smtaMetadata = udtaMetadata.second; if (udtaMetaMetadata != null) { @@ -727,29 +727,12 @@ public final class Mp4Extractor implements Extractor, SeekMap { } } - /** - * Possibly skips the version and flags fields (1+3 byte) of a full meta atom of the {@code - * input}. - * - *

Atoms of type {@link Atom#TYPE_meta} are defined to be full atoms which have four additional - * bytes for a version and a flags field (see 4.2 'Object Structure' in ISO/IEC 14496-12:2005). - * QuickTime do not have such a full box structure. Since some of these files are encoded wrongly, - * we can't rely on the file type though. Instead we must check the 8 bytes after the common - * header bytes ourselves. - */ private void maybeSkipRemainingMetaAtomHeaderBytes(ExtractorInput input) throws IOException { scratch.reset(8); - // Peek the next 8 bytes which can be either - // (iso) [1 byte version + 3 bytes flags][4 byte size of next atom] - // (qt) [4 byte size of next atom ][4 byte hdlr atom type ] - // In case of (iso) we need to skip the next 4 bytes. input.peekFully(scratch.getData(), 0, 8); - scratch.skipBytes(4); - if (scratch.readInt() == Atom.TYPE_hdlr) { - input.resetPeekPosition(); - } else { - input.skipFully(4); - } + AtomParsers.maybeSkipRemainingMetaAtomHeaderBytes(scratch); + input.skipFully(scratch.getPosition()); + input.resetPeekPosition(); } /** Processes an atom whose payload does not need to be parsed. */ diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerControlView.java index 7f9259d678..0d7340e270 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerControlView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerControlView.java @@ -913,10 +913,10 @@ public class PlayerControlView extends FrameLayout { timeline.getWindow(player.getCurrentWindowIndex(), window); boolean isSeekable = window.isSeekable; enableSeeking = isSeekable; - enablePrevious = isSeekable || !window.isDynamic || player.hasPrevious(); + enablePrevious = isSeekable || !window.isLive() || player.hasPrevious(); enableRewind = isSeekable && controlDispatcher.isRewindEnabled(); enableFastForward = isSeekable && controlDispatcher.isFastForwardEnabled(); - enableNext = window.isDynamic || player.hasNext(); + enableNext = (window.isLive() && window.isDynamic) || player.hasNext(); } } diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java index e5b29b6a85..af3acc5880 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java @@ -1227,10 +1227,11 @@ public class PlayerNotificationManager { Timeline timeline = player.getCurrentTimeline(); if (!timeline.isEmpty() && !player.isPlayingAd()) { timeline.getWindow(player.getCurrentWindowIndex(), window); - enablePrevious = window.isSeekable || !window.isDynamic || player.hasPrevious(); - enableRewind = controlDispatcher.isRewindEnabled(); - enableFastForward = controlDispatcher.isFastForwardEnabled(); - enableNext = window.isDynamic || player.hasNext(); + boolean isSeekable = window.isSeekable; + enablePrevious = isSeekable || !window.isLive() || player.hasPrevious(); + enableRewind = isSeekable && controlDispatcher.isRewindEnabled(); + enableFastForward = isSeekable && controlDispatcher.isFastForwardEnabled(); + enableNext = (window.isLive() && window.isDynamic) || player.hasNext(); } List stringActions = new ArrayList<>(); diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerControlView.java index 464bb5a477..0035b59399 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerControlView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerControlView.java @@ -1141,10 +1141,10 @@ public class StyledPlayerControlView extends FrameLayout { timeline.getWindow(player.getCurrentWindowIndex(), window); boolean isSeekable = window.isSeekable; enableSeeking = isSeekable; - enablePrevious = isSeekable || !window.isDynamic || player.hasPrevious(); + enablePrevious = isSeekable || !window.isLive() || player.hasPrevious(); enableRewind = isSeekable && controlDispatcher.isRewindEnabled(); enableFastForward = isSeekable && controlDispatcher.isFastForwardEnabled(); - enableNext = window.isDynamic || player.hasNext(); + enableNext = (window.isLive() && window.isDynamic) || player.hasNext(); } } diff --git a/library/ui/src/main/res/layout/exo_styled_player_control_ffwd_button.xml b/library/ui/src/main/res/layout/exo_styled_player_control_ffwd_button.xml index f4f7dd6e91..bf4a0f94f0 100644 --- a/library/ui/src/main/res/layout/exo_styled_player_control_ffwd_button.xml +++ b/library/ui/src/main/res/layout/exo_styled_player_control_ffwd_button.xml @@ -22,7 +22,7 @@ android:addStatesFromChildren="true" style="@style/ExoStyledControls.Button.Center"> -