From 035cb096d9c8a0200014993f6d275d7954401827 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 31 Dec 2019 16:30:35 +0000 Subject: [PATCH 01/44] Mark final field PiperOrigin-RevId: 287669425 --- .../exoplayer2/mediacodec/AsynchronousMediaCodecAdapter.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecAdapter.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecAdapter.java index 0d126ff27f..b5eb8efee3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecAdapter.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecAdapter.java @@ -33,7 +33,7 @@ import com.google.android.exoplayer2.util.Assertions; */ @RequiresApi(21) /* package */ final class AsynchronousMediaCodecAdapter implements MediaCodecAdapter { - private MediaCodecAsyncCallback mediaCodecAsyncCallback; + private final MediaCodecAsyncCallback mediaCodecAsyncCallback; private final Handler handler; private final MediaCodec codec; @Nullable private IllegalStateException internalException; @@ -51,7 +51,7 @@ import com.google.android.exoplayer2.util.Assertions; @VisibleForTesting /* package */ AsynchronousMediaCodecAdapter(MediaCodec codec, Looper looper) { - this.mediaCodecAsyncCallback = new MediaCodecAsyncCallback(); + mediaCodecAsyncCallback = new MediaCodecAsyncCallback(); handler = new Handler(looper); this.codec = codec; this.codec.setCallback(mediaCodecAsyncCallback); From b77717ce91542dfb9ced8e13b1d15d36fc8ca3dd Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 2 Jan 2020 09:59:39 +0000 Subject: [PATCH 02/44] Remove buffer size based adaptation. An experiment with this algorithm didn't show positive results. We can therefore keep the simpler default algorithm. Startblock: is submitted PiperOrigin-RevId: 287807538 --- .../BufferSizeAdaptationBuilder.java | 494 ------------------ .../BufferSizeAdaptiveTrackSelectionTest.java | 248 --------- 2 files changed, 742 deletions(-) delete mode 100644 library/core/src/main/java/com/google/android/exoplayer2/trackselection/BufferSizeAdaptationBuilder.java delete mode 100644 library/core/src/test/java/com/google/android/exoplayer2/trackselection/BufferSizeAdaptiveTrackSelectionTest.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/BufferSizeAdaptationBuilder.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/BufferSizeAdaptationBuilder.java deleted file mode 100644 index b850a08aeb..0000000000 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/BufferSizeAdaptationBuilder.java +++ /dev/null @@ -1,494 +0,0 @@ -/* - * Copyright (C) 2018 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.trackselection; - -import android.util.Pair; -import androidx.annotation.Nullable; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.DefaultLoadControl; -import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.LoadControl; -import com.google.android.exoplayer2.source.TrackGroup; -import com.google.android.exoplayer2.source.chunk.MediaChunk; -import com.google.android.exoplayer2.source.chunk.MediaChunkIterator; -import com.google.android.exoplayer2.trackselection.TrackSelection.Definition; -import com.google.android.exoplayer2.upstream.BandwidthMeter; -import com.google.android.exoplayer2.upstream.DefaultAllocator; -import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.Clock; -import java.util.List; -import org.checkerframework.checker.nullness.compatqual.NullableType; - -/** - * Builder for a {@link TrackSelection.Factory} and {@link LoadControl} that implement buffer size - * based track adaptation. - */ -public final class BufferSizeAdaptationBuilder { - - /** Dynamic filter for formats, which is applied when selecting a new track. */ - public interface DynamicFormatFilter { - - /** Filter which allows all formats. */ - DynamicFormatFilter NO_FILTER = (format, trackBitrate, isInitialSelection) -> true; - - /** - * Called when updating the selected track to determine whether a candidate track is allowed. If - * no format is allowed or eligible, the lowest quality format will be used. - * - * @param format The {@link Format} of the candidate track. - * @param trackBitrate The estimated bitrate of the track. May differ from {@link - * Format#bitrate} if a more accurate estimate of the current track bitrate is available. - * @param isInitialSelection Whether this is for the initial track selection. - */ - boolean isFormatAllowed(Format format, int trackBitrate, boolean isInitialSelection); - } - - /** - * The default minimum duration of media that the player will attempt to ensure is buffered at all - * times, in milliseconds. - */ - public static final int DEFAULT_MIN_BUFFER_MS = 15000; - - /** - * The default maximum duration of media that the player will attempt to buffer, in milliseconds. - */ - public static final int DEFAULT_MAX_BUFFER_MS = 50000; - - /** - * The default duration of media that must be buffered for playback to start or resume following a - * user action such as a seek, in milliseconds. - */ - public static final int DEFAULT_BUFFER_FOR_PLAYBACK_MS = - DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS; - - /** - * The default duration of media that must be buffered for playback to resume after a rebuffer, in - * milliseconds. A rebuffer is defined to be caused by buffer depletion rather than a user action. - */ - public static final int DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS = - DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS; - - /** - * The default offset the current duration of buffered media must deviate from the ideal duration - * of buffered media for the currently selected format, before the selected format is changed. - */ - public static final int DEFAULT_HYSTERESIS_BUFFER_MS = 5000; - - /** - * During start-up phase, the default fraction of the available bandwidth that the selection - * should consider available for use. Setting to a value less than 1 is recommended to account for - * inaccuracies in the bandwidth estimator. - */ - public static final float DEFAULT_START_UP_BANDWIDTH_FRACTION = - AdaptiveTrackSelection.DEFAULT_BANDWIDTH_FRACTION; - - /** - * During start-up phase, the default minimum duration of buffered media required for the selected - * track to switch to one of higher quality based on measured bandwidth. - */ - public static final int DEFAULT_START_UP_MIN_BUFFER_FOR_QUALITY_INCREASE_MS = - AdaptiveTrackSelection.DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS; - - @Nullable private DefaultAllocator allocator; - private Clock clock; - private int minBufferMs; - private int maxBufferMs; - private int bufferForPlaybackMs; - private int bufferForPlaybackAfterRebufferMs; - private int hysteresisBufferMs; - private float startUpBandwidthFraction; - private int startUpMinBufferForQualityIncreaseMs; - private DynamicFormatFilter dynamicFormatFilter; - private boolean buildCalled; - - /** Creates builder with default values. */ - public BufferSizeAdaptationBuilder() { - clock = Clock.DEFAULT; - minBufferMs = DEFAULT_MIN_BUFFER_MS; - maxBufferMs = DEFAULT_MAX_BUFFER_MS; - bufferForPlaybackMs = DEFAULT_BUFFER_FOR_PLAYBACK_MS; - bufferForPlaybackAfterRebufferMs = DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS; - hysteresisBufferMs = DEFAULT_HYSTERESIS_BUFFER_MS; - startUpBandwidthFraction = DEFAULT_START_UP_BANDWIDTH_FRACTION; - startUpMinBufferForQualityIncreaseMs = DEFAULT_START_UP_MIN_BUFFER_FOR_QUALITY_INCREASE_MS; - dynamicFormatFilter = DynamicFormatFilter.NO_FILTER; - } - - /** - * Set the clock to use. Should only be set for testing purposes. - * - * @param clock The {@link Clock}. - * @return This builder, for convenience. - * @throws IllegalStateException If {@link #buildPlayerComponents()} has already been called. - */ - public BufferSizeAdaptationBuilder setClock(Clock clock) { - Assertions.checkState(!buildCalled); - this.clock = clock; - return this; - } - - /** - * Sets the {@link DefaultAllocator} used by the loader. - * - * @param allocator The {@link DefaultAllocator}. - * @return This builder, for convenience. - * @throws IllegalStateException If {@link #buildPlayerComponents()} has already been called. - */ - public BufferSizeAdaptationBuilder setAllocator(DefaultAllocator allocator) { - Assertions.checkState(!buildCalled); - this.allocator = allocator; - return this; - } - - /** - * Sets the buffer duration parameters. - * - * @param minBufferMs The minimum duration of media that the player will attempt to ensure is - * buffered at all times, in milliseconds. - * @param maxBufferMs The maximum duration of media that the player will attempt to buffer, in - * milliseconds. - * @param bufferForPlaybackMs The duration of media that must be buffered for playback to start or - * resume following a user action such as a seek, in milliseconds. - * @param bufferForPlaybackAfterRebufferMs The default duration of media that must be buffered for - * playback to resume after a rebuffer, in milliseconds. A rebuffer is defined to be caused by - * buffer depletion rather than a user action. - * @return This builder, for convenience. - * @throws IllegalStateException If {@link #buildPlayerComponents()} has already been called. - */ - public BufferSizeAdaptationBuilder setBufferDurationsMs( - int minBufferMs, - int maxBufferMs, - int bufferForPlaybackMs, - int bufferForPlaybackAfterRebufferMs) { - Assertions.checkState(!buildCalled); - this.minBufferMs = minBufferMs; - this.maxBufferMs = maxBufferMs; - this.bufferForPlaybackMs = bufferForPlaybackMs; - this.bufferForPlaybackAfterRebufferMs = bufferForPlaybackAfterRebufferMs; - return this; - } - - /** - * Sets the hysteresis buffer used to prevent repeated format switching. - * - * @param hysteresisBufferMs The offset the current duration of buffered media must deviate from - * the ideal duration of buffered media for the currently selected format, before the selected - * format is changed. This value must be smaller than {@code maxBufferMs - minBufferMs}. - * @return This builder, for convenience. - * @throws IllegalStateException If {@link #buildPlayerComponents()} has already been called. - */ - public BufferSizeAdaptationBuilder setHysteresisBufferMs(int hysteresisBufferMs) { - Assertions.checkState(!buildCalled); - this.hysteresisBufferMs = hysteresisBufferMs; - return this; - } - - /** - * Sets track selection parameters used during the start-up phase before the selection can be made - * purely on based on buffer size. During the start-up phase the selection is based on the current - * bandwidth estimate. - * - * @param bandwidthFraction The fraction of the available bandwidth that the selection should - * consider available for use. Setting to a value less than 1 is recommended to account for - * inaccuracies in the bandwidth estimator. - * @param minBufferForQualityIncreaseMs The minimum duration of buffered media required for the - * selected track to switch to one of higher quality. - * @return This builder, for convenience. - * @throws IllegalStateException If {@link #buildPlayerComponents()} has already been called. - */ - public BufferSizeAdaptationBuilder setStartUpTrackSelectionParameters( - float bandwidthFraction, int minBufferForQualityIncreaseMs) { - Assertions.checkState(!buildCalled); - this.startUpBandwidthFraction = bandwidthFraction; - this.startUpMinBufferForQualityIncreaseMs = minBufferForQualityIncreaseMs; - return this; - } - - /** - * Sets the {@link DynamicFormatFilter} to use when updating the selected track. - * - * @param dynamicFormatFilter The {@link DynamicFormatFilter}. - * @return This builder, for convenience. - * @throws IllegalStateException If {@link #buildPlayerComponents()} has already been called. - */ - public BufferSizeAdaptationBuilder setDynamicFormatFilter( - DynamicFormatFilter dynamicFormatFilter) { - Assertions.checkState(!buildCalled); - this.dynamicFormatFilter = dynamicFormatFilter; - return this; - } - - /** - * Builds player components for buffer size based track adaptation. - * - * @return A pair of a {@link TrackSelection.Factory} and a {@link LoadControl}, which should be - * used to construct the player. - */ - public Pair buildPlayerComponents() { - Assertions.checkArgument(hysteresisBufferMs < maxBufferMs - minBufferMs); - Assertions.checkState(!buildCalled); - buildCalled = true; - - DefaultLoadControl.Builder loadControlBuilder = - new DefaultLoadControl.Builder() - .setTargetBufferBytes(/* targetBufferBytes = */ Integer.MAX_VALUE) - .setBufferDurationsMs( - /* minBufferMs= */ maxBufferMs, - maxBufferMs, - bufferForPlaybackMs, - bufferForPlaybackAfterRebufferMs); - if (allocator != null) { - loadControlBuilder.setAllocator(allocator); - } - - TrackSelection.Factory trackSelectionFactory = - new TrackSelection.Factory() { - @Override - public @NullableType TrackSelection[] createTrackSelections( - @NullableType Definition[] definitions, BandwidthMeter bandwidthMeter) { - return TrackSelectionUtil.createTrackSelectionsForDefinitions( - definitions, - definition -> - new BufferSizeAdaptiveTrackSelection( - definition.group, - definition.tracks, - bandwidthMeter, - minBufferMs, - maxBufferMs, - hysteresisBufferMs, - startUpBandwidthFraction, - startUpMinBufferForQualityIncreaseMs, - dynamicFormatFilter, - clock)); - } - }; - - return Pair.create(trackSelectionFactory, loadControlBuilder.createDefaultLoadControl()); - } - - private static final class BufferSizeAdaptiveTrackSelection extends BaseTrackSelection { - - private static final int BITRATE_BLACKLISTED = Format.NO_VALUE; - - private final BandwidthMeter bandwidthMeter; - private final Clock clock; - private final DynamicFormatFilter dynamicFormatFilter; - private final int[] formatBitrates; - private final long minBufferUs; - private final long maxBufferUs; - private final long hysteresisBufferUs; - private final float startUpBandwidthFraction; - private final long startUpMinBufferForQualityIncreaseUs; - private final int minBitrate; - private final int maxBitrate; - private final double bitrateToBufferFunctionSlope; - private final double bitrateToBufferFunctionIntercept; - - private boolean isInSteadyState; - private int selectedIndex; - private int selectionReason; - private float playbackSpeed; - - private BufferSizeAdaptiveTrackSelection( - TrackGroup trackGroup, - int[] tracks, - BandwidthMeter bandwidthMeter, - int minBufferMs, - int maxBufferMs, - int hysteresisBufferMs, - float startUpBandwidthFraction, - int startUpMinBufferForQualityIncreaseMs, - DynamicFormatFilter dynamicFormatFilter, - Clock clock) { - super(trackGroup, tracks); - this.bandwidthMeter = bandwidthMeter; - this.minBufferUs = C.msToUs(minBufferMs); - this.maxBufferUs = C.msToUs(maxBufferMs); - this.hysteresisBufferUs = C.msToUs(hysteresisBufferMs); - this.startUpBandwidthFraction = startUpBandwidthFraction; - this.startUpMinBufferForQualityIncreaseUs = C.msToUs(startUpMinBufferForQualityIncreaseMs); - this.dynamicFormatFilter = dynamicFormatFilter; - this.clock = clock; - - formatBitrates = new int[length]; - maxBitrate = getFormat(/* index= */ 0).bitrate; - minBitrate = getFormat(/* index= */ length - 1).bitrate; - selectionReason = C.SELECTION_REASON_UNKNOWN; - playbackSpeed = 1.0f; - - // We use a log-linear function to map from bitrate to buffer size: - // buffer = slope * ln(bitrate) + intercept, - // with buffer(minBitrate) = minBuffer and buffer(maxBitrate) = maxBuffer - hysteresisBuffer. - bitrateToBufferFunctionSlope = - (maxBufferUs - hysteresisBufferUs - minBufferUs) - / Math.log((double) maxBitrate / minBitrate); - bitrateToBufferFunctionIntercept = - minBufferUs - bitrateToBufferFunctionSlope * Math.log(minBitrate); - } - - @Override - public void onPlaybackSpeed(float playbackSpeed) { - this.playbackSpeed = playbackSpeed; - } - - @Override - public void onDiscontinuity() { - isInSteadyState = false; - } - - @Override - public int getSelectedIndex() { - return selectedIndex; - } - - @Override - public int getSelectionReason() { - return selectionReason; - } - - @Override - @Nullable - public Object getSelectionData() { - return null; - } - - @Override - public void updateSelectedTrack( - long playbackPositionUs, - long bufferedDurationUs, - long availableDurationUs, - List queue, - MediaChunkIterator[] mediaChunkIterators) { - updateFormatBitrates(/* nowMs= */ clock.elapsedRealtime()); - - // Make initial selection - if (selectionReason == C.SELECTION_REASON_UNKNOWN) { - selectionReason = C.SELECTION_REASON_INITIAL; - selectedIndex = selectIdealIndexUsingBandwidth(/* isInitialSelection= */ true); - return; - } - - long bufferUs = getCurrentPeriodBufferedDurationUs(playbackPositionUs, bufferedDurationUs); - int oldSelectedIndex = selectedIndex; - if (isInSteadyState) { - selectIndexSteadyState(bufferUs); - } else { - selectIndexStartUpPhase(bufferUs); - } - if (selectedIndex != oldSelectedIndex) { - selectionReason = C.SELECTION_REASON_ADAPTIVE; - } - } - - // Steady state. - - private void selectIndexSteadyState(long bufferUs) { - if (isOutsideHysteresis(bufferUs)) { - selectedIndex = selectIdealIndexUsingBufferSize(bufferUs); - } - } - - private boolean isOutsideHysteresis(long bufferUs) { - if (formatBitrates[selectedIndex] == BITRATE_BLACKLISTED) { - return true; - } - long targetBufferForCurrentBitrateUs = - getTargetBufferForBitrateUs(formatBitrates[selectedIndex]); - long bufferDiffUs = bufferUs - targetBufferForCurrentBitrateUs; - return Math.abs(bufferDiffUs) > hysteresisBufferUs; - } - - private int selectIdealIndexUsingBufferSize(long bufferUs) { - int lowestBitrateNonBlacklistedIndex = 0; - for (int i = 0; i < formatBitrates.length; i++) { - if (formatBitrates[i] != BITRATE_BLACKLISTED) { - if (getTargetBufferForBitrateUs(formatBitrates[i]) <= bufferUs - && dynamicFormatFilter.isFormatAllowed( - getFormat(i), formatBitrates[i], /* isInitialSelection= */ false)) { - return i; - } - lowestBitrateNonBlacklistedIndex = i; - } - } - return lowestBitrateNonBlacklistedIndex; - } - - // Startup. - - private void selectIndexStartUpPhase(long bufferUs) { - int startUpSelectedIndex = selectIdealIndexUsingBandwidth(/* isInitialSelection= */ false); - int steadyStateSelectedIndex = selectIdealIndexUsingBufferSize(bufferUs); - if (steadyStateSelectedIndex <= selectedIndex) { - // Switch to steady state if we have enough buffer to maintain current selection. - selectedIndex = steadyStateSelectedIndex; - isInSteadyState = true; - } else { - if (bufferUs < startUpMinBufferForQualityIncreaseUs - && startUpSelectedIndex < selectedIndex - && formatBitrates[selectedIndex] != BITRATE_BLACKLISTED) { - // Switching up from a non-blacklisted track is only allowed if we have enough buffer. - return; - } - selectedIndex = startUpSelectedIndex; - } - } - - private int selectIdealIndexUsingBandwidth(boolean isInitialSelection) { - long effectiveBitrate = - (long) (bandwidthMeter.getBitrateEstimate() * startUpBandwidthFraction); - int lowestBitrateNonBlacklistedIndex = 0; - for (int i = 0; i < formatBitrates.length; i++) { - if (formatBitrates[i] != BITRATE_BLACKLISTED) { - if (Math.round(formatBitrates[i] * playbackSpeed) <= effectiveBitrate - && dynamicFormatFilter.isFormatAllowed( - getFormat(i), formatBitrates[i], isInitialSelection)) { - return i; - } - lowestBitrateNonBlacklistedIndex = i; - } - } - return lowestBitrateNonBlacklistedIndex; - } - - // Utility methods. - - private void updateFormatBitrates(long nowMs) { - for (int i = 0; i < length; i++) { - if (nowMs == Long.MIN_VALUE || !isBlacklisted(i, nowMs)) { - formatBitrates[i] = getFormat(i).bitrate; - } else { - formatBitrates[i] = BITRATE_BLACKLISTED; - } - } - } - - private long getTargetBufferForBitrateUs(int bitrate) { - if (bitrate <= minBitrate) { - return minBufferUs; - } - if (bitrate >= maxBitrate) { - return maxBufferUs - hysteresisBufferUs; - } - return (int) - (bitrateToBufferFunctionSlope * Math.log(bitrate) + bitrateToBufferFunctionIntercept); - } - - private static long getCurrentPeriodBufferedDurationUs( - long playbackPositionUs, long bufferedDurationUs) { - return playbackPositionUs >= 0 ? bufferedDurationUs : playbackPositionUs + bufferedDurationUs; - } - } -} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/BufferSizeAdaptiveTrackSelectionTest.java b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/BufferSizeAdaptiveTrackSelectionTest.java deleted file mode 100644 index 8b20630a23..0000000000 --- a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/BufferSizeAdaptiveTrackSelectionTest.java +++ /dev/null @@ -1,248 +0,0 @@ -/* - * Copyright (C) 2019 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.trackselection; - -import static com.google.common.truth.Truth.assertThat; -import static org.mockito.Mockito.when; -import static org.mockito.MockitoAnnotations.initMocks; - -import android.util.Pair; -import androidx.test.ext.junit.runners.AndroidJUnit4; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.LoadControl; -import com.google.android.exoplayer2.source.TrackGroup; -import com.google.android.exoplayer2.source.chunk.MediaChunkIterator; -import com.google.android.exoplayer2.upstream.BandwidthMeter; -import com.google.android.exoplayer2.util.MimeTypes; -import java.util.Collections; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; - -/** Unit test for the track selection created by {@link BufferSizeAdaptationBuilder}. */ -@RunWith(AndroidJUnit4.class) -public final class BufferSizeAdaptiveTrackSelectionTest { - - private static final int MIN_BUFFER_MS = 15_000; - private static final int MAX_BUFFER_MS = 50_000; - private static final int HYSTERESIS_BUFFER_MS = 10_000; - private static final float BANDWIDTH_FRACTION = 0.5f; - private static final int MIN_BUFFER_FOR_QUALITY_INCREASE_MS = 10_000; - - /** - * Factor between bitrates is always the same (=2.2). That means buffer levels should be linearly - * distributed between MIN_BUFFER=15s and MAX_BUFFER-HYSTERESIS=50s-10s=40s. - */ - private static final Format format1 = - createVideoFormat(/* bitrate= */ 500, /* width= */ 320, /* height= */ 240); - - private static final Format format2 = - createVideoFormat(/* bitrate= */ 1100, /* width= */ 640, /* height= */ 480); - private static final Format format3 = - createVideoFormat(/* bitrate= */ 2420, /* width= */ 960, /* height= */ 720); - private static final int BUFFER_LEVEL_FORMAT_2 = - (MIN_BUFFER_MS + MAX_BUFFER_MS - HYSTERESIS_BUFFER_MS) / 2; - private static final int BUFFER_LEVEL_FORMAT_3 = MAX_BUFFER_MS - HYSTERESIS_BUFFER_MS; - - @Mock private BandwidthMeter mockBandwidthMeter; - private TrackSelection trackSelection; - - @Before - public void setUp() { - initMocks(this); - Pair trackSelectionFactoryAndLoadControl = - new BufferSizeAdaptationBuilder() - .setBufferDurationsMs( - MIN_BUFFER_MS, - MAX_BUFFER_MS, - /* bufferForPlaybackMs= */ 1000, - /* bufferForPlaybackAfterRebufferMs= */ 1000) - .setHysteresisBufferMs(HYSTERESIS_BUFFER_MS) - .setStartUpTrackSelectionParameters( - BANDWIDTH_FRACTION, MIN_BUFFER_FOR_QUALITY_INCREASE_MS) - .buildPlayerComponents(); - trackSelection = - trackSelectionFactoryAndLoadControl - .first - .createTrackSelections( - new TrackSelection.Definition[] { - new TrackSelection.Definition( - new TrackGroup(format1, format2, format3), /* tracks= */ 0, 1, 2) - }, - mockBandwidthMeter)[0]; - trackSelection.enable(); - } - - @Test - public void updateSelectedTrack_usesBandwidthEstimateForInitialSelection() { - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(getBitrateEstimateEnoughFor(format2)); - - updateSelectedTrack(/* bufferedDurationMs= */ 0); - - assertThat(trackSelection.getSelectedFormat()).isEqualTo(format2); - assertThat(trackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_INITIAL); - } - - @Test - public void updateSelectedTrack_withLowerBandwidthEstimateDuringStartUp_switchesDown() { - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(getBitrateEstimateEnoughFor(format2)); - updateSelectedTrack(/* bufferedDurationMs= */ 0); - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(0L); - - updateSelectedTrack(/* bufferedDurationMs= */ 0); - - assertThat(trackSelection.getSelectedFormat()).isEqualTo(format1); - assertThat(trackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_ADAPTIVE); - } - - @Test - public void - updateSelectedTrack_withHigherBandwidthEstimateDuringStartUp_andLowBuffer_keepsSelection() { - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(getBitrateEstimateEnoughFor(format2)); - updateSelectedTrack(/* bufferedDurationMs= */ 0); - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(getBitrateEstimateEnoughFor(format3)); - - updateSelectedTrack(/* bufferedDurationMs= */ MIN_BUFFER_FOR_QUALITY_INCREASE_MS - 1); - - assertThat(trackSelection.getSelectedFormat()).isEqualTo(format2); - assertThat(trackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_INITIAL); - } - - @Test - public void - updateSelectedTrack_withHigherBandwidthEstimateDuringStartUp_andHighBuffer_switchesUp() { - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(getBitrateEstimateEnoughFor(format2)); - updateSelectedTrack(/* bufferedDurationMs= */ 0); - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(getBitrateEstimateEnoughFor(format3)); - - updateSelectedTrack(/* bufferedDurationMs= */ MIN_BUFFER_FOR_QUALITY_INCREASE_MS); - - assertThat(trackSelection.getSelectedFormat()).isEqualTo(format3); - assertThat(trackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_ADAPTIVE); - } - - @Test - public void - updateSelectedTrack_withIncreasedBandwidthEstimate_onceSteadyStateBufferIsReached_keepsSelection() { - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(getBitrateEstimateEnoughFor(format2)); - updateSelectedTrack(/* bufferedDurationMs= */ 0); - updateSelectedTrack(/* bufferedDurationMs= */ BUFFER_LEVEL_FORMAT_2); - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(getBitrateEstimateEnoughFor(format3)); - - updateSelectedTrack(/* bufferedDurationMs= */ BUFFER_LEVEL_FORMAT_2); - - assertThat(trackSelection.getSelectedFormat()).isEqualTo(format2); - assertThat(trackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_INITIAL); - } - - @Test - public void - updateSelectedTrack_withDecreasedBandwidthEstimate_onceSteadyStateBufferIsReached_keepsSelection() { - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(getBitrateEstimateEnoughFor(format2)); - updateSelectedTrack(/* bufferedDurationMs= */ 0); - updateSelectedTrack(/* bufferedDurationMs= */ BUFFER_LEVEL_FORMAT_2); - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(0L); - - updateSelectedTrack(/* bufferedDurationMs= */ BUFFER_LEVEL_FORMAT_2); - - assertThat(trackSelection.getSelectedFormat()).isEqualTo(format2); - assertThat(trackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_INITIAL); - } - - @Test - public void updateSelectedTrack_withIncreasedBufferInSteadyState_switchesUp() { - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(getBitrateEstimateEnoughFor(format2)); - updateSelectedTrack(/* bufferedDurationMs= */ 0); - updateSelectedTrack(/* bufferedDurationMs= */ BUFFER_LEVEL_FORMAT_2); - - updateSelectedTrack(/* bufferedDurationMs= */ BUFFER_LEVEL_FORMAT_3); - - assertThat(trackSelection.getSelectedFormat()).isEqualTo(format3); - assertThat(trackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_ADAPTIVE); - } - - @Test - public void updateSelectedTrack_withDecreasedBufferInSteadyState_switchesDown() { - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(getBitrateEstimateEnoughFor(format2)); - updateSelectedTrack(/* bufferedDurationMs= */ 0); - updateSelectedTrack(/* bufferedDurationMs= */ BUFFER_LEVEL_FORMAT_2); - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(0L); - - updateSelectedTrack(/* bufferedDurationMs= */ BUFFER_LEVEL_FORMAT_2 - HYSTERESIS_BUFFER_MS - 1); - - assertThat(trackSelection.getSelectedFormat()).isEqualTo(format1); - assertThat(trackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_ADAPTIVE); - } - - @Test - public void - updateSelectedTrack_withDecreasedBufferInSteadyState_withinHysteresis_keepsSelection() { - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(getBitrateEstimateEnoughFor(format2)); - updateSelectedTrack(/* bufferedDurationMs= */ 0); - updateSelectedTrack(/* bufferedDurationMs= */ BUFFER_LEVEL_FORMAT_2); - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(0L); - - updateSelectedTrack(/* bufferedDurationMs= */ BUFFER_LEVEL_FORMAT_2 - HYSTERESIS_BUFFER_MS); - - assertThat(trackSelection.getSelectedFormat()).isEqualTo(format2); - assertThat(trackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_INITIAL); - } - - @Test - public void onDiscontinuity_switchesBackToStartUpState() { - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(getBitrateEstimateEnoughFor(format2)); - updateSelectedTrack(/* bufferedDurationMs= */ 0); - updateSelectedTrack(/* bufferedDurationMs= */ BUFFER_LEVEL_FORMAT_2); - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(0L); - - trackSelection.onDiscontinuity(); - updateSelectedTrack(/* bufferedDurationMs= */ BUFFER_LEVEL_FORMAT_2 - 1); - - assertThat(trackSelection.getSelectedFormat()).isEqualTo(format1); - assertThat(trackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_ADAPTIVE); - } - - private void updateSelectedTrack(long bufferedDurationMs) { - trackSelection.updateSelectedTrack( - /* playbackPositionUs= */ 0, - /* bufferedDurationUs= */ C.msToUs(bufferedDurationMs), - /* availableDurationUs= */ C.TIME_UNSET, - /* queue= */ Collections.emptyList(), - /* mediaChunkIterators= */ new MediaChunkIterator[] { - MediaChunkIterator.EMPTY, MediaChunkIterator.EMPTY, MediaChunkIterator.EMPTY - }); - } - - private static Format createVideoFormat(int bitrate, int width, int height) { - return Format.createVideoSampleFormat( - /* id= */ null, - /* sampleMimeType= */ MimeTypes.VIDEO_H264, - /* codecs= */ null, - /* bitrate= */ bitrate, - /* maxInputSize= */ Format.NO_VALUE, - /* width= */ width, - /* height= */ height, - /* frameRate= */ Format.NO_VALUE, - /* initializationData= */ null, - /* drmInitData= */ null); - } - - private static long getBitrateEstimateEnoughFor(Format format) { - return (long) (format.bitrate / BANDWIDTH_FRACTION) + 1; - } -} From fefb1a17a0023e7c22ecf78ad63796f0e4668be3 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 2 Jan 2020 10:29:16 +0000 Subject: [PATCH 03/44] Fix typos PiperOrigin-RevId: 287810018 --- extensions/okhttp/build.gradle | 4 ++-- .../com/google/android/exoplayer2/scheduler/Requirements.java | 2 +- .../android/exoplayer2/upstream/cache/SimpleCacheTest.java | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/extensions/okhttp/build.gradle b/extensions/okhttp/build.gradle index 3af38397a8..bde4e127df 100644 --- a/extensions/okhttp/build.gradle +++ b/extensions/okhttp/build.gradle @@ -39,8 +39,8 @@ dependencies { testImplementation 'org.robolectric:robolectric:' + robolectricVersion // Do not update to 3.13.X or later until minSdkVersion is increased to 21: // https://cashapp.github.io/2019-02-05/okhttp-3-13-requires-android-5 - // Since OkHttp is distributed as a jar rather than an aar, Gradle wont stop - // us from making this mistake! + // Since OkHttp is distributed as a jar rather than an aar, Gradle won't + // stop us from making this mistake! api 'com.squareup.okhttp3:okhttp:3.12.5' } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Requirements.java b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Requirements.java index 87ea60bf73..8919a26720 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Requirements.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Requirements.java @@ -165,7 +165,7 @@ public final class Requirements implements Parcelable { private static boolean isInternetConnectivityValidated(ConnectivityManager connectivityManager) { // It's possible to query NetworkCapabilities from API level 23, but RequirementsWatcher only // fires an event to update its Requirements when NetworkCapabilities change from API level 24. - // Since Requirements wont be updated, we assume connectivity is validated on API level 23. + // Since Requirements won't be updated, we assume connectivity is validated on API level 23. if (Util.SDK_INT < 24) { return true; } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java index a8dbfe3b42..4d9a936c4e 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java @@ -204,7 +204,7 @@ public class SimpleCacheTest { simpleCache.releaseHoleSpan(cacheSpan2); simpleCache.removeSpan(simpleCache.getCachedSpans(KEY_2).first()); - // Don't release the cache. This means the index file wont have been written to disk after the + // Don't release the cache. This means the index file won't have been written to disk after the // data for KEY_2 was removed. Move the cache instead, so we can reload it without failing the // folder locking check. File cacheDir2 = From 77f01da66089bb2a580c0810c9c6016ac593ed2b Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 2 Jan 2020 11:37:03 +0000 Subject: [PATCH 04/44] Don't use WavHeader.averageBytesPerSecond It's unreliable for at least one IMA ADPCM file I've found. Calculating the blockIndex to seek to using exact properties also seems more robust. Note this doesn't change anything for the existing PCM test, since averageBytesPerSecond is set correctly. It does make a difference for an upcoming IMA ADPCM test though. PiperOrigin-RevId: 287814947 --- .../android/exoplayer2/extractor/wav/WavExtractor.java | 2 +- .../android/exoplayer2/extractor/wav/WavSeekMap.java | 10 ++++------ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java index 37edb07a1a..c1eb357bb9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java @@ -203,7 +203,7 @@ public final class WavExtractor implements Extractor { /* id= */ null, MimeTypes.AUDIO_RAW, /* codecs= */ null, - /* bitrate= */ header.averageBytesPerSecond * 8, + /* bitrate= */ header.frameRateHz * bytesPerFrame * 8, targetSampleSize, header.numChannels, header.frameRateHz, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavSeekMap.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavSeekMap.java index 53e0f45306..2a92c38431 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavSeekMap.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavSeekMap.java @@ -49,20 +49,18 @@ import com.google.android.exoplayer2.util.Util; @Override public SeekPoints getSeekPoints(long timeUs) { - // Calculate the expected number of bytes of sample data corresponding to the requested time. - long positionOffset = (timeUs * wavHeader.averageBytesPerSecond) / C.MICROS_PER_SECOND; // Calculate the containing block index, constraining to valid indices. - long blockSize = wavHeader.blockSize; - long blockIndex = Util.constrainValue(positionOffset / blockSize, 0, blockCount - 1); + long blockIndex = (timeUs * wavHeader.frameRateHz) / (C.MICROS_PER_SECOND * framesPerBlock); + blockIndex = Util.constrainValue(blockIndex, 0, blockCount - 1); - long seekPosition = firstBlockPosition + (blockIndex * blockSize); + long seekPosition = firstBlockPosition + (blockIndex * wavHeader.blockSize); long seekTimeUs = blockIndexToTimeUs(blockIndex); SeekPoint seekPoint = new SeekPoint(seekTimeUs, seekPosition); if (seekTimeUs >= timeUs || blockIndex == blockCount - 1) { return new SeekPoints(seekPoint); } else { long secondBlockIndex = blockIndex + 1; - long secondSeekPosition = firstBlockPosition + (secondBlockIndex * blockSize); + long secondSeekPosition = firstBlockPosition + (secondBlockIndex * wavHeader.blockSize); long secondSeekTimeUs = blockIndexToTimeUs(secondBlockIndex); SeekPoint secondSeekPoint = new SeekPoint(secondSeekTimeUs, secondSeekPosition); return new SeekPoints(seekPoint, secondSeekPoint); From cafffcb812726d662b097a1f7bd31a69799669c0 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 2 Jan 2020 12:02:41 +0000 Subject: [PATCH 05/44] Fix handling of E-AC-3 streams with AC-3 frames Issue: #6602 PiperOrigin-RevId: 287816831 --- RELEASENOTES.md | 2 + .../android/exoplayer2/audio/Ac3Util.java | 73 +++++++++---------- .../exoplayer2/audio/DefaultAudioSink.java | 3 +- 3 files changed, 39 insertions(+), 39 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index d3f7cf8067..1e736dd0da 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -32,6 +32,8 @@ ([#6733](https://github.com/google/ExoPlayer/issues/6733)). Incorrect handling could previously cause downloads to be paused when they should have been able to proceed. +* Fix handling of E-AC-3 streams that contain AC-3 syncframes + ([#6602](https://github.com/google/ExoPlayer/issues/6602)). ### 2.11.1 (2019-12-20) ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java index 05c20939ff..066c9f88ef 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java @@ -31,7 +31,7 @@ import java.nio.ByteBuffer; /** * Utility methods for parsing Dolby TrueHD and (E-)AC-3 syncframes. (E-)AC-3 parsing follows the - * definition in ETSI TS 102 366 V1.2.1. + * definition in ETSI TS 102 366 V1.4.1. */ public final class Ac3Util { @@ -39,8 +39,8 @@ public final class Ac3Util { public static final class SyncFrameInfo { /** - * AC3 stream types. See also ETSI TS 102 366 E.1.3.1.1. One of {@link #STREAM_TYPE_UNDEFINED}, - * {@link #STREAM_TYPE_TYPE0}, {@link #STREAM_TYPE_TYPE1} or {@link #STREAM_TYPE_TYPE2}. + * AC3 stream types. See also E.1.3.1.1. One of {@link #STREAM_TYPE_UNDEFINED}, {@link + * #STREAM_TYPE_TYPE0}, {@link #STREAM_TYPE_TYPE1} or {@link #STREAM_TYPE_TYPE2}. */ @Documented @Retention(RetentionPolicy.SOURCE) @@ -114,9 +114,7 @@ public final class Ac3Util { * The number of new samples per (E-)AC-3 audio block. */ private static final int AUDIO_SAMPLES_PER_AUDIO_BLOCK = 256; - /** - * Each syncframe has 6 blocks that provide 256 new audio samples. See ETSI TS 102 366 4.1. - */ + /** Each syncframe has 6 blocks that provide 256 new audio samples. See subsection 4.1. */ private static final int AC3_SYNCFRAME_AUDIO_SAMPLE_COUNT = 6 * AUDIO_SAMPLES_PER_AUDIO_BLOCK; /** * Number of audio blocks per E-AC-3 syncframe, indexed by numblkscod. @@ -134,20 +132,21 @@ public final class Ac3Util { * Channel counts, indexed by acmod. */ private static final int[] CHANNEL_COUNT_BY_ACMOD = new int[] {2, 1, 2, 3, 3, 4, 4, 5}; - /** - * Nominal bitrates in kbps, indexed by frmsizecod / 2. (See ETSI TS 102 366 table 4.13.) - */ - private static final int[] BITRATE_BY_HALF_FRMSIZECOD = new int[] {32, 40, 48, 56, 64, 80, 96, - 112, 128, 160, 192, 224, 256, 320, 384, 448, 512, 576, 640}; - /** - * 16-bit words per syncframe, indexed by frmsizecod / 2. (See ETSI TS 102 366 table 4.13.) - */ - private static final int[] SYNCFRAME_SIZE_WORDS_BY_HALF_FRMSIZECOD_44_1 = new int[] {69, 87, 104, - 121, 139, 174, 208, 243, 278, 348, 417, 487, 557, 696, 835, 975, 1114, 1253, 1393}; + /** Nominal bitrates in kbps, indexed by frmsizecod / 2. (See table 4.13.) */ + private static final int[] BITRATE_BY_HALF_FRMSIZECOD = + new int[] { + 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, 448, 512, 576, 640 + }; + /** 16-bit words per syncframe, indexed by frmsizecod / 2. (See table 4.13.) */ + private static final int[] SYNCFRAME_SIZE_WORDS_BY_HALF_FRMSIZECOD_44_1 = + new int[] { + 69, 87, 104, 121, 139, 174, 208, 243, 278, 348, 417, 487, 557, 696, 835, 975, 1114, 1253, + 1393 + }; /** - * Returns the AC-3 format given {@code data} containing the AC3SpecificBox according to ETSI TS - * 102 366 Annex F. The reading position of {@code data} will be modified. + * Returns the AC-3 format given {@code data} containing the AC3SpecificBox according to Annex F. + * The reading position of {@code data} will be modified. * * @param data The AC3SpecificBox to parse. * @param trackId The track identifier to set on the format. @@ -179,8 +178,8 @@ public final class Ac3Util { } /** - * Returns the E-AC-3 format given {@code data} containing the EC3SpecificBox according to ETSI TS - * 102 366 Annex F. The reading position of {@code data} will be modified. + * Returns the E-AC-3 format given {@code data} containing the EC3SpecificBox according to Annex + * F. The reading position of {@code data} will be modified. * * @param data The EC3SpecificBox to parse. * @param trackId The track identifier to set on the format. @@ -243,9 +242,10 @@ public final class Ac3Util { public static SyncFrameInfo parseAc3SyncframeInfo(ParsableBitArray data) { int initialPosition = data.getPosition(); data.skipBits(40); - boolean isEac3 = data.readBits(5) == 16; // See bsid in subsection E.1.3.1.6. + // Parse the bitstream ID for AC-3 and E-AC-3 (see subsections 4.3, E.1.2 and E.1.3.1.6). + boolean isEac3 = data.readBits(5) > 10; data.setPosition(initialPosition); - String mimeType; + @Nullable String mimeType; @StreamType int streamType = SyncFrameInfo.STREAM_TYPE_UNDEFINED; int sampleRate; int acmod; @@ -254,7 +254,7 @@ public final class Ac3Util { boolean lfeon; int channelCount; if (isEac3) { - // Syntax from ETSI TS 102 366 V1.2.1 subsections E.1.2.1 and E.1.2.2. + // Subsection E.1.2. data.skipBits(16); // syncword switch (data.readBits(2)) { // strmtyp case 0: @@ -472,7 +472,8 @@ public final class Ac3Util { if (data.length < 6) { return C.LENGTH_UNSET; } - boolean isEac3 = ((data[5] & 0xFF) >> 3) == 16; // See bsid in subsection E.1.3.1.6. + // Parse the bitstream ID for AC-3 and E-AC-3 (see subsections 4.3, E.1.2 and E.1.3.1.6). + boolean isEac3 = ((data[5] & 0xF8) >> 3) > 10; if (isEac3) { int frmsiz = (data[2] & 0x07) << 8; // Most significant 3 bits. frmsiz |= data[3] & 0xFF; // Least significant 8 bits. @@ -485,24 +486,22 @@ public final class Ac3Util { } /** - * Returns the number of audio samples in an AC-3 syncframe. - */ - public static int getAc3SyncframeAudioSampleCount() { - return AC3_SYNCFRAME_AUDIO_SAMPLE_COUNT; - } - - /** - * Reads the number of audio samples represented by the given E-AC-3 syncframe. The buffer's + * Reads the number of audio samples represented by the given (E-)AC-3 syncframe. The buffer's * position is not modified. * * @param buffer The {@link ByteBuffer} from which to read the syncframe. * @return The number of audio samples represented by the syncframe. */ - public static int parseEAc3SyncframeAudioSampleCount(ByteBuffer buffer) { - // See ETSI TS 102 366 subsection E.1.2.2. - int fscod = (buffer.get(buffer.position() + 4) & 0xC0) >> 6; - return AUDIO_SAMPLES_PER_AUDIO_BLOCK * (fscod == 0x03 ? 6 - : BLOCKS_PER_SYNCFRAME_BY_NUMBLKSCOD[(buffer.get(buffer.position() + 4) & 0x30) >> 4]); + public static int parseAc3SyncframeAudioSampleCount(ByteBuffer buffer) { + // Parse the bitstream ID for AC-3 and E-AC-3 (see subsections 4.3, E.1.2 and E.1.3.1.6). + boolean isEac3 = ((buffer.get(buffer.position() + 5) & 0xF8) >> 3) > 10; + if (isEac3) { + int fscod = (buffer.get(buffer.position() + 4) & 0xC0) >> 6; + int numblkscod = fscod == 0x03 ? 3 : (buffer.get(buffer.position() + 4) & 0x30) >> 4; + return BLOCKS_PER_SYNCFRAME_BY_NUMBLKSCOD[numblkscod] * AUDIO_SAMPLES_PER_AUDIO_BLOCK; + } else { + return AC3_SYNCFRAME_AUDIO_SAMPLE_COUNT; + } } /** 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 240a8554b7..d73cf0be40 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 @@ -1166,10 +1166,9 @@ public final class DefaultAudioSink implements AudioSink { case C.ENCODING_DTS_HD: return DtsUtil.parseDtsAudioSampleCount(buffer); case C.ENCODING_AC3: - return Ac3Util.getAc3SyncframeAudioSampleCount(); case C.ENCODING_E_AC3: case C.ENCODING_E_AC3_JOC: - return Ac3Util.parseEAc3SyncframeAudioSampleCount(buffer); + return Ac3Util.parseAc3SyncframeAudioSampleCount(buffer); case C.ENCODING_AC4: return Ac4Util.parseAc4SyncframeAudioSampleCount(buffer); case C.ENCODING_DOLBY_TRUEHD: From a3bad3680b8384d5ebf27e454668318c6311bc5a Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 2 Jan 2020 13:13:31 +0000 Subject: [PATCH 06/44] Document overriding drawables for notifications Issue: #6266 PiperOrigin-RevId: 287821640 --- .../ui/PlayerNotificationManager.java | 52 +++++++++++++------ 1 file changed, 37 insertions(+), 15 deletions(-) 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 aeb5292187..e572bc5a11 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 @@ -55,28 +55,28 @@ import java.util.List; import java.util.Map; /** - * A notification manager to start, update and cancel a media style notification reflecting the - * player state. + * Starts, updates and cancels a media style notification reflecting the player state. The actions + * displayed and the drawables used can both be customized, as described below. * *

The notification is cancelled when {@code null} is passed to {@link #setPlayer(Player)} or * when the notification is dismissed by the user. * *

If the player is released it must be removed from the manager by calling {@code - * setPlayer(null)} which will cancel the notification. + * setPlayer(null)}. * *

Action customization

* - * Standard playback actions can be shown or omitted as follows: + * Playback actions can be displayed or omitted as follows: * *
    - *
  • {@code useNavigationActions} - Sets whether the navigation previous and next actions - * are displayed. + *
  • {@code useNavigationActions} - Sets whether the previous and next actions are + * displayed. *
      *
    • Corresponding setter: {@link #setUseNavigationActions(boolean)} *
    • Default: {@code true} *
    - *
  • {@code useNavigationActionsInCompactView} - Sets whether the navigation previous and - * next actions should are displayed in compact view (including the lock screen notification). + *
  • {@code useNavigationActionsInCompactView} - Sets whether the previous and next + * actions are displayed in compact view (including the lock screen notification). *
      *
    • Corresponding setter: {@link #setUseNavigationActionsInCompactView(boolean)} *
    • Default: {@code false} @@ -98,12 +98,35 @@ import java.util.Map; *
    • Default: {@link #DEFAULT_REWIND_MS} (5000) *
    *
  • {@code fastForwardIncrementMs} - Sets the fast forward increment. If set to zero the - * fast forward action is not included in the notification. + * fast forward action is not displayed. *
      *
    • Corresponding setter: {@link #setFastForwardIncrementMs(long)} - *
    • Default: {@link #DEFAULT_FAST_FORWARD_MS} (5000) + *
    • Default: {@link #DEFAULT_FAST_FORWARD_MS} (15000) *
    *
+ * + *

Overriding drawables

+ * + * The drawables used by PlayerNotificationManager can be overridden by drawables with the same + * names defined in your application. The drawables that can be overridden are: + * + *
    + *
  • {@code exo_notification_small_icon} - The icon passed by default to {@link + * NotificationCompat.Builder#setSmallIcon(int)}. A different icon can also be specified + * programmatically by calling {@link #setSmallIcon(int)}. + *
  • {@code exo_notification_play} - The play icon. + *
  • {@code exo_notification_pause} - The pause icon. + *
  • {@code exo_notification_rewind} - The rewind icon. + *
  • {@code exo_notification_fastforward} - The fast forward icon. + *
  • {@code exo_notification_previous} - The previous icon. + *
  • {@code exo_notification_next} - The next icon. + *
  • {@code exo_notification_stop} - The stop icon. + *
+ * + * Unlike the drawables above, the large icon (i.e. the icon passed to {@link + * NotificationCompat.Builder#setLargeIcon(Bitmap)} cannot be overridden in this way. Instead, the + * large icon is obtained from the {@link MediaDescriptionAdapter} injected when creating the + * PlayerNotificationManager. */ public class PlayerNotificationManager { @@ -154,11 +177,10 @@ public class PlayerNotificationManager { /** * Gets the large icon for the current media item. * - *

When a bitmap initially needs to be asynchronously loaded, a placeholder (or null) can be - * returned and the bitmap asynchronously passed to the {@link BitmapCallback} once it is - * loaded. Because the adapter may be called multiple times for the same media item, the bitmap - * should be cached by the app and whenever possible be returned synchronously at subsequent - * calls for the same media item. + *

When a bitmap needs to be loaded asynchronously, a placeholder bitmap (or null) should be + * returned. The actual bitmap should be passed to the {@link BitmapCallback} once it has been + * loaded. Because the adapter may be called multiple times for the same media item, bitmaps + * should be cached by the app and returned synchronously when possible. * *

See {@link NotificationCompat.Builder#setLargeIcon(Bitmap)}. * From 2380f937f3c06145676eaae3224d65f09a987cc4 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 2 Jan 2020 14:40:47 +0000 Subject: [PATCH 07/44] Document overriding of drawables for PlayerControlView Issue: #6779 PiperOrigin-RevId: 287828273 --- .../exoplayer2/ui/PlayerControlView.java | 53 +++++++++++++++---- .../android/exoplayer2/ui/PlayerView.java | 9 +++- 2 files changed, 51 insertions(+), 11 deletions(-) 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 a6636d71be..248ac9fdaf 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 @@ -49,8 +49,8 @@ import java.util.concurrent.CopyOnWriteArrayList; * A view for controlling {@link Player} instances. * *

A PlayerControlView can be customized by setting attributes (or calling corresponding - * methods), overriding the view's layout file or by specifying a custom view layout file, as - * outlined below. + * methods), overriding drawables, overriding the view's layout file, or by specifying a custom view + * layout file. * *

Attributes

* @@ -104,6 +104,30 @@ import java.util.concurrent.CopyOnWriteArrayList; * layout is overridden to specify a custom {@code exo_progress} (see below). * * + *

Overriding drawables

+ * + * The drawables used by PlayerControlView (with its default layout file) can be overridden by + * drawables with the same names defined in your application. The drawables that can be overridden + * are: + * + *
    + *
  • {@code exo_controls_play} - The play icon. + *
  • {@code exo_controls_pause} - The pause icon. + *
  • {@code exo_controls_rewind} - The rewind icon. + *
  • {@code exo_controls_fastforward} - The fast forward icon. + *
  • {@code exo_controls_previous} - The previous icon. + *
  • {@code exo_controls_next} - The next icon. + *
  • {@code exo_controls_repeat_off} - The repeat icon for {@link + * Player#REPEAT_MODE_OFF}. + *
  • {@code exo_controls_repeat_one} - The repeat icon for {@link + * Player#REPEAT_MODE_ONE}. + *
  • {@code exo_controls_repeat_all} - The repeat icon for {@link + * Player#REPEAT_MODE_ALL}. + *
  • {@code exo_controls_shuffle_off} - The shuffle icon when shuffling is disabled. + *
  • {@code exo_controls_shuffle_on} - The shuffle icon when shuffling is enabled. + *
  • {@code exo_controls_vr} - The VR icon. + *
+ * *

Overriding the layout file

* * To customize the layout of PlayerControlView throughout your app, or just for certain @@ -123,29 +147,38 @@ import java.util.concurrent.CopyOnWriteArrayList; *
    *
  • Type: {@link View} *
- *
  • {@code exo_ffwd} - The fast forward button. - *
      - *
    • Type: {@link View} - *
    *
  • {@code exo_rew} - The rewind button. *
      *
    • Type: {@link View} *
    - *
  • {@code exo_prev} - The previous track button. + *
  • {@code exo_ffwd} - The fast forward button. *
      *
    • Type: {@link View} *
    - *
  • {@code exo_next} - The next track button. + *
  • {@code exo_prev} - The previous button. + *
      + *
    • Type: {@link View} + *
    + *
  • {@code exo_next} - The next button. *
      *
    • Type: {@link View} *
    *
  • {@code exo_repeat_toggle} - The repeat toggle button. *
      - *
    • Type: {@link View} + *
    • Type: {@link ImageView} + *
    • Note: PlayerControlView will programmatically set the drawable on the repeat toggle + * button according to the player's current repeat mode. The drawables used are {@code + * exo_controls_repeat_off}, {@code exo_controls_repeat_one} and {@code + * exo_controls_repeat_all}. See the section above for information on overriding these + * drawables. *
    *
  • {@code exo_shuffle} - The shuffle button. *
      - *
    • Type: {@link View} + *
    • Type: {@link ImageView} + *
    • Note: PlayerControlView will programmatically set the drawable on the shuffle button + * according to the player's current repeat mode. The drawables used are {@code + * exo_controls_shuffle_off} and {@code exo_controls_shuffle_on}. See the section above + * for information on overriding these drawables. *
    *
  • {@code exo_vr} - The VR mode button. *
      diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java index c55fe09f76..03168643cf 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java @@ -79,7 +79,8 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; * during playback, and displays playback controls using a {@link PlayerControlView}. * *

      A PlayerView can be customized by setting attributes (or calling corresponding methods), - * overriding the view's layout file or by specifying a custom view layout file, as outlined below. + * overriding drawables, overriding the view's layout file, or by specifying a custom view layout + * file. * *

      Attributes

      * @@ -172,6 +173,12 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; * exo_controller} (see below). *
    * + *

    Overriding drawables

    + * + * The drawables used by {@link PlayerControlView} (with its default layout file) can be overridden + * by drawables with the same names defined in your application. See the {@link PlayerControlView} + * documentation for a list of drawables that can be overridden. + * *

    Overriding the layout file

    * * To customize the layout of PlayerView throughout your app, or just for certain configurations, From f0e0ee421f02595f89c9b228676f81f1796ca466 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 2 Jan 2020 14:44:16 +0000 Subject: [PATCH 08/44] Support twos codec in MP4 Issue: #5789 PiperOrigin-RevId: 287828559 --- RELEASENOTES.md | 2 ++ .../java/com/google/android/exoplayer2/C.java | 17 +++++++++----- .../audio/ResamplingAudioProcessor.java | 23 +++++++++++++++---- .../exoplayer2/extractor/mp4/Atom.java | 3 +++ .../exoplayer2/extractor/mp4/AtomParsers.java | 9 +++++--- .../google/android/exoplayer2/util/Util.java | 2 ++ 6 files changed, 42 insertions(+), 14 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 1e736dd0da..12b08c3c80 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -34,6 +34,8 @@ to proceed. * Fix handling of E-AC-3 streams that contain AC-3 syncframes ([#6602](https://github.com/google/ExoPlayer/issues/6602)). +* Support "twos" codec (big endian PCM) in MP4 + ([#5789](https://github.com/google/ExoPlayer/issues/5789)). ### 2.11.1 (2019-12-20) ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/C.java b/library/core/src/main/java/com/google/android/exoplayer2/C.java index e431b2d899..776e79df97 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/C.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/C.java @@ -150,10 +150,11 @@ public final class C { /** * Represents an audio encoding, or an invalid or unset value. One of {@link Format#NO_VALUE}, * {@link #ENCODING_INVALID}, {@link #ENCODING_PCM_8BIT}, {@link #ENCODING_PCM_16BIT}, {@link - * #ENCODING_PCM_24BIT}, {@link #ENCODING_PCM_32BIT}, {@link #ENCODING_PCM_FLOAT}, {@link - * #ENCODING_PCM_MU_LAW}, {@link #ENCODING_PCM_A_LAW}, {@link #ENCODING_MP3}, {@link - * #ENCODING_AC3}, {@link #ENCODING_E_AC3}, {@link #ENCODING_E_AC3_JOC}, {@link #ENCODING_AC4}, - * {@link #ENCODING_DTS}, {@link #ENCODING_DTS_HD} or {@link #ENCODING_DOLBY_TRUEHD}. + * #ENCODING_PCM_16BIT_BIG_ENDIAN}, {@link #ENCODING_PCM_24BIT}, {@link #ENCODING_PCM_32BIT}, + * {@link #ENCODING_PCM_FLOAT}, {@link #ENCODING_PCM_MU_LAW}, {@link #ENCODING_PCM_A_LAW}, {@link + * #ENCODING_MP3}, {@link #ENCODING_AC3}, {@link #ENCODING_E_AC3}, {@link #ENCODING_E_AC3_JOC}, + * {@link #ENCODING_AC4}, {@link #ENCODING_DTS}, {@link #ENCODING_DTS_HD} or {@link + * #ENCODING_DOLBY_TRUEHD}. */ @Documented @Retention(RetentionPolicy.SOURCE) @@ -162,6 +163,7 @@ public final class C { ENCODING_INVALID, ENCODING_PCM_8BIT, ENCODING_PCM_16BIT, + ENCODING_PCM_16BIT_BIG_ENDIAN, ENCODING_PCM_24BIT, ENCODING_PCM_32BIT, ENCODING_PCM_FLOAT, @@ -181,8 +183,8 @@ public final class C { /** * Represents a PCM audio encoding, or an invalid or unset value. One of {@link Format#NO_VALUE}, * {@link #ENCODING_INVALID}, {@link #ENCODING_PCM_8BIT}, {@link #ENCODING_PCM_16BIT}, {@link - * #ENCODING_PCM_24BIT}, {@link #ENCODING_PCM_32BIT}, {@link #ENCODING_PCM_FLOAT}, {@link - * #ENCODING_PCM_MU_LAW} or {@link #ENCODING_PCM_A_LAW}. + * #ENCODING_PCM_16BIT_BIG_ENDIAN}, {@link #ENCODING_PCM_24BIT}, {@link #ENCODING_PCM_32BIT}, + * {@link #ENCODING_PCM_FLOAT}, {@link #ENCODING_PCM_MU_LAW} or {@link #ENCODING_PCM_A_LAW}. */ @Documented @Retention(RetentionPolicy.SOURCE) @@ -191,6 +193,7 @@ public final class C { ENCODING_INVALID, ENCODING_PCM_8BIT, ENCODING_PCM_16BIT, + ENCODING_PCM_16BIT_BIG_ENDIAN, ENCODING_PCM_24BIT, ENCODING_PCM_32BIT, ENCODING_PCM_FLOAT, @@ -204,6 +207,8 @@ public final class C { public static final int ENCODING_PCM_8BIT = AudioFormat.ENCODING_PCM_8BIT; /** @see AudioFormat#ENCODING_PCM_16BIT */ public static final int ENCODING_PCM_16BIT = AudioFormat.ENCODING_PCM_16BIT; + /** Like {@link #ENCODING_PCM_16BIT}, but with the bytes in big endian order. */ + public static final int ENCODING_PCM_16BIT_BIG_ENDIAN = 0x08000000; /** PCM encoding with 24 bits per sample. */ public static final int ENCODING_PCM_24BIT = 0x80000000; /** PCM encoding with 32 bits per sample. */ diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java index 1bfa1897c8..30bd4da472 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java @@ -29,8 +29,11 @@ import java.nio.ByteBuffer; public AudioFormat onConfigure(AudioFormat inputAudioFormat) throws UnhandledAudioFormatException { @C.PcmEncoding int encoding = inputAudioFormat.encoding; - if (encoding != C.ENCODING_PCM_8BIT && encoding != C.ENCODING_PCM_16BIT - && encoding != C.ENCODING_PCM_24BIT && encoding != C.ENCODING_PCM_32BIT) { + if (encoding != C.ENCODING_PCM_8BIT + && encoding != C.ENCODING_PCM_16BIT + && encoding != C.ENCODING_PCM_16BIT_BIG_ENDIAN + && encoding != C.ENCODING_PCM_24BIT + && encoding != C.ENCODING_PCM_32BIT) { throw new UnhandledAudioFormatException(inputAudioFormat); } return encoding != C.ENCODING_PCM_16BIT @@ -50,6 +53,9 @@ import java.nio.ByteBuffer; case C.ENCODING_PCM_8BIT: resampledSize = size * 2; break; + case C.ENCODING_PCM_16BIT_BIG_ENDIAN: + resampledSize = size; + break; case C.ENCODING_PCM_24BIT: resampledSize = (size / 3) * 2; break; @@ -70,21 +76,28 @@ import java.nio.ByteBuffer; ByteBuffer buffer = replaceOutputBuffer(resampledSize); switch (inputAudioFormat.encoding) { case C.ENCODING_PCM_8BIT: - // 8->16 bit resampling. Shift each byte from [0, 256) to [-128, 128) and scale up. + // 8 -> 16 bit resampling. Shift each byte from [0, 256) to [-128, 128) and scale up. for (int i = position; i < limit; i++) { buffer.put((byte) 0); buffer.put((byte) ((inputBuffer.get(i) & 0xFF) - 128)); } break; + case C.ENCODING_PCM_16BIT_BIG_ENDIAN: + // Big endian to little endian resampling. Swap the byte order. + for (int i = position; i < limit; i += 2) { + buffer.put(inputBuffer.get(i + 1)); + buffer.put(inputBuffer.get(i)); + } + break; case C.ENCODING_PCM_24BIT: - // 24->16 bit resampling. Drop the least significant byte. + // 24 -> 16 bit resampling. Drop the least significant byte. for (int i = position; i < limit; i += 3) { buffer.put(inputBuffer.get(i + 1)); buffer.put(inputBuffer.get(i + 2)); } break; case C.ENCODING_PCM_32BIT: - // 32->16 bit resampling. Drop the two least significant bytes. + // 32 -> 16 bit resampling. Drop the two least significant bytes. for (int i = position; i < limit; i += 4) { buffer.put(inputBuffer.get(i + 2)); buffer.put(inputBuffer.get(i + 3)); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java index 572efed1af..e86a873ed5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java @@ -379,6 +379,9 @@ import java.util.List; @SuppressWarnings("ConstantCaseForConstants") public static final int TYPE_dfLa = 0x64664c61; + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_twos = 0x74776f73; + public final int type; public Atom(int type) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java index f6b4f4d463..8f2a244d59 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java @@ -798,6 +798,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; || childAtomType == Atom.TYPE_sawb || childAtomType == Atom.TYPE_lpcm || childAtomType == Atom.TYPE_sowt + || childAtomType == Atom.TYPE_twos || childAtomType == Atom.TYPE__mp3 || childAtomType == Atom.TYPE_alac || childAtomType == Atom.TYPE_alaw @@ -1086,6 +1087,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; int channelCount; int sampleRate; + @C.PcmEncoding int pcmEncoding = Format.NO_VALUE; if (quickTimeSoundDescriptionVersion == 0 || quickTimeSoundDescriptionVersion == 1) { channelCount = parent.readUnsignedShort(); @@ -1147,6 +1149,10 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; mimeType = MimeTypes.AUDIO_AMR_WB; } else if (atomType == Atom.TYPE_lpcm || atomType == Atom.TYPE_sowt) { mimeType = MimeTypes.AUDIO_RAW; + pcmEncoding = C.ENCODING_PCM_16BIT; + } else if (atomType == Atom.TYPE_twos) { + mimeType = MimeTypes.AUDIO_RAW; + pcmEncoding = C.ENCODING_PCM_16BIT_BIG_ENDIAN; } else if (atomType == Atom.TYPE__mp3) { mimeType = MimeTypes.AUDIO_MPEG; } else if (atomType == Atom.TYPE_alac) { @@ -1233,9 +1239,6 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; } if (out.format == null && mimeType != null) { - // TODO: Determine the correct PCM encoding. - @C.PcmEncoding int pcmEncoding = - MimeTypes.AUDIO_RAW.equals(mimeType) ? C.ENCODING_PCM_16BIT : Format.NO_VALUE; out.format = Format.createAudioSampleFormat(Integer.toString(trackId), mimeType, null, Format.NO_VALUE, Format.NO_VALUE, channelCount, sampleRate, pcmEncoding, initializationData == null ? null : Collections.singletonList(initializationData), 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 54e65797f0..aa87096ebb 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 @@ -1355,6 +1355,7 @@ public final class Util { public static boolean isEncodingLinearPcm(@C.Encoding int encoding) { return encoding == C.ENCODING_PCM_8BIT || encoding == C.ENCODING_PCM_16BIT + || encoding == C.ENCODING_PCM_16BIT_BIG_ENDIAN || encoding == C.ENCODING_PCM_24BIT || encoding == C.ENCODING_PCM_32BIT || encoding == C.ENCODING_PCM_FLOAT; @@ -1423,6 +1424,7 @@ public final class Util { case C.ENCODING_PCM_8BIT: return channelCount; case C.ENCODING_PCM_16BIT: + case C.ENCODING_PCM_16BIT_BIG_ENDIAN: return channelCount * 2; case C.ENCODING_PCM_24BIT: return channelCount * 3; From 826083db923f26f2000ca42f7b54b9531726d713 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 2 Jan 2020 18:19:41 +0000 Subject: [PATCH 09/44] Add support for IMA ADPCM in WAV PiperOrigin-RevId: 287854701 --- RELEASENOTES.md | 1 + .../android/exoplayer2/audio/WavUtil.java | 12 +- .../extractor/wav/WavExtractor.java | 314 +++++++++++++++++- .../src/test/assets/wav/sample_ima_adpcm.wav | Bin 0 -> 22622 bytes .../assets/wav/sample_ima_adpcm.wav.0.dump | 75 +++++ .../assets/wav/sample_ima_adpcm.wav.1.dump | 59 ++++ .../assets/wav/sample_ima_adpcm.wav.2.dump | 47 +++ .../assets/wav/sample_ima_adpcm.wav.3.dump | 35 ++ .../extractor/wav/WavExtractorTest.java | 5 + 9 files changed, 526 insertions(+), 22 deletions(-) create mode 100644 library/core/src/test/assets/wav/sample_ima_adpcm.wav create mode 100644 library/core/src/test/assets/wav/sample_ima_adpcm.wav.0.dump create mode 100644 library/core/src/test/assets/wav/sample_ima_adpcm.wav.1.dump create mode 100644 library/core/src/test/assets/wav/sample_ima_adpcm.wav.2.dump create mode 100644 library/core/src/test/assets/wav/sample_ima_adpcm.wav.3.dump diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 12b08c3c80..2dba34486b 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -36,6 +36,7 @@ ([#6602](https://github.com/google/ExoPlayer/issues/6602)). * Support "twos" codec (big endian PCM) in MP4 ([#5789](https://github.com/google/ExoPlayer/issues/5789)). +* WAV: Support IMA ADPCM encoded data. ### 2.11.1 (2019-12-20) ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/WavUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/WavUtil.java index 29b772f838..25261f1686 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/WavUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/WavUtil.java @@ -32,15 +32,17 @@ public final class WavUtil { public static final int DATA_FOURCC = 0x64617461; /** WAVE type value for integer PCM audio data. */ - private static final int TYPE_PCM = 0x0001; + public static final int TYPE_PCM = 0x0001; /** WAVE type value for float PCM audio data. */ - private static final int TYPE_FLOAT = 0x0003; + public static final int TYPE_FLOAT = 0x0003; /** WAVE type value for 8-bit ITU-T G.711 A-law audio data. */ - private static final int TYPE_A_LAW = 0x0006; + public static final int TYPE_A_LAW = 0x0006; /** WAVE type value for 8-bit ITU-T G.711 mu-law audio data. */ - private static final int TYPE_MU_LAW = 0x0007; + public static final int TYPE_MU_LAW = 0x0007; + /** WAVE type value for IMA ADPCM audio data. */ + public static final int TYPE_IMA_ADPCM = 0x0011; /** WAVE type value for extended WAVE format. */ - private static final int TYPE_WAVE_FORMAT_EXTENSIBLE = 0xFFFE; + public static final int TYPE_WAVE_FORMAT_EXTENSIBLE = 0xFFFE; /** * Returns the WAVE format type value for the given {@link C.PcmEncoding}. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java index c1eb357bb9..0c6e538f43 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java @@ -28,6 +28,7 @@ import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; +import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; import java.io.IOException; @@ -91,12 +92,16 @@ public final class WavExtractor implements Extractor { throw new ParserException("Unsupported or unrecognized wav header."); } - @C.PcmEncoding - int pcmEncoding = WavUtil.getPcmEncodingForType(header.formatType, header.bitsPerSample); - if (pcmEncoding == C.ENCODING_INVALID) { - throw new ParserException("Unsupported WAV format type: " + header.formatType); + if (header.formatType == WavUtil.TYPE_IMA_ADPCM) { + outputWriter = new ImaAdPcmOutputWriter(extractorOutput, trackOutput, header); + } else { + @C.PcmEncoding + int pcmEncoding = WavUtil.getPcmEncodingForType(header.formatType, header.bitsPerSample); + if (pcmEncoding == C.ENCODING_INVALID) { + throw new ParserException("Unsupported WAV format type: " + header.formatType); + } + outputWriter = new PcmOutputWriter(extractorOutput, trackOutput, header, pcmEncoding); } - outputWriter = new PcmOutputWriter(extractorOutput, trackOutput, header, pcmEncoding); } if (dataStartPosition == C.POSITION_UNSET) { @@ -156,11 +161,22 @@ public final class WavExtractor implements Extractor { private final TrackOutput trackOutput; private final WavHeader header; private final @C.PcmEncoding int pcmEncoding; - private final int targetSampleSize; + /** The target size of each output sample, in bytes. */ + private final int targetSampleSizeBytes; + /** The time at which the writer was last {@link #reset}. */ private long startTimeUs; + /** + * The number of bytes that have been written to {@link #trackOutput} but have yet to be + * included as part of a sample (i.e. the corresponding call to {@link + * TrackOutput#sampleMetadata} has yet to be made). + */ + private int pendingOutputBytes; + /** + * The total number of frames in samples that have been written to the trackOutput since the + * last call to {@link #reset}. + */ private long outputFrameCount; - private int pendingBytes; public PcmOutputWriter( ExtractorOutput extractorOutput, @@ -173,15 +189,15 @@ public final class WavExtractor implements Extractor { this.pcmEncoding = pcmEncoding; // For PCM blocks correspond to single frames. This is validated in init(int, long). int bytesPerFrame = header.blockSize; - targetSampleSize = + targetSampleSizeBytes = Math.max(bytesPerFrame, header.frameRateHz * bytesPerFrame / TARGET_SAMPLES_PER_SECOND); } @Override public void reset(long timeUs) { startTimeUs = timeUs; + pendingOutputBytes = 0; outputFrameCount = 0; - pendingBytes = 0; } @Override @@ -204,7 +220,7 @@ public final class WavExtractor implements Extractor { MimeTypes.AUDIO_RAW, /* codecs= */ null, /* bitrate= */ header.frameRateHz * bytesPerFrame * 8, - targetSampleSize, + /* maxInputSize= */ targetSampleSizeBytes, header.numChannels, header.frameRateHz, pcmEncoding, @@ -220,34 +236,298 @@ public final class WavExtractor implements Extractor { throws IOException, InterruptedException { // Write sample data until we've reached the target sample size, or the end of the data. boolean endOfSampleData = bytesLeft == 0; - while (!endOfSampleData && pendingBytes < targetSampleSize) { - int bytesToRead = (int) Math.min(targetSampleSize - pendingBytes, bytesLeft); + while (!endOfSampleData && pendingOutputBytes < targetSampleSizeBytes) { + int bytesToRead = (int) Math.min(targetSampleSizeBytes - pendingOutputBytes, bytesLeft); int bytesAppended = trackOutput.sampleData(input, bytesToRead, true); if (bytesAppended == RESULT_END_OF_INPUT) { endOfSampleData = true; } else { - pendingBytes += bytesAppended; + pendingOutputBytes += bytesAppended; } } // Write the corresponding sample metadata. Samples must be a whole number of frames. It's - // possible pendingBytes is not a whole number of frames if the stream ended unexpectedly. + // possible that the number of pending output bytes is not a whole number of frames if the + // stream ended unexpectedly. int bytesPerFrame = header.blockSize; - int pendingFrames = pendingBytes / bytesPerFrame; + int pendingFrames = pendingOutputBytes / bytesPerFrame; if (pendingFrames > 0) { long timeUs = startTimeUs + Util.scaleLargeTimestamp( outputFrameCount, C.MICROS_PER_SECOND, header.frameRateHz); int size = pendingFrames * bytesPerFrame; - int offset = pendingBytes - size; + int offset = pendingOutputBytes - size; trackOutput.sampleMetadata( timeUs, C.BUFFER_FLAG_KEY_FRAME, size, offset, /* encryptionData= */ null); outputFrameCount += pendingFrames; - pendingBytes = offset; + pendingOutputBytes = offset; } return endOfSampleData; } } + + private static final class ImaAdPcmOutputWriter implements OutputWriter { + + private static final int[] INDEX_TABLE = { + -1, -1, -1, -1, 2, 4, 6, 8, -1, -1, -1, -1, 2, 4, 6, 8 + }; + + private static final int[] STEP_TABLE = { + 7, 8, 9, 10, 11, 12, 13, 14, 16, 17, 19, 21, 23, 25, 28, 31, 34, 37, 41, 45, 50, 55, 60, 66, + 73, 80, 88, 97, 107, 118, 130, 143, 157, 173, 190, 209, 230, 253, 279, 307, 337, 371, 408, + 449, 494, 544, 598, 658, 724, 796, 876, 963, 1060, 1166, 1282, 1411, 1552, 1707, 1878, 2066, + 2272, 2499, 2749, 3024, 3327, 3660, 4026, 4428, 4871, 5358, 5894, 6484, 7132, 7845, 8630, + 9493, 10442, 11487, 12635, 13899, 15289, 16818, 18500, 20350, 22385, 24623, 27086, 29794, + 32767 + }; + + private final ExtractorOutput extractorOutput; + private final TrackOutput trackOutput; + private final WavHeader header; + /** The target size of each output sample, in frames. */ + private final int targetSampleSizeFrames; + + // Properties of the input (yet to be decoded) data. + private int framesPerBlock; + private byte[] inputData; + private int pendingInputBytes; + + // Target for decoded (yet to be output) data. + private ParsableByteArray decodedData; + + // Properties of the output. + /** The time at which the writer was last {@link #reset}. */ + private long startTimeUs; + /** + * The number of bytes that have been written to {@link #trackOutput} but have yet to be + * included as part of a sample (i.e. the corresponding call to {@link + * TrackOutput#sampleMetadata} has yet to be made). + */ + private int pendingOutputBytes; + /** + * The total number of frames in samples that have been written to the trackOutput since the + * last call to {@link #reset}. + */ + private long outputFrameCount; + + public ImaAdPcmOutputWriter( + ExtractorOutput extractorOutput, TrackOutput trackOutput, WavHeader header) { + this.extractorOutput = extractorOutput; + this.trackOutput = trackOutput; + this.header = header; + targetSampleSizeFrames = Math.max(1, header.frameRateHz / TARGET_SAMPLES_PER_SECOND); + } + + @Override + public void reset(long timeUs) { + // Reset the input side. + pendingInputBytes = 0; + // Reset the output side. + startTimeUs = timeUs; + pendingOutputBytes = 0; + outputFrameCount = 0; + } + + @Override + public void init(int dataStartPosition, long dataEndPosition) throws ParserException { + // Validate the header. + ParsableByteArray scratch = new ParsableByteArray(header.extraData); + scratch.readLittleEndianUnsignedShort(); + framesPerBlock = scratch.readLittleEndianUnsignedShort(); + // This calculation is defined in "Microsoft Multimedia Standards Update - New Multimedia + // Types and Data Techniques" (1994). See the "IMA ADPCM Wave Type" and + // "DVI ADPCM Wave Type" sections, and the calculation of wSamplesPerBlock in the latter. + int numChannels = header.numChannels; + int expectedFramesPerBlock = + (((header.blockSize - (4 * numChannels)) * 8) / (header.bitsPerSample * numChannels)) + 1; + if (framesPerBlock != expectedFramesPerBlock) { + throw new ParserException( + "Expected frames per block: " + expectedFramesPerBlock + "; got: " + framesPerBlock); + } + + // Calculate the number of blocks we'll need to decode to obtain an output sample of the + // target sample size, and allocate suitably sized buffers for input and decoded data. + int maxBlocksToDecode = Util.ceilDivide(targetSampleSizeFrames, framesPerBlock); + inputData = new byte[maxBlocksToDecode * header.blockSize]; + decodedData = + new ParsableByteArray(maxBlocksToDecode * numOutputFramesToBytes(framesPerBlock)); + + // Output the seek map. + extractorOutput.seekMap( + new WavSeekMap(header, framesPerBlock, dataStartPosition, dataEndPosition)); + + // Output the format. We calculate the bitrate of the data before decoding, since this is the + // bitrate of the stream itself. + int bitrate = header.frameRateHz * header.blockSize * 8 / framesPerBlock; + Format format = + Format.createAudioSampleFormat( + /* id= */ null, + MimeTypes.AUDIO_RAW, + /* codecs= */ null, + bitrate, + /* maxInputSize= */ numOutputFramesToBytes(targetSampleSizeFrames), + header.numChannels, + header.frameRateHz, + C.ENCODING_PCM_16BIT, + /* initializationData= */ null, + /* drmInitData= */ null, + /* selectionFlags= */ 0, + /* language= */ null); + trackOutput.format(format); + } + + @Override + public boolean sampleData(ExtractorInput input, long bytesLeft) + throws IOException, InterruptedException { + // Calculate the number of additional frames that we need on the output side to complete a + // sample of the target size. + int targetFramesRemaining = + targetSampleSizeFrames - numOutputBytesToFrames(pendingOutputBytes); + // Calculate the whole number of blocks that we need to decode to obtain this many frames. + int blocksToDecode = Util.ceilDivide(targetFramesRemaining, framesPerBlock); + int targetReadBytes = blocksToDecode * header.blockSize; + + // Read input data until we've reached the target number of blocks, or the end of the data. + boolean endOfSampleData = bytesLeft == 0; + while (!endOfSampleData && pendingInputBytes < targetReadBytes) { + int bytesToRead = (int) Math.min(targetReadBytes - pendingInputBytes, bytesLeft); + int bytesAppended = input.read(inputData, pendingInputBytes, bytesToRead); + if (bytesAppended == RESULT_END_OF_INPUT) { + endOfSampleData = true; + } else { + pendingInputBytes += bytesAppended; + } + } + + int pendingBlockCount = pendingInputBytes / header.blockSize; + if (pendingBlockCount > 0) { + // We have at least one whole block to decode. + decode(inputData, pendingBlockCount, decodedData); + pendingInputBytes -= pendingBlockCount * header.blockSize; + + // Write all of the decoded data to the track output. + int decodedDataSize = decodedData.limit(); + trackOutput.sampleData(decodedData, decodedDataSize); + pendingOutputBytes += decodedDataSize; + + // Output the next sample at the target size. + int pendingOutputFrames = numOutputBytesToFrames(pendingOutputBytes); + if (pendingOutputFrames >= targetSampleSizeFrames) { + writeSampleMetadata(targetSampleSizeFrames); + } + } + + // If we've reached the end of the data, we might need to output a final partial sample. + if (endOfSampleData) { + int pendingOutputFrames = numOutputBytesToFrames(pendingOutputBytes); + if (pendingOutputFrames > 0) { + writeSampleMetadata(pendingOutputFrames); + } + } + + return endOfSampleData; + } + + private void writeSampleMetadata(int sampleFrames) { + long timeUs = + startTimeUs + + Util.scaleLargeTimestamp(outputFrameCount, C.MICROS_PER_SECOND, header.frameRateHz); + int size = numOutputFramesToBytes(sampleFrames); + int offset = pendingOutputBytes - size; + trackOutput.sampleMetadata( + timeUs, C.BUFFER_FLAG_KEY_FRAME, size, offset, /* encryptionData= */ null); + outputFrameCount += sampleFrames; + pendingOutputBytes -= size; + } + + /** + * Decodes IMA ADPCM data to 16 bit PCM. + * + * @param input The input data to decode. + * @param blockCount The number of blocks to decode. + * @param output The output into which the decoded data will be written. + */ + private void decode(byte[] input, int blockCount, ParsableByteArray output) { + for (int blockIndex = 0; blockIndex < blockCount; blockIndex++) { + for (int channelIndex = 0; channelIndex < header.numChannels; channelIndex++) { + decodeBlockForChannel(input, blockIndex, channelIndex, output.data); + } + } + int decodedDataSize = numOutputFramesToBytes(framesPerBlock * blockCount); + output.reset(decodedDataSize); + } + + private void decodeBlockForChannel( + byte[] input, int blockIndex, int channelIndex, byte[] output) { + int blockSize = header.blockSize; + int numChannels = header.numChannels; + + // The input data consists for a four byte header [Ci] for each of the N channels, followed + // by interleaved data segments [Ci-DATAj], each of which are four bytes long. + // + // [C1][C2]...[CN] [C1-Data0][C2-Data0]...[CN-Data0] [C1-Data1][C2-Data1]...[CN-Data1] etc + // + // Compute the start indices for the [Ci] and [Ci-Data0] for the current channel, as well as + // the number of data bytes for the channel in the block. + int blockStartIndex = blockIndex * blockSize; + int headerStartIndex = blockStartIndex + channelIndex * 4; + int dataStartIndex = headerStartIndex + numChannels * 4; + int dataSizeBytes = blockSize / numChannels - 4; + + // Decode initialization. Casting to a short is necessary for the most significant bit to be + // treated as -2^15 rather than 2^15. + int predictedSample = + (short) (((input[headerStartIndex + 1] & 0xFF) << 8) | (input[headerStartIndex] & 0xFF)); + int stepIndex = Math.min(input[headerStartIndex + 2] & 0xFF, 88); + int step = STEP_TABLE[stepIndex]; + + // Output the initial 16 bit PCM sample from the header. + int outputIndex = (blockIndex * framesPerBlock * numChannels + channelIndex) * 2; + output[outputIndex] = (byte) (predictedSample & 0xFF); + output[outputIndex + 1] = (byte) (predictedSample >> 8); + + // We examine each data byte twice during decode. + for (int i = 0; i < dataSizeBytes * 2; i++) { + int dataSegmentIndex = i / 8; + int dataSegmentOffset = (i / 2) % 4; + int dataIndex = dataStartIndex + (dataSegmentIndex * numChannels * 4) + dataSegmentOffset; + + int originalSample = input[dataIndex] & 0xFF; + if (i % 2 == 0) { + originalSample &= 0x0F; // Bottom four bits. + } else { + originalSample >>= 4; // Top four bits. + } + + int delta = originalSample & 0x07; + int difference = ((2 * delta + 1) * step) >> 3; + + if ((originalSample & 0x08) != 0) { + difference = -difference; + } + + predictedSample += difference; + predictedSample = Util.constrainValue(predictedSample, /* min= */ -32768, /* max= */ 32767); + + // Output the next 16 bit PCM sample to the correct position in the output. + outputIndex += 2 * numChannels; + output[outputIndex] = (byte) (predictedSample & 0xFF); + output[outputIndex + 1] = (byte) (predictedSample >> 8); + + stepIndex += INDEX_TABLE[originalSample]; + stepIndex = Util.constrainValue(stepIndex, /* min= */ 0, /* max= */ STEP_TABLE.length - 1); + step = STEP_TABLE[stepIndex]; + } + } + + private int numOutputBytesToFrames(int bytes) { + return bytes / (2 * header.numChannels); + } + + private int numOutputFramesToBytes(int frames) { + return frames * 2 * header.numChannels; + } + } } diff --git a/library/core/src/test/assets/wav/sample_ima_adpcm.wav b/library/core/src/test/assets/wav/sample_ima_adpcm.wav new file mode 100644 index 0000000000000000000000000000000000000000..661d54d1d716df8f4e7deef62e4cdf8a30783b44 GIT binary patch literal 22622 zcmeFZ-EZV(p6~aNr8$QzIRVM4-l>v-fN$!Ne2{~xBCFj!3j``wsXg9vkXTZCcF*0o zd(NJ#T*MyanpK_zB-I;`@;Ly>s&-1=T<}dj@*)>1lIrQXQ@zNZuw() zCwJ>vVDr`9=3iI?ZM0vy#3G;P`F($%@At?3_a8p|`JWN;r@f!={`r6VE3r-p;R!_s z86oC>g@0IEBQ*KDfBxqmJ^9rd{_)R$L&(2=|KQOEe*gY|{qVne|KQ=H{~W*n*B|}& zf8M?SFFUP&*>1JSFF*R#M+BexPk;T>9r&j^@K1N(|9=Ne2ImWYS@`91JVi0Qzzf8U z(_pqrnSD*TNw?^jrQK#%h?k}FSf^*_jVH3tiVFR@EQ0pkn$MDpq^Vbusy+<&RDmp3 zY2f4hl9XYje#<|EF9C~c+(MgHOiX4N*kRTPUu5Dy5v;&rWO|-5dRLg` z>@YFoWVYR<@WH(6bYxoe94i*-*b)1RFgq_As_Z2}B7RsqOVh>Torj7UZ>+@$kN+^7 zoe+7tEI9tED4KsRQ;ZqENu0;ZY02uUPoz81>ZZz37vV<(B|3K~I$o^v3~5ip>VDmv zO?t8*?aRU}B*7{b|CF}#aS#NoE=XpuELw&t(LLFXC%s2?ca;P|7&moIy*kgUqCVv) zSr<;fE}We~M@Uv19pyBne8y1RlQQAlW{m_bqh?9N(A-g?ctRx%#d~2QlAbbLEc5jz zDxEv~-A|WzOznF7A`Jtc^S%zZb_J91-Qc^i^jPCh(}iaYIwoqs5q~77bx~l57~NaJrb}59EO?8w#~Z%Z#H0%H@293St|p zgBM&;|1u8^Rpn(fFDGPqQ-r-;#Y|X^rXt)(`6{)gVf7-b)_$dol6=B>i;UYc3y3pbMkkunj zs{7oUXKAXefNl z8A6<#aK_MQkH*&y=^b4O>VyykhZS4 zF}n!gu*9^C%~877C1+*A3InY&4$@WA;+c*+HqXn6w5!rgkTlvt%(}}l!3x|_z4C9u zK$2=htoJ&WnXs=!Hkp@)SSTdgK2W_ynT1@v z75sccOn=ezs{A7%TG%|t4{s8YX5~6ZFN%hKPoXG3;HO_roYsDgpOzD*QOlbYG1kKJ zU+7qj%aHJc(LAxYHhJ1jqV2VDHISEIrU}2b?B%$tP*jsU-3DK{x;>b zJM#(Gr{gl*SWA`+>XBCCTMj(&-+~;$e6> z-cotmP)9c_han6l@scuqYH^nPl^qa6Kh7A3EErKH?K%!|I^h&A5bp#D>%OY;{ zh2s1mW;Hjok|f<2XkKE_oUDw3Cd5^JJ&C$hHPG^N`=UC!nCHbbNiD zY^!)aNw`YPd=+dx(uUWeI1t=%-u$-;S&SLOoHaGdr}Fuw-7qxa_?aYotI!zK{OJ6t zxI0{AEazJ+G3q=veO1PR>N3L1@>RIs878EoSK`E8tati+T#i|e$K@IIv-8DMv#H&q z;z=B5jMNR9m9Pzp&3pUWblIeNw|S2;Z^k<+KTSC9o4nDHx#=SD4?pSG zrb%cAO{G(dmuc%EbyhaD-<9dlc855~8=EG}lPF%~d9$zh@vB49`gK~abAn8nlO45w zv2w(Y;wIHjJqUZeZ6}GS9%s$I7KweGjxK|Ei|Y_q^djg1%5zB29LUqJtDMdcizaDn zyt`U8+p_yD+urnXvS?<|l&OZ$q}(w2K%=veRTUw&80B}k$D5P5ziBwA+i`tfu7 z5xzpRU&)iCQDG^Gc>b*>8m&&)UhS;NGDnnkUY>><_3zWxK(-Rx$&PUPlv>>6mhV@4 zGD_P1+H`5fJC77M3)q*JPP_UA)b;O$0ftvG0h-KcQoocOAUo{C!!LM_hjnQ zmgqPXk21FHm5fxMJtKWOC_bp0X3F)~;_HxTE)&=>bLOyyBJjUE4|i(P%{DJv*UuQH zEz`mL1Laox$%N-9bF~bIRaw7tRX!6t0v&{6#f`&H6p5NG&R30Ee-!6gGY+hmq5$&^fqBZuF)Md4(6OmMXn0GvczH!KR-%#B|HI2L{gIo3QnlPQEwf z*7BHhQNM*`VjLW;s!H9A?*F{zUyXfMuzZm-I>PG{UzA7ZK|`iQSqK)}{W|BRX@fq6 zoQSG5u5Jd40OdM-wC2Y_Q}Dz6zDkidPQ&B`Uj8Oc&1R?4t8>%K2{Vu>H^_X}l9;}# z{2!ArNyfdNPQ5DDRUz6KoR+rSRa`^y7w1VeXy__^b&hi*h}Z!o&699rVEWQ#-J3U# z&a1rlmCca@+AK4cMMWf{L8VPoj2NlE=3h4Z!+4Tx)Q!{7wz%?58jBz5k`-ODhT!@k zi6(|hFRIeu_|17k>5k&`AO4|2menUR9i3y*uEyd(rK60I<)g(aPx_sj%ku6`vc2j0 z3{TFQT3=bb!kSahRsm75n(PC;GCLpRS+2_i)Ru`z;^Y@K-j&@F>P7EV=F6>jKV6ko zN#G^99+fu1)!+YjH5>)}?e-&*L??vfN6k$VrA>N%&d3MC&0<|fkqfPXFw8G4u_MiF zzbP3qVLJL3i#JVKrKy`N6zMi}E(tiwt$NMlWziXUS-2&m=Ll?nPf@NXP5u-M7aMpO zd>!(VCZBG|2ct628v6phWF*yO;$#(mphiLQU{^i;D&+by31Wsme(t0G>330YLN(vsYuzIH*O}H%Z~N@73Im z&ZtCr)kncH$=& zZ!Pl%PKg&G=~q}$RwDDZ^69E~c_M1cF*d_0vs?FgGh?{nNw}@()BMG_-P^?TwkTeP z+;u{KwqnUH_0P!~33%o#+qyTX&B}mWhmCuZxh0E`=;OtBUlp#h#kN7m=lBsql(-^o z@^O&b?CEOK8qogYlI1zV?5nl>JTZ)}%UYC&Rozsj<5k);WcPDU=pSYH?VyhjMR^h~ zSg1l`m<=zE=O{h$Mab_fd6K=a4vX+{jXc)L*^5*(b7xPdf@DHkn`9P_dsCYW9+{*{ zqcHPBJdt>@jGKxkXvXxs=ye3nY{*LgWs+^Fm5XJ*t=E#Y2)X%ky!|2i2eh4?hXN<^ ze6So_qKXR1?Ru6Szpat^NlTzFiUhcTT&0ZzW%@dlc<#qN|9CE>U3Sd zME7Yw(1+!=7m8jUd?4^dc5lj-4wba5{!*(gigj%m1UQ3ao~=8 zt-RT@hM?BuAb6mn^l~zZPr~Y_lc4t)hfuYzT&%YHio0y$SHRIW*}462heAnE_#aAJ zqGX!tX=kq|ev_veiD#zO>AFVGUpeexGvh!+n+%3?A~u3D;T6*B>d<%VJAD{_V1<*WExms3Zk+k*){<6NA)9G^g>@vLWds zmc%QZ8beO1xp|tcOFgYtpkki|^DNj>WWIkeEGKqYj{P<*x`!e!lQ~OuBsGYNxN%P& zO_(IK0;vY&gp;LTGubG2cJ8>SkPwVjql?DBy01`YOGmC2Dqe(C6lLXldkx)4Ume;$Cwt|#*%E$8fj#`aeeGzqaty_su^Qj--jeUR zuN`(%BxL;ePenzqT%+nIFN$$bANntU(~xj9HH47Jw0To}pwgzz(NVd<#|?pg9kLC< z=Hwrt|Hp+d3Ri5uD-TJ0`aH0b;-sqdPuDx(b;b6(0{>;kx+1^HT_hH1E2L3x{Af|I zdz)0ea)OW@3~$gIa+H6awDuKu<~*UTJhaeA#q20tq4lB`AW5iO9JhAu2yHs>Erv%| z>Myt53&-qyq)TUSCaggG)^43Mm+?)=2R@#d94(%zn^C~f*VV^L?V^~3&0p5sCUuiu zgyk^DVG=MkjtYC&Fw=5uhX)4j`sZvi=A=G#gPJ?2S@R@r3)A0NenTNa zAPwV1@L^wb^K($k`D)!zjuIy5cVr5SkUbFG%S06Qcrj^jO2=%_#+Fa)S-8<3nAX;& za8T1Z!hr@vn9V22p9vD*p>WvAG2f<>L%|fqF6BSCC(O$1 zoi1S03O^o;V>;gNNJq;uL{a*^y>m}9Z&rb*AK_%;6gOB-Skjh9hZNd~=}nn#{9ENL zAMfZDC=a7fc~y_~Y{sG(9W5t+V~g@xbr>!tOb<;@6eRH^5P5D#ovt#UH#C)>Wo%_R z5iGKgPK5P}L)j245hqg|%*PZRO0rAxIOW`1Nv+ezlkG13269@jd8=FSx?FJWyVU7< zC@MT3dZ1InA_R2hH{EcaGvIp+9VTPuPCk3a>B8LRy2I;C><)v5>fv@yvjs}9d7~>IuY>+98{K;3?4W?JQjXyU9ixVi zG&@A&iEy^U*^mm7n{RI(J&*0SdJ}#`$IGg?KlGc&&l{Cym$DOaR|&v{`#!Y_KGN0E zB8-Co4at6AIn5f9w=AllMf;t~@3-{~`XU zg{rkEsynJO#}T8Af*?O(i0n&}I%lD>Vuy+oKt-t%=HbpJMO?;@L$1RcUiB*Bj+&e* zyXi^tjNH-`iDv)qgz40W5w_-WSp}J?b_In1BcGn9JE{gTnE|Xy!=#FqV3ua)_7Suf z$TmOAZE_J(2MYgP%D0E*x8s%~Q?pd$KdDK_=V|j`%|CnRk9g`bv<%)Aha5-DWUq3~ zn(i`%?jhzP=xO|H_9A9w#a)ei2MR|*m+2GXRq;SqaS7Fa-E0U{9N4@`O`ismV07re zPgzMjF5cp0j_2ojxxU{4_7}UtC^>N*=@ICC_*H62^&nkts9h>BonRbYPOM&C=11iv zC`~=Gw?6b1h11!T<78b&Rp$Ecs|jVOly8G-v3}D-foExnM|oqpQJPd;QR`HG02-G2 z$DuSmugcxp*`Wad{Ya-)q3H+YXlsoPoM5s&sC#8RX1a>^$x(u)-Vhc!^C8N2x-6R= zUJ)PfQbAGW`S{QZta+LM)o^XxK*6h;5{-5PxRzMZz?<6Iav8tBE6kPvX+k_dtclco ztanxFra+T$nm1)(QDk;p6^3Ej+#Mw`@GaNl88@tUYO}?d;_9NxMs^&cts%Tby_^Ie z3*0<01BUNZuEF&>_$p&o9pU*YQXOE0=^Jvfssb6j4zVTTB4b*D`-(Lx+$gw7+@4Ca zo5wdvvJ@Z6ZrIX~PeL2q$qL@lr{&uRG8%7RqWlKcI}KVRT*MRHO93bsW|jT@82Z0X zdKLD#JYg84D@>o^WWLH0Tl|TnMXQM+eResfB(;N*v0RKfZVI+8jzV#mgp>FtCB1v} zw0Hn@UL?-G4A#5RtCMd*bGtG@<*!ZiATE+UaB7ygi;39Prtt=3%Q>vjh$f<)@YvKWk`@>VA>I zv_nJvj>?i{j(tc`qCzbbNjQdyb_L2E^k`;AU{9_7fslQj&Z}VJRnifrVt)yk$)}=U3&g{ zLiUyE#FkFSRlW9d1q9se@A4#YOsNuwJ(--A+nc;T1ToO3Cm|~w)G6N)hu@cDW}wlj zL!wnth~0|W+_aV4^aQzMS~E9qb5vU+s);EhoV6z-5oK>tx0m^<+8rgRMrMs=bcdsl;RkHA26J z@KxPq!adR4%OZqrl8Aydoe=K3B8gK`*dL7YB-l{}lk?lP>AAE0Bc?x4V0jgG8@QcB zS++My!;jRc9QWwi^A_J0E}C-vn=BD4J-zeUZzl~%EPpJ@zduiV{ThFKa%(~4V)q_! z1fB@h^fG3VWIdONbRNb@Zy;7gaCWlRG(DQ-77*zb8=h!vneXax!bxglPtZyBt%GMo}5=^vz_9bEa`nv7)Bi zAaRqXD0x(d;u<_(=5*5Q(2FH(s&G*y;B=zkXE&^d2HRx%_0e^aG$;w6AiQ$qwoE=0 zhN}~Fi#)34VzQ^01-R%#VSZ9=A1J0{6X9&o%W8h zS3BdBj*8kqeyLqR^zbYrt%<(|kAM3vFHBKkaF;oXOtG*6?qS7D%e0D({pL{Y3)2?` z`-uul9fuN*^yWeT)7jS!rwrrq|AqaJKgNN;CoJVckUr7q+4du#hGkwAI=jl*GW7T8 zwEUCZni;!`{CW6TUvqE9CPl@7yz+GkvE?tFTb)KNqH4QC-;{9L=)JlShpDxrNW(E_ z24z*-70xc9y)~)s`azOp4f&xwyiSvFyz@vE;HVzvCskpf_<;59dCK#;A@CIG9WPda z4|7bKf)xbh71}7+lEGf5!v{%z4c|;x21@%H*#d}USvDI}7#MB_|46M7aatbkDL%Po z`9)^iJq|{yUOzq0w(fe2??d*zNrT2<7*%1NN}t|b24aVjhIVZEL8GH6evmUJJYb8{ zXCXyosEYc29T(u+QB~1AU{%=5XJ9yYO^!c;%9_5)s&c<#LQ}?ZVD8oVVX{Qo*aB-2 z4^(=2{gjbMHjy4u(<0$iFJyZUZvzW^3UzgEw<_@xnTqM8DbQyh#r^#Da8(8Bnx2>e*OuT#*XX^bax0BhWWHD^?k8+>W^KaTzm0<22lUsGQACI0Qhl@Z*{{ zgXoK!k_7WN5D{F+(;!Ya)C1+s#3qE=gJ{Fv9@K2O-Cnp%s}Le(ChDWbgxw!qubf_A z#v?-K5hUkLX;UyQR z72aI3vNp;sFtLU*sPjnHq)ot{dEAuIQm2esyIzHj-d!48bBp&F7)tZ;H2@kssi8GX z%4$y&W=YH%yEWK-oavmh;+@`6@gjs3C`fmS8T9&ei6ez?&;dt;z=CE;0Wxr-E{YX* zP$3`yl&F7tvrIN*nh1|5o-?Fbjk%z^^pnJ1b$ zeUswBftJ7<9;hB%r270;iJc{50^uE_nm-Xv9eM2nw*ujVXxZ#|WtLZU1w7BJ%24c4 z&=W*nqpzGK81LzV*`&?Ys@fS)ejb`Zs}5cvbS_q917gg~C$=4IjKr==R%tojkvkIX zg`1M~*$&ThaBi!FES_@xIzbo&j0#{wp}s}o)`i!3mE*xeimkywfY<5p+A#S@t(`rK zY&3cZ1P{mDk2VnXnzdsOSNHw;jn2V4nb3{zqHNv}TdANhl2}+G-P;1)4 zYgXAE=71Y$Fx$fHvb4lH*Y6*%zOvg;1I`Y}C5eHixzouk2yx6;mQiMjN01LHdXM5d zA^WNc-es~q>MDzEf?OAa^{4}hUX{y+Fvs`SN3YIp3Gq62!dUn>V`;w*8`Mk~*=$ot z34K*XcXl-~-?`%hn6p1u>Dld};LiSU)c;<8z(4`i{E<=+L;#CE??Y-r;5Xy*G_Q#rxqWD#4 zi87k{)AQt~Yf1Lgfqd<-f)|$rA+AGH)O1}l2K_uI7b+OLV;QEelivMI22uHBBdBINx}r z*W#pUh87lE(*Oe(q*3dyvXxC_3O9;7{k8x!hokAUZn{KwN-siA{h|PTn>8gKTiA~# ztQoJiaI(%>FJDchiqKc7s?7ax65J}Y)~2~gw-CYE!9Fwt3y^*vSx@n9ErQDH%T$7{ zS061wZhm*^urOU};?cTFV5K!t!RN(9^ez*t)33?sreZ|!p!GH$$-;ES-acGSKe~Rw z@E+8$zii6VfF?5*4IoEDug~oG`nL3V%f!M>LPjsi0Mf7NHADD1SD##V524kSj?N z%9Rry^wp?naZ@;xMyEcFCuXp->0XCYzjiU{4E?b?!6W@q%%3x-fB|}2 zW-wM{li?*CgxeKtMx3hUMjLIP9elG2(V+2X*@PK3&FyIwIQ^b1)_iH?K&2pY7lrWZ zdg?F^+n%$?v#_-}DykAnMx3_gP2w#f73rJM-&Z8%qS`t77L*BjO$L@{=Ja%Ex)n}) z<1bH^PNUa>Rcia}prib7*i)eKMZH7Mu)68%b>4S=p_1!K%2}kvW@WR{Jao}G~ z<%`RWC(vtI%E;X#gjd>#&?e#D9V^e+z984BSB`jxk5*%*v-Y}#CDc6V3T_dcho*=u zOrDlM>(d>YzhZgT+X3RArBxbHftKFU1pfwMFUboKGK)fc9yl_y8Rbtk9?;h-(NM1u zVJPbKb;vAUjTrzJ6L8T4M*gw=u0R^L99s;{iz;>)b(%4Ko+Zp$wD3WJVP60Nx;e}(LWS;fd*GLES^i?H zuL^DsIp@8|HuO*P<%5S1Em4{^g@@`8#kx&jjd2uuOP@^bbWhVRN=C2buN>0YuS`QK z!4iFqSi+!g#tmt7{=(VPNAXsN{w@opdS{wF5Filb)LEm)#bC3YNhvUW=ExF*-yK; zPi3(xoz^`S8+Vmut-f-GPzd_&YJ1a7;Jb?z!W&R3C;4F;DM>$mY7>`O#sHzWITA>c zCb)?>Fv~J+y(hfPYzha0s(VS8aHydu0U3gXg5TKJ>Wh41K>e_6AcLDF)iyssK5eTb z2?4Q9GS69&E*L?o1;IXrRP=*KHT9-KxeFIgwsX)HHMh_k=fm#q4b_4FTJio6Cu^q?EJby|d zFF}J!H!0Q)H`Q&Apl-!&KZmRxs}r~7N}Wk`sPdoWBFEC;!G{~;6&U!iD*H!blDuBQRm ze$BS~l{9baYsqRGA%h#_d>{r(rjL}t{B6~{Nez}%NPHdwhL~m|9=BYsw??@?9HKk2 zg8)4xvgIP&V~`qo28)>TrONbqqoWGjcg$s3JP^d*E`OGv+jc`B;=@t?s*0dcf=G#W zdhW0T5=0?FP;UMlJ}pWpf-Ou03EDO`gR|d+t-G{I5Ehlb$ylJ-Z;PY_xxw}o&Tk+V zbdCJ5#$Q(zgixNtJ*e69b+PWw;+16)%CrFhnE|rCvy_vFC3a6Q8%Ocj+L!x0x3*e9 zE$^$HX&E#sNzdC>)EQ15VGhaFlyLmQD?#;5I70=aOBObA>c%b&7rol<%k#9M%;5G0 z9aN~RG~}dRdKedQBZ$o?-U)I$e-tx>Uz^^*$v_TNR^nxFHDNn-!ZOMk8z%)qe90V~l8Ry;pLgbmXNtLOK3<)}ofi6ysu5Ora>Vjsz$G~puuY$ zsNd^M5>%MX#%GAIK9*(wB6|nP$g#kWc8<#Ds5zVqRMKp#<}zn}3Un7yKA44ZMh;M* zkTb9O*JJ+!RU9tPmv&p*N8)IaE`yjyM>|g--(53koY@SIT3>=^8E9+*uSs8@k2|^$ z#3D6Zg!+(yZHn0pSz;9u&Z=z<|92J|!rU>%mzfoh4S^HsBp=~x7qF^EHCQ0>-Te_2Hz7=~|BR+Fi=2O5D-hd>Y0 zU)1RUIbR(Bv!P;m<7TK4s=lYwsb2A9KiQe|C zsUNM<*~CVt)cUka@!TeCc?@cxpEuNl+Au26?O9>CO+|1@I+{^&;DcxDRIdgDXo?TzyUa*&Ys&Wl{6pKqJn`m!tIi{d4c-jIT#-xw`p>>d}PRJ5F7 zF`R~$4L5Z)M&g~6O{3mJah^CLfG=dWw>WqB{wUj)6>XY~;T>@7fR_vz`EE`UzK4=k z=)jr22=Ym*0~k9CgZTsp(lG{9Y8|xCiz+aqEg^KNsP;XGKRdm`f9L_+Z^jM9N3yfd!5e*9 zMa}F8n6R-Ji4u~XIzNTn1KRYG2cK2la_b0j=noOw=eg->%<>V>?NbAIY#>Hc zzSF(UJry=Tv=3Bwabh)1hNWcc;+bP&f z(!S%id6S*C`WkhEehJ*RGGq1Qb2v9JU|eqA(B=OE`u{Z?J|r4g9yFQ-3WGqOzB-iD z`f#3_t$Q&4pSsD4<$D9!$83mm06^OGnsC?gy< zJ327fDllWKtqd;@F^S^((!m;LYHUfPO-AO%wZ?J)XF$ z3IRpP0KzaDUu3MIPO_$MBfVFw~23(l8Fx zN}N|&w?`+o8OB+%bCho#*-4c+0}iGzcmY__cLgFQ@`b&RxsS_n6Dvd>omBZ?aTx%t z8q&aUvu?Hf26u ze>r)RI{TWY9w)0T$q|E_vip^@6~gR#w>HlYxsFnYZG-};D^z|nv3Ec~mW9L2kVjR$ zYa}*s3=_YnD#tHa?ues_zXNZW$M7Ske)?6CGUoio>D;~D)>D>27lVW4D2&#nV@De7 zYRDZTD0-aO`}FLpTwvbtp;WoG>E_Rwj`~H3ZpSb3s?R}k7+}8FS=d(YSTImxn8s~o zgf<(6-wd|yVc=`)ClZ~P4rfl3UhOgMW{do7RsP~R-1CYO;7%Tgr5#Wb#!;4OV}Pj% zOr!|X8fV(^PK_*IB@Rka%$O2tU_NWgl^nAZoGI#GG^c43u|T6EUu4cBI?U7Ux=FZ$ zE{&prFxG$$;wXfOu3`ekMV0STS7|0brl4^=v|b%3|LF@IhPD(tOnM5TT*N|O829fc*hH$D0E z#bM)JNJHirUFQ?6?j_k-)0?jgYUk)OMP*F?izIwdcWv?>@7rNuj-6-&Y9<&^v+u{~|^Q?N<601ycCejj;lmA&e+4F6Ve#kcKhJ7w5p~b*Kh`$2aGn)()R}nLmIVx z3?;D!|LJd-4S`*=7fZkn0`|SxKP}$YEAtm1DEzFjxyW9>2jv91vb}cjMIHv7i19M` zo=B30qI*mJ{IJal<{T!l24zeKkKNARl@Bl z`q3X!R6IQ2w+_@ZK=Jmwm{&>K`ti*P+qn}(liQ(1B+piX+wT54iHeLo5y*Ujn2RtE zx9+&FmtdvCfW;(@c5#Ewh{*GWy@$joE{!aMZ8JD&y@xg>vD^G@j0swGff21wxmkR_ zi#eg?gI$fzvoWu!rw*eHyc<~XC@W~@kDsG~k)q`}@<6j4ole=ffKle&q=%1a)6Y5m z%VZKAVsz)_5)(7LvPMN$U!^bs267+ss^iq@-IZhtgIWxZ3@d7PDejvPU@P;%!EUc4 zaC+#N4rMRpcyzSLCmf%EUDibe+miFDcXN3EJO)YTV;7#}^kx=!5+sz&@cc zLUGr_bfb(>t$&=3VxYu*#MWmCW=Gw0{{FkPUo79*1oI65QD*0kVD>0JW_Q3K$2KDf zVy!bDiw`UFWp)2QMi#$zsx`rTrR~EJb zm)D(zy?Xo3^m@_|qhv!zv=pRQ5-Kt0!FQyNSNS>9s-Gs=zv;@u#e$Xb>VRvT>Bx}? z|2>FXE(w9RY5QVg3qGqK`SXNF1dW@Ab5_&80RmJ07JU4jeix0$SGFii`TVURyCl6w z3dsTRUtr#*PjMI#FCKIyie(HwT@-oqT?|s^A-mT{Y|xoa{9d=~K{RF*M6a#;GF;?amkH2u8xcGC!udVK5DClJukltnoaw54`xd z;8xLi%A%!(eU3K~h{|-l*Tqcu6l(mX?QYz$Fi3(qH;w1N%ZR*Yh8Pc-J%t_yRY4Ph z*j4!NxQN<(h?`OZM`W=|VOlc0@!FsB~S(F(0whjTO12OYuW z2+bmKbGvF@J`Y6|D96P>Xa*maiiq8&gdhScKb#%j4n7pyUDcc(BK&V3P>xM){Mv~S zeL%9Z4a$fLqToD+5@O-{Mj1K2bdW=>!<-o#5{Yw+bz{h&>Hh4VHoZh=szxUcd~L9< zsS`B}_uI*qhTQbVK&ef~31e<6m=_fM`DGdtdLPM9LBJg8)k)ZZbuvraXj_o!5qXRR z$j8+Vd0pbjt_2PDY?ZTZd|5s-vD-sn6d&|U;%!wSyY=<;S3OsCDtrOm@NtRWlYAf z1nlp2IO`f6xTNtI<>RG`Rn|bFXpu2(ER}qWEC|&heBZNTTbw#TO=nx1l#TNhQ8?$Z z>N>VT-<%^Efnc?vYSVHxiJ7|fM42Xm(fJZaM!!CtIY`cpw{-0o_K}3p3}y|gw}aH{ z(%_c4^RnxRU6Vw1bW%O2k)Eu|wQsUjB0j23gA}>!d4bUYbX-M>mw+r*Rq-C0NpPbM zV$I)&=yZ{^!Sqg(0lS2;eCy~Z4C%=79hd-* zPSsy08~r-tTG4fxZaqYF!P$nJ^$I%GJU~3b9&~>HB5Y`0j;7{#l{DLH*5Z&yqJb66 ztU4^mAL%t3Oofwh0Rke@hd?EUT4_dpo^pe3B|5nYF^hVjsZgfp)ep7f)dcD@N*V}@ zMPH?un{ln2CH#H|<;Xk$ttFVXaQcsUk}o(4Ct&k2Ml$^wW`&T>$^nQm2=U;(+A!dN zYBI5NhQLnk%S#ryq>!+cpcH>uiQ~mv+F(ek-hX+$=uE%Pafk>QxoVR|v;AZK*iP|c5d8q8H<;&+ zykCF4L;{lcB4?QVc2zR$(4#sGrB@mtM1 zO=0bAUSNmURAmt=^KRvP+7S9sGFjM&Q#K&IX<6`j(g@0K%{rws>{=11y50^{h(qIepq-lJDTS%lj-pzs5?Ks)^ z{W4^fozI-DO*+10MNawvY1xwJUZOW`E0_~KOE5M=C1hul`yxx270dpIf3+sZuD?O) zptPdC*{lSY?u0hHi}5=i`IG-;ulV;B#2 z&n>($VbQT`4%Sph`TpTj-St~+|PMySS*vn>03uX z>rz0Tn+LE%9CT5p`x_p|*IU$w08wER@x|VmpZrG$eiVJyoM!n6f`VT>$0;JyHeUWj z`o6g7FWwqjcp1tsHIp4Rk6)eGVn?T&{Num+EqZ&oeOJY0Rp+Y=nbbS( zEDgldld|f^3}9!oJd z@5y)R(@T&_yn=?&h2wmc-XFlspPJ!(VgiM^W9e?Xhp-)) zbmco3WJAE1HjChA^_K95b;6JdYanjbRy=3^067R~a}VPo7{tXFDfaFu!}%n%^JHuc zD2R|M=RGq?a}zINX&PO4aLXzuUj?b(QxKNl?u}j`5_-~xa&Ha<`U8da=#;=NhG)2`#?#x&_eSwti=?mv06|>F>@+0y+(1HyW%iDPj4-{q} zA*b0*m|z5bjyJW?XP3+qUBI9rlNaybBk_deu9M%Ob@S8EZw`10x=H>2+BtjW#LX}Y zO9IbG&`83b6{+-RkG*L?$h+ANVZ573m9@Qn;>m>|w&AZ3n?^r)22!C9k=b-0WXI(q zLnf`Ua|7!;*i2?}xQi5V$BY}nC+U69InU9c+)3I`*m@QnT1c@5s;)C#Jjci|^xRE{ z{g4cPFq`f~lKTig3NX&G{`s*b4UOQ=e z;j7^vUC6V7%g&>l4y)?5OqV2FOP00%+V_=eVN5JohRG@~7@zI}>*RcQ4zxr*hG(VJ zjs7P2s-`0(!Ch*N2OqSC&_Gi~QM_Qhv}4$JL;@b4bmh!LTzZXYHRi$0Q<6jVfi6I_ z;h7nxx)8>#*&yp>mpT&xVk>)d4275j0|8(LW`4<*Ps;*AILJ3a(~!g&=b*?&oUg7Z zt=SHXaMKW)7ARRa`cpK8spy|j>tGdfX0s}W@xtfe&&?t%4B-knhe`p?8@61skkxFP zUamX~lW#(vt2mdOrBk>Qev7)+F~%b>TSKx9Z-acT#t3ZMaiBD0b>D{I06r$nQ+0G#OyW2v{KyF6?R8YdkAjhsSt9%< z@8FK2Y)e4=5sNHBx0{KDRu^ZI=Ei{As@Vp|8S)-IiwEpQVHC%f*+>W5K-g>zX`0BvM&0yvsbQO@ddF#?6ihGRMrT$+-Mz?FVMwQR6k7bisum32?kgbnzw|w0Z$hz_SY}b*VB{D z48CE(Vp@x3PwYPoiVolqH@*rF)ptmG+PIGn>m!^tf+bN#=YXq(ATmoBpueCcpz6pa|hHa$uB>Kn%4Ad=#iBV55*W1cXwTO{){&I+_*pE}PvX2U%PwhIdLuQ;2-f?TR4X1I0l=6= z65^*2-|-%Hv3#3M$7_d#!Zaf)wSp@d&LWVpk^SY%hN_AGL}*rE;#J$g!yu7hF1twc UP!6gOpHVE!w*LS7&wK;_0whkDivR!s literal 0 HcmV?d00001 diff --git a/library/core/src/test/assets/wav/sample_ima_adpcm.wav.0.dump b/library/core/src/test/assets/wav/sample_ima_adpcm.wav.0.dump new file mode 100644 index 0000000000..a16ad68dfa --- /dev/null +++ b/library/core/src/test/assets/wav/sample_ima_adpcm.wav.0.dump @@ -0,0 +1,75 @@ +seekMap: + isSeekable = true + duration = 1018185 + getPosition(0) = [[timeUs=0, position=94]] +numberOfTracks = 1 +track 0: + format: + bitrate = 177004 + id = null + containerMimeType = null + sampleMimeType = audio/raw + maxInputSize = 8820 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 1 + sampleRate = 44100 + pcmEncoding = 2 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = null + drmInitData = - + metadata = null + initializationData: + total output bytes = 89804 + sample count = 11 + sample 0: + time = 0 + flags = 1 + data = length 8820, hash E90A457C + sample 1: + time = 100000 + flags = 1 + data = length 8820, hash EA798370 + sample 2: + time = 200000 + flags = 1 + data = length 8820, hash A57ED989 + sample 3: + time = 300000 + flags = 1 + data = length 8820, hash 8B681816 + sample 4: + time = 400000 + flags = 1 + data = length 8820, hash 48177BEB + sample 5: + time = 500000 + flags = 1 + data = length 8820, hash 70197776 + sample 6: + time = 600000 + flags = 1 + data = length 8820, hash DB4A4704 + sample 7: + time = 700000 + flags = 1 + data = length 8820, hash 84A525D0 + sample 8: + time = 800000 + flags = 1 + data = length 8820, hash 197A4377 + sample 9: + time = 900000 + flags = 1 + data = length 8820, hash 6982BC91 + sample 10: + time = 1000000 + flags = 1 + data = length 1604, hash 3DED68ED +tracksEnded = true diff --git a/library/core/src/test/assets/wav/sample_ima_adpcm.wav.1.dump b/library/core/src/test/assets/wav/sample_ima_adpcm.wav.1.dump new file mode 100644 index 0000000000..3eb13e82bf --- /dev/null +++ b/library/core/src/test/assets/wav/sample_ima_adpcm.wav.1.dump @@ -0,0 +1,59 @@ +seekMap: + isSeekable = true + duration = 1018185 + getPosition(0) = [[timeUs=0, position=94]] +numberOfTracks = 1 +track 0: + format: + bitrate = 177004 + id = null + containerMimeType = null + sampleMimeType = audio/raw + maxInputSize = 8820 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 1 + sampleRate = 44100 + pcmEncoding = 2 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = null + drmInitData = - + metadata = null + initializationData: + total output bytes = 61230 + sample count = 7 + sample 0: + time = 339395 + flags = 1 + data = length 8820, hash 25FCA092 + sample 1: + time = 439395 + flags = 1 + data = length 8820, hash 9400B4BE + sample 2: + time = 539395 + flags = 1 + data = length 8820, hash 5BA7E45D + sample 3: + time = 639395 + flags = 1 + data = length 8820, hash 5AC42905 + sample 4: + time = 739395 + flags = 1 + data = length 8820, hash D57059C + sample 5: + time = 839395 + flags = 1 + data = length 8820, hash DEF5C480 + sample 6: + time = 939395 + flags = 1 + data = length 8310, hash 10B3FC93 +tracksEnded = true diff --git a/library/core/src/test/assets/wav/sample_ima_adpcm.wav.2.dump b/library/core/src/test/assets/wav/sample_ima_adpcm.wav.2.dump new file mode 100644 index 0000000000..bef16523d4 --- /dev/null +++ b/library/core/src/test/assets/wav/sample_ima_adpcm.wav.2.dump @@ -0,0 +1,47 @@ +seekMap: + isSeekable = true + duration = 1018185 + getPosition(0) = [[timeUs=0, position=94]] +numberOfTracks = 1 +track 0: + format: + bitrate = 177004 + id = null + containerMimeType = null + sampleMimeType = audio/raw + maxInputSize = 8820 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 1 + sampleRate = 44100 + pcmEncoding = 2 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = null + drmInitData = - + metadata = null + initializationData: + total output bytes = 32656 + sample count = 4 + sample 0: + time = 678790 + flags = 1 + data = length 8820, hash DB7FF64C + sample 1: + time = 778790 + flags = 1 + data = length 8820, hash B895DFDC + sample 2: + time = 878790 + flags = 1 + data = length 8820, hash E3AB416D + sample 3: + time = 978790 + flags = 1 + data = length 6196, hash E27E175A +tracksEnded = true diff --git a/library/core/src/test/assets/wav/sample_ima_adpcm.wav.3.dump b/library/core/src/test/assets/wav/sample_ima_adpcm.wav.3.dump new file mode 100644 index 0000000000..085fe5e592 --- /dev/null +++ b/library/core/src/test/assets/wav/sample_ima_adpcm.wav.3.dump @@ -0,0 +1,35 @@ +seekMap: + isSeekable = true + duration = 1018185 + getPosition(0) = [[timeUs=0, position=94]] +numberOfTracks = 1 +track 0: + format: + bitrate = 177004 + id = null + containerMimeType = null + sampleMimeType = audio/raw + maxInputSize = 8820 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 1 + sampleRate = 44100 + pcmEncoding = 2 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = null + drmInitData = - + metadata = null + initializationData: + total output bytes = 4082 + sample count = 1 + sample 0: + time = 1018185 + flags = 1 + data = length 4082, hash 4CB1A490 +tracksEnded = true 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 c617b672e2..7f9549ea75 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 @@ -28,4 +28,9 @@ public final class WavExtractorTest { public void testSample() throws Exception { ExtractorAsserts.assertBehavior(WavExtractor::new, "wav/sample.wav"); } + + @Test + public void testSampleImaAdpcm() throws Exception { + ExtractorAsserts.assertBehavior(WavExtractor::new, "wav/sample_ima_adpcm.wav"); + } } From a67394b2308d1d65a4187f81280b8682d084410c Mon Sep 17 00:00:00 2001 From: ybai001 Date: Mon, 6 Jan 2020 15:21:43 +0800 Subject: [PATCH 10/44] Optimize AC-4 code and add related test cases -- Optimize Mp4Extractor for AC-4 -- Optimize FragmentedMp4Extractor for AC-4 -- Add test case for AC-4 in MP4 -- Add test case for AC-4 in Fragmented MP4 --- .../android/exoplayer2/audio/Ac4Util.java | 6 + .../extractor/mp4/FragmentedMp4Extractor.java | 20 ++-- .../extractor/mp4/Mp4Extractor.java | 19 ++-- .../core/src/test/assets/mp4/sample_ac4.mp4 | Bin 0 -> 8238 bytes .../src/test/assets/mp4/sample_ac4.mp4.0.dump | 107 ++++++++++++++++++ .../src/test/assets/mp4/sample_ac4.mp4.1.dump | 107 ++++++++++++++++++ .../src/test/assets/mp4/sample_ac4.mp4.2.dump | 107 ++++++++++++++++++ .../src/test/assets/mp4/sample_ac4.mp4.3.dump | 107 ++++++++++++++++++ .../test/assets/mp4/sample_ac4_fragmented.mp4 | Bin 0 -> 8404 bytes .../mp4/sample_ac4_fragmented.mp4.0.dump | 107 ++++++++++++++++++ .../mp4/sample_ac4_fragmented.mp4.1.dump | 83 ++++++++++++++ .../mp4/sample_ac4_fragmented.mp4.2.dump | 59 ++++++++++ .../mp4/sample_ac4_fragmented.mp4.3.dump | 35 ++++++ .../mp4/FragmentedMp4ExtractorTest.java | 6 + .../extractor/mp4/Mp4ExtractorTest.java | 5 + 15 files changed, 744 insertions(+), 24 deletions(-) create mode 100644 library/core/src/test/assets/mp4/sample_ac4.mp4 create mode 100644 library/core/src/test/assets/mp4/sample_ac4.mp4.0.dump create mode 100644 library/core/src/test/assets/mp4/sample_ac4.mp4.1.dump create mode 100644 library/core/src/test/assets/mp4/sample_ac4.mp4.2.dump create mode 100644 library/core/src/test/assets/mp4/sample_ac4.mp4.3.dump create mode 100644 library/core/src/test/assets/mp4/sample_ac4_fragmented.mp4 create mode 100644 library/core/src/test/assets/mp4/sample_ac4_fragmented.mp4.0.dump create mode 100644 library/core/src/test/assets/mp4/sample_ac4_fragmented.mp4.1.dump create mode 100644 library/core/src/test/assets/mp4/sample_ac4_fragmented.mp4.2.dump create mode 100644 library/core/src/test/assets/mp4/sample_ac4_fragmented.mp4.3.dump diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac4Util.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac4Util.java index c54e3844a3..f3b3a8fc1d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac4Util.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac4Util.java @@ -57,6 +57,12 @@ public final class Ac4Util { /** The channel count of AC-4 stream. */ // TODO: Parse AC-4 stream channel count. private static final int CHANNEL_COUNT_2 = 2; + /** + * The AC-4 sync frame header size for extractor. + * The 7 bytes are 0xAC, 0x40, 0xFF, 0xFF, sizeByte1, sizeByte2, sizeByte3. + * See ETSI TS 103 190-1 V1.3.1, Annex G + */ + public static final int SAMPLE_HEADER_SIZE = 7; /** * The header size for AC-4 parser. Only needs to be as big as we need to read, not the full * header size. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java index 1172f8665a..967f71b328 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java @@ -168,7 +168,6 @@ public class FragmentedMp4Extractor implements Extractor { private int sampleBytesWritten; private int sampleCurrentNalBytesRemaining; private boolean processSeiNalUnitPayload; - private boolean isAc4HeaderRequired; // Extractor output. @MonotonicNonNull private ExtractorOutput extractorOutput; @@ -302,7 +301,6 @@ public class FragmentedMp4Extractor implements Extractor { pendingMetadataSampleBytes = 0; pendingSeekTimeUs = timeUs; containerAtoms.clear(); - isAc4HeaderRequired = false; enterReadingAtomHeaderState(); } @@ -1267,12 +1265,18 @@ public class FragmentedMp4Extractor implements Extractor { sampleSize -= Atom.HEADER_SIZE; input.skipFully(Atom.HEADER_SIZE); } + boolean isAc4HeaderRequired = + MimeTypes.AUDIO_AC4.equals(currentTrackBundle.track.format.sampleMimeType); sampleBytesWritten = currentTrackBundle.outputSampleEncryptionData(); sampleSize += sampleBytesWritten; + if (isAc4HeaderRequired) { + Ac4Util.getAc4SampleHeader(sampleSize, scratch); + currentTrackBundle.output.sampleData(scratch, Ac4Util.SAMPLE_HEADER_SIZE); + sampleBytesWritten += Ac4Util.SAMPLE_HEADER_SIZE; + sampleSize += Ac4Util.SAMPLE_HEADER_SIZE; + } parserState = STATE_READING_SAMPLE_CONTINUE; sampleCurrentNalBytesRemaining = 0; - isAc4HeaderRequired = - MimeTypes.AUDIO_AC4.equals(currentTrackBundle.track.format.sampleMimeType); } TrackFragment fragment = currentTrackBundle.fragment; @@ -1337,14 +1341,6 @@ public class FragmentedMp4Extractor implements Extractor { } } } else { - if (isAc4HeaderRequired) { - Ac4Util.getAc4SampleHeader(sampleSize, scratch); - int length = scratch.limit(); - output.sampleData(scratch, length); - sampleSize += length; - sampleBytesWritten += length; - isAc4HeaderRequired = false; - } while (sampleBytesWritten < sampleSize) { int writtenBytes = output.sampleData(input, sampleSize - sampleBytesWritten, false); sampleBytesWritten += writtenBytes; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java index 971cc27d13..651db26b5e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java @@ -112,7 +112,6 @@ public final class Mp4Extractor implements Extractor, SeekMap { private int sampleTrackIndex; private int sampleBytesWritten; private int sampleCurrentNalBytesRemaining; - private boolean isAc4HeaderRequired; // Extractor outputs. @MonotonicNonNull private ExtractorOutput extractorOutput; @@ -162,7 +161,6 @@ public final class Mp4Extractor implements Extractor, SeekMap { sampleTrackIndex = C.INDEX_UNSET; sampleBytesWritten = 0; sampleCurrentNalBytesRemaining = 0; - isAc4HeaderRequired = false; if (position == 0) { enterReadingAtomHeaderState(); } else if (tracks != null) { @@ -507,8 +505,6 @@ public final class Mp4Extractor implements Extractor, SeekMap { if (sampleTrackIndex == C.INDEX_UNSET) { return RESULT_END_OF_INPUT; } - isAc4HeaderRequired = - MimeTypes.AUDIO_AC4.equals(tracks[sampleTrackIndex].track.format.sampleMimeType); } Mp4Track track = tracks[sampleTrackIndex]; TrackOutput trackOutput = track.trackOutput; @@ -527,6 +523,13 @@ public final class Mp4Extractor implements Extractor, SeekMap { sampleSize -= Atom.HEADER_SIZE; } input.skipFully((int) skipAmount); + if (MimeTypes.AUDIO_AC4.equals(track.track.format.sampleMimeType)) { + Ac4Util.getAc4SampleHeader(sampleSize, scratch); + int length = scratch.limit(); + trackOutput.sampleData(scratch, length); + sampleSize += length; + sampleBytesWritten += length; + } if (track.track.nalUnitLengthFieldLength != 0) { // Zero the top three bytes of the array that we'll use to decode nal unit lengths, in case // they're only 1 or 2 bytes long. @@ -562,14 +565,6 @@ public final class Mp4Extractor implements Extractor, SeekMap { } } } else { - if (isAc4HeaderRequired) { - Ac4Util.getAc4SampleHeader(sampleSize, scratch); - int length = scratch.limit(); - trackOutput.sampleData(scratch, length); - sampleSize += length; - sampleBytesWritten += length; - isAc4HeaderRequired = false; - } while (sampleBytesWritten < sampleSize) { int writtenBytes = trackOutput.sampleData(input, sampleSize - sampleBytesWritten, false); sampleBytesWritten += writtenBytes; diff --git a/library/core/src/test/assets/mp4/sample_ac4.mp4 b/library/core/src/test/assets/mp4/sample_ac4.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..d649632c745ef055934dda90e750e1b99f991593 GIT binary patch literal 8238 zcmeHMc{r6_*S}36PLElMDDzAXk1;1xND_`AGM-~Tren%@l7z>Usgxp*!Vxl8#zLer zlSrsY9b?FR?)Sbcz2DdSz1Q`8SKs^3_q48STWhbq?zQ**TWhaZ^i z!vFwKz}yw@g#!S}L7bOY0HpK41-M`!4h$>$9fun@oS%?E(@^7q`4VGj2(d@lB$ z7!NFLL=lB^^>l&+COqz|SJ(<70nArDH^vtWkHZA7gVEpDgYTMVR1u;U)s<9AO1SP&B{jXuol|zw4W9 z2iXNf@(w{hDO~;+Z5nb9%|~ejJyQ%dXQ)L&&Fd=%xbc3Dko<+HAiK{Hl$`*;VGaO1 zC>+M#ud#D7i;q$axIA7)-7!jG6lonfv<6(i!*$z=zCAhDf;oLz>@*4x@Y>!(fy#j( zIsyQp0L800AZM6P7sFy?r!Y(`aw86)(YPwK*#MeIB#yWdi-|>OG`e~KZG|=?5({=- z@9)bA2i`$AYZExfWg(manFH3;Q05#1a*Qa(VN{UR1epDzpG1Hl1>=kYc5v4D0cYSi zUo0gRwDbQfk$-a1zv1JABLWc4I^Xk|`$wF0|AwIa2VR+Ai20Sz{{dWobV;y*asHLh zKjQL#-6wd$^_l;BeKv&a1~+^T0y)4MK(PQ%_^U?$H0*@%U-kK?LHr922#LSy^Iw4J zr%ebqe%0rn2JtUAAe6xMS@8RJUCGz)I?A2jDNt?xlM=xi%^*;1u-}Yzqy;6%GP`EK zPq5ssx_zT>c)G_3IT86}_mkdoN-l%cHsH+ri6~6QlUokYd9{3~wdcEeJXT_D9rgAKp0Shn zdoWSeVbZi8(U>O?;bYGzj?M-@p4#X&BxfdGvuwAplZ@x zZpFUe)}#NdOJq`a;*dzy37OyOSM5#d%`89$%oR4;8p`+xF_<++X^p;jY%K*zPsn9V z>LnSARyVhQ>uE?ru_yn*#*{1}jP%SYo4i%vWy6phVdFwgPxftX+$-4{P|>s8<}&l9tie+qRgsS$bb|1cq*ra|YzwgclxhS@+v6 zIwq=Cb!w=qCoUzYg;4@Bof3z}K0f6cea*ltE{()jN4$QZED-n>%pX0B0;s@<>EEZo z-m!$nRdPb3F!|5ru8oLpKo=l%!gWjPdz^>hI7iuHF?s39GRoX%mMK1zpkN((-?uI6>|Um~w<{aki>z9T9_!u=eleIJj?f3)s%DHT zOo0)Xn7a|Z8}TQ};4FfRPv1$D+f)lBb1tNI(rr#(TQT0-Nm;OiCQJ27#{FX*<7*lW zMnqO&@U%UwJgsP^7C zhHkR-n$-!v=bAU%N-$xZYxl*b2mw!;UQ(cCNs0;vXX=IOs2fFAozx_=Ov+{Q=HKm= z=5iJ;`0Q)e75B(bvE^VY-c6WtZM48`pS!~Zf13G9lSszq7ITPbt`wNgVto1?wGcwI1ODE z03eLQ31G960LTC+0jNE#@zldlNK?tilfqA;)wXhuC+QG^%#zt@;|)wOQ}h?>4=^cE#3! z#CFw5?6(i;6|4r2ZieQX*PZe(_2AVwpLmHsGIFbx39XEDV`tqb@c}iB7O?Kh;-;gp zY^sSnstmBB2x)xMHc~@Q^MboCHXRUV<$6Cn*?M+rWfy3v6Yjn7rl;%U3r#K(k)6oXpVlmem^0BVqAdst~^KU zc(Q{Vl8WzKw8%M!-OS89YC0c2%FqE+h?y-gyEXzOFRg~G%GHFC_k(BFR{}O)HZE;P zlnymI&jc5bMP!U^)~>8Y0dPT?g%9Z6JNYCh1LYItpe@!Rl7APS5W975Vh8nwX7baI zE6){1JkqU5B$BvjUHYIuj?UcNr7Q1k0Id^6GxowCLas5@+D|>WMbzfsBe>{gHCH}U zMMtGNM3dj!dTGov=VQXvRh@Kdgp890>i1lGJ~~qDe%?YPfKPNL|$EdA3y;u69S1Vl_5J7ZjOU9U&a$u7}kD{aLFKTK}X zzPKXfz`6=%3%;M09<}pD^+X*tW}3ctG87S2imedtx{jOf(9$5__^JDK31L;nV&|vF zL~(jSrRWP2vFn(pEGL|AH)IQox*Qq2tFM15g6O_3Gv?Uv==!|ZisB0lEpbvL#_yjMzL{3z5PVZIkK-0wm1O^_!q~QCKuOn2{Y}5hW$XNs z4lJ_LPqdDZ#~FHKJBd!ntqZFeFr*_O2F=O{7tqRXV;DKO_<-UuP^N7hBDxl`wSq)@9kYH<}+?TC-=Rx0%R_ zZSr)1wkFJx$DMLt#KycmUXYhGdou|gaW>m+IXGhNZX*EzHWN=aI)zoAY}|(T#5n|9 zV?}rN#NlBW<^o&CdH9m|Gr%R|5@|Q^hS6d-OV&(ibLQ0m(lw)u38`K;kC7P78iJ!< z5Le~pBMMJ7`+Z}q(}ZwXhX5g?Q7)2yLZK&^{f1{ldZc7=KU3!Iob>79T$@uTEUTp2 z3{n>3Lv#%gn@*n}&LP>Xuu`@{g0*YYUUgd90BU@5vYVC zaL3kh^=kwUowV=INT&KulxkkKH zmP68Y4Bjg*kd#eK+Tu8jYJEeOmwrCCv9?izxtiK?V24$n(X}nu`?<%6VImKsa$pC z%}S9M%T@BOloac*DrDhKGo$EZ@(ZE8CJ|QJ}_^1zt=&t%XV>SWb0!fJoP?vw<_O;k)6^%2}Rz z$9Fv_@FqxV&z3AQ*4(!pv7RVa+LQxq@hb!keMxvp_z*bp9qw-lkHzRQBpYzm7^I1^ z947ViP8}MHB2%fK=!%G)^yHE2D-XT6P#-A@CM}HVC8+vMnn|5GdYzh(^M{Z7?dVS? zR^-%23}wj16t_mRxr<_?stS&wgqY%Z?Yv<(rTY&LRy)r0(O<8%Ew#lBkW_l0cM>XUpP5;hPl zky>Lj?!6;KO9ev`+A774w0uYnh~pO4zV}G}p?i}TqqOY% b-oeDizO2Xc2Qp{?8omZ)=pUE=<}UId;b<$Z literal 0 HcmV?d00001 diff --git a/library/core/src/test/assets/mp4/sample_ac4.mp4.0.dump b/library/core/src/test/assets/mp4/sample_ac4.mp4.0.dump new file mode 100644 index 0000000000..92ba157e3f --- /dev/null +++ b/library/core/src/test/assets/mp4/sample_ac4.mp4.0.dump @@ -0,0 +1,107 @@ +seekMap: + isSeekable = true + duration = 760000 + getPosition(0) = [[timeUs=0, position=758]] +numberOfTracks = 1 +track 0: + format: + bitrate = -1 + id = 1 + containerMimeType = null + sampleMimeType = audio/ac4 + maxInputSize = 622 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = und + drmInitData = - + metadata = null + initializationData: + total output bytes = 7613 + sample count = 19 + sample 0: + time = 0 + flags = 1 + data = length 367, hash D2762FA + sample 1: + time = 40000 + flags = 0 + data = length 367, hash BDD3224A + sample 2: + time = 80000 + flags = 0 + data = length 367, hash 9302227B + sample 3: + time = 120000 + flags = 0 + data = length 367, hash 72996003 + sample 4: + time = 160000 + flags = 0 + data = length 367, hash 88AE5A1B + sample 5: + time = 200000 + flags = 0 + data = length 367, hash E5346FE3 + sample 6: + time = 240000 + flags = 0 + data = length 367, hash CE558362 + sample 7: + time = 280000 + flags = 0 + data = length 367, hash 51AD3043 + sample 8: + time = 320000 + flags = 0 + data = length 367, hash EB72E95B + sample 9: + time = 360000 + flags = 0 + data = length 367, hash 47F8FF23 + sample 10: + time = 400000 + flags = 0 + data = length 367, hash 8133883D + sample 11: + time = 440000 + flags = 0 + data = length 495, hash E14BDFEE + sample 12: + time = 480000 + flags = 0 + data = length 520, hash FEE56928 + sample 13: + time = 519999 + flags = 0 + data = length 599, hash 41F496C5 + sample 14: + time = 560000 + flags = 0 + data = length 436, hash 76D6404 + sample 15: + time = 600000 + flags = 0 + data = length 366, hash 56D49D4D + sample 16: + time = 640000 + flags = 0 + data = length 393, hash 822FC8 + sample 17: + time = 680000 + flags = 0 + data = length 374, hash FA8AE217 + sample 18: + time = 720000 + flags = 536870912 + data = length 393, hash 8506A1B +tracksEnded = true diff --git a/library/core/src/test/assets/mp4/sample_ac4.mp4.1.dump b/library/core/src/test/assets/mp4/sample_ac4.mp4.1.dump new file mode 100644 index 0000000000..92ba157e3f --- /dev/null +++ b/library/core/src/test/assets/mp4/sample_ac4.mp4.1.dump @@ -0,0 +1,107 @@ +seekMap: + isSeekable = true + duration = 760000 + getPosition(0) = [[timeUs=0, position=758]] +numberOfTracks = 1 +track 0: + format: + bitrate = -1 + id = 1 + containerMimeType = null + sampleMimeType = audio/ac4 + maxInputSize = 622 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = und + drmInitData = - + metadata = null + initializationData: + total output bytes = 7613 + sample count = 19 + sample 0: + time = 0 + flags = 1 + data = length 367, hash D2762FA + sample 1: + time = 40000 + flags = 0 + data = length 367, hash BDD3224A + sample 2: + time = 80000 + flags = 0 + data = length 367, hash 9302227B + sample 3: + time = 120000 + flags = 0 + data = length 367, hash 72996003 + sample 4: + time = 160000 + flags = 0 + data = length 367, hash 88AE5A1B + sample 5: + time = 200000 + flags = 0 + data = length 367, hash E5346FE3 + sample 6: + time = 240000 + flags = 0 + data = length 367, hash CE558362 + sample 7: + time = 280000 + flags = 0 + data = length 367, hash 51AD3043 + sample 8: + time = 320000 + flags = 0 + data = length 367, hash EB72E95B + sample 9: + time = 360000 + flags = 0 + data = length 367, hash 47F8FF23 + sample 10: + time = 400000 + flags = 0 + data = length 367, hash 8133883D + sample 11: + time = 440000 + flags = 0 + data = length 495, hash E14BDFEE + sample 12: + time = 480000 + flags = 0 + data = length 520, hash FEE56928 + sample 13: + time = 519999 + flags = 0 + data = length 599, hash 41F496C5 + sample 14: + time = 560000 + flags = 0 + data = length 436, hash 76D6404 + sample 15: + time = 600000 + flags = 0 + data = length 366, hash 56D49D4D + sample 16: + time = 640000 + flags = 0 + data = length 393, hash 822FC8 + sample 17: + time = 680000 + flags = 0 + data = length 374, hash FA8AE217 + sample 18: + time = 720000 + flags = 536870912 + data = length 393, hash 8506A1B +tracksEnded = true diff --git a/library/core/src/test/assets/mp4/sample_ac4.mp4.2.dump b/library/core/src/test/assets/mp4/sample_ac4.mp4.2.dump new file mode 100644 index 0000000000..92ba157e3f --- /dev/null +++ b/library/core/src/test/assets/mp4/sample_ac4.mp4.2.dump @@ -0,0 +1,107 @@ +seekMap: + isSeekable = true + duration = 760000 + getPosition(0) = [[timeUs=0, position=758]] +numberOfTracks = 1 +track 0: + format: + bitrate = -1 + id = 1 + containerMimeType = null + sampleMimeType = audio/ac4 + maxInputSize = 622 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = und + drmInitData = - + metadata = null + initializationData: + total output bytes = 7613 + sample count = 19 + sample 0: + time = 0 + flags = 1 + data = length 367, hash D2762FA + sample 1: + time = 40000 + flags = 0 + data = length 367, hash BDD3224A + sample 2: + time = 80000 + flags = 0 + data = length 367, hash 9302227B + sample 3: + time = 120000 + flags = 0 + data = length 367, hash 72996003 + sample 4: + time = 160000 + flags = 0 + data = length 367, hash 88AE5A1B + sample 5: + time = 200000 + flags = 0 + data = length 367, hash E5346FE3 + sample 6: + time = 240000 + flags = 0 + data = length 367, hash CE558362 + sample 7: + time = 280000 + flags = 0 + data = length 367, hash 51AD3043 + sample 8: + time = 320000 + flags = 0 + data = length 367, hash EB72E95B + sample 9: + time = 360000 + flags = 0 + data = length 367, hash 47F8FF23 + sample 10: + time = 400000 + flags = 0 + data = length 367, hash 8133883D + sample 11: + time = 440000 + flags = 0 + data = length 495, hash E14BDFEE + sample 12: + time = 480000 + flags = 0 + data = length 520, hash FEE56928 + sample 13: + time = 519999 + flags = 0 + data = length 599, hash 41F496C5 + sample 14: + time = 560000 + flags = 0 + data = length 436, hash 76D6404 + sample 15: + time = 600000 + flags = 0 + data = length 366, hash 56D49D4D + sample 16: + time = 640000 + flags = 0 + data = length 393, hash 822FC8 + sample 17: + time = 680000 + flags = 0 + data = length 374, hash FA8AE217 + sample 18: + time = 720000 + flags = 536870912 + data = length 393, hash 8506A1B +tracksEnded = true diff --git a/library/core/src/test/assets/mp4/sample_ac4.mp4.3.dump b/library/core/src/test/assets/mp4/sample_ac4.mp4.3.dump new file mode 100644 index 0000000000..92ba157e3f --- /dev/null +++ b/library/core/src/test/assets/mp4/sample_ac4.mp4.3.dump @@ -0,0 +1,107 @@ +seekMap: + isSeekable = true + duration = 760000 + getPosition(0) = [[timeUs=0, position=758]] +numberOfTracks = 1 +track 0: + format: + bitrate = -1 + id = 1 + containerMimeType = null + sampleMimeType = audio/ac4 + maxInputSize = 622 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = und + drmInitData = - + metadata = null + initializationData: + total output bytes = 7613 + sample count = 19 + sample 0: + time = 0 + flags = 1 + data = length 367, hash D2762FA + sample 1: + time = 40000 + flags = 0 + data = length 367, hash BDD3224A + sample 2: + time = 80000 + flags = 0 + data = length 367, hash 9302227B + sample 3: + time = 120000 + flags = 0 + data = length 367, hash 72996003 + sample 4: + time = 160000 + flags = 0 + data = length 367, hash 88AE5A1B + sample 5: + time = 200000 + flags = 0 + data = length 367, hash E5346FE3 + sample 6: + time = 240000 + flags = 0 + data = length 367, hash CE558362 + sample 7: + time = 280000 + flags = 0 + data = length 367, hash 51AD3043 + sample 8: + time = 320000 + flags = 0 + data = length 367, hash EB72E95B + sample 9: + time = 360000 + flags = 0 + data = length 367, hash 47F8FF23 + sample 10: + time = 400000 + flags = 0 + data = length 367, hash 8133883D + sample 11: + time = 440000 + flags = 0 + data = length 495, hash E14BDFEE + sample 12: + time = 480000 + flags = 0 + data = length 520, hash FEE56928 + sample 13: + time = 519999 + flags = 0 + data = length 599, hash 41F496C5 + sample 14: + time = 560000 + flags = 0 + data = length 436, hash 76D6404 + sample 15: + time = 600000 + flags = 0 + data = length 366, hash 56D49D4D + sample 16: + time = 640000 + flags = 0 + data = length 393, hash 822FC8 + sample 17: + time = 680000 + flags = 0 + data = length 374, hash FA8AE217 + sample 18: + time = 720000 + flags = 536870912 + data = length 393, hash 8506A1B +tracksEnded = true diff --git a/library/core/src/test/assets/mp4/sample_ac4_fragmented.mp4 b/library/core/src/test/assets/mp4/sample_ac4_fragmented.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..2056348768808517cbbf07c417c230d701eb9d35 GIT binary patch literal 8404 zcmeHMc|29y+ushNI9;=wh%!$hhigb?AxSufDAPISV>ss+%5;*1YsyqgQCH!JGFON~ zq%u>HP?f(Zzu>QsKY%X-pLDUwkLXRjLGyD(XZN|asNz+4d!>_{Lkakp>Y@wCrE`5 z4)bMQf2yk{!DV#g21m&q4WA;Qm3m2<8i)AA`rj?Qnvp%^HCB7W!JQArPItp|T%=NZ0`UE8o${MHU5>;q>QH z5da&^_%dZ+KgP)gC6tTMf>?qOmcs}RDhZkTnT~aq;~NPX!l<4TkwR?B=>R3?)RwZ}4GJcH<>$kVs1YQojW`K6`ohByzZ(=o z#0U7$0SHE@ofFFC2)J-Mgb-4| zh2av0)SKu$li zA%?@uQMsR7=t=HHqj42zt6nsjOdjwg7m*9mXmn*S+74|+Cg*QV{y+Lb+Vvj7S)ar^ ztN`H*$nUaefV|-#kYh$S3a672^E z{SWL;Iv@h!Z1ByW`M<~6@IMfgf5VbVrkJ1p{BOYZdzGXUFwQ^y`FmXcuj?cqI6e!1 zi_fNT+~9}LK_CZ!Uf1W~0e{BmABLS2{xd%RFo=Ic0V(lkeEu6S{jdn>>d*N6!yx_* z1*Bp)K8t>PuPgrYUWeHHPJv?c9|$C8G>b^3$xbWIfkrJwj%ljX4$)G_%9fSx{)tXA z)JWu$ZBM#N5qu`8&A{>Tktj^YlN-*@1@-(H^k+K+ycc5~oPQPl8tzOg|WAgCPDC z*iII0P}99%hYvlcqCx<{^^65)E-lSs&Q^zo%v*86TWj01W?9<=&ty=989_4n4kn^y zgtx^>K9pl>E<8B+^_hkSq{Q7F=Z~<>b;)}*)SECgJc{r$`i)%X0J|HHmDR zt~@fS1|hR+J^Q;-x-#=ofinel4yN+{Vk}k-QF?>pE={*V$`e`%n`Uvwyxq0+UpgNo zX>lk2&c&82BaZUPDH*+y?|XtJIpTyn!&Vx;sc!vE5N`w15O}_RrEp2ajx(c;o7;KU z!V~S({d?S^Im$zmWjQIj0oO)PSSqAPbKvjTR8iO;ym`eLs2=RSTx}Yt9%XHzS6wLuFui+qBSUVjh54^)GR*th|j*qVcsQie?^;)re@+o za#|PykZ+UOGxXu9z~CDeL1{S@p)%sleKnDwcVOP&J}rPAjF|Xs4D1?8s9T~X)QQvn z_|(1<(E%6&q&7Hi?fquwm$03q9I%+&^kjLp^gTT`p0eDmzz#Z+FsYG^4#R!gObTHZ z#m(G^K`5}rhmw@-L+|06vrax{_YezHw2{^iV>(AXvMiC^s@!77kx?r!eJ$ruw~mFi zUYBzzM@eMGLiA9_>Zuof3DQVo(6eId`PQ2%_bDZ}RGlvDEu8FZ<&~-xUoLMqsx}cS z$Z&a>$s5X8>C6WKiXd2^#C$<|nIhTbdBU`uxc2>0j^NvdR=0aF4kY(X|qP zga%F{`GkyJCHXD&wB%2RRF8Vj80)LV`?;!#wlZezznF3FVCyhdhsDh7*zOdc8Ri%) zy8`Ro<6iG0$6hOB^E%#+*F`GDw2BNjD+HOV%UT#LP2jH2YX@nJo5wJZ-o9*inD|`x zs%J4KjF);(YK#>4r2Z8hT7jaXY;wF-tcIaZV#!rQHp`+^K6m#0uX22D;`yKPR_$?* zh$@Y{Qwg5ph?N^Q>)z)r@_VVaxh?(zJ8vl_h=)6iH~f|s7M_?57A$IB1b8AVE@)?F zl4rEwchvKdNX^9;YaQ&G;4*lYQ9=`>l^d3C(FE;j?j14B^>I3e8~{KXgdO0-rURe? zAqQaau_rL}KcS3K{R7c^A2RN?vC-Q`ztxFFA;HIGQU@6AdAq%RK4h8x7?T;1Y;wUu zJ+C0P$b8%O>$`Hg6?%pG3k05e7{|yw>rydgyCX1nsqCY$?R>=LKl+XZ?o=$%EsXMG zV~x(YFSOX*UJS@@xqQfb&XR{``oRI~nJ{YSRPZn-FWJ@tJMq5xs}0qb|ao;rlui{HHJ@n$)`&1gkSB;j}%-3728-E$Xo5%g|DjA zEPde771mn2Kw0dr@Hp1jS~xJ+Fov=*uPDmESkFmEoo6^3g{r@0X{D`WFDa7Anba&r zcYc9?`xWtomKU|}L>C`V6*ZPv86MPJzEab#{1lU)qWv1_h?TZJ5ulpAtete%(w8f} z#OP+|{SM2biAg2lqWam4w*@<3ABiPdlo-4oo#(poVFUktwl-` zk~^{YrR%KdwsZBn#5wuK`$wBjjxBBhZ4JWxR^E1&+~VcpaJ z+Ibs0yMTtlcFxpP=`0`@l>Z`hi^@!Uq>)fVJ6%9pX z46Rl#QlkJkpiILD^tMfZQj~}M3DM_(b&eF?!YsyZe?764;cNr#>4(MV$^+i%b`%Ol zTCyg+F964EZSCHk`!0~lm97DM_IELlm`eSp-ux08v*Sp4qGuEq7_(K^VoyCxZqz@wDCW$$1ZInl zPsj~A;#~ zn5P_v-EKa}7MFBC(09k!_-F*#YgvBC<-wyXv%ZTeFEF_DpgkJVX)+&^OKi~Ltdm-H zPHCyv+p{B#wtQ4iOKkIEvJW2VT-6n~?G<d@&itmNeh21DK0 z{b%{yS83VclBM9(ACeaV?#fr_Ew={Z;hmp~0RHKYyAVevSCWg+XsZF}Dja=`OwNHe z|2llis{&tdBlqRP#>#TQI?~-pkT768p|zN^nlffi!{SVw_|kQ1*d=v82VqfFV@+J- zx=(%v_x;rfJEz63rBykfx+a^;bAziwwr5FsXk9k2xc!y!DzW^NeO_@Z7FAA^EUO%H zKwfZDkd^8Q&_>M{#EQC6*)w;!RqH;OG_1^h7b~k0pMw}s+a}mq)^x7cu;z(Q{l$KR ztZYv8w`e~>Ic`p+wog7TjhX>+w50AQ`ugjRhvSmHjCPW9Ix(uxoI0g1$@-_@P{xm? z8D*-dF2Plw7gvu6Q3^{g;vQ)>5AqtGBYi%u3J?AyXLvu z8+leyX3ci#gxiFd5^+j3=3$FE8A-870G4BrL=O#^GOF~DS%=Y|rDuQ~ajsyU!iAU;O!zz!|t;2ibG!l-nlAC*CUq1|U zzJtpwe8>A4;FEWcbnJc0YO{?aYbvxM^HLz?vRTH+-Y!q?ff(H?l8aF=U-^Xt%1?EB z@GjCUzhBX7smNV$r}#BNj20_Cs;ZaKIteX+pq z!&-VA$!0Qq_jMQ?Nj@G2^Bg|Dm;I&j;9|kF+X}xZTkJql+v%!4z5!qLPa)|BCgW;z z6g3Nr<~Sa+YJ3Q-yxDd|vcm9lf=@z=lY`q}dGlK1X#Xq0%6bu&fVE0nK3&ff1^bc7 z9i6zg_y>doql>l)rJnwZsa2~fgvZnG7P*^GHtP2FX=mfE)t;3|12c{UuFcnhXdp}z zTnOhmkY@v!h~(r)MjkVdConMgi5dHRbYHvTdw#v;;Bj^Nk!KU{)h~GnW~C@eU03xh zmz8R@E8yTyv!d%}BZe?t7KtsJ@#n5+`Ym-31jS>0-aehlvzx4;d=hkTbj9s!H7^_Q z6R$jGqD0uQgyO#OC`jkxoFJx_$ws{nETzBGOJ?peSixCW30?K#{tA@VMRV7IDHM>qvw z=P^)b>yY-=*K*<%zWI4^ar!s?-{+qfhcbK472)-k?}?qD)+YHsB&{GhBK3x*yt)R+ zx`K9%W@pkgd}Uw84S+>uW9*)XMDI4(w?vjKAKcSNwLO#VFz}K+vn9zvh_TePv`={@q7P554MrS>+VQyZRFAy0acD z?aE*T7=<3FK|e14^28nL|idO$GP`*QP literal 0 HcmV?d00001 diff --git a/library/core/src/test/assets/mp4/sample_ac4_fragmented.mp4.0.dump b/library/core/src/test/assets/mp4/sample_ac4_fragmented.mp4.0.dump new file mode 100644 index 0000000000..505c85e51f --- /dev/null +++ b/library/core/src/test/assets/mp4/sample_ac4_fragmented.mp4.0.dump @@ -0,0 +1,107 @@ +seekMap: + isSeekable = true + duration = 760000 + getPosition(0) = [[timeUs=0, position=685]] +numberOfTracks = 1 +track 0: + format: + bitrate = -1 + id = 1 + containerMimeType = null + sampleMimeType = audio/ac4 + maxInputSize = -1 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = und + drmInitData = - + metadata = null + initializationData: + total output bytes = 7613 + sample count = 19 + sample 0: + time = 0 + flags = 1 + data = length 367, hash D2762FA + sample 1: + time = 40000 + flags = 1 + data = length 367, hash BDD3224A + sample 2: + time = 80000 + flags = 1 + data = length 367, hash 9302227B + sample 3: + time = 120000 + flags = 1 + data = length 367, hash 72996003 + sample 4: + time = 160000 + flags = 1 + data = length 367, hash 88AE5A1B + sample 5: + time = 200000 + flags = 1 + data = length 367, hash E5346FE3 + sample 6: + time = 240000 + flags = 1 + data = length 367, hash CE558362 + sample 7: + time = 280000 + flags = 1 + data = length 367, hash 51AD3043 + sample 8: + time = 320000 + flags = 1 + data = length 367, hash EB72E95B + sample 9: + time = 360000 + flags = 1 + data = length 367, hash 47F8FF23 + sample 10: + time = 400000 + flags = 1 + data = length 367, hash 8133883D + sample 11: + time = 440000 + flags = 1 + data = length 495, hash E14BDFEE + sample 12: + time = 480000 + flags = 1 + data = length 520, hash FEE56928 + sample 13: + time = 520000 + flags = 1 + data = length 599, hash 41F496C5 + sample 14: + time = 560000 + flags = 1 + data = length 436, hash 76D6404 + sample 15: + time = 600000 + flags = 1 + data = length 366, hash 56D49D4D + sample 16: + time = 640000 + flags = 1 + data = length 393, hash 822FC8 + sample 17: + time = 680000 + flags = 1 + data = length 374, hash FA8AE217 + sample 18: + time = 720000 + flags = 1 + data = length 393, hash 8506A1B +tracksEnded = true diff --git a/library/core/src/test/assets/mp4/sample_ac4_fragmented.mp4.1.dump b/library/core/src/test/assets/mp4/sample_ac4_fragmented.mp4.1.dump new file mode 100644 index 0000000000..8bee343bd9 --- /dev/null +++ b/library/core/src/test/assets/mp4/sample_ac4_fragmented.mp4.1.dump @@ -0,0 +1,83 @@ +seekMap: + isSeekable = true + duration = 760000 + getPosition(0) = [[timeUs=0, position=685]] +numberOfTracks = 1 +track 0: + format: + bitrate = -1 + id = 1 + containerMimeType = null + sampleMimeType = audio/ac4 + maxInputSize = -1 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = und + drmInitData = - + metadata = null + initializationData: + total output bytes = 5411 + sample count = 13 + sample 0: + time = 240000 + flags = 1 + data = length 367, hash CE558362 + sample 1: + time = 280000 + flags = 1 + data = length 367, hash 51AD3043 + sample 2: + time = 320000 + flags = 1 + data = length 367, hash EB72E95B + sample 3: + time = 360000 + flags = 1 + data = length 367, hash 47F8FF23 + sample 4: + time = 400000 + flags = 1 + data = length 367, hash 8133883D + sample 5: + time = 440000 + flags = 1 + data = length 495, hash E14BDFEE + sample 6: + time = 480000 + flags = 1 + data = length 520, hash FEE56928 + sample 7: + time = 520000 + flags = 1 + data = length 599, hash 41F496C5 + sample 8: + time = 560000 + flags = 1 + data = length 436, hash 76D6404 + sample 9: + time = 600000 + flags = 1 + data = length 366, hash 56D49D4D + sample 10: + time = 640000 + flags = 1 + data = length 393, hash 822FC8 + sample 11: + time = 680000 + flags = 1 + data = length 374, hash FA8AE217 + sample 12: + time = 720000 + flags = 1 + data = length 393, hash 8506A1B +tracksEnded = true diff --git a/library/core/src/test/assets/mp4/sample_ac4_fragmented.mp4.2.dump b/library/core/src/test/assets/mp4/sample_ac4_fragmented.mp4.2.dump new file mode 100644 index 0000000000..ee1cf91a57 --- /dev/null +++ b/library/core/src/test/assets/mp4/sample_ac4_fragmented.mp4.2.dump @@ -0,0 +1,59 @@ +seekMap: + isSeekable = true + duration = 760000 + getPosition(0) = [[timeUs=0, position=685]] +numberOfTracks = 1 +track 0: + format: + bitrate = -1 + id = 1 + containerMimeType = null + sampleMimeType = audio/ac4 + maxInputSize = -1 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = und + drmInitData = - + metadata = null + initializationData: + total output bytes = 3081 + sample count = 7 + sample 0: + time = 480000 + flags = 1 + data = length 520, hash FEE56928 + sample 1: + time = 520000 + flags = 1 + data = length 599, hash 41F496C5 + sample 2: + time = 560000 + flags = 1 + data = length 436, hash 76D6404 + sample 3: + time = 600000 + flags = 1 + data = length 366, hash 56D49D4D + sample 4: + time = 640000 + flags = 1 + data = length 393, hash 822FC8 + sample 5: + time = 680000 + flags = 1 + data = length 374, hash FA8AE217 + sample 6: + time = 720000 + flags = 1 + data = length 393, hash 8506A1B +tracksEnded = true diff --git a/library/core/src/test/assets/mp4/sample_ac4_fragmented.mp4.3.dump b/library/core/src/test/assets/mp4/sample_ac4_fragmented.mp4.3.dump new file mode 100644 index 0000000000..419f0444bf --- /dev/null +++ b/library/core/src/test/assets/mp4/sample_ac4_fragmented.mp4.3.dump @@ -0,0 +1,35 @@ +seekMap: + isSeekable = true + duration = 760000 + getPosition(0) = [[timeUs=0, position=685]] +numberOfTracks = 1 +track 0: + format: + bitrate = -1 + id = 1 + containerMimeType = null + sampleMimeType = audio/ac4 + maxInputSize = -1 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = und + drmInitData = - + metadata = null + initializationData: + total output bytes = 393 + sample count = 1 + sample 0: + time = 720000 + flags = 1 + data = length 393, hash 8506A1B +tracksEnded = true diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java index a29dfcc310..1f49aee293 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java @@ -51,6 +51,12 @@ public final class FragmentedMp4ExtractorTest { ExtractorAsserts.assertBehavior(extractorFactory, "mp4/sample_fragmented_sei.mp4"); } + @Test + public void testSampleWithAc4Track() throws Exception { + ExtractorAsserts.assertBehavior( + getExtractorFactory(Collections.emptyList()), "mp4/sample_ac4_fragmented.mp4"); + } + private static ExtractorFactory getExtractorFactory(final List closedCaptionFormats) { return () -> new FragmentedMp4Extractor(0, null, null, null, closedCaptionFormats); } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/Mp4ExtractorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/Mp4ExtractorTest.java index b5c3b26a23..6ddc74c797 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/Mp4ExtractorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/Mp4ExtractorTest.java @@ -42,4 +42,9 @@ public final class Mp4ExtractorTest { public void testMp4SampleWithMdatTooLong() throws Exception { ExtractorAsserts.assertBehavior(Mp4Extractor::new, "mp4/sample_mdat_too_long.mp4"); } + + @Test + public void testMp4SampleWithAc4Track() throws Exception { + ExtractorAsserts.assertBehavior(Mp4Extractor::new, "mp4/sample_ac4.mp4"); + } } From b5fa338367819c0a1338efbb3da0403c4ca91387 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 3 Jan 2020 10:22:27 +0000 Subject: [PATCH 11/44] Show ad markers after the window duration Issue: #6552 PiperOrigin-RevId: 287964221 --- RELEASENOTES.md | 3 +++ .../com/google/android/exoplayer2/ui/PlayerControlView.java | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 2dba34486b..027f27da7a 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -37,6 +37,9 @@ * Support "twos" codec (big endian PCM) in MP4 ([#5789](https://github.com/google/ExoPlayer/issues/5789)). * WAV: Support IMA ADPCM encoded data. +* Show ad group markers in `DefaultTimeBar` even if they are after the end of + the current window + ([#6552](https://github.com/google/ExoPlayer/issues/6552)). ### 2.11.1 (2019-12-20) ### 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 248ac9fdaf..bfb4e018f0 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 @@ -943,7 +943,7 @@ public class PlayerControlView extends FrameLayout { adGroupTimeInPeriodUs = period.durationUs; } long adGroupTimeInWindowUs = adGroupTimeInPeriodUs + period.getPositionInWindowUs(); - if (adGroupTimeInWindowUs >= 0 && adGroupTimeInWindowUs <= window.durationUs) { + if (adGroupTimeInWindowUs >= 0) { if (adGroupCount == adGroupTimesMs.length) { int newLength = adGroupTimesMs.length == 0 ? 1 : adGroupTimesMs.length * 2; adGroupTimesMs = Arrays.copyOf(adGroupTimesMs, newLength); From f76b4fe63ece2df4d94319110f91186a390abffb Mon Sep 17 00:00:00 2001 From: kimvde Date: Fri, 3 Jan 2020 12:11:10 +0000 Subject: [PATCH 12/44] Add unit tests to FLAC extractor related classes PiperOrigin-RevId: 287973192 --- .../exoplayer2/extractor/FlacFrameReader.java | 2 +- .../extractor/FlacFrameReaderTest.java | 323 ++++++++++++++ .../extractor/FlacMetadataReaderTest.java | 408 ++++++++++++++++++ .../util/FlacStreamMetadataTest.java | 24 ++ 4 files changed, 756 insertions(+), 1 deletion(-) create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/extractor/FlacFrameReaderTest.java create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/extractor/FlacMetadataReaderTest.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/FlacFrameReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/FlacFrameReader.java index 1e498cb677..f014eaa565 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/FlacFrameReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/FlacFrameReader.java @@ -167,7 +167,7 @@ public final class FlacFrameReader { * @param data The array to read the data from, whose position must correspond to the block size * bits. * @param blockSizeKey The key in the block size lookup table. - * @return The block size in samples. + * @return The block size in samples, or -1 if the {@code blockSizeKey} is invalid. */ public static int readFrameBlockSizeSamplesFromKey(ParsableByteArray data, int blockSizeKey) { switch (blockSizeKey) { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/FlacFrameReaderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/FlacFrameReaderTest.java new file mode 100644 index 0000000000..87487a4199 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/FlacFrameReaderTest.java @@ -0,0 +1,323 @@ +/* + * 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.extractor; + +import static com.google.common.truth.Truth.assertThat; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.extractor.FlacFrameReader.SampleNumberHolder; +import com.google.android.exoplayer2.extractor.FlacMetadataReader.FlacStreamMetadataHolder; +import com.google.android.exoplayer2.testutil.FakeExtractorInput; +import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.android.exoplayer2.util.FlacConstants; +import com.google.android.exoplayer2.util.ParsableByteArray; +import java.io.IOException; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Unit tests for {@link FlacFrameReader}. + * + *

    Some expected results in these tests have been retrieved using the flac command. + */ +@RunWith(AndroidJUnit4.class) +public class FlacFrameReaderTest { + + @Test + public void checkAndReadFrameHeader_validData_updatesPosition() throws Exception { + FlacStreamMetadataHolder streamMetadataHolder = + new FlacStreamMetadataHolder(/* flacStreamMetadata= */ null); + ExtractorInput input = + buildExtractorInputReadingFromFirstFrame( + "flac/bear_one_metadata_block.flac", streamMetadataHolder); + int frameStartMarker = FlacMetadataReader.getFrameStartMarker(input); + ParsableByteArray scratch = new ParsableByteArray(FlacConstants.MAX_FRAME_HEADER_SIZE); + input.read(scratch.data, 0, FlacConstants.MAX_FRAME_HEADER_SIZE); + + FlacFrameReader.checkAndReadFrameHeader( + scratch, + streamMetadataHolder.flacStreamMetadata, + frameStartMarker, + new SampleNumberHolder()); + + assertThat(scratch.getPosition()).isEqualTo(FlacConstants.MIN_FRAME_HEADER_SIZE); + } + + @Test + public void checkAndReadFrameHeader_validData_isTrue() throws Exception { + FlacStreamMetadataHolder streamMetadataHolder = + new FlacStreamMetadataHolder(/* flacStreamMetadata= */ null); + ExtractorInput input = + buildExtractorInputReadingFromFirstFrame( + "flac/bear_one_metadata_block.flac", streamMetadataHolder); + int frameStartMarker = FlacMetadataReader.getFrameStartMarker(input); + ParsableByteArray scratch = new ParsableByteArray(FlacConstants.MAX_FRAME_HEADER_SIZE); + input.read(scratch.data, 0, FlacConstants.MAX_FRAME_HEADER_SIZE); + + boolean result = + FlacFrameReader.checkAndReadFrameHeader( + scratch, + streamMetadataHolder.flacStreamMetadata, + frameStartMarker, + new SampleNumberHolder()); + + assertThat(result).isTrue(); + } + + @Test + public void checkAndReadFrameHeader_validData_writesSampleNumber() throws Exception { + FlacStreamMetadataHolder streamMetadataHolder = + new FlacStreamMetadataHolder(/* flacStreamMetadata= */ null); + ExtractorInput input = + buildExtractorInputReadingFromFirstFrame( + "flac/bear_one_metadata_block.flac", streamMetadataHolder); + int frameStartMarker = FlacMetadataReader.getFrameStartMarker(input); + // Skip first frame. + input.skip(5030); + ParsableByteArray scratch = new ParsableByteArray(FlacConstants.MAX_FRAME_HEADER_SIZE); + input.read(scratch.data, 0, FlacConstants.MAX_FRAME_HEADER_SIZE); + SampleNumberHolder sampleNumberHolder = new SampleNumberHolder(); + + FlacFrameReader.checkAndReadFrameHeader( + scratch, streamMetadataHolder.flacStreamMetadata, frameStartMarker, sampleNumberHolder); + + assertThat(sampleNumberHolder.sampleNumber).isEqualTo(4096); + } + + @Test + public void checkAndReadFrameHeader_invalidData_isFalse() throws Exception { + FlacStreamMetadataHolder streamMetadataHolder = + new FlacStreamMetadataHolder(/* flacStreamMetadata= */ null); + ExtractorInput input = + buildExtractorInputReadingFromFirstFrame( + "flac/bear_one_metadata_block.flac", streamMetadataHolder); + ParsableByteArray scratch = new ParsableByteArray(FlacConstants.MAX_FRAME_HEADER_SIZE); + input.read(scratch.data, 0, FlacConstants.MAX_FRAME_HEADER_SIZE); + + // The first bytes of the frame are not equal to the frame start marker. + boolean result = + FlacFrameReader.checkAndReadFrameHeader( + scratch, + streamMetadataHolder.flacStreamMetadata, + /* frameStartMarker= */ -1, + new SampleNumberHolder()); + + assertThat(result).isFalse(); + } + + @Test + public void checkFrameHeaderFromPeek_validData_doesNotUpdatePositions() throws Exception { + String file = "flac/bear_one_metadata_block.flac"; + FlacStreamMetadataHolder streamMetadataHolder = + new FlacStreamMetadataHolder(/* flacStreamMetadata= */ null); + ExtractorInput input = buildExtractorInputReadingFromFirstFrame(file, streamMetadataHolder); + int frameStartMarker = FlacMetadataReader.getFrameStartMarker(input); + long peekPosition = input.getPosition(); + // Set read position to 0. + input = buildExtractorInput(file); + input.advancePeekPosition((int) peekPosition); + + FlacFrameReader.checkFrameHeaderFromPeek( + input, streamMetadataHolder.flacStreamMetadata, frameStartMarker, new SampleNumberHolder()); + + assertThat(input.getPosition()).isEqualTo(0); + assertThat(input.getPeekPosition()).isEqualTo(peekPosition); + } + + @Test + public void checkFrameHeaderFromPeek_validData_isTrue() throws Exception { + FlacStreamMetadataHolder streamMetadataHolder = + new FlacStreamMetadataHolder(/* flacStreamMetadata= */ null); + ExtractorInput input = + buildExtractorInputReadingFromFirstFrame( + "flac/bear_one_metadata_block.flac", streamMetadataHolder); + int frameStartMarker = FlacMetadataReader.getFrameStartMarker(input); + + boolean result = + FlacFrameReader.checkFrameHeaderFromPeek( + input, + streamMetadataHolder.flacStreamMetadata, + frameStartMarker, + new SampleNumberHolder()); + + assertThat(result).isTrue(); + } + + @Test + public void checkFrameHeaderFromPeek_validData_writesSampleNumber() throws Exception { + FlacStreamMetadataHolder streamMetadataHolder = + new FlacStreamMetadataHolder(/* flacStreamMetadata= */ null); + ExtractorInput input = + buildExtractorInputReadingFromFirstFrame( + "flac/bear_one_metadata_block.flac", streamMetadataHolder); + int frameStartMarker = FlacMetadataReader.getFrameStartMarker(input); + // Skip first frame. + input.skip(5030); + SampleNumberHolder sampleNumberHolder = new SampleNumberHolder(); + + FlacFrameReader.checkFrameHeaderFromPeek( + input, streamMetadataHolder.flacStreamMetadata, frameStartMarker, sampleNumberHolder); + + assertThat(sampleNumberHolder.sampleNumber).isEqualTo(4096); + } + + @Test + public void checkFrameHeaderFromPeek_invalidData_isFalse() throws Exception { + FlacStreamMetadataHolder streamMetadataHolder = + new FlacStreamMetadataHolder(/* flacStreamMetadata= */ null); + ExtractorInput input = + buildExtractorInputReadingFromFirstFrame( + "flac/bear_one_metadata_block.flac", streamMetadataHolder); + + // The first bytes of the frame are not equal to the frame start marker. + boolean result = + FlacFrameReader.checkFrameHeaderFromPeek( + input, + streamMetadataHolder.flacStreamMetadata, + /* frameStartMarker= */ -1, + new SampleNumberHolder()); + + assertThat(result).isFalse(); + } + + @Test + public void checkFrameHeaderFromPeek_invalidData_doesNotUpdatePositions() throws Exception { + String file = "flac/bear_one_metadata_block.flac"; + FlacStreamMetadataHolder streamMetadataHolder = + new FlacStreamMetadataHolder(/* flacStreamMetadata= */ null); + ExtractorInput input = buildExtractorInputReadingFromFirstFrame(file, streamMetadataHolder); + long peekPosition = input.getPosition(); + // Set read position to 0. + input = buildExtractorInput(file); + input.advancePeekPosition((int) peekPosition); + + // The first bytes of the frame are not equal to the frame start marker. + FlacFrameReader.checkFrameHeaderFromPeek( + input, + streamMetadataHolder.flacStreamMetadata, + /* frameStartMarker= */ -1, + new SampleNumberHolder()); + + assertThat(input.getPosition()).isEqualTo(0); + assertThat(input.getPeekPosition()).isEqualTo(peekPosition); + } + + @Test + public void getFirstSampleNumber_doesNotUpdateReadPositionAndAlignsPeekPosition() + throws Exception { + FlacStreamMetadataHolder streamMetadataHolder = + new FlacStreamMetadataHolder(/* flacStreamMetadata= */ null); + ExtractorInput input = + buildExtractorInputReadingFromFirstFrame( + "flac/bear_one_metadata_block.flac", streamMetadataHolder); + long initialReadPosition = input.getPosition(); + // Advance peek position after block size bits. + input.advancePeekPosition(FlacConstants.MAX_FRAME_HEADER_SIZE); + + FlacFrameReader.getFirstSampleNumber(input, streamMetadataHolder.flacStreamMetadata); + + assertThat(input.getPosition()).isEqualTo(initialReadPosition); + assertThat(input.getPeekPosition()).isEqualTo(input.getPosition()); + } + + @Test + public void getFirstSampleNumber_returnsSampleNumber() throws Exception { + FlacStreamMetadataHolder streamMetadataHolder = + new FlacStreamMetadataHolder(/* flacStreamMetadata= */ null); + ExtractorInput input = + buildExtractorInputReadingFromFirstFrame( + "flac/bear_one_metadata_block.flac", streamMetadataHolder); + // Skip first frame. + input.skip(5030); + + long result = + FlacFrameReader.getFirstSampleNumber(input, streamMetadataHolder.flacStreamMetadata); + + assertThat(result).isEqualTo(4096); + } + + @Test + public void readFrameBlockSizeSamplesFromKey_keyIs1_returnsCorrectBlockSize() { + int result = + FlacFrameReader.readFrameBlockSizeSamplesFromKey( + new ParsableByteArray(/* limit= */ 0), /* blockSizeKey= */ 1); + + assertThat(result).isEqualTo(192); + } + + @Test + public void readFrameBlockSizeSamplesFromKey_keyBetween2and5_returnsCorrectBlockSize() { + int result = + FlacFrameReader.readFrameBlockSizeSamplesFromKey( + new ParsableByteArray(/* limit= */ 0), /* blockSizeKey= */ 3); + + assertThat(result).isEqualTo(1152); + } + + @Test + public void readFrameBlockSizeSamplesFromKey_keyBetween6And7_returnsCorrectBlockSize() + throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear_one_metadata_block.flac"); + // Skip to block size bits of last frame. + input.skipFully(164033); + ParsableByteArray scratch = new ParsableByteArray(2); + input.readFully(scratch.data, 0, 2); + + int result = FlacFrameReader.readFrameBlockSizeSamplesFromKey(scratch, /* blockSizeKey= */ 7); + + assertThat(result).isEqualTo(496); + } + + @Test + public void readFrameBlockSizeSamplesFromKey_keyBetween8and15_returnsCorrectBlockSize() { + int result = + FlacFrameReader.readFrameBlockSizeSamplesFromKey( + new ParsableByteArray(/* limit= */ 0), /* blockSizeKey= */ 11); + + assertThat(result).isEqualTo(2048); + } + + @Test + public void readFrameBlockSizeSamplesFromKey_invalidKey_returnsCorrectBlockSize() { + int result = + FlacFrameReader.readFrameBlockSizeSamplesFromKey( + new ParsableByteArray(/* limit= */ 0), /* blockSizeKey= */ 25); + + assertThat(result).isEqualTo(-1); + } + + private static ExtractorInput buildExtractorInput(String file) throws IOException { + byte[] fileData = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), file); + return new FakeExtractorInput.Builder().setData(fileData).build(); + } + + private ExtractorInput buildExtractorInputReadingFromFirstFrame( + String file, FlacStreamMetadataHolder streamMetadataHolder) + throws IOException, InterruptedException { + ExtractorInput input = buildExtractorInput(file); + + input.skipFully(FlacConstants.STREAM_MARKER_SIZE); + + boolean lastMetadataBlock = false; + while (!lastMetadataBlock) { + lastMetadataBlock = FlacMetadataReader.readMetadataBlock(input, streamMetadataHolder); + } + + return input; + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/FlacMetadataReaderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/FlacMetadataReaderTest.java new file mode 100644 index 0000000000..390e806807 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/FlacMetadataReaderTest.java @@ -0,0 +1,408 @@ +/* + * 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.extractor; + +import static com.google.common.truth.Truth.assertThat; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.ParserException; +import com.google.android.exoplayer2.extractor.FlacMetadataReader.FlacStreamMetadataHolder; +import com.google.android.exoplayer2.metadata.Metadata; +import com.google.android.exoplayer2.metadata.flac.PictureFrame; +import com.google.android.exoplayer2.metadata.flac.VorbisComment; +import com.google.android.exoplayer2.testutil.FakeExtractorInput; +import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.android.exoplayer2.util.FlacConstants; +import com.google.android.exoplayer2.util.FlacStreamMetadata; +import com.google.android.exoplayer2.util.ParsableByteArray; +import java.io.IOException; +import java.util.ArrayList; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Unit tests for {@link FlacMetadataReader}. + * + *

    Most expected results in these tests have been retrieved using the metaflac command. + */ +@RunWith(AndroidJUnit4.class) +public class FlacMetadataReaderTest { + + @Test + public void peekId3Metadata_updatesPeekPosition() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear_with_id3_enabled.flac"); + + FlacMetadataReader.peekId3Metadata(input, /* parseData= */ false); + + assertThat(input.getPosition()).isEqualTo(0); + assertThat(input.getPeekPosition()).isNotEqualTo(0); + } + + @Test + public void peekId3Metadata_parseData_returnsNonEmptyMetadata() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear_with_id3_enabled.flac"); + + Metadata metadata = FlacMetadataReader.peekId3Metadata(input, /* parseData= */ true); + + assertThat(metadata).isNotNull(); + assertThat(metadata.length()).isNotEqualTo(0); + } + + @Test + public void peekId3Metadata_doNotParseData_returnsNull() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear_with_id3_enabled.flac"); + + Metadata metadata = FlacMetadataReader.peekId3Metadata(input, /* parseData= */ false); + + assertThat(metadata).isNull(); + } + + @Test + public void peekId3Metadata_noId3Metadata_returnsNull() throws Exception { + String fileWithoutId3Metadata = "flac/bear.flac"; + ExtractorInput input = buildExtractorInput(fileWithoutId3Metadata); + + Metadata metadata = FlacMetadataReader.peekId3Metadata(input, /* parseData= */ true); + + assertThat(metadata).isNull(); + } + + @Test + public void checkAndPeekStreamMarker_updatesPeekPosition() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear.flac"); + + FlacMetadataReader.checkAndPeekStreamMarker(input); + + assertThat(input.getPosition()).isEqualTo(0); + assertThat(input.getPeekPosition()).isEqualTo(FlacConstants.STREAM_MARKER_SIZE); + } + + @Test + public void checkAndPeekStreamMarker_validData_isTrue() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear.flac"); + + boolean result = FlacMetadataReader.checkAndPeekStreamMarker(input); + + assertThat(result).isTrue(); + } + + @Test + public void checkAndPeekStreamMarker_invalidData_isFalse() throws Exception { + ExtractorInput input = buildExtractorInput("mp3/bear.mp3"); + + boolean result = FlacMetadataReader.checkAndPeekStreamMarker(input); + + assertThat(result).isFalse(); + } + + @Test + public void readId3Metadata_updatesReadPositionAndAlignsPeekPosition() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear_with_id3_enabled.flac"); + // Advance peek position after ID3 metadata. + FlacMetadataReader.peekId3Metadata(input, /* parseData= */ false); + input.advancePeekPosition(1); + + FlacMetadataReader.readId3Metadata(input, /* parseData= */ false); + + assertThat(input.getPosition()).isNotEqualTo(0); + assertThat(input.getPeekPosition()).isEqualTo(input.getPosition()); + } + + @Test + public void readId3Metadata_parseData_returnsNonEmptyMetadata() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear_with_id3_enabled.flac"); + + Metadata metadata = FlacMetadataReader.readId3Metadata(input, /* parseData= */ true); + + assertThat(metadata).isNotNull(); + assertThat(metadata.length()).isNotEqualTo(0); + } + + @Test + public void readId3Metadata_doNotParseData_returnsNull() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear_with_id3_enabled.flac"); + + Metadata metadata = FlacMetadataReader.readId3Metadata(input, /* parseData= */ false); + + assertThat(metadata).isNull(); + } + + @Test + public void readId3Metadata_noId3Metadata_returnsNull() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear.flac"); + + Metadata metadata = FlacMetadataReader.readId3Metadata(input, /* parseData= */ true); + + assertThat(metadata).isNull(); + } + + @Test + public void readStreamMarker_updatesReadPosition() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear.flac"); + + FlacMetadataReader.readStreamMarker(input); + + assertThat(input.getPosition()).isEqualTo(FlacConstants.STREAM_MARKER_SIZE); + assertThat(input.getPeekPosition()).isEqualTo(input.getPosition()); + } + + @Test(expected = ParserException.class) + public void readStreamMarker_invalidData_throwsException() throws Exception { + ExtractorInput input = buildExtractorInput("mp3/bear.mp3"); + + FlacMetadataReader.readStreamMarker(input); + } + + @Test + public void readMetadataBlock_updatesReadPositionAndAlignsPeekPosition() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear.flac"); + input.skipFully(FlacConstants.STREAM_MARKER_SIZE); + // Advance peek position after metadata block. + input.advancePeekPosition(FlacConstants.STREAM_INFO_BLOCK_SIZE + 1); + + FlacMetadataReader.readMetadataBlock( + input, new FlacStreamMetadataHolder(/* flacStreamMetadata= */ null)); + + assertThat(input.getPosition()).isNotEqualTo(0); + assertThat(input.getPeekPosition()).isEqualTo(input.getPosition()); + } + + @Test + public void readMetadataBlock_lastMetadataBlock_isTrue() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear_one_metadata_block.flac"); + input.skipFully(FlacConstants.STREAM_MARKER_SIZE); + + boolean result = + FlacMetadataReader.readMetadataBlock( + input, new FlacStreamMetadataHolder(/* flacStreamMetadata= */ null)); + + assertThat(result).isTrue(); + } + + @Test + public void readMetadataBlock_notLastMetadataBlock_isFalse() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear.flac"); + input.skipFully(FlacConstants.STREAM_MARKER_SIZE); + + boolean result = + FlacMetadataReader.readMetadataBlock( + input, new FlacStreamMetadataHolder(/* flacStreamMetadata= */ null)); + + assertThat(result).isFalse(); + } + + @Test + public void readMetadataBlock_streamInfoBlock_setsStreamMetadata() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear.flac"); + input.skipFully(FlacConstants.STREAM_MARKER_SIZE); + FlacStreamMetadataHolder metadataHolder = + new FlacStreamMetadataHolder(/* flacStreamMetadata= */ null); + + FlacMetadataReader.readMetadataBlock(input, metadataHolder); + + assertThat(metadataHolder.flacStreamMetadata).isNotNull(); + assertThat(metadataHolder.flacStreamMetadata.sampleRate).isEqualTo(48000); + } + + @Test + public void readMetadataBlock_seekTableBlock_updatesStreamMetadata() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear.flac"); + // Skip to seek table block. + input.skipFully(FlacConstants.STREAM_MARKER_SIZE + FlacConstants.STREAM_INFO_BLOCK_SIZE); + FlacStreamMetadataHolder metadataHolder = new FlacStreamMetadataHolder(buildStreamMetadata()); + long originalSampleRate = metadataHolder.flacStreamMetadata.sampleRate; + + FlacMetadataReader.readMetadataBlock(input, metadataHolder); + + assertThat(metadataHolder.flacStreamMetadata).isNotNull(); + // Check that metadata passed has not been erased. + assertThat(metadataHolder.flacStreamMetadata.sampleRate).isEqualTo(originalSampleRate); + assertThat(metadataHolder.flacStreamMetadata.seekTable).isNotNull(); + assertThat(metadataHolder.flacStreamMetadata.seekTable.pointSampleNumbers.length).isEqualTo(32); + } + + @Test + public void readMetadataBlock_vorbisCommentBlock_updatesStreamMetadata() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear_with_vorbis_comments.flac"); + // Skip to Vorbis comment block. + input.skipFully(640); + FlacStreamMetadataHolder metadataHolder = new FlacStreamMetadataHolder(buildStreamMetadata()); + long originalSampleRate = metadataHolder.flacStreamMetadata.sampleRate; + + FlacMetadataReader.readMetadataBlock(input, metadataHolder); + + assertThat(metadataHolder.flacStreamMetadata).isNotNull(); + // Check that metadata passed has not been erased. + assertThat(metadataHolder.flacStreamMetadata.sampleRate).isEqualTo(originalSampleRate); + Metadata metadata = + metadataHolder.flacStreamMetadata.getMetadataCopyWithAppendedEntriesFrom(null); + assertThat(metadata).isNotNull(); + VorbisComment vorbisComment = (VorbisComment) metadata.get(0); + assertThat(vorbisComment.key).isEqualTo("TITLE"); + assertThat(vorbisComment.value).isEqualTo("test title"); + } + + @Test + public void readMetadataBlock_pictureBlock_updatesStreamMetadata() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear_with_picture.flac"); + // Skip to picture block. + input.skipFully(640); + FlacStreamMetadataHolder metadataHolder = new FlacStreamMetadataHolder(buildStreamMetadata()); + long originalSampleRate = metadataHolder.flacStreamMetadata.sampleRate; + + FlacMetadataReader.readMetadataBlock(input, metadataHolder); + + assertThat(metadataHolder.flacStreamMetadata).isNotNull(); + // Check that metadata passed has not been erased. + assertThat(metadataHolder.flacStreamMetadata.sampleRate).isEqualTo(originalSampleRate); + Metadata metadata = + metadataHolder.flacStreamMetadata.getMetadataCopyWithAppendedEntriesFrom(null); + assertThat(metadata).isNotNull(); + PictureFrame pictureFrame = (PictureFrame) metadata.get(0); + assertThat(pictureFrame.pictureType).isEqualTo(3); + assertThat(pictureFrame.mimeType).isEqualTo("image/png"); + assertThat(pictureFrame.description).isEqualTo(""); + assertThat(pictureFrame.width).isEqualTo(371); + assertThat(pictureFrame.height).isEqualTo(320); + assertThat(pictureFrame.depth).isEqualTo(24); + assertThat(pictureFrame.colors).isEqualTo(0); + assertThat(pictureFrame.pictureData).hasLength(30943); + } + + @Test + public void readMetadataBlock_blockToSkip_updatesReadPosition() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear.flac"); + // Skip to padding block. + input.skipFully(640); + FlacStreamMetadataHolder metadataHolder = new FlacStreamMetadataHolder(buildStreamMetadata()); + + FlacMetadataReader.readMetadataBlock(input, metadataHolder); + + assertThat(input.getPosition()).isGreaterThan(640); + assertThat(input.getPeekPosition()).isEqualTo(input.getPosition()); + } + + @Test(expected = IllegalArgumentException.class) + public void readMetadataBlock_nonStreamInfoBlockWithNullStreamMetadata_throwsException() + throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear.flac"); + // Skip to seek table block. + input.skipFully(FlacConstants.STREAM_MARKER_SIZE + FlacConstants.STREAM_INFO_BLOCK_SIZE); + + FlacMetadataReader.readMetadataBlock( + input, new FlacStreamMetadataHolder(/* flacStreamMetadata= */ null)); + } + + @Test + public void readSeekTableMetadataBlock_updatesPosition() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear.flac"); + // Skip to seek table block. + input.skipFully(FlacConstants.STREAM_MARKER_SIZE + FlacConstants.STREAM_INFO_BLOCK_SIZE); + int seekTableBlockSize = 598; + ParsableByteArray scratch = new ParsableByteArray(seekTableBlockSize); + input.read(scratch.data, 0, seekTableBlockSize); + + FlacMetadataReader.readSeekTableMetadataBlock(scratch); + + assertThat(scratch.getPosition()).isEqualTo(seekTableBlockSize); + } + + @Test + public void readSeekTableMetadataBlock_returnsCorrectSeekPoints() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear.flac"); + // Skip to seek table block. + input.skipFully(FlacConstants.STREAM_MARKER_SIZE + FlacConstants.STREAM_INFO_BLOCK_SIZE); + int seekTableBlockSize = 598; + ParsableByteArray scratch = new ParsableByteArray(seekTableBlockSize); + input.read(scratch.data, 0, seekTableBlockSize); + + FlacStreamMetadata.SeekTable seekTable = FlacMetadataReader.readSeekTableMetadataBlock(scratch); + + assertThat(seekTable.pointOffsets[0]).isEqualTo(0); + assertThat(seekTable.pointSampleNumbers[0]).isEqualTo(0); + assertThat(seekTable.pointOffsets[31]).isEqualTo(160602); + assertThat(seekTable.pointSampleNumbers[31]).isEqualTo(126976); + } + + @Test + public void readSeekTableMetadataBlock_ignoresPlaceholders() throws IOException { + byte[] fileData = + TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), "flac/bear.flac"); + ParsableByteArray scratch = new ParsableByteArray(fileData); + // Skip to seek table block. + scratch.skipBytes(FlacConstants.STREAM_MARKER_SIZE + FlacConstants.STREAM_INFO_BLOCK_SIZE); + + FlacStreamMetadata.SeekTable seekTable = FlacMetadataReader.readSeekTableMetadataBlock(scratch); + + // Seek point at index 32 is a placeholder. + assertThat(seekTable.pointSampleNumbers).hasLength(32); + } + + @Test + public void getFrameStartMarker_doesNotUpdateReadPositionAndAlignsPeekPosition() + throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear.flac"); + int firstFramePosition = 8880; + input.skipFully(firstFramePosition); + // Advance the peek position after the frame start marker. + input.advancePeekPosition(3); + + FlacMetadataReader.getFrameStartMarker(input); + + assertThat(input.getPosition()).isEqualTo(firstFramePosition); + assertThat(input.getPeekPosition()).isEqualTo(input.getPosition()); + } + + @Test + public void getFrameStartMarker_returnsCorrectFrameStartMarker() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear.flac"); + // Skip to first frame. + input.skipFully(8880); + + int result = FlacMetadataReader.getFrameStartMarker(input); + + assertThat(result).isEqualTo(0xFFF8); + } + + @Test(expected = ParserException.class) + public void getFrameStartMarker_invalidData_throwsException() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear.flac"); + + // Input position is incorrect. + FlacMetadataReader.getFrameStartMarker(input); + } + + private static ExtractorInput buildExtractorInput(String file) throws IOException { + byte[] fileData = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), file); + return new FakeExtractorInput.Builder().setData(fileData).build(); + } + + private static FlacStreamMetadata buildStreamMetadata() { + return new FlacStreamMetadata( + /* minBlockSizeSamples= */ 10, + /* maxBlockSizeSamples= */ 20, + /* minFrameSize= */ 5, + /* maxFrameSize= */ 10, + /* sampleRate= */ 44100, + /* channels= */ 2, + /* bitsPerSample= */ 8, + /* totalSamples= */ 1000, + /* vorbisComments= */ new ArrayList<>(), + /* pictureFrames= */ new ArrayList<>()); + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/util/FlacStreamMetadataTest.java b/library/core/src/test/java/com/google/android/exoplayer2/util/FlacStreamMetadataTest.java index ddaa550b7f..d1b0363d20 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/util/FlacStreamMetadataTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/util/FlacStreamMetadataTest.java @@ -17,9 +17,12 @@ package com.google.android.exoplayer2.util; import static com.google.common.truth.Truth.assertThat; +import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.flac.VorbisComment; +import com.google.android.exoplayer2.testutil.TestUtil; +import java.io.IOException; import java.util.ArrayList; import org.junit.Test; import org.junit.runner.RunWith; @@ -28,6 +31,27 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public final class FlacStreamMetadataTest { + @Test + public void constructFromByteArray_setsFieldsCorrectly() throws IOException { + byte[] fileData = + TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), "flac/bear.flac"); + + FlacStreamMetadata streamMetadata = + new FlacStreamMetadata( + fileData, FlacConstants.STREAM_MARKER_SIZE + FlacConstants.METADATA_BLOCK_HEADER_SIZE); + + assertThat(streamMetadata.minBlockSizeSamples).isEqualTo(4096); + assertThat(streamMetadata.maxBlockSizeSamples).isEqualTo(4096); + assertThat(streamMetadata.minFrameSize).isEqualTo(445); + assertThat(streamMetadata.maxFrameSize).isEqualTo(5776); + assertThat(streamMetadata.sampleRate).isEqualTo(48000); + assertThat(streamMetadata.sampleRateLookupKey).isEqualTo(10); + assertThat(streamMetadata.channels).isEqualTo(2); + assertThat(streamMetadata.bitsPerSample).isEqualTo(16); + assertThat(streamMetadata.bitsPerSampleLookupKey).isEqualTo(4); + assertThat(streamMetadata.totalSamples).isEqualTo(131568); + } + @Test public void parseVorbisComments() { ArrayList commentsList = new ArrayList<>(); From 1c0e69789fbf936b5ea2e066396d4886fd7cca98 Mon Sep 17 00:00:00 2001 From: ibaker Date: Fri, 3 Jan 2020 15:31:37 +0000 Subject: [PATCH 13/44] Clear existing Spans when applying CSS styles in WebvttCueParser Relying on the precedence of spans seems risky - I can't find it defined anywhere. It might have changed in Android 6.0? https://stackoverflow.com/q/34631851 PiperOrigin-RevId: 287989365 --- .../text/webvtt/WebvttCueParser.java | 46 ++++++++++++------- .../text/webvtt/WebvttDecoderTest.java | 2 +- 2 files changed, 31 insertions(+), 17 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java index 3a07a74042..f4c0f26fc8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java @@ -492,8 +492,7 @@ public final class WebvttCueParser { return; } if (style.getStyle() != WebvttCssStyle.UNSPECIFIED) { - spannedText.setSpan(new StyleSpan(style.getStyle()), start, end, - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + addOrReplaceSpan(spannedText, new StyleSpan(style.getStyle()), start, end); } if (style.isLinethrough()) { spannedText.setSpan(new StrikethroughSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); @@ -502,34 +501,29 @@ public final class WebvttCueParser { spannedText.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } if (style.hasFontColor()) { - spannedText.setSpan(new ForegroundColorSpan(style.getFontColor()), start, end, - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + addOrReplaceSpan(spannedText, new ForegroundColorSpan(style.getFontColor()), start, end); } if (style.hasBackgroundColor()) { - spannedText.setSpan(new BackgroundColorSpan(style.getBackgroundColor()), start, end, - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + addOrReplaceSpan( + spannedText, new BackgroundColorSpan(style.getBackgroundColor()), start, end); } if (style.getFontFamily() != null) { - spannedText.setSpan(new TypefaceSpan(style.getFontFamily()), start, end, - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + addOrReplaceSpan(spannedText, new TypefaceSpan(style.getFontFamily()), start, end); } Layout.Alignment textAlign = style.getTextAlign(); if (textAlign != null) { - spannedText.setSpan( - new AlignmentSpan.Standard(textAlign), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + addOrReplaceSpan(spannedText, new AlignmentSpan.Standard(textAlign), start, end); } switch (style.getFontSizeUnit()) { case WebvttCssStyle.FONT_SIZE_UNIT_PIXEL: - spannedText.setSpan(new AbsoluteSizeSpan((int) style.getFontSize(), true), start, end, - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + addOrReplaceSpan( + spannedText, new AbsoluteSizeSpan((int) style.getFontSize(), true), start, end); break; case WebvttCssStyle.FONT_SIZE_UNIT_EM: - spannedText.setSpan(new RelativeSizeSpan(style.getFontSize()), start, end, - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + addOrReplaceSpan(spannedText, new RelativeSizeSpan(style.getFontSize()), start, end); break; case WebvttCssStyle.FONT_SIZE_UNIT_PERCENT: - spannedText.setSpan(new RelativeSizeSpan(style.getFontSize() / 100), start, end, - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + addOrReplaceSpan(spannedText, new RelativeSizeSpan(style.getFontSize() / 100), start, end); break; case WebvttCssStyle.UNSPECIFIED: // Do nothing. @@ -537,6 +531,26 @@ public final class WebvttCueParser { } } + /** + * Adds {@code span} to {@code spannedText} between {@code start} and {@code end}, removing any + * existing spans of the same type and with the same indices. + * + *

    This is useful for types of spans that don't make sense to duplicate and where the + * evaluation order might have an unexpected impact on the final text, e.g. {@link + * ForegroundColorSpan}. + */ + private static void addOrReplaceSpan( + SpannableStringBuilder spannedText, Object span, int start, int end) { + Object[] existingSpans = spannedText.getSpans(start, end, span.getClass()); + for (Object existingSpan : existingSpans) { + if (spannedText.getSpanStart(existingSpan) == start + && spannedText.getSpanEnd(existingSpan) == end) { + spannedText.removeSpan(existingSpan); + } + } + spannedText.setSpan(span, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + /** * Returns the tag name for the given tag contents. * diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java index f405f1c407..5c044c029b 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java @@ -405,7 +405,7 @@ public class WebvttDecoderTest { Spanned s4 = getUniqueSpanTextAt(subtitle, /* timeUs= */ 25000000); assertThat(s1.getSpans(/* start= */ 0, s1.length(), ForegroundColorSpan.class)).hasLength(1); assertThat(s1.getSpans(/* start= */ 0, s1.length(), BackgroundColorSpan.class)).hasLength(1); - assertThat(s2.getSpans(/* start= */ 0, s2.length(), ForegroundColorSpan.class)).hasLength(2); + assertThat(s2.getSpans(/* start= */ 0, s2.length(), ForegroundColorSpan.class)).hasLength(1); assertThat(s3.getSpans(/* start= */ 10, s3.length(), UnderlineSpan.class)).hasLength(1); assertThat(s4.getSpans(/* start= */ 0, /* end= */ 16, BackgroundColorSpan.class)).hasLength(2); assertThat(s4.getSpans(/* start= */ 17, s4.length(), StyleSpan.class)).hasLength(1); From 0587180f147e842dc3e589826f4606cb3ef2ccd3 Mon Sep 17 00:00:00 2001 From: ibaker Date: Fri, 3 Jan 2020 15:32:06 +0000 Subject: [PATCH 14/44] Make SpannedSubject more fluent I decided the flags bit was a bit unclear so I played around with this It's also needed for more 'complex' assertions like colors - I didn't want to just chuck in a fourth int parameter to create: hasForegroundColorSpan(int start, int end, int flags, int color) PiperOrigin-RevId: 287989424 --- .../text/webvtt/WebvttCueParserTest.java | 16 +- .../text/webvtt/WebvttDecoderTest.java | 31 +- .../testutil/truth/SpannedSubject.java | 320 ++++++++++++++++-- .../testutil/truth/SpannedSubjectTest.java | 165 ++++++++- 4 files changed, 478 insertions(+), 54 deletions(-) diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParserTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParserTest.java index d23ed00e95..c9e8488c60 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParserTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParserTest.java @@ -34,8 +34,7 @@ public final class WebvttCueParserTest { + "This is text with html tags"); assertThat(text.toString()).isEqualTo("This is text with html tags"); - assertThat(text) - .hasUnderlineSpan("This ".length(), "This is".length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + assertThat(text).hasUnderlineSpanBetween("This ".length(), "This is".length()); assertThat(text) .hasBoldItalicSpan( "This is text with ".length(), @@ -59,10 +58,7 @@ public final class WebvttCueParserTest { assertThat(text.toString()).isEqualTo("An unclosed u tag with italic inside"); assertThat(text) - .hasUnderlineSpan( - "An ".length(), - "An unclosed u tag with italic inside".length(), - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + .hasUnderlineSpanBetween("An ".length(), "An unclosed u tag with italic inside".length()); assertThat(text) .hasItalicSpan( "An unclosed u tag with ".length(), @@ -81,10 +77,9 @@ public final class WebvttCueParserTest { "An italic tag with unclosed underline".length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); assertThat(text) - .hasUnderlineSpan( + .hasUnderlineSpanBetween( "An italic tag with unclosed ".length(), - "An italic tag with unclosed underline".length(), - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + "An italic tag with unclosed underline".length()); } @Test @@ -95,8 +90,7 @@ public final class WebvttCueParserTest { assertThat(text.toString()).isEqualTo(expectedText); assertThat(text).hasBoldSpan(0, expectedText.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); // Text between the tags is underlined. - assertThat(text) - .hasUnderlineSpan(0, "Overlapping u and".length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + assertThat(text).hasUnderlineSpanBetween(0, "Overlapping u and".length()); // Only text from to <\\u> is italic (unexpected - but simplifies the parsing). assertThat(text) .hasItalicSpan( diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java index 5c044c029b..a3ab3e8b1a 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java @@ -15,23 +15,23 @@ */ package com.google.android.exoplayer2.text.webvtt; +import static com.google.android.exoplayer2.testutil.truth.SpannedSubject.assertThat; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; import android.graphics.Typeface; import android.text.Layout.Alignment; import android.text.Spanned; -import android.text.style.BackgroundColorSpan; import android.text.style.ForegroundColorSpan; import android.text.style.StyleSpan; import android.text.style.TypefaceSpan; -import android.text.style.UnderlineSpan; import androidx.annotation.Nullable; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.SubtitleDecoderException; +import com.google.android.exoplayer2.util.ColorParser; import com.google.common.truth.Expect; import java.io.IOException; import java.util.List; @@ -403,14 +403,25 @@ public class WebvttDecoderTest { Spanned s2 = getUniqueSpanTextAt(subtitle, /* timeUs= */ 2345000); Spanned s3 = getUniqueSpanTextAt(subtitle, /* timeUs= */ 20000000); Spanned s4 = getUniqueSpanTextAt(subtitle, /* timeUs= */ 25000000); - assertThat(s1.getSpans(/* start= */ 0, s1.length(), ForegroundColorSpan.class)).hasLength(1); - assertThat(s1.getSpans(/* start= */ 0, s1.length(), BackgroundColorSpan.class)).hasLength(1); - assertThat(s2.getSpans(/* start= */ 0, s2.length(), ForegroundColorSpan.class)).hasLength(1); - assertThat(s3.getSpans(/* start= */ 10, s3.length(), UnderlineSpan.class)).hasLength(1); - assertThat(s4.getSpans(/* start= */ 0, /* end= */ 16, BackgroundColorSpan.class)).hasLength(2); - assertThat(s4.getSpans(/* start= */ 17, s4.length(), StyleSpan.class)).hasLength(1); - assertThat(s4.getSpans(/* start= */ 17, s4.length(), StyleSpan.class)[0].getStyle()) - .isEqualTo(Typeface.BOLD); + assertThat(s1) + .hasForegroundColorSpanBetween(0, s1.length()) + .withColor(ColorParser.parseCssColor("papayawhip")); + assertThat(s1) + .hasBackgroundColorSpanBetween(0, s1.length()) + .withColor(ColorParser.parseCssColor("green")); + assertThat(s2) + .hasForegroundColorSpanBetween(0, s2.length()) + .withColor(ColorParser.parseCssColor("peachpuff")); + + assertThat(s3).hasUnderlineSpanBetween(10, s3.length()); + assertThat(s4) + .hasBackgroundColorSpanBetween(0, 16) + .withColor(ColorParser.parseCssColor("lime")); + assertThat(s4) + .hasBoldSpan( + /* startIndex= */ 17, + /* endIndex= */ s4.length(), + /* flags= */ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } @Test diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/truth/SpannedSubject.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/truth/SpannedSubject.java index 0015634c1f..84d40fb6f1 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/truth/SpannedSubject.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/truth/SpannedSubject.java @@ -23,8 +23,12 @@ import static com.google.common.truth.Truth.assertAbout; import android.graphics.Typeface; import android.text.Spanned; import android.text.TextUtils; +import android.text.style.BackgroundColorSpan; +import android.text.style.ForegroundColorSpan; import android.text.style.StyleSpan; import android.text.style.UnderlineSpan; +import androidx.annotation.CheckResult; +import androidx.annotation.ColorInt; import androidx.annotation.Nullable; import com.google.common.truth.FailureMetadata; import com.google.common.truth.Subject; @@ -64,7 +68,7 @@ public final class SpannedSubject extends Subject { failWithoutActual( simpleFact("Expected no spans"), fact("in text", actual), - fact("but found", actualSpansString())); + fact("but found", getAllSpansAsStringWithoutFlags(actual))); } } @@ -76,6 +80,7 @@ public final class SpannedSubject extends Subject { * @param flags The flags of the expected span. See constants on {@link Spanned} for more * information. */ + // TODO: swap this to fluent-style. public void hasItalicSpan(int startIndex, int endIndex, int flags) { hasStyleSpan(startIndex, endIndex, flags, Typeface.ITALIC); } @@ -88,6 +93,7 @@ public final class SpannedSubject extends Subject { * @param flags The flags of the expected span. See constants on {@link Spanned} for more * information. */ + // TODO: swap this to fluent-style. public void hasBoldSpan(int startIndex, int endIndex, int flags) { hasStyleSpan(startIndex, endIndex, flags, Typeface.BOLD); } @@ -104,7 +110,7 @@ public final class SpannedSubject extends Subject { } } - failWithExpectedSpan( + failWithExpectedSpanWithFlags( startIndex, endIndex, flags, @@ -129,6 +135,7 @@ public final class SpannedSubject extends Subject { * @param flags The flags of the expected span. See constants on {@link Spanned} for more * information. */ + // TODO: swap this to fluent-style. public void hasBoldItalicSpan(int startIndex, int endIndex, int flags) { if (actual == null) { failWithoutActual(simpleFact("Spanned must not be null")); @@ -149,11 +156,13 @@ public final class SpannedSubject extends Subject { String spannedSubstring = actual.toString().substring(startIndex, endIndex); String boldSpan = - spanToString(startIndex, endIndex, flags, new StyleSpan(Typeface.BOLD), spannedSubstring); + getSpanAsStringWithFlags( + startIndex, endIndex, flags, new StyleSpan(Typeface.BOLD), spannedSubstring); String italicSpan = - spanToString(startIndex, endIndex, flags, new StyleSpan(Typeface.ITALIC), spannedSubstring); + getSpanAsStringWithFlags( + startIndex, endIndex, flags, new StyleSpan(Typeface.ITALIC), spannedSubstring); String boldItalicSpan = - spanToString( + getSpanAsStringWithFlags( startIndex, endIndex, flags, new StyleSpan(Typeface.BOLD_ITALIC), spannedSubstring); failWithoutActual( @@ -161,34 +170,89 @@ public final class SpannedSubject extends Subject { fact("in text", actual.toString()), fact("expected either", boldItalicSpan), fact("or both", boldSpan + "\n" + italicSpan), - fact("but found", actualSpansString())); + fact("but found", getAllSpansAsStringWithFlags(actual))); } /** - * Checks that the subject has an underline span from {@code startIndex} to {@code endIndex}. + * Checks that the subject has an {@link UnderlineSpan} from {@code start} to {@code end}. * - * @param startIndex The start of the expected span. - * @param endIndex The end of the expected span. - * @param flags The flags of the expected span. See constants on {@link Spanned} for more - * information. + * @param start The start of the expected span. + * @param end The end of the expected span. + * @return A {@link WithSpanFlags} object for optional additional assertions on the flags. */ - public void hasUnderlineSpan(int startIndex, int endIndex, int flags) { + public WithSpanFlags hasUnderlineSpanBetween(int start, int end) { if (actual == null) { failWithoutActual(simpleFact("Spanned must not be null")); - return; + return ALREADY_FAILED_WITH_FLAGS; } - List underlineSpans = - findMatchingSpans(startIndex, endIndex, flags, UnderlineSpan.class); - if (underlineSpans.size() == 1) { - return; + List underlineSpans = findMatchingSpans(start, end, UnderlineSpan.class); + List allFlags = new ArrayList<>(); + for (UnderlineSpan span : underlineSpans) { + allFlags.add(actual.getSpanFlags(span)); } - failWithExpectedSpan( - startIndex, - endIndex, - flags, - new UnderlineSpan(), - actual.toString().substring(startIndex, endIndex)); + if (underlineSpans.size() == 1) { + return check("UnderlineSpan (start=%s,end=%s)", start, end).about(spanFlags()).that(allFlags); + } + failWithExpectedSpanWithoutFlags( + start, end, UnderlineSpan.class, actual.toString().substring(start, end)); + return ALREADY_FAILED_WITH_FLAGS; + } + + /** + * Checks that the subject as a {@link ForegroundColorSpan} from {@code start} to {@code end}. + * + *

    The color is asserted in a follow-up method call on the return {@link Colored} object. + * + * @param start The start of the expected span. + * @param end The end of the expected span. + * @return A {@link Colored} object to assert on the color of the matching spans. + */ + @CheckResult + public Colored hasForegroundColorSpanBetween(int start, int end) { + if (actual == null) { + failWithoutActual(simpleFact("Spanned must not be null")); + return ALREADY_FAILED_COLORED; + } + + List foregroundColorSpans = + findMatchingSpans(start, end, ForegroundColorSpan.class); + if (foregroundColorSpans.isEmpty()) { + failWithExpectedSpanWithoutFlags( + start, end, ForegroundColorSpan.class, actual.toString().substring(start, end)); + return ALREADY_FAILED_COLORED; + } + return check("ForegroundColorSpan (start=%s,end=%s)", start, end) + .about(foregroundColorSpans(actual)) + .that(foregroundColorSpans); + } + + /** + * Checks that the subject as a {@link ForegroundColorSpan} from {@code start} to {@code end}. + * + *

    The color is asserted in a follow-up method call on the return {@link Colored} object. + * + * @param start The start of the expected span. + * @param end The end of the expected span. + * @return A {@link Colored} object to assert on the color of the matching spans. + */ + @CheckResult + public Colored hasBackgroundColorSpanBetween(int start, int end) { + if (actual == null) { + failWithoutActual(simpleFact("Spanned must not be null")); + return ALREADY_FAILED_COLORED; + } + + List backgroundColorSpans = + findMatchingSpans(start, end, BackgroundColorSpan.class); + if (backgroundColorSpans.isEmpty()) { + failWithExpectedSpanWithoutFlags( + start, end, BackgroundColorSpan.class, actual.toString().substring(start, end)); + return ALREADY_FAILED_COLORED; + } + return check("BackgroundColorSpan (start=%s,end=%s)", start, end) + .about(backgroundColorSpans(actual)) + .that(backgroundColorSpans); } private List findMatchingSpans( @@ -204,27 +268,46 @@ public final class SpannedSubject extends Subject { return spans; } - private void failWithExpectedSpan( + private List findMatchingSpans(int startIndex, int endIndex, Class spanClazz) { + List spans = new ArrayList<>(); + for (T span : actual.getSpans(startIndex, endIndex, spanClazz)) { + if (actual.getSpanStart(span) == startIndex && actual.getSpanEnd(span) == endIndex) { + spans.add(span); + } + } + return spans; + } + + private void failWithExpectedSpanWithFlags( int start, int end, int flags, Object span, String spannedSubstring) { failWithoutActual( simpleFact("No matching span found"), fact("in text", actual), - fact("expected", spanToString(start, end, flags, span, spannedSubstring)), - fact("but found", actualSpansString())); + fact("expected", getSpanAsStringWithFlags(start, end, flags, span, spannedSubstring)), + fact("but found", getAllSpansAsStringWithFlags(actual))); } - private String actualSpansString() { + private void failWithExpectedSpanWithoutFlags( + int start, int end, Class spanType, String spannedSubstring) { + failWithoutActual( + simpleFact("No matching span found"), + fact("in text", actual), + fact("expected", getSpanAsStringWithoutFlags(start, end, spanType, spannedSubstring)), + fact("but found", getAllSpansAsStringWithoutFlags(actual))); + } + + private static String getAllSpansAsStringWithFlags(Spanned spanned) { List actualSpanStrings = new ArrayList<>(); - for (Object span : actual.getSpans(0, actual.length(), /* type= */ Object.class)) { - actualSpanStrings.add(spanToString(span, actual)); + for (Object span : spanned.getSpans(0, spanned.length(), Object.class)) { + actualSpanStrings.add(getSpanAsStringWithFlags(span, spanned)); } return TextUtils.join("\n", actualSpanStrings); } - private static String spanToString(Object span, Spanned spanned) { + private static String getSpanAsStringWithFlags(Object span, Spanned spanned) { int spanStart = spanned.getSpanStart(span); int spanEnd = spanned.getSpanEnd(span); - return spanToString( + return getSpanAsStringWithFlags( spanStart, spanEnd, spanned.getSpanFlags(span), @@ -232,7 +315,7 @@ public final class SpannedSubject extends Subject { spanned.toString().substring(spanStart, spanEnd)); } - private static String spanToString( + private static String getSpanAsStringWithFlags( int start, int end, int flags, Object span, String spannedSubstring) { String suffix; if (span instanceof StyleSpan) { @@ -244,4 +327,177 @@ public final class SpannedSubject extends Subject { "start=%s\tend=%s\tflags=%s\ttype=%s\tsubstring='%s'%s", start, end, flags, span.getClass().getSimpleName(), spannedSubstring, suffix); } + + private static String getAllSpansAsStringWithoutFlags(Spanned spanned) { + List actualSpanStrings = new ArrayList<>(); + for (Object span : spanned.getSpans(0, spanned.length(), Object.class)) { + actualSpanStrings.add(getSpanAsStringWithoutFlags(span, spanned)); + } + return TextUtils.join("\n", actualSpanStrings); + } + + private static String getSpanAsStringWithoutFlags(Object span, Spanned spanned) { + int spanStart = spanned.getSpanStart(span); + int spanEnd = spanned.getSpanEnd(span); + return getSpanAsStringWithoutFlags( + spanStart, spanEnd, span.getClass(), spanned.toString().substring(spanStart, spanEnd)); + } + + private static String getSpanAsStringWithoutFlags( + int start, int end, Class span, String spannedSubstring) { + return String.format( + "start=%s\tend=%s\ttype=%s\tsubstring='%s'", + start, end, span.getSimpleName(), spannedSubstring); + } + + /** + * Allows additional assertions to be made on the flags of matching spans. + * + *

    Identical to {@link WithSpanFlags}, but this should be returned from {@code with...()} + * methods while {@link WithSpanFlags} should be returned from {@code has...()} methods. + * + *

    See Flag constants on {@link Spanned} for possible values. + */ + public interface AndSpanFlags { + + /** + * Checks that one of the matched spans has the expected {@code flags}. + * + * @param flags The expected flags. See SPAN_* constants on {@link Spanned} for possible values. + */ + void andFlags(int flags); + } + + private static final AndSpanFlags ALREADY_FAILED_AND_FLAGS = flags -> {}; + + /** + * Allows additional assertions to be made on the flags of matching spans. + * + *

    Identical to {@link AndSpanFlags}, but this should be returned from {@code has...()} methods + * while {@link AndSpanFlags} should be returned from {@code with...()} methods. + */ + public interface WithSpanFlags { + + /** + * Checks that one of the matched spans has the expected {@code flags}. + * + * @param flags The expected flags. See SPAN_* constants on {@link Spanned} for possible values. + */ + void withFlags(int flags); + } + + private static final WithSpanFlags ALREADY_FAILED_WITH_FLAGS = flags -> {}; + + private static Factory> spanFlags() { + return SpanFlagsSubject::new; + } + + private static final class SpanFlagsSubject extends Subject + implements AndSpanFlags, WithSpanFlags { + + private final List flags; + + private SpanFlagsSubject(FailureMetadata metadata, List flags) { + super(metadata, flags); + this.flags = flags; + } + + @Override + public void andFlags(int flags) { + check("contains()").that(this.flags).contains(flags); + } + + @Override + public void withFlags(int flags) { + andFlags(flags); + } + } + + /** Allows assertions about the color of a span. */ + public interface Colored { + + /** + * Checks that at least one of the matched spans has the expected {@code color}. + * + * @param color The expected color. + * @return A {@link WithSpanFlags} object for optional additional assertions on the flags. + */ + AndSpanFlags withColor(@ColorInt int color); + } + + private static final Colored ALREADY_FAILED_COLORED = color -> ALREADY_FAILED_AND_FLAGS; + + private Factory> foregroundColorSpans( + Spanned actualSpanned) { + return (FailureMetadata metadata, List spans) -> + new ForegroundColorSpansSubject(metadata, spans, actualSpanned); + } + + private static final class ForegroundColorSpansSubject extends Subject implements Colored { + + private final List actualSpans; + private final Spanned actualSpanned; + + private ForegroundColorSpansSubject( + FailureMetadata metadata, List actualSpans, Spanned actualSpanned) { + super(metadata, actualSpans); + this.actualSpans = actualSpans; + this.actualSpanned = actualSpanned; + } + + @Override + public AndSpanFlags withColor(@ColorInt int color) { + List matchingSpanFlags = new ArrayList<>(); + // Use hex strings for comparison so the values in error messages are more human readable. + List spanColors = new ArrayList<>(); + + for (ForegroundColorSpan span : actualSpans) { + spanColors.add(String.format("0x%08X", span.getForegroundColor())); + if (span.getForegroundColor() == color) { + matchingSpanFlags.add(actualSpanned.getSpanFlags(span)); + } + } + + String expectedColorString = String.format("0x%08X", color); + check("foregroundColor").that(spanColors).containsExactly(expectedColorString); + return check("flags").about(spanFlags()).that(matchingSpanFlags); + } + } + + private Factory> backgroundColorSpans( + Spanned actualSpanned) { + return (FailureMetadata metadata, List spans) -> + new BackgroundColorSpansSubject(metadata, spans, actualSpanned); + } + + private static final class BackgroundColorSpansSubject extends Subject implements Colored { + + private final List actualSpans; + private final Spanned actualSpanned; + + private BackgroundColorSpansSubject( + FailureMetadata metadata, List actualSpans, Spanned actualSpanned) { + super(metadata, actualSpans); + this.actualSpans = actualSpans; + this.actualSpanned = actualSpanned; + } + + @Override + public AndSpanFlags withColor(@ColorInt int color) { + List matchingSpanFlags = new ArrayList<>(); + // Use hex strings for comparison so the values in error messages are more human readable. + List spanColors = new ArrayList<>(); + + for (BackgroundColorSpan span : actualSpans) { + spanColors.add(String.format("0x%08X", span.getBackgroundColor())); + if (span.getBackgroundColor() == color) { + matchingSpanFlags.add(actualSpanned.getSpanFlags(span)); + } + } + + String expectedColorString = String.format("0x%08X", color); + check("backgroundColor").that(spanColors).containsExactly(expectedColorString); + return check("flags").about(spanFlags()).that(matchingSpanFlags); + } + } } diff --git a/testutils/src/test/java/com/google/android/exoplayer2/testutil/truth/SpannedSubjectTest.java b/testutils/src/test/java/com/google/android/exoplayer2/testutil/truth/SpannedSubjectTest.java index 37ccef6908..a01f6f66e0 100644 --- a/testutils/src/test/java/com/google/android/exoplayer2/testutil/truth/SpannedSubjectTest.java +++ b/testutils/src/test/java/com/google/android/exoplayer2/testutil/truth/SpannedSubjectTest.java @@ -21,9 +21,12 @@ import static com.google.android.exoplayer2.testutil.truth.SpannedSubject.spanne import static com.google.common.truth.ExpectFailure.assertThat; import static com.google.common.truth.ExpectFailure.expectFailureAbout; +import android.graphics.Color; import android.graphics.Typeface; import android.text.SpannableString; import android.text.Spanned; +import android.text.style.BackgroundColorSpan; +import android.text.style.ForegroundColorSpan; import android.text.style.StyleSpan; import android.text.style.UnderlineSpan; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -153,7 +156,167 @@ public class SpannedSubjectTest { int end = start + "underlined".length(); spannable.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); - assertThat(spannable).hasUnderlineSpan(start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + assertThat(spannable) + .hasUnderlineSpanBetween(start, end) + .withFlags(Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + } + + @Test + public void foregroundColorSpan_success() { + SpannableString spannable = SpannableString.valueOf("test with cyan section"); + int start = "test with ".length(); + int end = start + "cyan".length(); + spannable.setSpan( + new ForegroundColorSpan(Color.CYAN), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + assertThat(spannable) + .hasForegroundColorSpanBetween(start, end) + .withColor(Color.CYAN) + .andFlags(Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + } + + @Test + public void foregroundColorSpan_wrongEndIndex() { + SpannableString spannable = SpannableString.valueOf("test with cyan section"); + int start = "test with ".length(); + int end = start + "cyan".length(); + spannable.setSpan( + new ForegroundColorSpan(Color.CYAN), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + int incorrectEnd = end + 2; + AssertionError expected = + expectFailure( + whenTesting -> + whenTesting + .that(spannable) + .hasForegroundColorSpanBetween(start, incorrectEnd) + .withColor(Color.CYAN)); + assertThat(expected).factValue("expected").contains("end=" + incorrectEnd); + assertThat(expected).factValue("but found").contains("end=" + end); + } + + @Test + public void foregroundColorSpan_wrongColor() { + SpannableString spannable = SpannableString.valueOf("test with cyan section"); + int start = "test with ".length(); + int end = start + "cyan".length(); + spannable.setSpan( + new ForegroundColorSpan(Color.CYAN), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + AssertionError expected = + expectFailure( + whenTesting -> + whenTesting + .that(spannable) + .hasForegroundColorSpanBetween(start, end) + .withColor(Color.BLUE)); + assertThat(expected).factValue("value of").contains("foregroundColor"); + assertThat(expected).factValue("expected").contains("0xFF0000FF"); // Color.BLUE + assertThat(expected).factValue("but was").contains("0xFF00FFFF"); // Color.CYAN + } + + @Test + public void foregroundColorSpan_wrongFlags() { + SpannableString spannable = SpannableString.valueOf("test with cyan section"); + int start = "test with ".length(); + int end = start + "cyan".length(); + spannable.setSpan( + new ForegroundColorSpan(Color.CYAN), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + AssertionError expected = + expectFailure( + whenTesting -> + whenTesting + .that(spannable) + .hasForegroundColorSpanBetween(start, end) + .withColor(Color.CYAN) + .andFlags(Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)); + assertThat(expected).factValue("value of").contains("flags"); + assertThat(expected) + .factValue("expected to contain") + .contains(String.valueOf(Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)); + assertThat(expected) + .factValue("but was") + .contains(String.valueOf(Spanned.SPAN_INCLUSIVE_EXCLUSIVE)); + } + + @Test + public void backgroundColorSpan_success() { + SpannableString spannable = SpannableString.valueOf("test with cyan section"); + int start = "test with ".length(); + int end = start + "cyan".length(); + spannable.setSpan( + new BackgroundColorSpan(Color.CYAN), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + assertThat(spannable) + .hasBackgroundColorSpanBetween(start, end) + .withColor(Color.CYAN) + .andFlags(Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + } + + @Test + public void backgroundColorSpan_wrongEndIndex() { + SpannableString spannable = SpannableString.valueOf("test with cyan section"); + int start = "test with ".length(); + int end = start + "cyan".length(); + spannable.setSpan( + new BackgroundColorSpan(Color.CYAN), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + int incorrectEnd = end + 2; + AssertionError expected = + expectFailure( + whenTesting -> + whenTesting + .that(spannable) + .hasBackgroundColorSpanBetween(start, incorrectEnd) + .withColor(Color.CYAN)); + assertThat(expected).factValue("expected").contains("end=" + incorrectEnd); + assertThat(expected).factValue("but found").contains("end=" + end); + } + + @Test + public void backgroundColorSpan_wrongColor() { + SpannableString spannable = SpannableString.valueOf("test with cyan section"); + int start = "test with ".length(); + int end = start + "cyan".length(); + spannable.setSpan( + new BackgroundColorSpan(Color.CYAN), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + AssertionError expected = + expectFailure( + whenTesting -> + whenTesting + .that(spannable) + .hasBackgroundColorSpanBetween(start, end) + .withColor(Color.BLUE)); + assertThat(expected).factValue("value of").contains("backgroundColor"); + assertThat(expected).factValue("expected").contains("0xFF0000FF"); // Color.BLUE + assertThat(expected).factValue("but was").contains("0xFF00FFFF"); // Color.CYAN + } + + @Test + public void backgroundColorSpan_wrongFlags() { + SpannableString spannable = SpannableString.valueOf("test with cyan section"); + int start = "test with ".length(); + int end = start + "cyan".length(); + spannable.setSpan( + new BackgroundColorSpan(Color.CYAN), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + AssertionError expected = + expectFailure( + whenTesting -> + whenTesting + .that(spannable) + .hasBackgroundColorSpanBetween(start, end) + .withColor(Color.CYAN) + .andFlags(Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)); + assertThat(expected).factValue("value of").contains("flags"); + assertThat(expected) + .factValue("expected to contain") + .contains(String.valueOf(Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)); + assertThat(expected) + .factValue("but was") + .contains(String.valueOf(Spanned.SPAN_INCLUSIVE_EXCLUSIVE)); } private static AssertionError expectFailure( From 88e70d7c1b237c25a21f063462defb665e54bae1 Mon Sep 17 00:00:00 2001 From: ibaker Date: Fri, 3 Jan 2020 15:32:35 +0000 Subject: [PATCH 15/44] Remove WebvttCssStyle.cascadeFrom() It's not used. I was trying to work out how to correctly cascade my text-combine-upright styling, but deleting the method seemed easier... PiperOrigin-RevId: 287989480 --- .../text/webvtt/WebvttCssStyle.java | 31 ------------------- 1 file changed, 31 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.java b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.java index 97c0acb1ec..1369859552 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.java @@ -287,37 +287,6 @@ public final class WebvttCssStyle { return fontSize; } - public void cascadeFrom(WebvttCssStyle style) { - if (style.hasFontColor) { - setFontColor(style.fontColor); - } - if (style.bold != UNSPECIFIED) { - bold = style.bold; - } - if (style.italic != UNSPECIFIED) { - italic = style.italic; - } - if (style.fontFamily != null) { - fontFamily = style.fontFamily; - } - if (linethrough == UNSPECIFIED) { - linethrough = style.linethrough; - } - if (underline == UNSPECIFIED) { - underline = style.underline; - } - if (textAlign == null) { - textAlign = style.textAlign; - } - if (fontSizeUnit == UNSPECIFIED) { - fontSizeUnit = style.fontSizeUnit; - fontSize = style.fontSize; - } - if (style.hasBackgroundColor) { - setBackgroundColor(style.backgroundColor); - } - } - private static int updateScoreForMatch( int currentScore, String target, @Nullable String actual, int score) { if (target.isEmpty() || currentScore == -1) { From f1f0ff3a658e7734d58ea19810e27abe1c122dc4 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 3 Jan 2020 16:59:05 +0000 Subject: [PATCH 16/44] Use MIME types rather than PCM encodings for ALAW and MLAW PiperOrigin-RevId: 287999703 --- RELEASENOTES.md | 3 + .../ext/ffmpeg/FfmpegAudioRenderer.java | 3 +- .../exoplayer2/ext/ffmpeg/FfmpegDecoder.java | 12 +--- .../exoplayer2/ext/ffmpeg/FfmpegLibrary.java | 20 +++--- .../java/com/google/android/exoplayer2/C.java | 27 +++----- .../com/google/android/exoplayer2/Format.java | 8 +-- .../exoplayer2/audio/DefaultAudioSink.java | 2 - .../audio/ResamplingAudioProcessor.java | 4 -- .../android/exoplayer2/audio/WavUtil.java | 12 +--- .../extractor/flv/AudioTagPayloadReader.java | 17 ++++- .../extractor/wav/WavExtractor.java | 62 ++++++++++++------- .../google/android/exoplayer2/util/Util.java | 2 - 12 files changed, 79 insertions(+), 93 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 027f27da7a..9b2f0e7cbc 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -40,6 +40,9 @@ * Show ad group markers in `DefaultTimeBar` even if they are after the end of the current window ([#6552](https://github.com/google/ExoPlayer/issues/6552)). +* WAV: + * Support IMA ADPCM encoded data. + * Improve support for G.711 A-law and mu-law encoded data. ### 2.11.1 (2019-12-20) ### diff --git a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java index 17292cec34..0673f7893a 100644 --- a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java +++ b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java @@ -98,8 +98,7 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer { Assertions.checkNotNull(format.sampleMimeType); if (!FfmpegLibrary.isAvailable()) { return FORMAT_UNSUPPORTED_TYPE; - } else if (!FfmpegLibrary.supportsFormat(format.sampleMimeType, format.pcmEncoding) - || !isOutputSupported(format)) { + } else if (!FfmpegLibrary.supportsFormat(format.sampleMimeType) || !isOutputSupported(format)) { return FORMAT_UNSUPPORTED_SUBTYPE; } else if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) { return FORMAT_UNSUPPORTED_DRM; diff --git a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoder.java b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoder.java index 5314835d1e..6fa3d888db 100644 --- a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoder.java +++ b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoder.java @@ -64,9 +64,7 @@ import java.util.List; throw new FfmpegDecoderException("Failed to load decoder native libraries."); } Assertions.checkNotNull(format.sampleMimeType); - codecName = - Assertions.checkNotNull( - FfmpegLibrary.getCodecName(format.sampleMimeType, format.pcmEncoding)); + codecName = Assertions.checkNotNull(FfmpegLibrary.getCodecName(format.sampleMimeType)); extraData = getExtraData(format.sampleMimeType, format.initializationData); encoding = outputFloat ? C.ENCODING_PCM_FLOAT : C.ENCODING_PCM_16BIT; outputBufferSize = outputFloat ? OUTPUT_BUFFER_SIZE_32BIT : OUTPUT_BUFFER_SIZE_16BIT; @@ -145,16 +143,12 @@ import java.util.List; nativeContext = 0; } - /** - * Returns the channel count of output audio. May only be called after {@link #decode}. - */ + /** Returns the channel count of output audio. */ public int getChannelCount() { return channelCount; } - /** - * Returns the sample rate of output audio. May only be called after {@link #decode}. - */ + /** Returns the sample rate of output audio. */ public int getSampleRate() { return sampleRate; } diff --git a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java index 5b816b8c20..4639851263 100644 --- a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java +++ b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java @@ -16,7 +16,6 @@ package com.google.android.exoplayer2.ext.ffmpeg; import androidx.annotation.Nullable; -import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.util.LibraryLoader; import com.google.android.exoplayer2.util.Log; @@ -65,13 +64,12 @@ public final class FfmpegLibrary { * Returns whether the underlying library supports the specified MIME type. * * @param mimeType The MIME type to check. - * @param encoding The PCM encoding for raw audio. */ - public static boolean supportsFormat(String mimeType, @C.PcmEncoding int encoding) { + public static boolean supportsFormat(String mimeType) { if (!isAvailable()) { return false; } - String codecName = getCodecName(mimeType, encoding); + String codecName = getCodecName(mimeType); if (codecName == null) { return false; } @@ -86,7 +84,7 @@ public final class FfmpegLibrary { * Returns the name of the FFmpeg decoder that could be used to decode the format, or {@code null} * if it's unsupported. */ - /* package */ static @Nullable String getCodecName(String mimeType, @C.PcmEncoding int encoding) { + /* package */ static @Nullable String getCodecName(String mimeType) { switch (mimeType) { case MimeTypes.AUDIO_AAC: return "aac"; @@ -116,14 +114,10 @@ public final class FfmpegLibrary { return "flac"; case MimeTypes.AUDIO_ALAC: return "alac"; - case MimeTypes.AUDIO_RAW: - if (encoding == C.ENCODING_PCM_MU_LAW) { - return "pcm_mulaw"; - } else if (encoding == C.ENCODING_PCM_A_LAW) { - return "pcm_alaw"; - } else { - return null; - } + case MimeTypes.AUDIO_MLAW: + return "pcm_mulaw"; + case MimeTypes.AUDIO_ALAW: + return "pcm_alaw"; default: return null; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/C.java b/library/core/src/main/java/com/google/android/exoplayer2/C.java index 776e79df97..46f20a20f4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/C.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/C.java @@ -151,10 +151,9 @@ public final class C { * Represents an audio encoding, or an invalid or unset value. One of {@link Format#NO_VALUE}, * {@link #ENCODING_INVALID}, {@link #ENCODING_PCM_8BIT}, {@link #ENCODING_PCM_16BIT}, {@link * #ENCODING_PCM_16BIT_BIG_ENDIAN}, {@link #ENCODING_PCM_24BIT}, {@link #ENCODING_PCM_32BIT}, - * {@link #ENCODING_PCM_FLOAT}, {@link #ENCODING_PCM_MU_LAW}, {@link #ENCODING_PCM_A_LAW}, {@link - * #ENCODING_MP3}, {@link #ENCODING_AC3}, {@link #ENCODING_E_AC3}, {@link #ENCODING_E_AC3_JOC}, - * {@link #ENCODING_AC4}, {@link #ENCODING_DTS}, {@link #ENCODING_DTS_HD} or {@link - * #ENCODING_DOLBY_TRUEHD}. + * {@link #ENCODING_PCM_FLOAT}, {@link #ENCODING_MP3}, {@link #ENCODING_AC3}, {@link + * #ENCODING_E_AC3}, {@link #ENCODING_E_AC3_JOC}, {@link #ENCODING_AC4}, {@link #ENCODING_DTS}, + * {@link #ENCODING_DTS_HD} or {@link #ENCODING_DOLBY_TRUEHD}. */ @Documented @Retention(RetentionPolicy.SOURCE) @@ -167,8 +166,6 @@ public final class C { ENCODING_PCM_24BIT, ENCODING_PCM_32BIT, ENCODING_PCM_FLOAT, - ENCODING_PCM_MU_LAW, - ENCODING_PCM_A_LAW, ENCODING_MP3, ENCODING_AC3, ENCODING_E_AC3, @@ -176,7 +173,7 @@ public final class C { ENCODING_AC4, ENCODING_DTS, ENCODING_DTS_HD, - ENCODING_DOLBY_TRUEHD, + ENCODING_DOLBY_TRUEHD }) public @interface Encoding {} @@ -184,7 +181,7 @@ public final class C { * Represents a PCM audio encoding, or an invalid or unset value. One of {@link Format#NO_VALUE}, * {@link #ENCODING_INVALID}, {@link #ENCODING_PCM_8BIT}, {@link #ENCODING_PCM_16BIT}, {@link * #ENCODING_PCM_16BIT_BIG_ENDIAN}, {@link #ENCODING_PCM_24BIT}, {@link #ENCODING_PCM_32BIT}, - * {@link #ENCODING_PCM_FLOAT}, {@link #ENCODING_PCM_MU_LAW} or {@link #ENCODING_PCM_A_LAW}. + * {@link #ENCODING_PCM_FLOAT}. */ @Documented @Retention(RetentionPolicy.SOURCE) @@ -196,9 +193,7 @@ public final class C { ENCODING_PCM_16BIT_BIG_ENDIAN, ENCODING_PCM_24BIT, ENCODING_PCM_32BIT, - ENCODING_PCM_FLOAT, - ENCODING_PCM_MU_LAW, - ENCODING_PCM_A_LAW + ENCODING_PCM_FLOAT }) public @interface PcmEncoding {} /** @see AudioFormat#ENCODING_INVALID */ @@ -208,17 +203,13 @@ public final class C { /** @see AudioFormat#ENCODING_PCM_16BIT */ public static final int ENCODING_PCM_16BIT = AudioFormat.ENCODING_PCM_16BIT; /** Like {@link #ENCODING_PCM_16BIT}, but with the bytes in big endian order. */ - public static final int ENCODING_PCM_16BIT_BIG_ENDIAN = 0x08000000; + public static final int ENCODING_PCM_16BIT_BIG_ENDIAN = 0x10000000; /** PCM encoding with 24 bits per sample. */ - public static final int ENCODING_PCM_24BIT = 0x80000000; + public static final int ENCODING_PCM_24BIT = 0x20000000; /** PCM encoding with 32 bits per sample. */ - public static final int ENCODING_PCM_32BIT = 0x40000000; + public static final int ENCODING_PCM_32BIT = 0x30000000; /** @see AudioFormat#ENCODING_PCM_FLOAT */ public static final int ENCODING_PCM_FLOAT = AudioFormat.ENCODING_PCM_FLOAT; - /** Audio encoding for mu-law. */ - public static final int ENCODING_PCM_MU_LAW = 0x10000000; - /** Audio encoding for A-law. */ - public static final int ENCODING_PCM_A_LAW = 0x20000000; /** @see AudioFormat#ENCODING_MP3 */ public static final int ENCODING_MP3 = AudioFormat.ENCODING_MP3; /** @see AudioFormat#ENCODING_AC3 */ diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Format.java b/library/core/src/main/java/com/google/android/exoplayer2/Format.java index 4fb6cec1e8..19ed34405a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Format.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Format.java @@ -138,13 +138,7 @@ public final class Format implements Parcelable { * The audio sampling rate in Hz, or {@link #NO_VALUE} if unknown or not applicable. */ public final int sampleRate; - /** - * The encoding for PCM audio streams. If {@link #sampleMimeType} is {@link MimeTypes#AUDIO_RAW} - * then one of {@link C#ENCODING_PCM_8BIT}, {@link C#ENCODING_PCM_16BIT}, {@link - * C#ENCODING_PCM_24BIT}, {@link C#ENCODING_PCM_32BIT}, {@link C#ENCODING_PCM_FLOAT}, {@link - * C#ENCODING_PCM_MU_LAW} or {@link C#ENCODING_PCM_A_LAW}. Set to {@link #NO_VALUE} for other - * media types. - */ + /** The {@link C.PcmEncoding} for PCM audio. Set to {@link #NO_VALUE} for other media types. */ public final @C.PcmEncoding int pcmEncoding; /** * The number of frames to trim from the start of the decoded audio stream, or 0 if not 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 d73cf0be40..27abf486fa 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 @@ -1149,9 +1149,7 @@ public final class DefaultAudioSink implements AudioSink { case C.ENCODING_PCM_24BIT: case C.ENCODING_PCM_32BIT: case C.ENCODING_PCM_8BIT: - case C.ENCODING_PCM_A_LAW: case C.ENCODING_PCM_FLOAT: - case C.ENCODING_PCM_MU_LAW: case Format.NO_VALUE: default: throw new IllegalArgumentException(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java index 30bd4da472..7175b93614 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java @@ -64,8 +64,6 @@ import java.nio.ByteBuffer; break; case C.ENCODING_PCM_16BIT: case C.ENCODING_PCM_FLOAT: - case C.ENCODING_PCM_A_LAW: - case C.ENCODING_PCM_MU_LAW: case C.ENCODING_INVALID: case Format.NO_VALUE: default: @@ -105,8 +103,6 @@ import java.nio.ByteBuffer; break; case C.ENCODING_PCM_16BIT: case C.ENCODING_PCM_FLOAT: - case C.ENCODING_PCM_A_LAW: - case C.ENCODING_PCM_MU_LAW: case C.ENCODING_INVALID: case Format.NO_VALUE: default: diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/WavUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/WavUtil.java index 25261f1686..dff81021de 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/WavUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/WavUtil.java @@ -36,9 +36,9 @@ public final class WavUtil { /** WAVE type value for float PCM audio data. */ public static final int TYPE_FLOAT = 0x0003; /** WAVE type value for 8-bit ITU-T G.711 A-law audio data. */ - public static final int TYPE_A_LAW = 0x0006; + public static final int TYPE_ALAW = 0x0006; /** WAVE type value for 8-bit ITU-T G.711 mu-law audio data. */ - public static final int TYPE_MU_LAW = 0x0007; + public static final int TYPE_MLAW = 0x0007; /** WAVE type value for IMA ADPCM audio data. */ public static final int TYPE_IMA_ADPCM = 0x0011; /** WAVE type value for extended WAVE format. */ @@ -59,10 +59,6 @@ public final class WavUtil { case C.ENCODING_PCM_24BIT: case C.ENCODING_PCM_32BIT: return TYPE_PCM; - case C.ENCODING_PCM_A_LAW: - return TYPE_A_LAW; - case C.ENCODING_PCM_MU_LAW: - return TYPE_MU_LAW; case C.ENCODING_PCM_FLOAT: return TYPE_FLOAT; case C.ENCODING_INVALID: @@ -83,10 +79,6 @@ public final class WavUtil { return Util.getPcmEncoding(bitsPerSample); case TYPE_FLOAT: return bitsPerSample == 32 ? C.ENCODING_PCM_FLOAT : C.ENCODING_INVALID; - case TYPE_A_LAW: - return C.ENCODING_PCM_A_LAW; - case TYPE_MU_LAW: - return C.ENCODING_PCM_MU_LAW; default: return C.ENCODING_INVALID; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/AudioTagPayloadReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/AudioTagPayloadReader.java index b10f2bf80b..4a904844ee 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/AudioTagPayloadReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/AudioTagPayloadReader.java @@ -69,9 +69,20 @@ import java.util.Collections; } else if (audioFormat == AUDIO_FORMAT_ALAW || audioFormat == AUDIO_FORMAT_ULAW) { String type = audioFormat == AUDIO_FORMAT_ALAW ? MimeTypes.AUDIO_ALAW : MimeTypes.AUDIO_MLAW; - int pcmEncoding = (header & 0x01) == 1 ? C.ENCODING_PCM_16BIT : C.ENCODING_PCM_8BIT; - Format format = Format.createAudioSampleFormat(null, type, null, Format.NO_VALUE, - Format.NO_VALUE, 1, 8000, pcmEncoding, null, null, 0, null); + Format format = + Format.createAudioSampleFormat( + /* id= */ null, + /* sampleMimeType= */ type, + /* codecs= */ null, + /* bitrate= */ Format.NO_VALUE, + /* maxInputSize= */ Format.NO_VALUE, + /* channelCount= */ 1, + /* sampleRate= */ 8000, + /* pcmEncoding= */ Format.NO_VALUE, + /* initializationData= */ null, + /* drmInitData= */ null, + /* selectionFlags= */ 0, + /* language= */ null); output.format(format); hasOutputFormat = true; } else if (audioFormat != AUDIO_FORMAT_AAC) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java index 0c6e538f43..d9989aeaf6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java @@ -94,13 +94,31 @@ public final class WavExtractor implements Extractor { if (header.formatType == WavUtil.TYPE_IMA_ADPCM) { outputWriter = new ImaAdPcmOutputWriter(extractorOutput, trackOutput, header); + } else if (header.formatType == WavUtil.TYPE_ALAW) { + outputWriter = + new PassthroughOutputWriter( + extractorOutput, + trackOutput, + header, + MimeTypes.AUDIO_ALAW, + /* pcmEncoding= */ Format.NO_VALUE); + } else if (header.formatType == WavUtil.TYPE_MLAW) { + outputWriter = + new PassthroughOutputWriter( + extractorOutput, + trackOutput, + header, + MimeTypes.AUDIO_MLAW, + /* pcmEncoding= */ Format.NO_VALUE); } else { @C.PcmEncoding int pcmEncoding = WavUtil.getPcmEncodingForType(header.formatType, header.bitsPerSample); if (pcmEncoding == C.ENCODING_INVALID) { throw new ParserException("Unsupported WAV format type: " + header.formatType); } - outputWriter = new PcmOutputWriter(extractorOutput, trackOutput, header, pcmEncoding); + outputWriter = + new PassthroughOutputWriter( + extractorOutput, trackOutput, header, MimeTypes.AUDIO_RAW, pcmEncoding); } } @@ -155,12 +173,12 @@ public final class WavExtractor implements Extractor { throws IOException, InterruptedException; } - private static final class PcmOutputWriter implements OutputWriter { + private static final class PassthroughOutputWriter implements OutputWriter { private final ExtractorOutput extractorOutput; private final TrackOutput trackOutput; private final WavHeader header; - private final @C.PcmEncoding int pcmEncoding; + private final Format format; /** The target size of each output sample, in bytes. */ private final int targetSampleSizeBytes; @@ -178,19 +196,33 @@ public final class WavExtractor implements Extractor { */ private long outputFrameCount; - public PcmOutputWriter( + public PassthroughOutputWriter( ExtractorOutput extractorOutput, TrackOutput trackOutput, WavHeader header, + String mimeType, @C.PcmEncoding int pcmEncoding) { this.extractorOutput = extractorOutput; this.trackOutput = trackOutput; this.header = header; - this.pcmEncoding = pcmEncoding; - // For PCM blocks correspond to single frames. This is validated in init(int, long). + // Blocks are expected to correspond to single frames. This is validated in init(int, long). int bytesPerFrame = header.blockSize; targetSampleSizeBytes = Math.max(bytesPerFrame, header.frameRateHz * bytesPerFrame / TARGET_SAMPLES_PER_SECOND); + format = + Format.createAudioSampleFormat( + /* id= */ null, + mimeType, + /* codecs= */ null, + /* bitrate= */ header.frameRateHz * bytesPerFrame * 8, + /* maxInputSize= */ targetSampleSizeBytes, + header.numChannels, + header.frameRateHz, + pcmEncoding, + /* initializationData= */ null, + /* drmInitData= */ null, + /* selectionFlags= */ 0, + /* language= */ null); } @Override @@ -209,25 +241,9 @@ public final class WavExtractor implements Extractor { "Expected block size: " + bytesPerFrame + "; got: " + header.blockSize); } - // Output the seek map. + // Output the seek map and format. extractorOutput.seekMap( new WavSeekMap(header, /* framesPerBlock= */ 1, dataStartPosition, dataEndPosition)); - - // Output the format. - Format format = - Format.createAudioSampleFormat( - /* id= */ null, - MimeTypes.AUDIO_RAW, - /* codecs= */ null, - /* bitrate= */ header.frameRateHz * bytesPerFrame * 8, - /* maxInputSize= */ targetSampleSizeBytes, - header.numChannels, - header.frameRateHz, - pcmEncoding, - /* initializationData= */ null, - /* drmInitData= */ null, - /* selectionFlags= */ 0, - /* language= */ null); trackOutput.format(format); } 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 aa87096ebb..3ca86ef13d 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 @@ -1431,8 +1431,6 @@ public final class Util { case C.ENCODING_PCM_32BIT: case C.ENCODING_PCM_FLOAT: return channelCount * 4; - case C.ENCODING_PCM_A_LAW: - case C.ENCODING_PCM_MU_LAW: case C.ENCODING_INVALID: case Format.NO_VALUE: default: From 1c1c0ed88a5755915d0a3b6da49d48cd56fcab39 Mon Sep 17 00:00:00 2001 From: christosts Date: Fri, 3 Jan 2020 17:44:27 +0000 Subject: [PATCH 17/44] Remove getDequeueOutputBufferTimeoutUs Remove unused method MediaCodecRenderer#getDequeueOutputBufferTimeoutUs(). PiperOrigin-RevId: 288005572 --- .../exoplayer2/mediacodec/MediaCodecRenderer.java | 11 +---------- .../mediacodec/SynchronousMediaCodecAdapter.java | 6 ++---- 2 files changed, 3 insertions(+), 14 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java index e973b70204..dbfeed4063 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java @@ -1001,7 +1001,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { codecAdapter = new MultiLockAsyncMediaCodecAdapter(codec, getTrackType()); ((MultiLockAsyncMediaCodecAdapter) codecAdapter).start(); } else { - codecAdapter = new SynchronousMediaCodecAdapter(codec, getDequeueOutputBufferTimeoutUs()); + codecAdapter = new SynchronousMediaCodecAdapter(codec); } TraceUtil.endSection(); @@ -1460,15 +1460,6 @@ public abstract class MediaCodecRenderer extends BaseRenderer { && SystemClock.elapsedRealtime() < codecHotswapDeadlineMs)); } - /** - * Returns the maximum time to block whilst waiting for a decoded output buffer. - * - * @return The maximum time to block, in microseconds. - */ - protected long getDequeueOutputBufferTimeoutUs() { - return 0; - } - /** * Returns the {@link MediaFormat#KEY_OPERATING_RATE} value for a given renderer operating rate, * current {@link Format} and set of possible stream formats. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/SynchronousMediaCodecAdapter.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/SynchronousMediaCodecAdapter.java index 8caf72ecf4..7dd7ef8f20 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/SynchronousMediaCodecAdapter.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/SynchronousMediaCodecAdapter.java @@ -24,11 +24,9 @@ import android.media.MediaFormat; */ /* package */ final class SynchronousMediaCodecAdapter implements MediaCodecAdapter { private final MediaCodec codec; - private final long dequeueOutputBufferTimeoutMs; - public SynchronousMediaCodecAdapter(MediaCodec mediaCodec, long dequeueOutputBufferTimeoutMs) { + public SynchronousMediaCodecAdapter(MediaCodec mediaCodec) { this.codec = mediaCodec; - this.dequeueOutputBufferTimeoutMs = dequeueOutputBufferTimeoutMs; } @Override @@ -38,7 +36,7 @@ import android.media.MediaFormat; @Override public int dequeueOutputBufferIndex(MediaCodec.BufferInfo bufferInfo) { - return codec.dequeueOutputBuffer(bufferInfo, dequeueOutputBufferTimeoutMs); + return codec.dequeueOutputBuffer(bufferInfo, 0); } @Override From 29df73e2681e1dcab2227e08dfd2c949ebece942 Mon Sep 17 00:00:00 2001 From: ibaker Date: Mon, 6 Jan 2020 11:48:54 +0000 Subject: [PATCH 18/44] Fix MatroskaExtractor to use blockDurationUs not durationUs This typo was introduced in https://github.com/google/ExoPlayer/commit/ddb70d96ad99f07fe10f53a76ce3262fe625be70 when migrating a static method with parameter `durationUs` to an instance method where the correct field to use was `blockDurationUs` (but `durationUs` also exists). The test that catches this was only added in https://github.com/google/ExoPlayer/commit/45013ece1e3fe054ff8960355a89559241eeb288 (and therefore configured with the wrong expected output data). issue:#6833 PiperOrigin-RevId: 288274197 --- RELEASENOTES.md | 2 ++ .../android/exoplayer2/extractor/mkv/MatroskaExtractor.java | 4 ++-- library/core/src/test/assets/mkv/full_blocks.mkv.0.dump | 6 +++--- library/core/src/test/assets/mkv/full_blocks.mkv.1.dump | 6 +++--- library/core/src/test/assets/mkv/full_blocks.mkv.2.dump | 6 +++--- library/core/src/test/assets/mkv/full_blocks.mkv.3.dump | 6 +++--- 6 files changed, 16 insertions(+), 14 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 9b2f0e7cbc..8df1b1f698 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -43,6 +43,8 @@ * WAV: * Support IMA ADPCM encoded data. * Improve support for G.711 A-law and mu-law encoded data. +* Fix MKV subtitles to disappear when intended instead of lasting until the + next cue ([#6833](https://github.com/google/ExoPlayer/issues/6833)). ### 2.11.1 (2019-12-20) ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java index ed2acc5898..ee57bbec90 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java @@ -1250,10 +1250,10 @@ public class MatroskaExtractor implements Extractor { if (CODEC_ID_SUBRIP.equals(track.codecId) || CODEC_ID_ASS.equals(track.codecId)) { if (blockSampleCount > 1) { Log.w(TAG, "Skipping subtitle sample in laced block."); - } else if (durationUs == C.TIME_UNSET) { + } else if (blockDurationUs == C.TIME_UNSET) { Log.w(TAG, "Skipping subtitle sample with no duration."); } else { - setSubtitleEndTime(track.codecId, durationUs, subtitleSample.data); + setSubtitleEndTime(track.codecId, blockDurationUs, subtitleSample.data); // Note: If we ever want to support DRM protected subtitles then we'll need to output the // appropriate encryption data here. track.output.sampleData(subtitleSample, subtitleSample.limit()); diff --git a/library/core/src/test/assets/mkv/full_blocks.mkv.0.dump b/library/core/src/test/assets/mkv/full_blocks.mkv.0.dump index ac111d0c62..70b22f16aa 100644 --- a/library/core/src/test/assets/mkv/full_blocks.mkv.0.dump +++ b/library/core/src/test/assets/mkv/full_blocks.mkv.0.dump @@ -31,13 +31,13 @@ track 1: sample 0: time = 0 flags = 1 - data = length 59, hash A0217393 + data = length 59, hash 1AD38625 sample 1: time = 2345000 flags = 1 - data = length 95, hash 4904F2 + data = length 95, hash F331C282 sample 2: time = 4567000 flags = 1 - data = length 59, hash EFAB6D8A + data = length 59, hash F8CD7C60 tracksEnded = true diff --git a/library/core/src/test/assets/mkv/full_blocks.mkv.1.dump b/library/core/src/test/assets/mkv/full_blocks.mkv.1.dump index ac111d0c62..70b22f16aa 100644 --- a/library/core/src/test/assets/mkv/full_blocks.mkv.1.dump +++ b/library/core/src/test/assets/mkv/full_blocks.mkv.1.dump @@ -31,13 +31,13 @@ track 1: sample 0: time = 0 flags = 1 - data = length 59, hash A0217393 + data = length 59, hash 1AD38625 sample 1: time = 2345000 flags = 1 - data = length 95, hash 4904F2 + data = length 95, hash F331C282 sample 2: time = 4567000 flags = 1 - data = length 59, hash EFAB6D8A + data = length 59, hash F8CD7C60 tracksEnded = true diff --git a/library/core/src/test/assets/mkv/full_blocks.mkv.2.dump b/library/core/src/test/assets/mkv/full_blocks.mkv.2.dump index ac111d0c62..70b22f16aa 100644 --- a/library/core/src/test/assets/mkv/full_blocks.mkv.2.dump +++ b/library/core/src/test/assets/mkv/full_blocks.mkv.2.dump @@ -31,13 +31,13 @@ track 1: sample 0: time = 0 flags = 1 - data = length 59, hash A0217393 + data = length 59, hash 1AD38625 sample 1: time = 2345000 flags = 1 - data = length 95, hash 4904F2 + data = length 95, hash F331C282 sample 2: time = 4567000 flags = 1 - data = length 59, hash EFAB6D8A + data = length 59, hash F8CD7C60 tracksEnded = true diff --git a/library/core/src/test/assets/mkv/full_blocks.mkv.3.dump b/library/core/src/test/assets/mkv/full_blocks.mkv.3.dump index ac111d0c62..70b22f16aa 100644 --- a/library/core/src/test/assets/mkv/full_blocks.mkv.3.dump +++ b/library/core/src/test/assets/mkv/full_blocks.mkv.3.dump @@ -31,13 +31,13 @@ track 1: sample 0: time = 0 flags = 1 - data = length 59, hash A0217393 + data = length 59, hash 1AD38625 sample 1: time = 2345000 flags = 1 - data = length 95, hash 4904F2 + data = length 95, hash F331C282 sample 2: time = 4567000 flags = 1 - data = length 59, hash EFAB6D8A + data = length 59, hash F8CD7C60 tracksEnded = true From 1e7db22ee256b473c6cb3c8dc16091530672299e Mon Sep 17 00:00:00 2001 From: ibaker Date: Mon, 6 Jan 2020 11:56:24 +0000 Subject: [PATCH 19/44] Convert StyleSpan assertions in SpannedSubject to fluent style PiperOrigin-RevId: 288274998 --- .../text/webvtt/WebvttCueParserTest.java | 36 +--- .../text/webvtt/WebvttDecoderTest.java | 6 +- .../testutil/truth/SpannedSubject.java | 188 ++++++------------ .../testutil/truth/SpannedSubjectTest.java | 45 +++-- 4 files changed, 107 insertions(+), 168 deletions(-) diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParserTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParserTest.java index c9e8488c60..ec4ed10f3d 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParserTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParserTest.java @@ -36,10 +36,7 @@ public final class WebvttCueParserTest { assertThat(text.toString()).isEqualTo("This is text with html tags"); assertThat(text).hasUnderlineSpanBetween("This ".length(), "This is".length()); assertThat(text) - .hasBoldItalicSpan( - "This is text with ".length(), - "This is text with html".length(), - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + .hasBoldItalicSpanBetween("This is text with ".length(), "This is text with html".length()); } @Test @@ -60,10 +57,8 @@ public final class WebvttCueParserTest { assertThat(text) .hasUnderlineSpanBetween("An ".length(), "An unclosed u tag with italic inside".length()); assertThat(text) - .hasItalicSpan( - "An unclosed u tag with ".length(), - "An unclosed u tag with italic".length(), - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + .hasItalicSpanBetween( + "An unclosed u tag with ".length(), "An unclosed u tag with italic".length()); } @Test @@ -72,10 +67,9 @@ public final class WebvttCueParserTest { assertThat(text.toString()).isEqualTo("An italic tag with unclosed underline inside"); assertThat(text) - .hasItalicSpan( + .hasItalicSpanBetween( "An italic tag with unclosed ".length(), - "An italic tag with unclosed underline".length(), - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + "An italic tag with unclosed underline".length()); assertThat(text) .hasUnderlineSpanBetween( "An italic tag with unclosed ".length(), @@ -88,15 +82,11 @@ public final class WebvttCueParserTest { String expectedText = "Overlapping u and i tags"; assertThat(text.toString()).isEqualTo(expectedText); - assertThat(text).hasBoldSpan(0, expectedText.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + assertThat(text).hasBoldSpanBetween(0, expectedText.length()); // Text between the tags is underlined. assertThat(text).hasUnderlineSpanBetween(0, "Overlapping u and".length()); // Only text from to <\\u> is italic (unexpected - but simplifies the parsing). - assertThat(text) - .hasItalicSpan( - "Overlapping u ".length(), - "Overlapping u and".length(), - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + assertThat(text).hasItalicSpanBetween("Overlapping u ".length(), "Overlapping u and".length()); } @Test @@ -105,8 +95,7 @@ public final class WebvttCueParserTest { assertThat(text.toString()).isEqualTo("foobarbazbuzz"); // endIndex should be 9 when valid (i.e. "foobarbaz".length() - assertThat(text) - .hasBoldSpan("foo".length(), "foobar".length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + assertThat(text).hasBoldSpanBetween("foo".length(), "foobar".length()); } @Test @@ -156,13 +145,8 @@ public final class WebvttCueParserTest { Spanned text = parseCueText("blah blah blah foo"); assertThat(text.toString()).isEqualTo("blah blah blah foo"); - assertThat(text) - .hasBoldSpan("blah ".length(), "blah blah".length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - assertThat(text) - .hasBoldSpan( - "blah blah blah ".length(), - "blah blah blah foo".length(), - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + assertThat(text).hasBoldSpanBetween("blah ".length(), "blah blah".length()); + assertThat(text).hasBoldSpanBetween("blah blah blah ".length(), "blah blah blah foo".length()); } @Test diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java index a3ab3e8b1a..063d4e1bfd 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java @@ -417,11 +417,7 @@ public class WebvttDecoderTest { assertThat(s4) .hasBackgroundColorSpanBetween(0, 16) .withColor(ColorParser.parseCssColor("lime")); - assertThat(s4) - .hasBoldSpan( - /* startIndex= */ 17, - /* endIndex= */ s4.length(), - /* flags= */ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + assertThat(s4).hasBoldSpanBetween(/* startIndex= */ 17, /* endIndex= */ s4.length()); } @Test diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/truth/SpannedSubject.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/truth/SpannedSubject.java index 84d40fb6f1..16144a170b 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/truth/SpannedSubject.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/truth/SpannedSubject.java @@ -33,6 +33,7 @@ import androidx.annotation.Nullable; import com.google.common.truth.FailureMetadata; import com.google.common.truth.Subject; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; /** A Truth {@link Subject} for assertions on {@link Spanned} instances containing text styling. */ @@ -68,59 +69,59 @@ public final class SpannedSubject extends Subject { failWithoutActual( simpleFact("Expected no spans"), fact("in text", actual), - fact("but found", getAllSpansAsStringWithoutFlags(actual))); + fact("but found", getAllSpansAsString(actual))); } } /** - * Checks that the subject has an italic span from {@code startIndex} to {@code endIndex}. + * Checks that the subject has an italic span from {@code start} to {@code end}. * - * @param startIndex The start of the expected span. - * @param endIndex The end of the expected span. - * @param flags The flags of the expected span. See constants on {@link Spanned} for more - * information. + * @param start The start of the expected span. + * @param end The end of the expected span. + * @return A {@link WithSpanFlags} object for optional additional assertions on the flags. */ - // TODO: swap this to fluent-style. - public void hasItalicSpan(int startIndex, int endIndex, int flags) { - hasStyleSpan(startIndex, endIndex, flags, Typeface.ITALIC); + public WithSpanFlags hasItalicSpanBetween(int start, int end) { + return hasStyleSpan(start, end, Typeface.ITALIC); } /** - * Checks that the subject has a bold span from {@code startIndex} to {@code endIndex}. + * Checks that the subject has a bold span from {@code start} to {@code end}. * - * @param startIndex The start of the expected span. - * @param endIndex The end of the expected span. - * @param flags The flags of the expected span. See constants on {@link Spanned} for more - * information. + * @param start The start of the expected span. + * @param end The end of the expected span. + * @return A {@link WithSpanFlags} object for optional additional assertions on the flags. */ - // TODO: swap this to fluent-style. - public void hasBoldSpan(int startIndex, int endIndex, int flags) { - hasStyleSpan(startIndex, endIndex, flags, Typeface.BOLD); + public WithSpanFlags hasBoldSpanBetween(int start, int end) { + return hasStyleSpan(start, end, Typeface.BOLD); } - private void hasStyleSpan(int startIndex, int endIndex, int flags, int style) { + private WithSpanFlags hasStyleSpan(int start, int end, int style) { if (actual == null) { failWithoutActual(simpleFact("Spanned must not be null")); - return; + return ALREADY_FAILED_WITH_FLAGS; } - for (StyleSpan span : findMatchingSpans(startIndex, endIndex, flags, StyleSpan.class)) { + List allFlags = new ArrayList<>(); + boolean matchingSpanFound = false; + for (StyleSpan span : findMatchingSpans(start, end, StyleSpan.class)) { + allFlags.add(actual.getSpanFlags(span)); if (span.getStyle() == style) { - return; + matchingSpanFound = true; + break; } } + if (matchingSpanFound) { + return check("StyleSpan (start=%s,end=%s,style=%s)", start, end, style) + .about(spanFlags()) + .that(allFlags); + } - failWithExpectedSpanWithFlags( - startIndex, - endIndex, - flags, - new StyleSpan(style), - actual.toString().substring(startIndex, endIndex)); + failWithExpectedSpan(start, end, StyleSpan.class, actual.toString().substring(start, end)); + return ALREADY_FAILED_WITH_FLAGS; } /** - * Checks that the subject has bold and italic styling from {@code startIndex} to {@code - * endIndex}. + * Checks that the subject has bold and italic styling from {@code start} to {@code end}. * *

    This can either be: * @@ -130,47 +131,41 @@ public final class SpannedSubject extends Subject { * with {@code span.getStyle() == Typeface.ITALIC}. * * - * @param startIndex The start of the expected span. - * @param endIndex The end of the expected span. - * @param flags The flags of the expected span. See constants on {@link Spanned} for more - * information. + * @param start The start of the expected span. + * @param end The end of the expected span. + * @return A {@link WithSpanFlags} object for optional additional assertions on the flags. */ - // TODO: swap this to fluent-style. - public void hasBoldItalicSpan(int startIndex, int endIndex, int flags) { + public WithSpanFlags hasBoldItalicSpanBetween(int start, int end) { if (actual == null) { failWithoutActual(simpleFact("Spanned must not be null")); - return; + return ALREADY_FAILED_WITH_FLAGS; } + List allFlags = new ArrayList<>(); List styles = new ArrayList<>(); - for (StyleSpan span : findMatchingSpans(startIndex, endIndex, flags, StyleSpan.class)) { + for (StyleSpan span : findMatchingSpans(start, end, StyleSpan.class)) { + allFlags.add(actual.getSpanFlags(span)); styles.add(span.getStyle()); } - if (styles.size() == 1 && styles.contains(Typeface.BOLD_ITALIC)) { - return; - } else if (styles.size() == 2 - && styles.contains(Typeface.BOLD) - && styles.contains(Typeface.ITALIC)) { - return; + if (styles.isEmpty()) { + failWithExpectedSpan(start, end, StyleSpan.class, actual.subSequence(start, end).toString()); + return ALREADY_FAILED_WITH_FLAGS; } - String spannedSubstring = actual.toString().substring(startIndex, endIndex); - String boldSpan = - getSpanAsStringWithFlags( - startIndex, endIndex, flags, new StyleSpan(Typeface.BOLD), spannedSubstring); - String italicSpan = - getSpanAsStringWithFlags( - startIndex, endIndex, flags, new StyleSpan(Typeface.ITALIC), spannedSubstring); - String boldItalicSpan = - getSpanAsStringWithFlags( - startIndex, endIndex, flags, new StyleSpan(Typeface.BOLD_ITALIC), spannedSubstring); - + if (styles.size() == 1 && styles.contains(Typeface.BOLD_ITALIC) + || styles.size() == 2 + && styles.contains(Typeface.BOLD) + && styles.contains(Typeface.ITALIC)) { + return check("StyleSpan (start=%s,end=%s)", start, end).about(spanFlags()).that(allFlags); + } failWithoutActual( - simpleFact("No matching span found"), + simpleFact( + String.format("No matching StyleSpans found between start=%s,end=%s", start, end)), fact("in text", actual.toString()), - fact("expected either", boldItalicSpan), - fact("or both", boldSpan + "\n" + italicSpan), - fact("but found", getAllSpansAsStringWithFlags(actual))); + fact("expected either styles", Arrays.asList(Typeface.BOLD_ITALIC)), + fact("or styles", Arrays.asList(Typeface.BOLD, Typeface.BOLD_ITALIC)), + fact("but found styles", styles)); + return ALREADY_FAILED_WITH_FLAGS; } /** @@ -194,8 +189,7 @@ public final class SpannedSubject extends Subject { if (underlineSpans.size() == 1) { return check("UnderlineSpan (start=%s,end=%s)", start, end).about(spanFlags()).that(allFlags); } - failWithExpectedSpanWithoutFlags( - start, end, UnderlineSpan.class, actual.toString().substring(start, end)); + failWithExpectedSpan(start, end, UnderlineSpan.class, actual.toString().substring(start, end)); return ALREADY_FAILED_WITH_FLAGS; } @@ -218,7 +212,7 @@ public final class SpannedSubject extends Subject { List foregroundColorSpans = findMatchingSpans(start, end, ForegroundColorSpan.class); if (foregroundColorSpans.isEmpty()) { - failWithExpectedSpanWithoutFlags( + failWithExpectedSpan( start, end, ForegroundColorSpan.class, actual.toString().substring(start, end)); return ALREADY_FAILED_COLORED; } @@ -246,7 +240,7 @@ public final class SpannedSubject extends Subject { List backgroundColorSpans = findMatchingSpans(start, end, BackgroundColorSpan.class); if (backgroundColorSpans.isEmpty()) { - failWithExpectedSpanWithoutFlags( + failWithExpectedSpan( start, end, BackgroundColorSpan.class, actual.toString().substring(start, end)); return ALREADY_FAILED_COLORED; } @@ -255,19 +249,6 @@ public final class SpannedSubject extends Subject { .that(backgroundColorSpans); } - private List findMatchingSpans( - int startIndex, int endIndex, int flags, Class spanClazz) { - List spans = new ArrayList<>(); - for (T span : actual.getSpans(startIndex, endIndex, spanClazz)) { - if (actual.getSpanStart(span) == startIndex - && actual.getSpanEnd(span) == endIndex - && actual.getSpanFlags(span) == flags) { - spans.add(span); - } - } - return spans; - } - private List findMatchingSpans(int startIndex, int endIndex, Class spanClazz) { List spans = new ArrayList<>(); for (T span : actual.getSpans(startIndex, endIndex, spanClazz)) { @@ -278,72 +259,31 @@ public final class SpannedSubject extends Subject { return spans; } - private void failWithExpectedSpanWithFlags( - int start, int end, int flags, Object span, String spannedSubstring) { - failWithoutActual( - simpleFact("No matching span found"), - fact("in text", actual), - fact("expected", getSpanAsStringWithFlags(start, end, flags, span, spannedSubstring)), - fact("but found", getAllSpansAsStringWithFlags(actual))); - } - - private void failWithExpectedSpanWithoutFlags( + private void failWithExpectedSpan( int start, int end, Class spanType, String spannedSubstring) { failWithoutActual( simpleFact("No matching span found"), fact("in text", actual), - fact("expected", getSpanAsStringWithoutFlags(start, end, spanType, spannedSubstring)), - fact("but found", getAllSpansAsStringWithoutFlags(actual))); + fact("expected", getSpanAsString(start, end, spanType, spannedSubstring)), + fact("but found", getAllSpansAsString(actual))); } - private static String getAllSpansAsStringWithFlags(Spanned spanned) { + private static String getAllSpansAsString(Spanned spanned) { List actualSpanStrings = new ArrayList<>(); for (Object span : spanned.getSpans(0, spanned.length(), Object.class)) { - actualSpanStrings.add(getSpanAsStringWithFlags(span, spanned)); + actualSpanStrings.add(getSpanAsString(span, spanned)); } return TextUtils.join("\n", actualSpanStrings); } - private static String getSpanAsStringWithFlags(Object span, Spanned spanned) { + private static String getSpanAsString(Object span, Spanned spanned) { int spanStart = spanned.getSpanStart(span); int spanEnd = spanned.getSpanEnd(span); - return getSpanAsStringWithFlags( - spanStart, - spanEnd, - spanned.getSpanFlags(span), - span, - spanned.toString().substring(spanStart, spanEnd)); - } - - private static String getSpanAsStringWithFlags( - int start, int end, int flags, Object span, String spannedSubstring) { - String suffix; - if (span instanceof StyleSpan) { - suffix = "\tstyle=" + ((StyleSpan) span).getStyle(); - } else { - suffix = ""; - } - return String.format( - "start=%s\tend=%s\tflags=%s\ttype=%s\tsubstring='%s'%s", - start, end, flags, span.getClass().getSimpleName(), spannedSubstring, suffix); - } - - private static String getAllSpansAsStringWithoutFlags(Spanned spanned) { - List actualSpanStrings = new ArrayList<>(); - for (Object span : spanned.getSpans(0, spanned.length(), Object.class)) { - actualSpanStrings.add(getSpanAsStringWithoutFlags(span, spanned)); - } - return TextUtils.join("\n", actualSpanStrings); - } - - private static String getSpanAsStringWithoutFlags(Object span, Spanned spanned) { - int spanStart = spanned.getSpanStart(span); - int spanEnd = spanned.getSpanEnd(span); - return getSpanAsStringWithoutFlags( + return getSpanAsString( spanStart, spanEnd, span.getClass(), spanned.toString().substring(spanStart, spanEnd)); } - private static String getSpanAsStringWithoutFlags( + private static String getSpanAsString( int start, int end, Class span, String spannedSubstring) { return String.format( "start=%s\tend=%s\ttype=%s\tsubstring='%s'", diff --git a/testutils/src/test/java/com/google/android/exoplayer2/testutil/truth/SpannedSubjectTest.java b/testutils/src/test/java/com/google/android/exoplayer2/testutil/truth/SpannedSubjectTest.java index a01f6f66e0..c33a1128a0 100644 --- a/testutils/src/test/java/com/google/android/exoplayer2/testutil/truth/SpannedSubjectTest.java +++ b/testutils/src/test/java/com/google/android/exoplayer2/testutil/truth/SpannedSubjectTest.java @@ -63,7 +63,9 @@ public class SpannedSubjectTest { int end = start + "italic".length(); spannable.setSpan(new StyleSpan(Typeface.ITALIC), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); - assertThat(spannable).hasItalicSpan(start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + assertThat(spannable) + .hasItalicSpanBetween(start, end) + .withFlags(Spanned.SPAN_INCLUSIVE_EXCLUSIVE); } @Test @@ -78,14 +80,21 @@ public class SpannedSubjectTest { whenTesting -> whenTesting .that(spannable) - .hasItalicSpan(start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)); + .hasItalicSpanBetween(start, end) + .withFlags(Spanned.SPAN_INCLUSIVE_EXCLUSIVE)); - assertThat(failure).factKeys().contains("No matching span found"); - assertThat(failure).factValue("in text").isEqualTo(spannable.toString()); - assertThat(failure).factValue("expected").contains("flags=" + Spanned.SPAN_INCLUSIVE_EXCLUSIVE); assertThat(failure) - .factValue("but found") - .contains("flags=" + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + .factValue("value of") + .isEqualTo( + String.format( + "spanned.StyleSpan (start=%s,end=%s,style=%s).contains()", + start, end, Typeface.ITALIC)); + assertThat(failure) + .factValue("expected to contain") + .contains(String.valueOf(Spanned.SPAN_INCLUSIVE_EXCLUSIVE)); + assertThat(failure) + .factValue("but was") + .contains(String.valueOf(Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)); } @Test @@ -93,7 +102,10 @@ public class SpannedSubjectTest { AssertionError failure = expectFailure( whenTesting -> - whenTesting.that(null).hasItalicSpan(0, 5, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)); + whenTesting + .that(null) + .hasItalicSpanBetween(0, 5) + .withFlags(Spanned.SPAN_INCLUSIVE_EXCLUSIVE)); assertThat(failure).factKeys().containsExactly("Spanned must not be null"); } @@ -105,7 +117,9 @@ public class SpannedSubjectTest { int end = start + "bold".length(); spannable.setSpan(new StyleSpan(Typeface.BOLD), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); - assertThat(spannable).hasBoldSpan(start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + assertThat(spannable) + .hasBoldSpanBetween(start, end) + .withFlags(Spanned.SPAN_INCLUSIVE_EXCLUSIVE); } @Test @@ -116,7 +130,9 @@ public class SpannedSubjectTest { spannable.setSpan( new StyleSpan(Typeface.BOLD_ITALIC), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); - assertThat(spannable).hasBoldItalicSpan(start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + assertThat(spannable) + .hasBoldItalicSpanBetween(start, end) + .withFlags(Spanned.SPAN_INCLUSIVE_EXCLUSIVE); } @Test @@ -127,7 +143,9 @@ public class SpannedSubjectTest { spannable.setSpan(new StyleSpan(Typeface.BOLD), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); spannable.setSpan(new StyleSpan(Typeface.ITALIC), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); - assertThat(spannable).hasBoldItalicSpan(start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + assertThat(spannable) + .hasBoldItalicSpanBetween(start, end) + .withFlags(Spanned.SPAN_INCLUSIVE_EXCLUSIVE); } @Test @@ -144,8 +162,9 @@ public class SpannedSubjectTest { whenTesting -> whenTesting .that(spannable) - .hasBoldItalicSpan(incorrectStart, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)); - assertThat(expected).factValue("expected either").contains("start=" + incorrectStart); + .hasBoldItalicSpanBetween(incorrectStart, end) + .withFlags(Spanned.SPAN_INCLUSIVE_EXCLUSIVE)); + assertThat(expected).factValue("expected").contains("start=" + incorrectStart); assertThat(expected).factValue("but found").contains("start=" + start); } From e55af3e3c8480eac8f9afb9658b8da0057feac76 Mon Sep 17 00:00:00 2001 From: ibaker Date: Mon, 6 Jan 2020 12:49:43 +0000 Subject: [PATCH 20/44] Add RubySpan This will be used when parsing Ruby info from WebVTT and TTML/IMSC subtitles. PiperOrigin-RevId: 288280181 --- .../exoplayer2/text/span/RubySpan.java | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/text/span/RubySpan.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/span/RubySpan.java b/library/core/src/main/java/com/google/android/exoplayer2/text/span/RubySpan.java new file mode 100644 index 0000000000..8ed84d6f6b --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/span/RubySpan.java @@ -0,0 +1,86 @@ +/* + * 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.text.span; + +import static java.lang.annotation.RetentionPolicy.SOURCE; + +import androidx.annotation.IntDef; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; + +/** + * A styling span for ruby text. + * + *

    The text covered by this span is known as the "base text", and the ruby text is stored in + * {@link #rubyText}. + * + *

    More information on ruby characters + * and span styling. + */ +// NOTE: There's no Android layout support for rubies, so this span currently doesn't extend any +// styling superclasses (e.g. MetricAffectingSpan). The only way to render these rubies is to +// extract the spans and do the layout manually. +// TODO: Consider adding support for parenthetical text to be used when rendering doesn't support +// rubies (e.g. HTML tag). +public final class RubySpan { + + /** The ruby position is unknown. */ + public static final int POSITION_UNKNOWN = -1; + + /** + * The ruby text should be positioned above the base text. + * + *

    For vertical text it should be positioned to the right, same as CSS's ruby-position. + */ + public static final int POSITION_OVER = 1; + + /** + * The ruby text should be positioned below the base text. + * + *

    For vertical text it should be positioned to the left, same as CSS's ruby-position. + */ + public static final int POSITION_UNDER = 2; + + /** + * The possible positions of the ruby text relative to the base text. + * + *

    One of: + * + *

      + *
    • {@link #POSITION_UNKNOWN} + *
    • {@link #POSITION_OVER} + *
    • {@link #POSITION_UNDER} + *
    + */ + @Documented + @Retention(SOURCE) + @IntDef({POSITION_UNKNOWN, POSITION_OVER, POSITION_UNDER}) + public @interface Position {} + + /** The ruby text, i.e. the smaller explanatory characters. */ + public final String rubyText; + + /** The position of the ruby text relative to the base text. */ + @Position public final int position; + + public RubySpan(String rubyText, @Position int position) { + this.rubyText = rubyText; + this.position = position; + } +} From 2b1a06633903d4fbe1eec5f001cda502e2a712aa Mon Sep 17 00:00:00 2001 From: ibaker Date: Mon, 6 Jan 2020 12:52:00 +0000 Subject: [PATCH 21/44] Add RubySpan support to SpannedSubject PiperOrigin-RevId: 288280332 --- .../testutil/truth/SpannedSubject.java | 117 +++++++++++++++++- .../testutil/truth/SpannedSubjectTest.java | 115 +++++++++++++++++ 2 files changed, 230 insertions(+), 2 deletions(-) diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/truth/SpannedSubject.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/truth/SpannedSubject.java index 16144a170b..55e2117e04 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/truth/SpannedSubject.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/truth/SpannedSubject.java @@ -30,6 +30,7 @@ import android.text.style.UnderlineSpan; import androidx.annotation.CheckResult; import androidx.annotation.ColorInt; import androidx.annotation.Nullable; +import com.google.android.exoplayer2.text.span.RubySpan; import com.google.common.truth.FailureMetadata; import com.google.common.truth.Subject; import java.util.ArrayList; @@ -194,7 +195,7 @@ public final class SpannedSubject extends Subject { } /** - * Checks that the subject as a {@link ForegroundColorSpan} from {@code start} to {@code end}. + * Checks that the subject has a {@link ForegroundColorSpan} from {@code start} to {@code end}. * *

    The color is asserted in a follow-up method call on the return {@link Colored} object. * @@ -222,7 +223,7 @@ public final class SpannedSubject extends Subject { } /** - * Checks that the subject as a {@link ForegroundColorSpan} from {@code start} to {@code end}. + * Checks that the subject has a {@link BackgroundColorSpan} from {@code start} to {@code end}. * *

    The color is asserted in a follow-up method call on the return {@link Colored} object. * @@ -249,6 +250,30 @@ public final class SpannedSubject extends Subject { .that(backgroundColorSpans); } + /** + * Checks that the subject has a {@link RubySpan} from {@code start} to {@code end}. + * + *

    The ruby-text is asserted in a follow-up method call on the return {@link RubyText} object. + * + * @param start The start of the expected span. + * @param end The end of the expected span. + * @return A {@link Colored} object to assert on the color of the matching spans. + */ + @CheckResult + public RubyText hasRubySpanBetween(int start, int end) { + if (actual == null) { + failWithoutActual(simpleFact("Spanned must not be null")); + return ALREADY_FAILED_WITH_TEXT; + } + + List rubySpans = findMatchingSpans(start, end, RubySpan.class); + if (rubySpans.isEmpty()) { + failWithExpectedSpan(start, end, RubySpan.class, actual.toString().substring(start, end)); + return ALREADY_FAILED_WITH_TEXT; + } + return check("RubySpan (start=%s,end=%s)", start, end).about(rubySpans(actual)).that(rubySpans); + } + private List findMatchingSpans(int startIndex, int endIndex, Class spanClazz) { List spans = new ArrayList<>(); for (T span : actual.getSpans(startIndex, endIndex, spanClazz)) { @@ -440,4 +465,92 @@ public final class SpannedSubject extends Subject { return check("flags").about(spanFlags()).that(matchingSpanFlags); } } + + /** Allows assertions about a span's ruby text and its position. */ + public interface RubyText { + + /** + * Checks that at least one of the matched spans has the expected {@code text}. + * + * @param text The expected text. + * @param position The expected position of the text. + * @return A {@link WithSpanFlags} object for optional additional assertions on the flags. + */ + AndSpanFlags withTextAndPosition(String text, @RubySpan.Position int position); + } + + private static final RubyText ALREADY_FAILED_WITH_TEXT = + (text, position) -> ALREADY_FAILED_AND_FLAGS; + + private Factory> rubySpans(Spanned actualSpanned) { + return (FailureMetadata metadata, List spans) -> + new RubySpansSubject(metadata, spans, actualSpanned); + } + + private static final class RubySpansSubject extends Subject implements RubyText { + + private final List actualSpans; + private final Spanned actualSpanned; + + private RubySpansSubject( + FailureMetadata metadata, List actualSpans, Spanned actualSpanned) { + super(metadata, actualSpans); + this.actualSpans = actualSpans; + this.actualSpanned = actualSpanned; + } + + @Override + public AndSpanFlags withTextAndPosition(String text, @RubySpan.Position int position) { + List matchingSpanFlags = new ArrayList<>(); + List spanTextsAndPositions = new ArrayList<>(); + for (RubySpan span : actualSpans) { + spanTextsAndPositions.add(new TextAndPosition(span.rubyText, span.position)); + if (span.rubyText.equals(text)) { + matchingSpanFlags.add(actualSpanned.getSpanFlags(span)); + } + } + check("rubyTextAndPosition") + .that(spanTextsAndPositions) + .containsExactly(new TextAndPosition(text, position)); + return check("flags").about(spanFlags()).that(matchingSpanFlags); + } + + private static class TextAndPosition { + private final String text; + @RubySpan.Position private final int position; + + private TextAndPosition(String text, int position) { + this.text = text; + this.position = position; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + TextAndPosition that = (TextAndPosition) o; + if (position != that.position) { + return false; + } + return text.equals(that.text); + } + + @Override + public int hashCode() { + int result = text.hashCode(); + result = 31 * result + position; + return result; + } + + @Override + public String toString() { + return String.format("{text='%s',position=%s}", text, position); + } + } + } } diff --git a/testutils/src/test/java/com/google/android/exoplayer2/testutil/truth/SpannedSubjectTest.java b/testutils/src/test/java/com/google/android/exoplayer2/testutil/truth/SpannedSubjectTest.java index c33a1128a0..3eb0509eb4 100644 --- a/testutils/src/test/java/com/google/android/exoplayer2/testutil/truth/SpannedSubjectTest.java +++ b/testutils/src/test/java/com/google/android/exoplayer2/testutil/truth/SpannedSubjectTest.java @@ -30,6 +30,7 @@ import android.text.style.ForegroundColorSpan; import android.text.style.StyleSpan; import android.text.style.UnderlineSpan; import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.text.span.RubySpan; import com.google.common.truth.ExpectFailure; import org.junit.Test; import org.junit.runner.RunWith; @@ -338,6 +339,120 @@ public class SpannedSubjectTest { .contains(String.valueOf(Spanned.SPAN_INCLUSIVE_EXCLUSIVE)); } + @Test + public void rubySpan_success() { + SpannableString spannable = SpannableString.valueOf("test with rubied section"); + int start = "test with ".length(); + int end = start + "rubied".length(); + spannable.setSpan( + new RubySpan("ruby text", RubySpan.POSITION_OVER), + start, + end, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + assertThat(spannable) + .hasRubySpanBetween(start, end) + .withTextAndPosition("ruby text", RubySpan.POSITION_OVER) + .andFlags(Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + } + + @Test + public void rubySpan_wrongEndIndex() { + SpannableString spannable = SpannableString.valueOf("test with cyan section"); + int start = "test with ".length(); + int end = start + "cyan".length(); + spannable.setSpan( + new RubySpan("ruby text", RubySpan.POSITION_OVER), + start, + end, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + int incorrectEnd = end + 2; + AssertionError expected = + expectFailure( + whenTesting -> + whenTesting + .that(spannable) + .hasRubySpanBetween(start, incorrectEnd) + .withTextAndPosition("ruby text", RubySpan.POSITION_OVER)); + assertThat(expected).factValue("expected").contains("end=" + incorrectEnd); + assertThat(expected).factValue("but found").contains("end=" + end); + } + + @Test + public void rubySpan_wrongText() { + SpannableString spannable = SpannableString.valueOf("test with rubied section"); + int start = "test with ".length(); + int end = start + "rubied".length(); + spannable.setSpan( + new RubySpan("ruby text", RubySpan.POSITION_OVER), + start, + end, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + AssertionError expected = + expectFailure( + whenTesting -> + whenTesting + .that(spannable) + .hasRubySpanBetween(start, end) + .withTextAndPosition("incorrect text", RubySpan.POSITION_OVER)); + assertThat(expected).factValue("value of").contains("rubyTextAndPosition"); + assertThat(expected).factValue("expected").contains("text='incorrect text'"); + assertThat(expected).factValue("but was").contains("text='ruby text'"); + } + + @Test + public void rubySpan_wrongPosition() { + SpannableString spannable = SpannableString.valueOf("test with rubied section"); + int start = "test with ".length(); + int end = start + "rubied".length(); + spannable.setSpan( + new RubySpan("ruby text", RubySpan.POSITION_OVER), + start, + end, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + AssertionError expected = + expectFailure( + whenTesting -> + whenTesting + .that(spannable) + .hasRubySpanBetween(start, end) + .withTextAndPosition("ruby text", RubySpan.POSITION_UNDER)); + assertThat(expected).factValue("value of").contains("rubyTextAndPosition"); + assertThat(expected).factValue("expected").contains("position=" + RubySpan.POSITION_UNDER); + assertThat(expected).factValue("but was").contains("position=" + RubySpan.POSITION_OVER); + } + + @Test + public void rubySpan_wrongFlags() { + SpannableString spannable = SpannableString.valueOf("test with rubied section"); + int start = "test with ".length(); + int end = start + "rubied".length(); + spannable.setSpan( + new RubySpan("ruby text", RubySpan.POSITION_OVER), + start, + end, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + AssertionError expected = + expectFailure( + whenTesting -> + whenTesting + .that(spannable) + .hasRubySpanBetween(start, end) + .withTextAndPosition("ruby text", RubySpan.POSITION_OVER) + .andFlags(Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)); + assertThat(expected).factValue("value of").contains("flags"); + assertThat(expected) + .factValue("expected to contain") + .contains(String.valueOf(Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)); + assertThat(expected) + .factValue("but was") + .contains(String.valueOf(Spanned.SPAN_INCLUSIVE_EXCLUSIVE)); + } + private static AssertionError expectFailure( ExpectFailure.SimpleSubjectBuilderCallback callback) { return expectFailureAbout(spanned(), callback); From 6f312c054ef2fe46b969584ecc3f089a3a21be06 Mon Sep 17 00:00:00 2001 From: ibaker Date: Mon, 6 Jan 2020 12:52:59 +0000 Subject: [PATCH 22/44] Add tag support to WebvttCueParser There's currently no rendering support for ruby text in SubtitleView or SubtitlePainter, but this does have a visible impact with the current implementation by stripping the ruby text from Cue.text meaning it doesn't show up at all under the 'naive' rendering. This is an improvement over the current behaviour of including the ruby text in-line with the base text (no rubies is better than wrongly rendered rubies). PiperOrigin-RevId: 288280416 --- RELEASENOTES.md | 2 + .../text/webvtt/WebvttCueParser.java | 71 +++++++++++++++++-- .../text/webvtt/WebvttCueParserTest.java | 31 ++++++++ 3 files changed, 97 insertions(+), 7 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 8df1b1f698..0810d4fd97 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -45,6 +45,8 @@ * Improve support for G.711 A-law and mu-law encoded data. * Fix MKV subtitles to disappear when intended instead of lasting until the next cue ([#6833](https://github.com/google/ExoPlayer/issues/6833)). +* Parse \ and \ tags in WebVTT subtitles (rendering is coming + later). ### 2.11.1 (2019-12-20) ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java index f4c0f26fc8..6de57783e0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java @@ -37,6 +37,7 @@ import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.google.android.exoplayer2.text.Cue; +import com.google.android.exoplayer2.text.span.RubySpan; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.ParsableByteArray; @@ -120,11 +121,13 @@ public final class WebvttCueParser { private static final String ENTITY_NON_BREAK_SPACE = "nbsp"; private static final String TAG_BOLD = "b"; - private static final String TAG_ITALIC = "i"; - private static final String TAG_UNDERLINE = "u"; private static final String TAG_CLASS = "c"; - private static final String TAG_VOICE = "v"; + private static final String TAG_ITALIC = "i"; private static final String TAG_LANG = "lang"; + private static final String TAG_RUBY = "ruby"; + private static final String TAG_RUBY_TEXT = "rt"; + private static final String TAG_UNDERLINE = "u"; + private static final String TAG_VOICE = "v"; private static final int STYLE_BOLD = Typeface.BOLD; private static final int STYLE_ITALIC = Typeface.ITALIC; @@ -197,6 +200,7 @@ public final class WebvttCueParser { ArrayDeque startTagStack = new ArrayDeque<>(); List scratchStyleMatches = new ArrayList<>(); int pos = 0; + List nestedElements = new ArrayList<>(); while (pos < markup.length()) { char curr = markup.charAt(pos); switch (curr) { @@ -225,8 +229,14 @@ public final class WebvttCueParser { break; } startTag = startTagStack.pop(); - applySpansForTag(id, startTag, spannedText, styles, scratchStyleMatches); - } while(!startTag.name.equals(tagName)); + applySpansForTag( + id, startTag, nestedElements, spannedText, styles, scratchStyleMatches); + if (!startTagStack.isEmpty()) { + nestedElements.add(new Element(startTag, spannedText.length())); + } else { + nestedElements.clear(); + } + } while (!startTag.name.equals(tagName)); } else if (!isVoidTag) { startTagStack.push(StartTag.buildStartTag(fullTagExpression, spannedText.length())); } @@ -256,9 +266,15 @@ public final class WebvttCueParser { } // apply unclosed tags while (!startTagStack.isEmpty()) { - applySpansForTag(id, startTagStack.pop(), spannedText, styles, scratchStyleMatches); + applySpansForTag( + id, startTagStack.pop(), nestedElements, spannedText, styles, scratchStyleMatches); } - applySpansForTag(id, StartTag.buildWholeCueVirtualTag(), spannedText, styles, + applySpansForTag( + id, + StartTag.buildWholeCueVirtualTag(), + /* nestedElements= */ Collections.emptyList(), + spannedText, + styles, scratchStyleMatches); return SpannedString.valueOf(spannedText); } @@ -442,6 +458,8 @@ public final class WebvttCueParser { case TAG_CLASS: case TAG_ITALIC: case TAG_LANG: + case TAG_RUBY: + case TAG_RUBY_TEXT: case TAG_UNDERLINE: case TAG_VOICE: return true; @@ -453,6 +471,7 @@ public final class WebvttCueParser { private static void applySpansForTag( @Nullable String cueId, StartTag startTag, + List nestedElements, SpannableStringBuilder text, List styles, List scratchStyleMatches) { @@ -467,6 +486,29 @@ public final class WebvttCueParser { text.setSpan(new StyleSpan(STYLE_ITALIC), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); break; + case TAG_RUBY: + @Nullable Element rubyTextElement = null; + for (int i = 0; i < nestedElements.size(); i++) { + if (TAG_RUBY_TEXT.equals(nestedElements.get(i).startTag.name)) { + rubyTextElement = nestedElements.get(i); + // Behaviour of multiple tags inside is undefined, so use the first one. + break; + } + } + if (rubyTextElement == null) { + break; + } + // Move the rubyText from spannedText into the RubySpan. + CharSequence rubyText = + text.subSequence(rubyTextElement.startTag.position, rubyTextElement.endPosition); + text.delete(rubyTextElement.startTag.position, rubyTextElement.endPosition); + end -= rubyText.length(); + text.setSpan( + new RubySpan(rubyText.toString(), RubySpan.POSITION_OVER), + start, + end, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + break; case TAG_UNDERLINE: text.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); break; @@ -787,4 +829,19 @@ public final class WebvttCueParser { } } + + /** Information about a complete element (i.e. start tag and end position). */ + private static class Element { + private final StartTag startTag; + /** + * The position of the end of this element's text in the un-marked-up cue text (i.e. the + * corollary to {@link StartTag#position}). + */ + private final int endPosition; + + private Element(StartTag startTag, int endPosition) { + this.startTag = startTag; + this.endPosition = endPosition; + } + } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParserTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParserTest.java index ec4ed10f3d..aa83fbc8ed 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParserTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParserTest.java @@ -20,6 +20,7 @@ import static com.google.common.truth.Truth.assertThat; import android.text.Spanned; import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.text.span.RubySpan; import java.util.Collections; import org.junit.Test; import org.junit.runner.RunWith; @@ -48,6 +49,36 @@ public final class WebvttCueParserTest { assertThat(text).hasNoSpans(); } + @Test + public void testParseRubyTag() throws Exception { + Spanned text = + parseCueText("Some base textwith ruby and undecorated text"); + + // The text between the tags is stripped from Cue.text and only present on the RubySpan. + assertThat(text.toString()).isEqualTo("Some base text and undecorated text"); + assertThat(text) + .hasRubySpanBetween("Some ".length(), "Some base text".length()) + .withTextAndPosition("with ruby", RubySpan.POSITION_OVER); + } + + @Test + public void testParseRubyTagWithNoTextTag() throws Exception { + Spanned text = parseCueText("Some base text with no ruby text"); + + assertThat(text.toString()).isEqualTo("Some base text with no ruby text"); + assertThat(text).hasNoSpans(); + } + + @Test + public void testParseRubyTagWithEmptyTextTag() throws Exception { + Spanned text = parseCueText("Some base text with empty ruby text"); + + assertThat(text.toString()).isEqualTo("Some base text with empty ruby text"); + assertThat(text) + .hasRubySpanBetween("Some ".length(), "Some base text with".length()) + .withTextAndPosition("", RubySpan.POSITION_OVER); + } + @Test public void testParseWellFormedUnclosedEndAtCueEnd() throws Exception { Spanned text = parseCueText("An unclosed u tag with " From 3a31bc17245974e667bcdb5eaa886a1d9a9999fb Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 6 Jan 2020 12:54:05 +0000 Subject: [PATCH 23/44] Support 5G in network type detection PiperOrigin-RevId: 288280500 --- .../main/java/com/google/android/exoplayer2/C.java | 12 ++++++------ .../exoplayer2/upstream/DefaultBandwidthMeter.java | 3 ++- .../com/google/android/exoplayer2/util/Util.java | 2 ++ 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/C.java b/library/core/src/main/java/com/google/android/exoplayer2/C.java index 46f20a20f4..e926e90d22 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/C.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/C.java @@ -977,8 +977,8 @@ public final class C { /** * Network connection type. One of {@link #NETWORK_TYPE_UNKNOWN}, {@link #NETWORK_TYPE_OFFLINE}, * {@link #NETWORK_TYPE_WIFI}, {@link #NETWORK_TYPE_2G}, {@link #NETWORK_TYPE_3G}, {@link - * #NETWORK_TYPE_4G}, {@link #NETWORK_TYPE_CELLULAR_UNKNOWN}, {@link #NETWORK_TYPE_ETHERNET} or - * {@link #NETWORK_TYPE_OTHER}. + * #NETWORK_TYPE_4G}, {@link #NETWORK_TYPE_5G}, {@link #NETWORK_TYPE_CELLULAR_UNKNOWN}, {@link + * #NETWORK_TYPE_ETHERNET} or {@link #NETWORK_TYPE_OTHER}. */ @Documented @Retention(RetentionPolicy.SOURCE) @@ -989,6 +989,7 @@ public final class C { NETWORK_TYPE_2G, NETWORK_TYPE_3G, NETWORK_TYPE_4G, + NETWORK_TYPE_5G, NETWORK_TYPE_CELLULAR_UNKNOWN, NETWORK_TYPE_ETHERNET, NETWORK_TYPE_OTHER @@ -1006,6 +1007,8 @@ public final class C { public static final int NETWORK_TYPE_3G = 4; /** Network type for a 4G cellular connection. */ public static final int NETWORK_TYPE_4G = 5; + /** Network type for a 5G cellular connection. */ + public static final int NETWORK_TYPE_5G = 9; /** * Network type for cellular connections which cannot be mapped to one of {@link * #NETWORK_TYPE_2G}, {@link #NETWORK_TYPE_3G}, or {@link #NETWORK_TYPE_4G}. @@ -1013,10 +1016,7 @@ public final class C { public static final int NETWORK_TYPE_CELLULAR_UNKNOWN = 6; /** Network type for an Ethernet connection. */ public static final int NETWORK_TYPE_ETHERNET = 7; - /** - * Network type for other connections which are not Wifi or cellular (e.g. Ethernet, VPN, - * Bluetooth). - */ + /** Network type for other connections which are not Wifi or cellular (e.g. VPN, Bluetooth). */ public static final int NETWORK_TYPE_OTHER = 8; /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultBandwidthMeter.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultBandwidthMeter.java index 1b69455695..2491cc93a9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultBandwidthMeter.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultBandwidthMeter.java @@ -203,9 +203,10 @@ public final class DefaultBandwidthMeter implements BandwidthMeter, TransferList result.append(C.NETWORK_TYPE_2G, DEFAULT_INITIAL_BITRATE_ESTIMATES_2G[groupIndices[1]]); result.append(C.NETWORK_TYPE_3G, DEFAULT_INITIAL_BITRATE_ESTIMATES_3G[groupIndices[2]]); result.append(C.NETWORK_TYPE_4G, DEFAULT_INITIAL_BITRATE_ESTIMATES_4G[groupIndices[3]]); - // Assume default Wifi bitrate for Ethernet to prevent using the slower fallback bitrate. + // Assume default Wifi bitrate for Ethernet and 5G to prevent using the slower fallback. result.append( C.NETWORK_TYPE_ETHERNET, DEFAULT_INITIAL_BITRATE_ESTIMATES_WIFI[groupIndices[0]]); + result.append(C.NETWORK_TYPE_5G, DEFAULT_INITIAL_BITRATE_ESTIMATES_WIFI[groupIndices[0]]); return result; } 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 3ca86ef13d..65ffcf351e 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 @@ -2126,6 +2126,8 @@ public final class Util { return C.NETWORK_TYPE_3G; case TelephonyManager.NETWORK_TYPE_LTE: return C.NETWORK_TYPE_4G; + case TelephonyManager.NETWORK_TYPE_NR: + return C.NETWORK_TYPE_5G; case TelephonyManager.NETWORK_TYPE_IWLAN: return C.NETWORK_TYPE_WIFI; case TelephonyManager.NETWORK_TYPE_GSM: From 06fcf29edd527db2af535207497a25d6e12b1c66 Mon Sep 17 00:00:00 2001 From: kimvde Date: Mon, 6 Jan 2020 13:41:53 +0000 Subject: [PATCH 24/44] Simulate IO exceptions in all FlacExtractor tests - Simulate IO exceptions in the test using FlacBinarySearchSeeker for seeking in FlacExtractorTests. This makes the test slower but covers more test cases. PiperOrigin-RevId: 288285057 --- .../extractor/flac/FlacExtractorTest.java | 66 +------------------ 1 file changed, 1 insertion(+), 65 deletions(-) diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/flac/FlacExtractorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/flac/FlacExtractorTest.java index 97bfc949de..061d0902b6 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/flac/FlacExtractorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/flac/FlacExtractorTest.java @@ -15,13 +15,8 @@ */ package com.google.android.exoplayer2.extractor.flac; -import android.content.Context; -import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; -import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.testutil.ExtractorAsserts; -import com.google.android.exoplayer2.testutil.TestUtil; -import java.io.IOException; import org.junit.Test; import org.junit.runner.RunWith; @@ -66,9 +61,7 @@ public class FlacExtractorTest { @Test public void testOneMetadataBlock() throws Exception { - // Don't simulate IO errors as it is too slow when using the binary search seek map (see - // [Internal: b/145994869]). - assertBehaviorWithoutSimulatingIOErrors("flac/bear_one_metadata_block.flac"); + ExtractorAsserts.assertBehavior(FlacExtractor::new, "flac/bear_one_metadata_block.flac"); } @Test @@ -85,61 +78,4 @@ public class FlacExtractorTest { public void testUncommonSampleRate() throws Exception { ExtractorAsserts.assertBehavior(FlacExtractor::new, "flac/bear_uncommon_sample_rate.flac"); } - - private static void assertBehaviorWithoutSimulatingIOErrors(String file) - throws IOException, InterruptedException { - // Check behavior prior to initialization. - Extractor extractor = new FlacExtractor(); - extractor.seek(0, 0); - extractor.release(); - - // Assert output. - Context context = ApplicationProvider.getApplicationContext(); - byte[] data = TestUtil.getByteArray(context, file); - ExtractorAsserts.assertOutput( - new FlacExtractor(), - file, - data, - context, - /* sniffFirst= */ true, - /* simulateIOErrors= */ false, - /* simulateUnknownLength= */ false, - /* simulatePartialReads= */ false); - ExtractorAsserts.assertOutput( - new FlacExtractor(), - file, - data, - context, - /* sniffFirst= */ true, - /* simulateIOErrors= */ false, - /* simulateUnknownLength= */ false, - /* simulatePartialReads= */ true); - ExtractorAsserts.assertOutput( - new FlacExtractor(), - file, - data, - context, - /* sniffFirst= */ true, - /* simulateIOErrors= */ false, - /* simulateUnknownLength= */ true, - /* simulatePartialReads= */ false); - ExtractorAsserts.assertOutput( - new FlacExtractor(), - file, - data, - context, - /* sniffFirst= */ true, - /* simulateIOErrors= */ false, - /* simulateUnknownLength= */ true, - /* simulatePartialReads= */ true); - ExtractorAsserts.assertOutput( - new FlacExtractor(), - file, - data, - context, - /* sniffFirst= */ false, - /* simulateIOErrors= */ false, - /* simulateUnknownLength= */ false, - /* simulatePartialReads= */ false); - } } From a98fc7ca487269d6c803907ee3d9ced9a6f86620 Mon Sep 17 00:00:00 2001 From: ibaker Date: Mon, 6 Jan 2020 13:51:42 +0000 Subject: [PATCH 25/44] Add tate-chu-yoko support to WebVTT decoding PiperOrigin-RevId: 288285953 --- RELEASENOTES.md | 2 + .../HorizontalTextInVerticalContextSpan.java | 32 +++++++++++++++ .../exoplayer2/text/webvtt/CssParser.java | 11 +++++- .../text/webvtt/WebvttCssStyle.java | 10 +++++ .../text/webvtt/WebvttCueParser.java | 5 +++ .../webvtt/with_css_text_combine_upright | 18 +++++++++ .../text/webvtt/WebvttDecoderTest.java | 16 ++++++++ .../testutil/truth/SpannedSubject.java | 39 ++++++++++++++++--- .../testutil/truth/SpannedSubjectTest.java | 14 +++++++ 9 files changed, 140 insertions(+), 7 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/text/span/HorizontalTextInVerticalContextSpan.java create mode 100644 library/core/src/test/assets/webvtt/with_css_text_combine_upright diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 0810d4fd97..5f411b7100 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -47,6 +47,8 @@ next cue ([#6833](https://github.com/google/ExoPlayer/issues/6833)). * Parse \ and \ tags in WebVTT subtitles (rendering is coming later). +* Parse `text-combine-upright` CSS property (i.e. tate-chu-yoko) in WebVTT + subtitles (rendering is coming later). ### 2.11.1 (2019-12-20) ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/span/HorizontalTextInVerticalContextSpan.java b/library/core/src/main/java/com/google/android/exoplayer2/text/span/HorizontalTextInVerticalContextSpan.java new file mode 100644 index 0000000000..587e1647c6 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/span/HorizontalTextInVerticalContextSpan.java @@ -0,0 +1,32 @@ +/* + * 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.text.span; + +/** + * A styling span for horizontal text in a vertical context. + * + *

    This is used in vertical text to write some characters in a horizontal orientation, known in + * Japanese as tate-chu-yoko. + * + *

    More information on tate-chu-yoko and span styling. + */ +// NOTE: There's no Android layout support for this, so this span currently doesn't extend any +// styling superclasses (e.g. MetricAffectingSpan). The only way to render this styling is to +// extract the spans and do the layout manually. +public final class HorizontalTextInVerticalContextSpan {} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/CssParser.java b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/CssParser.java index 9a5ac40a05..7d5d51b706 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/CssParser.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/CssParser.java @@ -31,14 +31,19 @@ import java.util.regex.Pattern; */ /* package */ final class CssParser { + private static final String TAG = "CssParser"; + + private static final String RULE_START = "{"; + private static final String RULE_END = "}"; private static final String PROPERTY_BGCOLOR = "background-color"; private static final String PROPERTY_FONT_FAMILY = "font-family"; private static final String PROPERTY_FONT_WEIGHT = "font-weight"; + private static final String PROPERTY_TEXT_COMBINE_UPRIGHT = "text-combine-upright"; + private static final String VALUE_ALL = "all"; + private static final String VALUE_DIGITS = "digits"; private static final String PROPERTY_TEXT_DECORATION = "text-decoration"; private static final String VALUE_BOLD = "bold"; private static final String VALUE_UNDERLINE = "underline"; - private static final String RULE_START = "{"; - private static final String RULE_END = "}"; private static final String PROPERTY_FONT_STYLE = "font-style"; private static final String VALUE_ITALIC = "italic"; @@ -182,6 +187,8 @@ import java.util.regex.Pattern; style.setFontColor(ColorParser.parseCssColor(value)); } else if (PROPERTY_BGCOLOR.equals(property)) { style.setBackgroundColor(ColorParser.parseCssColor(value)); + } else if (PROPERTY_TEXT_COMBINE_UPRIGHT.equals(property)) { + style.setCombineUpright(VALUE_ALL.equals(value) || value.startsWith(VALUE_DIGITS)); } else if (PROPERTY_TEXT_DECORATION.equals(property)) { if (VALUE_UNDERLINE.equals(value)) { style.setUnderline(true); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.java b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.java index 1369859552..cd08ad18cf 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.java @@ -95,6 +95,7 @@ public final class WebvttCssStyle { @FontSizeUnit private int fontSizeUnit; private float fontSize; @Nullable private Layout.Alignment textAlign; + private boolean combineUpright; // Calling reset() is forbidden because `this` isn't initialized. This can be safely suppressed // because reset() only assigns fields, it doesn't read any. @@ -118,6 +119,7 @@ public final class WebvttCssStyle { italic = UNSPECIFIED; fontSizeUnit = UNSPECIFIED; textAlign = null; + combineUpright = false; } public void setTargetId(String targetId) { @@ -287,6 +289,14 @@ public final class WebvttCssStyle { return fontSize; } + public void setCombineUpright(boolean enabled) { + this.combineUpright = enabled; + } + + public boolean getCombineUpright() { + return combineUpright; + } + private static int updateScoreForMatch( int currentScore, String target, @Nullable String actual, int score) { if (target.isEmpty() || currentScore == -1) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java index 6de57783e0..fe36043800 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java @@ -37,6 +37,7 @@ import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.google.android.exoplayer2.text.Cue; +import com.google.android.exoplayer2.text.span.HorizontalTextInVerticalContextSpan; import com.google.android.exoplayer2.text.span.RubySpan; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Log; @@ -571,6 +572,10 @@ public final class WebvttCueParser { // Do nothing. break; } + if (style.getCombineUpright()) { + spannedText.setSpan( + new HorizontalTextInVerticalContextSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } } /** diff --git a/library/core/src/test/assets/webvtt/with_css_text_combine_upright b/library/core/src/test/assets/webvtt/with_css_text_combine_upright new file mode 100644 index 0000000000..fd198a9c71 --- /dev/null +++ b/library/core/src/test/assets/webvtt/with_css_text_combine_upright @@ -0,0 +1,18 @@ +WEBVTT + +NOTE https://developer.mozilla.org/en-US/docs/Web/CSS/text-combine-upright +NOTE The `digits` values are ignored in CssParser and all assumed to be `all` + +STYLE +::cue(.tcu-all) { + text-combine-upright: all; +} +::cue(.tcu-digits) { + text-combine-upright: digits 4; +} + +00:00:00.000 --> 00:00:01.000 vertical:rl +Combine all test + +00:03.000 --> 00:04.000 vertical:rl +Combine 0004 digits diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java index 063d4e1bfd..b33439f4f3 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java @@ -53,6 +53,8 @@ public class WebvttDecoderTest { private static final String WITH_TAGS_FILE = "webvtt/with_tags"; private static final String WITH_CSS_STYLES = "webvtt/with_css_styles"; private static final String WITH_CSS_COMPLEX_SELECTORS = "webvtt/with_css_complex_selectors"; + private static final String WITH_CSS_TEXT_COMBINE_UPRIGHT = + "webvtt/with_css_text_combine_upright"; private static final String WITH_BOM = "webvtt/with_bom"; private static final String EMPTY_FILE = "webvtt/empty"; @@ -460,6 +462,20 @@ public class WebvttDecoderTest { .isEqualTo(Typeface.ITALIC); } + @Test + public void testWebvttWithCssTextCombineUpright() throws Exception { + WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_CSS_TEXT_COMBINE_UPRIGHT); + + Spanned firstCueText = getUniqueSpanTextAt(subtitle, 500_000); + assertThat(firstCueText) + .hasHorizontalTextInVerticalContextSpanBetween("Combine ".length(), "Combine all".length()); + + Spanned secondCueText = getUniqueSpanTextAt(subtitle, 3_500_000); + assertThat(secondCueText) + .hasHorizontalTextInVerticalContextSpanBetween( + "Combine ".length(), "Combine 0004".length()); + } + private WebvttSubtitle getSubtitleForTestAsset(String asset) throws IOException, SubtitleDecoderException { WebvttDecoder decoder = new WebvttDecoder(); diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/truth/SpannedSubject.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/truth/SpannedSubject.java index 55e2117e04..b6efa1e7b7 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/truth/SpannedSubject.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/truth/SpannedSubject.java @@ -30,11 +30,13 @@ import android.text.style.UnderlineSpan; import androidx.annotation.CheckResult; import androidx.annotation.ColorInt; import androidx.annotation.Nullable; +import com.google.android.exoplayer2.text.span.HorizontalTextInVerticalContextSpan; import com.google.android.exoplayer2.text.span.RubySpan; import com.google.common.truth.FailureMetadata; import com.google.common.truth.Subject; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; /** A Truth {@link Subject} for assertions on {@link Spanned} instances containing text styling. */ @@ -183,12 +185,10 @@ public final class SpannedSubject extends Subject { } List underlineSpans = findMatchingSpans(start, end, UnderlineSpan.class); - List allFlags = new ArrayList<>(); - for (UnderlineSpan span : underlineSpans) { - allFlags.add(actual.getSpanFlags(span)); - } if (underlineSpans.size() == 1) { - return check("UnderlineSpan (start=%s,end=%s)", start, end).about(spanFlags()).that(allFlags); + return check("UnderlineSpan (start=%s,end=%s)", start, end) + .about(spanFlags()) + .that(Collections.singletonList(actual.getSpanFlags(underlineSpans.get(0)))); } failWithExpectedSpan(start, end, UnderlineSpan.class, actual.toString().substring(start, end)); return ALREADY_FAILED_WITH_FLAGS; @@ -274,6 +274,35 @@ public final class SpannedSubject extends Subject { return check("RubySpan (start=%s,end=%s)", start, end).about(rubySpans(actual)).that(rubySpans); } + /** + * Checks that the subject has an {@link HorizontalTextInVerticalContextSpan} from {@code start} + * to {@code end}. + * + * @param start The start of the expected span. + * @param end The end of the expected span. + * @return A {@link WithSpanFlags} object for optional additional assertions on the flags. + */ + public WithSpanFlags hasHorizontalTextInVerticalContextSpanBetween(int start, int end) { + if (actual == null) { + failWithoutActual(simpleFact("Spanned must not be null")); + return ALREADY_FAILED_WITH_FLAGS; + } + + List horizontalInVerticalSpans = + findMatchingSpans(start, end, HorizontalTextInVerticalContextSpan.class); + if (horizontalInVerticalSpans.size() == 1) { + return check("HorizontalTextInVerticalContextSpan (start=%s,end=%s)", start, end) + .about(spanFlags()) + .that(Collections.singletonList(actual.getSpanFlags(horizontalInVerticalSpans.get(0)))); + } + failWithExpectedSpan( + start, + end, + HorizontalTextInVerticalContextSpan.class, + actual.toString().substring(start, end)); + return ALREADY_FAILED_WITH_FLAGS; + } + private List findMatchingSpans(int startIndex, int endIndex, Class spanClazz) { List spans = new ArrayList<>(); for (T span : actual.getSpans(startIndex, endIndex, spanClazz)) { diff --git a/testutils/src/test/java/com/google/android/exoplayer2/testutil/truth/SpannedSubjectTest.java b/testutils/src/test/java/com/google/android/exoplayer2/testutil/truth/SpannedSubjectTest.java index 3eb0509eb4..c3badd9bb9 100644 --- a/testutils/src/test/java/com/google/android/exoplayer2/testutil/truth/SpannedSubjectTest.java +++ b/testutils/src/test/java/com/google/android/exoplayer2/testutil/truth/SpannedSubjectTest.java @@ -30,6 +30,7 @@ import android.text.style.ForegroundColorSpan; import android.text.style.StyleSpan; import android.text.style.UnderlineSpan; import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.text.span.HorizontalTextInVerticalContextSpan; import com.google.android.exoplayer2.text.span.RubySpan; import com.google.common.truth.ExpectFailure; import org.junit.Test; @@ -453,6 +454,19 @@ public class SpannedSubjectTest { .contains(String.valueOf(Spanned.SPAN_INCLUSIVE_EXCLUSIVE)); } + @Test + public void horizontalTextInVerticalContextSpan_success() { + SpannableString spannable = SpannableString.valueOf("vertical text with horizontal section"); + int start = "vertical text with ".length(); + int end = start + "horizontal".length(); + spannable.setSpan( + new HorizontalTextInVerticalContextSpan(), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + assertThat(spannable) + .hasHorizontalTextInVerticalContextSpanBetween(start, end) + .withFlags(Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + } + private static AssertionError expectFailure( ExpectFailure.SimpleSubjectBuilderCallback callback) { return expectFailureAbout(spanned(), callback); From 24743c77ce673492471557bf213bdee267f09b27 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 6 Jan 2020 14:51:34 +0000 Subject: [PATCH 26/44] Remove WavExtractor from the nullness blacklist PiperOrigin-RevId: 288292488 --- .../extractor/wav/WavExtractor.java | 113 ++++++++++-------- 1 file changed, 63 insertions(+), 50 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java index d9989aeaf6..45a8c24e67 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java @@ -31,6 +31,8 @@ import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; import java.io.IOException; +import org.checkerframework.checker.nullness.qual.EnsuresNonNull; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * Extracts data from WAV byte streams. @@ -47,9 +49,9 @@ public final class WavExtractor implements Extractor { /** Factory for {@link WavExtractor} instances. */ public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new WavExtractor()}; - private ExtractorOutput extractorOutput; - private TrackOutput trackOutput; - private OutputWriter outputWriter; + @MonotonicNonNull private ExtractorOutput extractorOutput; + @MonotonicNonNull private TrackOutput trackOutput; + @MonotonicNonNull private OutputWriter outputWriter; private int dataStartPosition; private long dataEndPosition; @@ -85,6 +87,7 @@ public final class WavExtractor implements Extractor { @Override public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException, InterruptedException { + assertInitialized(); if (outputWriter == null) { WavHeader header = WavHeaderReader.peek(input); if (header == null) { @@ -136,6 +139,12 @@ public final class WavExtractor implements Extractor { return outputWriter.sampleData(input, bytesLeft) ? RESULT_END_OF_INPUT : RESULT_CONTINUE; } + @EnsuresNonNull({"extractorOutput", "trackOutput"}) + private void assertInitialized() { + Assertions.checkStateNotNull(trackOutput); + Util.castNonNull(extractorOutput); + } + /** Writes to the extractor's output. */ private interface OutputWriter { @@ -201,12 +210,19 @@ public final class WavExtractor implements Extractor { TrackOutput trackOutput, WavHeader header, String mimeType, - @C.PcmEncoding int pcmEncoding) { + @C.PcmEncoding int pcmEncoding) + throws ParserException { this.extractorOutput = extractorOutput; this.trackOutput = trackOutput; this.header = header; - // Blocks are expected to correspond to single frames. This is validated in init(int, long). - int bytesPerFrame = header.blockSize; + + int bytesPerFrame = header.numChannels * header.bitsPerSample / 8; + // Validate the header. Blocks are expected to correspond to single frames. + if (header.blockSize != bytesPerFrame) { + throw new ParserException( + "Expected block size: " + bytesPerFrame + "; got: " + header.blockSize); + } + targetSampleSizeBytes = Math.max(bytesPerFrame, header.frameRateHz * bytesPerFrame / TARGET_SAMPLES_PER_SECOND); format = @@ -233,15 +249,7 @@ public final class WavExtractor implements Extractor { } @Override - public void init(int dataStartPosition, long dataEndPosition) throws ParserException { - // Validate the header. - int bytesPerFrame = header.numChannels * header.bitsPerSample / 8; - if (header.blockSize != bytesPerFrame) { - throw new ParserException( - "Expected block size: " + bytesPerFrame + "; got: " + header.blockSize); - } - - // Output the seek map and format. + public void init(int dataStartPosition, long dataEndPosition) { extractorOutput.seekMap( new WavSeekMap(header, /* framesPerBlock= */ 1, dataStartPosition, dataEndPosition)); trackOutput.format(format); @@ -302,18 +310,20 @@ public final class WavExtractor implements Extractor { private final ExtractorOutput extractorOutput; private final TrackOutput trackOutput; private final WavHeader header; + + /** Number of frames per block of the input (yet to be decoded) data. */ + private final int framesPerBlock; + /** Target for the input (yet to be decoded) data. */ + private final byte[] inputData; + /** Target for decoded (yet to be output) data. */ + private final ParsableByteArray decodedData; /** The target size of each output sample, in frames. */ private final int targetSampleSizeFrames; + /** The output format. */ + private final Format format; - // Properties of the input (yet to be decoded) data. - private int framesPerBlock; - private byte[] inputData; + /** The number of pending bytes in {@link #inputData}. */ private int pendingInputBytes; - - // Target for decoded (yet to be output) data. - private ParsableByteArray decodedData; - - // Properties of the output. /** The time at which the writer was last {@link #reset}. */ private long startTimeUs; /** @@ -329,33 +339,21 @@ public final class WavExtractor implements Extractor { private long outputFrameCount; public ImaAdPcmOutputWriter( - ExtractorOutput extractorOutput, TrackOutput trackOutput, WavHeader header) { + ExtractorOutput extractorOutput, TrackOutput trackOutput, WavHeader header) + throws ParserException { this.extractorOutput = extractorOutput; this.trackOutput = trackOutput; this.header = header; targetSampleSizeFrames = Math.max(1, header.frameRateHz / TARGET_SAMPLES_PER_SECOND); - } - @Override - public void reset(long timeUs) { - // Reset the input side. - pendingInputBytes = 0; - // Reset the output side. - startTimeUs = timeUs; - pendingOutputBytes = 0; - outputFrameCount = 0; - } - - @Override - public void init(int dataStartPosition, long dataEndPosition) throws ParserException { - // Validate the header. ParsableByteArray scratch = new ParsableByteArray(header.extraData); scratch.readLittleEndianUnsignedShort(); framesPerBlock = scratch.readLittleEndianUnsignedShort(); - // This calculation is defined in "Microsoft Multimedia Standards Update - New Multimedia - // Types and Data Techniques" (1994). See the "IMA ADPCM Wave Type" and - // "DVI ADPCM Wave Type" sections, and the calculation of wSamplesPerBlock in the latter. + int numChannels = header.numChannels; + // Validate the header. This calculation is defined in "Microsoft Multimedia Standards Update + // - New Multimedia Types and Data Techniques" (1994). See the "IMA ADPCM Wave Type" and "DVI + // ADPCM Wave Type" sections, and the calculation of wSamplesPerBlock in the latter. int expectedFramesPerBlock = (((header.blockSize - (4 * numChannels)) * 8) / (header.bitsPerSample * numChannels)) + 1; if (framesPerBlock != expectedFramesPerBlock) { @@ -368,22 +366,19 @@ public final class WavExtractor implements Extractor { int maxBlocksToDecode = Util.ceilDivide(targetSampleSizeFrames, framesPerBlock); inputData = new byte[maxBlocksToDecode * header.blockSize]; decodedData = - new ParsableByteArray(maxBlocksToDecode * numOutputFramesToBytes(framesPerBlock)); + new ParsableByteArray( + maxBlocksToDecode * numOutputFramesToBytes(framesPerBlock, numChannels)); - // Output the seek map. - extractorOutput.seekMap( - new WavSeekMap(header, framesPerBlock, dataStartPosition, dataEndPosition)); - - // Output the format. We calculate the bitrate of the data before decoding, since this is the + // Create the format. We calculate the bitrate of the data before decoding, since this is the // bitrate of the stream itself. int bitrate = header.frameRateHz * header.blockSize * 8 / framesPerBlock; - Format format = + format = Format.createAudioSampleFormat( /* id= */ null, MimeTypes.AUDIO_RAW, /* codecs= */ null, bitrate, - /* maxInputSize= */ numOutputFramesToBytes(targetSampleSizeFrames), + /* maxInputSize= */ numOutputFramesToBytes(targetSampleSizeFrames, numChannels), header.numChannels, header.frameRateHz, C.ENCODING_PCM_16BIT, @@ -391,6 +386,20 @@ public final class WavExtractor implements Extractor { /* drmInitData= */ null, /* selectionFlags= */ 0, /* language= */ null); + } + + @Override + public void reset(long timeUs) { + pendingInputBytes = 0; + startTimeUs = timeUs; + pendingOutputBytes = 0; + outputFrameCount = 0; + } + + @Override + public void init(int dataStartPosition, long dataEndPosition) { + extractorOutput.seekMap( + new WavSeekMap(header, framesPerBlock, dataStartPosition, dataEndPosition)); trackOutput.format(format); } @@ -543,7 +552,11 @@ public final class WavExtractor implements Extractor { } private int numOutputFramesToBytes(int frames) { - return frames * 2 * header.numChannels; + return numOutputFramesToBytes(frames, header.numChannels); + } + + private static int numOutputFramesToBytes(int frames, int numChannels) { + return frames * 2 * numChannels; } } } From 9618e5e00f1577e3c73b7d44463c701fde337eb7 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 6 Jan 2020 16:21:38 +0000 Subject: [PATCH 27/44] FlacExtractor: Fix possible skipping of frame boundaries PiperOrigin-RevId: 288304477 --- .../google/android/exoplayer2/extractor/flac/FlacExtractor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flac/FlacExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flac/FlacExtractor.java index 8a64d4243c..8c31bde2a2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flac/FlacExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flac/FlacExtractor.java @@ -272,7 +272,7 @@ public final class FlacExtractor implements Extractor { // Skip frame search on the bytes within the minimum frame size. if (currentFrameBytesWritten < minFrameSize) { - buffer.skipBytes(Math.min(minFrameSize, buffer.bytesLeft())); + buffer.skipBytes(Math.min(minFrameSize - currentFrameBytesWritten, buffer.bytesLeft())); } long nextFrameFirstSampleNumber = findFrame(buffer, foundEndOfInput); From 09ca5c0783c9770879b389b15930d1987016a7c1 Mon Sep 17 00:00:00 2001 From: ibaker Date: Tue, 7 Jan 2020 10:55:13 +0000 Subject: [PATCH 28/44] Remove assertWithMessage() calls from SsaDecoderTest As discussed with Olly, these don't add much info and are liable to go stale. PiperOrigin-RevId: 288463027 --- .../exoplayer2/text/ssa/SsaDecoderTest.java | 62 +++++++++---------- 1 file changed, 31 insertions(+), 31 deletions(-) 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 9112bec398..65536f277e 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 @@ -70,10 +70,10 @@ public final class SsaDecoderTest { assertWithMessage("Cue.positionAnchor") .that(firstCue.positionAnchor) .isEqualTo(Cue.ANCHOR_TYPE_MIDDLE); - assertWithMessage("Cue.position").that(firstCue.position).isEqualTo(0.5f); - assertWithMessage("Cue.lineAnchor").that(firstCue.lineAnchor).isEqualTo(Cue.ANCHOR_TYPE_END); - assertWithMessage("Cue.lineType").that(firstCue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION); - assertWithMessage("Cue.line").that(firstCue.line).isEqualTo(0.95f); + assertThat(firstCue.position).isEqualTo(0.5f); + assertThat(firstCue.lineAnchor).isEqualTo(Cue.ANCHOR_TYPE_END); + assertThat(firstCue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION); + assertThat(firstCue.line).isEqualTo(0.95f); assertTypicalCue1(subtitle, 0); assertTypicalCue2(subtitle, 2); @@ -158,33 +158,33 @@ public final class SsaDecoderTest { // Check \pos() sets position & line Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0))); - assertWithMessage("Cue.position").that(firstCue.position).isEqualTo(0.5f); - assertWithMessage("Cue.lineType").that(firstCue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION); - assertWithMessage("Cue.line").that(firstCue.line).isEqualTo(0.25f); + assertThat(firstCue.position).isEqualTo(0.5f); + assertThat(firstCue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION); + assertThat(firstCue.line).isEqualTo(0.25f); // Check the \pos() doesn't need to be at the start of the line. Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2))); - assertWithMessage("Cue.position").that(secondCue.position).isEqualTo(0.25f); - assertWithMessage("Cue.line").that(secondCue.line).isEqualTo(0.25f); + assertThat(secondCue.position).isEqualTo(0.25f); + assertThat(secondCue.line).isEqualTo(0.25f); // Check only the last \pos() value is used. Cue thirdCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(4))); - assertWithMessage("Cue.position").that(thirdCue.position).isEqualTo(0.25f); + assertThat(thirdCue.position).isEqualTo(0.25f); // Check \move() is treated as \pos() Cue fourthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(6))); - assertWithMessage("Cue.position").that(fourthCue.position).isEqualTo(0.5f); - assertWithMessage("Cue.line").that(fourthCue.line).isEqualTo(0.25f); + assertThat(fourthCue.position).isEqualTo(0.5f); + assertThat(fourthCue.line).isEqualTo(0.25f); // Check alignment override in a separate brace (to bottom-center) affects textAlignment and // both line & position anchors. Cue fifthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(8))); - assertWithMessage("Cue.position").that(fifthCue.position).isEqualTo(0.5f); - assertWithMessage("Cue.line").that(fifthCue.line).isEqualTo(0.5f); + assertThat(fifthCue.position).isEqualTo(0.5f); + assertThat(fifthCue.line).isEqualTo(0.5f); assertWithMessage("Cue.positionAnchor") .that(fifthCue.positionAnchor) .isEqualTo(Cue.ANCHOR_TYPE_MIDDLE); - assertWithMessage("Cue.lineAnchor").that(fifthCue.lineAnchor).isEqualTo(Cue.ANCHOR_TYPE_END); + assertThat(fifthCue.lineAnchor).isEqualTo(Cue.ANCHOR_TYPE_END); assertWithMessage("Cue.textAlignment") .that(fifthCue.textAlignment) .isEqualTo(Layout.Alignment.ALIGN_CENTER); @@ -192,12 +192,12 @@ public final class SsaDecoderTest { // Check alignment override in the same brace (to top-right) affects textAlignment and both line // & position anchors. Cue sixthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(10))); - assertWithMessage("Cue.position").that(sixthCue.position).isEqualTo(0.5f); - assertWithMessage("Cue.line").that(sixthCue.line).isEqualTo(0.5f); + assertThat(sixthCue.position).isEqualTo(0.5f); + assertThat(sixthCue.line).isEqualTo(0.5f); assertWithMessage("Cue.positionAnchor") .that(sixthCue.positionAnchor) .isEqualTo(Cue.ANCHOR_TYPE_END); - assertWithMessage("Cue.lineAnchor").that(sixthCue.lineAnchor).isEqualTo(Cue.ANCHOR_TYPE_START); + assertThat(sixthCue.lineAnchor).isEqualTo(Cue.ANCHOR_TYPE_START); assertWithMessage("Cue.textAlignment") .that(sixthCue.textAlignment) .isEqualTo(Layout.Alignment.ALIGN_OPPOSITE); @@ -212,31 +212,31 @@ public final class SsaDecoderTest { // Negative parameter to \pos() - fall back to the positions implied by middle-left alignment. Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0))); - assertWithMessage("Cue.position").that(firstCue.position).isEqualTo(0.05f); - assertWithMessage("Cue.lineType").that(firstCue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION); - assertWithMessage("Cue.line").that(firstCue.line).isEqualTo(0.5f); + assertThat(firstCue.position).isEqualTo(0.05f); + assertThat(firstCue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION); + assertThat(firstCue.line).isEqualTo(0.5f); // Negative parameter to \move() - fall back to the positions implied by middle-left alignment. Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2))); - assertWithMessage("Cue.position").that(secondCue.position).isEqualTo(0.05f); - assertWithMessage("Cue.lineType").that(secondCue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION); - assertWithMessage("Cue.line").that(secondCue.line).isEqualTo(0.5f); + assertThat(secondCue.position).isEqualTo(0.05f); + assertThat(secondCue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION); + assertThat(secondCue.line).isEqualTo(0.5f); // Check invalid alignment override (11) is skipped and style-provided one is used (4). Cue thirdCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(4))); assertWithMessage("Cue.positionAnchor") .that(thirdCue.positionAnchor) .isEqualTo(Cue.ANCHOR_TYPE_START); - assertWithMessage("Cue.lineAnchor").that(thirdCue.lineAnchor).isEqualTo(Cue.ANCHOR_TYPE_MIDDLE); + assertThat(thirdCue.lineAnchor).isEqualTo(Cue.ANCHOR_TYPE_MIDDLE); assertWithMessage("Cue.textAlignment") .that(thirdCue.textAlignment) .isEqualTo(Layout.Alignment.ALIGN_NORMAL); // No braces - fall back to the positions implied by middle-left alignment Cue fourthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(6))); - assertWithMessage("Cue.position").that(fourthCue.position).isEqualTo(0.05f); - assertWithMessage("Cue.lineType").that(fourthCue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION); - assertWithMessage("Cue.line").that(fourthCue.line).isEqualTo(0.5f); + assertThat(fourthCue.position).isEqualTo(0.05f); + assertThat(fourthCue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION); + assertThat(fourthCue.line).isEqualTo(0.5f); } @Test @@ -250,9 +250,9 @@ public final class SsaDecoderTest { // The dialogue line has a valid \pos() override, but it's ignored because PlayResY isn't // set (so we don't know the denominator). Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0))); - assertWithMessage("Cue.position").that(firstCue.position).isEqualTo(Cue.DIMEN_UNSET); - assertWithMessage("Cue.lineType").that(firstCue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION); - assertWithMessage("Cue.line").that(firstCue.line).isEqualTo(Cue.DIMEN_UNSET); + assertThat(firstCue.position).isEqualTo(Cue.DIMEN_UNSET); + assertThat(firstCue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION); + assertThat(firstCue.line).isEqualTo(Cue.DIMEN_UNSET); } @Test From 35fbb7f7cae26887c11d2a1972910782566c19ef Mon Sep 17 00:00:00 2001 From: kimvde Date: Tue, 7 Jan 2020 11:03:42 +0000 Subject: [PATCH 29/44] Add comment explaining FlacBinarySearchSeeker output PiperOrigin-RevId: 288464154 --- .../android/exoplayer2/ext/flac/FlacBinarySearchSeeker.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeeker.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeeker.java index cad5219883..34b3ad2df5 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeeker.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeeker.java @@ -126,6 +126,8 @@ import java.nio.ByteBuffer; if (targetSampleInLastFrame) { // We are holding the target frame in outputFrameHolder. Set its presentation time now. outputFrameHolder.timeUs = decoderJni.getLastFrameTimestamp(); + // The input position is passed even though it does not indicate the frame containing the + // target sample because the extractor must continue to read from this position. return TimestampSearchResult.targetFoundResult(input.getPosition()); } else if (nextFrameSampleIndex <= targetSampleIndex) { return TimestampSearchResult.underestimatedResult( From 692c8ee0acfb23dfc24e690c365d2ed052e11f9b Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 7 Jan 2020 11:47:25 +0000 Subject: [PATCH 30/44] Fix playback for Vivo codecs that output non-16-bit audio PiperOrigin-RevId: 288468497 --- .../exoplayer2/audio/MediaCodecAudioRenderer.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java index dfa13134ce..64a2dcfe37 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java @@ -79,6 +79,11 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media private static final int MAX_PENDING_STREAM_CHANGE_COUNT = 10; private static final String TAG = "MediaCodecAudioRenderer"; + /** + * Custom key used to indicate bits per sample by some decoders on Vivo devices. For example + * OMX.vivo.alac.decoder on the Vivo Z1 Pro. + */ + private static final String VIVO_BITS_PER_SAMPLE_KEY = "v-bits-per-sample"; private final Context context; private final EventDispatcher eventDispatcher; @@ -566,7 +571,11 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media mediaFormat.getString(MediaFormat.KEY_MIME)); } else { mediaFormat = outputMediaFormat; - encoding = getPcmEncoding(inputFormat); + if (outputMediaFormat.containsKey(VIVO_BITS_PER_SAMPLE_KEY)) { + encoding = Util.getPcmEncoding(outputMediaFormat.getInteger(VIVO_BITS_PER_SAMPLE_KEY)); + } else { + encoding = getPcmEncoding(inputFormat); + } } int channelCount = mediaFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT); int sampleRate = mediaFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE); From 181606137dbd51af5cd5e593167884fc56f3f8f2 Mon Sep 17 00:00:00 2001 From: ibaker Date: Tue, 7 Jan 2020 12:06:43 +0000 Subject: [PATCH 31/44] Remove assertCues() helper methods from WebvttDecoderTest These make the interesting bits of each assertion harder to follow imo. Also remove all the assertWithMessage() calls at the same time, Olly convinced me these are rarely useful since you can click from the stack trace to the failing line in the IDE. PiperOrigin-RevId: 288470704 --- .../src/test/assets/webvtt/with_positioning | 8 +- .../text/webvtt/WebvttDecoderTest.java | 506 ++++++------------ 2 files changed, 180 insertions(+), 334 deletions(-) diff --git a/library/core/src/test/assets/webvtt/with_positioning b/library/core/src/test/assets/webvtt/with_positioning index 6bb86b7c93..7db327ca62 100644 --- a/library/core/src/test/assets/webvtt/with_positioning +++ b/library/core/src/test/assets/webvtt/with_positioning @@ -8,12 +8,12 @@ This is the first subtitle. NOTE Wrong position provided. It should be provided as a percentage value -00:02.345 --> 00:03.456 position:10 align:end size:35% +00:02.345 --> 00:03.456 position:10 align:end This is the second subtitle. NOTE Line as percentage and line alignment -00:04.000 --> 00:05.000 line:45%,end align:middle size:35% +00:04.000 --> 00:05.000 line:45%,end align:middle This is the third subtitle. NOTE Line as absolute negative number and without line alignment. @@ -23,10 +23,10 @@ This is the fourth subtitle. NOTE The position and positioning alignment should be inherited from align. -00:07.000 --> 00:08.000 align:right +00:08.000 --> 00:09.000 align:right This is the fifth subtitle. NOTE In newer drafts, align:middle has been replaced by align:center -00:10.000 --> 00:11.000 line:45%,end align:center size:35% +00:10.000 --> 00:11.000 align:center This is the sixth subtitle. diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java index b33439f4f3..e07c412fd7 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java @@ -25,16 +25,15 @@ import android.text.Spanned; import android.text.style.ForegroundColorSpan; import android.text.style.StyleSpan; import android.text.style.TypefaceSpan; -import androidx.annotation.Nullable; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.SubtitleDecoderException; import com.google.android.exoplayer2.util.ColorParser; +import com.google.common.collect.Iterables; import com.google.common.truth.Expect; import java.io.IOException; -import java.util.List; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -76,350 +75,254 @@ public class WebvttDecoderTest { public void testDecodeTypical() throws Exception { WebvttSubtitle subtitle = getSubtitleForTestAsset(TYPICAL_FILE); - // Test event count. assertThat(subtitle.getEventTimeCount()).isEqualTo(4); - // Test cues. - assertCue( - subtitle, - /* eventTimeIndex= */ 0, - /* startTimeUs= */ 0, - /* endTimeUs= */ 1234000, - "This is the first subtitle."); - assertCue( - subtitle, - /* eventTimeIndex= */ 2, - /* startTimeUs= */ 2345000, - /* endTimeUs= */ 3456000, - "This is the second subtitle."); + assertThat(subtitle.getEventTime(0)).isEqualTo(0L); + assertThat(subtitle.getEventTime(1)).isEqualTo(1_234_000L); + Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0))); + assertThat(firstCue.text.toString()).isEqualTo("This is the first subtitle."); + + assertThat(subtitle.getEventTime(2)).isEqualTo(2_345_000L); + assertThat(subtitle.getEventTime(3)).isEqualTo(3_456_000L); + Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2))); + assertThat(secondCue.text.toString()).isEqualTo("This is the second subtitle."); } @Test public void testDecodeWithBom() throws Exception { WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_BOM); - // Test event count. assertThat(subtitle.getEventTimeCount()).isEqualTo(4); - // Test cues. - assertCue( - subtitle, - /* eventTimeIndex= */ 0, - /* startTimeUs= */ 0, - /* endTimeUs= */ 1234000, - "This is the first subtitle."); - assertCue( - subtitle, - /* eventTimeIndex= */ 2, - /* startTimeUs= */ 2345000, - /* endTimeUs= */ 3456000, - "This is the second subtitle."); + assertThat(subtitle.getEventTime(0)).isEqualTo(0L); + assertThat(subtitle.getEventTime(1)).isEqualTo(1_234_000L); + Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0))); + assertThat(firstCue.text.toString()).isEqualTo("This is the first subtitle."); + + assertThat(subtitle.getEventTime(2)).isEqualTo(2_345_000L); + assertThat(subtitle.getEventTime(3)).isEqualTo(3_456_000L); + Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2))); + assertThat(secondCue.text.toString()).isEqualTo("This is the second subtitle."); } @Test public void testDecodeTypicalWithBadTimestamps() throws Exception { WebvttSubtitle subtitle = getSubtitleForTestAsset(TYPICAL_WITH_BAD_TIMESTAMPS); - // Test event count. assertThat(subtitle.getEventTimeCount()).isEqualTo(4); - // Test cues. - assertCue( - subtitle, - /* eventTimeIndex= */ 0, - /* startTimeUs= */ 0, - /* endTimeUs= */ 1234000, - "This is the first subtitle."); - assertCue( - subtitle, - /* eventTimeIndex= */ 2, - /* startTimeUs= */ 2345000, - /* endTimeUs= */ 3456000, - "This is the second subtitle."); + assertThat(subtitle.getEventTime(0)).isEqualTo(0L); + assertThat(subtitle.getEventTime(1)).isEqualTo(1_234_000L); + Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0))); + assertThat(firstCue.text.toString()).isEqualTo("This is the first subtitle."); + + assertThat(subtitle.getEventTime(2)).isEqualTo(2_345_000L); + assertThat(subtitle.getEventTime(3)).isEqualTo(3_456_000L); + Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2))); + assertThat(secondCue.text.toString()).isEqualTo("This is the second subtitle."); } @Test public void testDecodeTypicalWithIds() throws Exception { WebvttSubtitle subtitle = getSubtitleForTestAsset(TYPICAL_WITH_IDS_FILE); - // Test event count. assertThat(subtitle.getEventTimeCount()).isEqualTo(4); - // Test cues. - assertCue( - subtitle, - /* eventTimeIndex= */ 0, - /* startTimeUs= */ 0, - /* endTimeUs= */ 1234000, - "This is the first subtitle."); - assertCue( - subtitle, - /* eventTimeIndex= */ 2, - /* startTimeUs= */ 2345000, - /* endTimeUs= */ 3456000, - "This is the second subtitle."); + assertThat(subtitle.getEventTime(0)).isEqualTo(0L); + assertThat(subtitle.getEventTime(1)).isEqualTo(1_234_000L); + Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0))); + assertThat(firstCue.text.toString()).isEqualTo("This is the first subtitle."); + + assertThat(subtitle.getEventTime(2)).isEqualTo(2_345_000L); + assertThat(subtitle.getEventTime(3)).isEqualTo(3_456_000L); + Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2))); + assertThat(secondCue.text.toString()).isEqualTo("This is the second subtitle."); } @Test public void testDecodeTypicalWithComments() throws Exception { WebvttSubtitle subtitle = getSubtitleForTestAsset(TYPICAL_WITH_COMMENTS_FILE); - // test event count assertThat(subtitle.getEventTimeCount()).isEqualTo(4); - // test cues - assertCue( - subtitle, - /* eventTimeIndex= */ 0, - /* startTimeUs= */ 0, - /* endTimeUs= */ 1234000, - "This is the first subtitle."); - assertCue( - subtitle, - /* eventTimeIndex= */ 2, - /* startTimeUs= */ 2345000, - /* endTimeUs= */ 3456000, - "This is the second subtitle."); + assertThat(subtitle.getEventTime(0)).isEqualTo(0L); + assertThat(subtitle.getEventTime(0 + 1)).isEqualTo(1_234_000L); + Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0))); + assertThat(firstCue.text.toString()).isEqualTo("This is the first subtitle."); + + assertThat(subtitle.getEventTime(2)).isEqualTo(2_345_000L); + assertThat(subtitle.getEventTime(2 + 1)).isEqualTo(3_456_000L); + Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2))); + assertThat(secondCue.text.toString()).isEqualTo("This is the second subtitle."); } @Test public void testDecodeWithTags() throws Exception { WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_TAGS_FILE); - // Test event count. assertThat(subtitle.getEventTimeCount()).isEqualTo(8); - // Test cues. - assertCue( - subtitle, - /* eventTimeIndex= */ 0, - /* startTimeUs= */ 0, - /* endTimeUs= */ 1234000, - "This is the first subtitle."); - assertCue( - subtitle, - /* eventTimeIndex= */ 2, - /* startTimeUs= */ 2345000, - /* endTimeUs= */ 3456000, - "This is the second subtitle."); - assertCue( - subtitle, - /* eventTimeIndex= */ 4, - /* startTimeUs= */ 4000000, - /* endTimeUs= */ 5000000, - "This is the third subtitle."); - assertCue( - subtitle, - /* eventTimeIndex= */ 6, - /* startTimeUs= */ 6000000, - /* endTimeUs= */ 7000000, - "This is the &subtitle."); + assertThat(subtitle.getEventTime(0)).isEqualTo(0L); + assertThat(subtitle.getEventTime(1)).isEqualTo(1_234_000L); + Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0))); + assertThat(firstCue.text.toString()).isEqualTo("This is the first subtitle."); + + assertThat(subtitle.getEventTime(2)).isEqualTo(2_345_000L); + assertThat(subtitle.getEventTime(3)).isEqualTo(3_456_000L); + Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2))); + assertThat(secondCue.text.toString()).isEqualTo("This is the second subtitle."); + + assertThat(subtitle.getEventTime(4)).isEqualTo(4_000_000L); + assertThat(subtitle.getEventTime(5)).isEqualTo(5_000_000L); + Cue thirdCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(4))); + assertThat(thirdCue.text.toString()).isEqualTo("This is the third subtitle."); + + assertThat(subtitle.getEventTime(6)).isEqualTo(6_000_000L); + assertThat(subtitle.getEventTime(7)).isEqualTo(7_000_000L); + Cue fourthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(6))); + assertThat(fourthCue.text.toString()).isEqualTo("This is the &subtitle."); } @Test public void testDecodeWithPositioning() throws Exception { WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_POSITIONING_FILE); - // Test event count. + assertThat(subtitle.getEventTimeCount()).isEqualTo(12); - // Test cues. - assertCue( - subtitle, - /* eventTimeIndex= */ 0, - /* startTimeUs= */ 0, - /* endTimeUs= */ 1234000, - "This is the first subtitle.", - Alignment.ALIGN_NORMAL, - /* line= */ Cue.DIMEN_UNSET, - /* lineType= */ Cue.LINE_TYPE_NUMBER, - /* lineAnchor= */ Cue.ANCHOR_TYPE_START, - /* position= */ 0.1f, - /* positionAnchor= */ Cue.ANCHOR_TYPE_START, - /* size= */ 0.35f, - /* verticalType= */ Cue.TYPE_UNSET); - assertCue( - subtitle, - /* eventTimeIndex= */ 2, - /* startTimeUs= */ 2345000, - /* endTimeUs= */ 3456000, - "This is the second subtitle.", - Alignment.ALIGN_OPPOSITE, - /* line= */ Cue.DIMEN_UNSET, - /* lineType= */ Cue.LINE_TYPE_NUMBER, - /* lineAnchor= */ Cue.ANCHOR_TYPE_START, - /* position= */ 0.5f, - /* positionAnchor= */ Cue.ANCHOR_TYPE_END, - /* size= */ 0.35f, - /* verticalType= */ Cue.TYPE_UNSET); - assertCue( - subtitle, - /* eventTimeIndex= */ 4, - /* startTimeUs= */ 4000000, - /* endTimeUs= */ 5000000, - "This is the third subtitle.", - Alignment.ALIGN_CENTER, - /* line= */ 0.45f, - /* lineType= */ Cue.LINE_TYPE_FRACTION, - /* lineAnchor= */ Cue.ANCHOR_TYPE_END, - /* position= */ 0.5f, - /* positionAnchor= */ Cue.ANCHOR_TYPE_MIDDLE, - /* size= */ 0.35f, - /* verticalType= */ Cue.TYPE_UNSET); - assertCue( - subtitle, - /* eventTimeIndex= */ 6, - /* startTimeUs= */ 6000000, - /* endTimeUs= */ 7000000, - "This is the fourth subtitle.", - Alignment.ALIGN_CENTER, - /* line= */ -11.0f, - /* lineType= */ Cue.LINE_TYPE_NUMBER, - /* lineAnchor= */ Cue.ANCHOR_TYPE_START, - /* position= */ 0.5f, - /* positionAnchor= */ Cue.ANCHOR_TYPE_MIDDLE, - /* size= */ 1.0f, - /* verticalType= */ Cue.TYPE_UNSET); - assertCue( - subtitle, - /* eventTimeIndex= */ 8, - /* startTimeUs= */ 7000000, - /* endTimeUs= */ 8000000, - "This is the fifth subtitle.", - Alignment.ALIGN_OPPOSITE, - /* line= */ Cue.DIMEN_UNSET, - /* lineType= */ Cue.LINE_TYPE_NUMBER, - /* lineAnchor= */ Cue.ANCHOR_TYPE_START, - /* position= */ 1.0f, - /* positionAnchor= */ Cue.ANCHOR_TYPE_END, - /* size= */ 1.0f, - /* verticalType= */ Cue.TYPE_UNSET); - assertCue( - subtitle, - /* eventTimeIndex= */ 10, - /* startTimeUs= */ 10000000, - /* endTimeUs= */ 11000000, - "This is the sixth subtitle.", - Alignment.ALIGN_CENTER, - /* line= */ 0.45f, - /* lineType= */ Cue.LINE_TYPE_FRACTION, - /* lineAnchor= */ Cue.ANCHOR_TYPE_END, - /* position= */ 0.5f, - /* positionAnchor= */ Cue.ANCHOR_TYPE_MIDDLE, - /* size= */ 0.35f, - /* verticalType= */ Cue.TYPE_UNSET); + + assertThat(subtitle.getEventTime(0)).isEqualTo(0L); + assertThat(subtitle.getEventTime(1)).isEqualTo(1_234_000L); + Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0))); + assertThat(firstCue.text.toString()).isEqualTo("This is the first subtitle."); + assertThat(firstCue.position).isEqualTo(0.1f); + assertThat(firstCue.positionAnchor).isEqualTo(Cue.ANCHOR_TYPE_START); + assertThat(firstCue.textAlignment).isEqualTo(Alignment.ALIGN_NORMAL); + assertThat(firstCue.size).isEqualTo(0.35f); + // Unspecified values should use WebVTT defaults + assertThat(firstCue.line).isEqualTo(Cue.DIMEN_UNSET); + assertThat(firstCue.lineType).isEqualTo(Cue.LINE_TYPE_NUMBER); + assertThat(firstCue.verticalType).isEqualTo(Cue.TYPE_UNSET); + + assertThat(subtitle.getEventTime(2)).isEqualTo(2_345_000L); + assertThat(subtitle.getEventTime(3)).isEqualTo(3_456_000L); + Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2))); + assertThat(secondCue.text.toString()).isEqualTo("This is the second subtitle."); + // Position is invalid so defaults to 0.5 + assertThat(secondCue.position).isEqualTo(0.5f); + assertThat(secondCue.textAlignment).isEqualTo(Alignment.ALIGN_OPPOSITE); + + assertThat(subtitle.getEventTime(4)).isEqualTo(4_000_000L); + assertThat(subtitle.getEventTime(5)).isEqualTo(5_000_000L); + Cue thirdCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(4))); + assertThat(thirdCue.text.toString()).isEqualTo("This is the third subtitle."); + assertThat(thirdCue.line).isEqualTo(0.45f); + assertThat(thirdCue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION); + assertThat(thirdCue.lineAnchor).isEqualTo(Cue.ANCHOR_TYPE_END); + assertThat(thirdCue.textAlignment).isEqualTo(Alignment.ALIGN_CENTER); + // Derived from `align:middle`: + assertThat(thirdCue.positionAnchor).isEqualTo(Cue.ANCHOR_TYPE_MIDDLE); + + assertThat(subtitle.getEventTime(6)).isEqualTo(6_000_000L); + assertThat(subtitle.getEventTime(7)).isEqualTo(7_000_000L); + Cue fourthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(6))); + assertThat(fourthCue.text.toString()).isEqualTo("This is the fourth subtitle."); + assertThat(fourthCue.line).isEqualTo(-11f); + assertThat(fourthCue.lineAnchor).isEqualTo(Cue.ANCHOR_TYPE_START); + assertThat(fourthCue.textAlignment).isEqualTo(Alignment.ALIGN_CENTER); + // Derived from `align:middle`: + assertThat(fourthCue.position).isEqualTo(0.5f); + assertThat(fourthCue.positionAnchor).isEqualTo(Cue.ANCHOR_TYPE_MIDDLE); + + assertThat(subtitle.getEventTime(8)).isEqualTo(8_000_000L); + assertThat(subtitle.getEventTime(9)).isEqualTo(9_000_000L); + Cue fifthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(8))); + assertThat(fifthCue.text.toString()).isEqualTo("This is the fifth subtitle."); + assertThat(fifthCue.textAlignment).isEqualTo(Alignment.ALIGN_OPPOSITE); + // Derived from `align:right`: + assertThat(fifthCue.position).isEqualTo(1.0f); + assertThat(fifthCue.positionAnchor).isEqualTo(Cue.ANCHOR_TYPE_END); + + assertThat(subtitle.getEventTime(10)).isEqualTo(10_000_000L); + assertThat(subtitle.getEventTime(11)).isEqualTo(11_000_000L); + Cue sixthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(10))); + assertThat(sixthCue.text.toString()).isEqualTo("This is the sixth subtitle."); + assertThat(sixthCue.textAlignment).isEqualTo(Alignment.ALIGN_CENTER); + // Derived from `align:center`: + assertThat(sixthCue.position).isEqualTo(0.5f); + assertThat(sixthCue.positionAnchor).isEqualTo(Cue.ANCHOR_TYPE_MIDDLE); } @Test public void testDecodeWithVertical() throws Exception { WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_VERTICAL_FILE); - // Test event count. + assertThat(subtitle.getEventTimeCount()).isEqualTo(6); - // Test cues. - assertCue( - subtitle, - /* eventTimeIndex= */ 0, - /* startTimeUs= */ 0, - /* endTimeUs= */ 1234000, - "Vertical right-to-left (e.g. Japanese)", - Alignment.ALIGN_CENTER, - /* line= */ Cue.DIMEN_UNSET, - /* lineType= */ Cue.LINE_TYPE_NUMBER, - /* lineAnchor= */ Cue.ANCHOR_TYPE_START, - /* position= */ 0.5f, - /* positionAnchor= */ Cue.ANCHOR_TYPE_MIDDLE, - /* size= */ 1.0f, - Cue.VERTICAL_TYPE_RL); - assertCue( - subtitle, - /* eventTimeIndex= */ 2, - /* startTimeUs= */ 2345000, - /* endTimeUs= */ 3456000, - "Vertical left-to-right (e.g. Mongolian)", - Alignment.ALIGN_CENTER, - /* line= */ Cue.DIMEN_UNSET, - /* lineType= */ Cue.LINE_TYPE_NUMBER, - /* lineAnchor= */ Cue.ANCHOR_TYPE_START, - /* position= */ 0.5f, - /* positionAnchor= */ Cue.ANCHOR_TYPE_MIDDLE, - /* size= */ 1.0f, - Cue.VERTICAL_TYPE_LR); - assertCue( - subtitle, - /* eventTimeIndex= */ 4, - /* startTimeUs= */ 4000000, - /* endTimeUs= */ 5000000, - "No vertical setting (i.e. horizontal)", - Alignment.ALIGN_CENTER, - /* line= */ Cue.DIMEN_UNSET, - /* lineType= */ Cue.LINE_TYPE_NUMBER, - /* lineAnchor= */ Cue.ANCHOR_TYPE_START, - /* position= */ 0.5f, - /* positionAnchor= */ Cue.ANCHOR_TYPE_MIDDLE, - /* size= */ 1.0f, - /* verticalType= */ Cue.TYPE_UNSET); + + assertThat(subtitle.getEventTime(0)).isEqualTo(0L); + assertThat(subtitle.getEventTime(1)).isEqualTo(1_234_000L); + Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0))); + assertThat(firstCue.text.toString()).isEqualTo("Vertical right-to-left (e.g. Japanese)"); + assertThat(firstCue.verticalType).isEqualTo(Cue.VERTICAL_TYPE_RL); + + assertThat(subtitle.getEventTime(2)).isEqualTo(2_345_000L); + assertThat(subtitle.getEventTime(3)).isEqualTo(3_456_000L); + Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2))); + assertThat(secondCue.text.toString()).isEqualTo("Vertical left-to-right (e.g. Mongolian)"); + assertThat(secondCue.verticalType).isEqualTo(Cue.VERTICAL_TYPE_LR); + + assertThat(subtitle.getEventTime(4)).isEqualTo(4_000_000L); + assertThat(subtitle.getEventTime(5)).isEqualTo(5_000_000L); + Cue thirdCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(4))); + assertThat(thirdCue.text.toString()).isEqualTo("No vertical setting (i.e. horizontal)"); + assertThat(thirdCue.verticalType).isEqualTo(Cue.TYPE_UNSET); } @Test public void testDecodeWithBadCueHeader() throws Exception { WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_BAD_CUE_HEADER_FILE); - // Test event count. assertThat(subtitle.getEventTimeCount()).isEqualTo(4); - // Test cues. - assertCue( - subtitle, - /* eventTimeIndex= */ 0, - /* startTimeUs= */ 0, - /* endTimeUs= */ 1234000, - "This is the first subtitle."); - assertCue( - subtitle, - /* eventTimeIndex= */ 2, - /* startTimeUs= */ 4000000, - /* endTimeUs= */ 5000000, - "This is the third subtitle."); + assertThat(subtitle.getEventTime(0)).isEqualTo(0L); + assertThat(subtitle.getEventTime(1)).isEqualTo(1_234_000L); + Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0))); + assertThat(firstCue.text.toString()).isEqualTo("This is the first subtitle."); + + assertThat(subtitle.getEventTime(2)).isEqualTo(4_000_000L); + assertThat(subtitle.getEventTime(3)).isEqualTo(5_000_000L); + Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2))); + assertThat(secondCue.text.toString()).isEqualTo("This is the third subtitle."); } @Test public void testWebvttWithCssStyle() throws Exception { WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_CSS_STYLES); - // Test event count. - assertThat(subtitle.getEventTimeCount()).isEqualTo(8); - - // Test cues. - assertCue( - subtitle, - /* eventTimeIndex= */ 0, - /* startTimeUs= */ 0, - /* endTimeUs= */ 1234000, - "This is the first subtitle."); - assertCue( - subtitle, - /* eventTimeIndex= */ 2, - /* startTimeUs= */ 2345000, - /* endTimeUs= */ 3456000, - "This is the second subtitle."); - - Spanned s1 = getUniqueSpanTextAt(subtitle, /* timeUs= */ 0); - Spanned s2 = getUniqueSpanTextAt(subtitle, /* timeUs= */ 2345000); - Spanned s3 = getUniqueSpanTextAt(subtitle, /* timeUs= */ 20000000); - Spanned s4 = getUniqueSpanTextAt(subtitle, /* timeUs= */ 25000000); - assertThat(s1) - .hasForegroundColorSpanBetween(0, s1.length()) + Spanned firstCueText = getUniqueSpanTextAt(subtitle, 0); + assertThat(firstCueText.toString()).isEqualTo("This is the first subtitle."); + assertThat(firstCueText) + .hasForegroundColorSpanBetween(0, firstCueText.length()) .withColor(ColorParser.parseCssColor("papayawhip")); - assertThat(s1) - .hasBackgroundColorSpanBetween(0, s1.length()) + assertThat(firstCueText) + .hasBackgroundColorSpanBetween(0, firstCueText.length()) .withColor(ColorParser.parseCssColor("green")); - assertThat(s2) - .hasForegroundColorSpanBetween(0, s2.length()) + + Spanned secondCueText = getUniqueSpanTextAt(subtitle, 2_345_000); + assertThat(secondCueText.toString()).isEqualTo("This is the second subtitle."); + assertThat(secondCueText) + .hasForegroundColorSpanBetween(0, secondCueText.length()) .withColor(ColorParser.parseCssColor("peachpuff")); - assertThat(s3).hasUnderlineSpanBetween(10, s3.length()); - assertThat(s4) - .hasBackgroundColorSpanBetween(0, 16) + Spanned thirdCueText = getUniqueSpanTextAt(subtitle, 20_000_000); + assertThat(thirdCueText.toString()).isEqualTo("This is a reference by element"); + assertThat(thirdCueText).hasUnderlineSpanBetween("This is a ".length(), thirdCueText.length()); + + Spanned fourthCueText = getUniqueSpanTextAt(subtitle, 25_000_000); + assertThat(fourthCueText.toString()).isEqualTo("You are an idiot\nYou don't have the guts"); + assertThat(fourthCueText) + .hasBackgroundColorSpanBetween(0, "You are an idiot".length()) .withColor(ColorParser.parseCssColor("lime")); - assertThat(s4).hasBoldSpanBetween(/* startIndex= */ 17, /* endIndex= */ s4.length()); + assertThat(fourthCueText) + .hasBoldSpanBetween("You are an idiot\n".length(), fourthCueText.length()); } @Test @@ -486,61 +389,4 @@ public class WebvttDecoderTest { private Spanned getUniqueSpanTextAt(WebvttSubtitle sub, long timeUs) { return (Spanned) sub.getCues(timeUs).get(0).text; } - - private void assertCue( - WebvttSubtitle subtitle, int eventTimeIndex, long startTimeUs, long endTimeUs, String text) { - assertCue( - subtitle, - eventTimeIndex, - startTimeUs, - endTimeUs, - text, - /* textAlignment= */ Alignment.ALIGN_CENTER, - /* line= */ Cue.DIMEN_UNSET, - /* lineType= */ Cue.LINE_TYPE_NUMBER, - /* lineAnchor= */ Cue.ANCHOR_TYPE_START, - /* position= */ 0.5f, - /* positionAnchor= */ Cue.ANCHOR_TYPE_MIDDLE, - /* size= */ 1.0f, - /* verticalType= */ Cue.TYPE_UNSET); - } - - private void assertCue( - WebvttSubtitle subtitle, - int eventTimeIndex, - long startTimeUs, - long endTimeUs, - String text, - @Nullable Alignment textAlignment, - float line, - @Cue.LineType int lineType, - @Cue.AnchorType int lineAnchor, - float position, - @Cue.AnchorType int positionAnchor, - float size, - @Cue.VerticalType int verticalType) { - expect - .withMessage("startTimeUs") - .that(subtitle.getEventTime(eventTimeIndex)) - .isEqualTo(startTimeUs); - expect - .withMessage("endTimeUs") - .that(subtitle.getEventTime(eventTimeIndex + 1)) - .isEqualTo(endTimeUs); - List cues = subtitle.getCues(subtitle.getEventTime(eventTimeIndex)); - assertThat(cues).hasSize(1); - // Assert cue properties. - Cue cue = cues.get(0); - expect.withMessage("cue.text").that(cue.text.toString()).isEqualTo(text); - expect.withMessage("cue.textAlignment").that(cue.textAlignment).isEqualTo(textAlignment); - expect.withMessage("cue.line").that(cue.line).isEqualTo(line); - expect.withMessage("cue.lineType").that(cue.lineType).isEqualTo(lineType); - expect.withMessage("cue.lineAnchor").that(cue.lineAnchor).isEqualTo(lineAnchor); - expect.withMessage("cue.position").that(cue.position).isEqualTo(position); - expect.withMessage("cue.positionAnchor").that(cue.positionAnchor).isEqualTo(positionAnchor); - expect.withMessage("cue.size").that(cue.size).isEqualTo(size); - expect.withMessage("cue.verticalType").that(cue.verticalType).isEqualTo(verticalType); - - assertThat(expect.hasFailures()).isFalse(); - } } From c5535e825e993e43f2cca63bfed258971d21ecda Mon Sep 17 00:00:00 2001 From: ibaker Date: Tue, 7 Jan 2020 12:54:13 +0000 Subject: [PATCH 32/44] Fix null-checker suppression introduced by 3.0.1 upgrade Suppression added in https://github.com/google/ExoPlayer/commit/6f9baffa0cc7daf8cbfd5e1f6c55a908190d2041 PiperOrigin-RevId: 288475120 --- .../com/google/android/exoplayer2/text/dvb/DvbParser.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbParser.java b/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbParser.java index 228973ce0c..8d99816ee1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbParser.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbParser.java @@ -481,8 +481,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; * * @return The parsed object data. */ - // incompatible types in argument. - @SuppressWarnings("nullness:argument.type.incompatible") private static ObjectData parseObjectData(ParsableBitArray data) { int objectId = data.readBits(16); data.skipBits(4); // Skip object_version_number @@ -490,8 +488,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; boolean nonModifyingColorFlag = data.readBit(); data.skipBits(1); // Skip reserved. - @Nullable byte[] topFieldData = null; - @Nullable byte[] bottomFieldData = null; + byte[] topFieldData = Util.EMPTY_BYTE_ARRAY; + byte[] bottomFieldData = Util.EMPTY_BYTE_ARRAY; if (objectCodingMethod == OBJECT_CODING_STRING) { int numberOfCodes = data.readBits(8); From fb42f818ec15e810e67aade70e04e36a52de9860 Mon Sep 17 00:00:00 2001 From: christosts Date: Tue, 7 Jan 2020 13:05:14 +0000 Subject: [PATCH 33/44] Add start() method in MediaCodecAdapter PiperOrigin-RevId: 288476415 --- .../AsynchronousMediaCodecAdapter.java | 15 +- ...DedicatedThreadAsyncMediaCodecAdapter.java | 32 +--- .../mediacodec/MediaCodecAdapter.java | 7 + .../mediacodec/MediaCodecRenderer.java | 4 +- .../MultiLockAsyncMediaCodecAdapter.java | 32 +--- .../SynchronousMediaCodecAdapter.java | 6 + .../AsynchronousMediaCodecAdapterTest.java | 75 ++++---- ...catedThreadAsyncMediaCodecAdapterTest.java | 160 ++++------------- .../MultiLockAsyncMediaCodecAdapterTest.java | 161 ++++-------------- 9 files changed, 135 insertions(+), 357 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecAdapter.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecAdapter.java index b5eb8efee3..18c7b1c201 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecAdapter.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecAdapter.java @@ -38,7 +38,7 @@ import com.google.android.exoplayer2.util.Assertions; private final MediaCodec codec; @Nullable private IllegalStateException internalException; private boolean flushing; - private Runnable onCodecStart; + private Runnable codecStartRunnable; /** * Create a new {@code AsynchronousMediaCodecAdapter}. @@ -55,7 +55,12 @@ import com.google.android.exoplayer2.util.Assertions; handler = new Handler(looper); this.codec = codec; this.codec.setCallback(mediaCodecAsyncCallback); - onCodecStart = () -> codec.start(); + codecStartRunnable = codec::start; + } + + @Override + public void start() { + codecStartRunnable.run(); } @Override @@ -105,7 +110,7 @@ import com.google.android.exoplayer2.util.Assertions; flushing = false; mediaCodecAsyncCallback.flush(); try { - onCodecStart.run(); + codecStartRunnable.run(); } catch (IllegalStateException e) { // Catch IllegalStateException directly so that we don't have to wrap it. internalException = e; @@ -115,8 +120,8 @@ import com.google.android.exoplayer2.util.Assertions; } @VisibleForTesting - /* package */ void setOnCodecStart(Runnable onCodecStart) { - this.onCodecStart = onCodecStart; + /* package */ void setCodecStartRunnable(Runnable codecStartRunnable) { + this.codecStartRunnable = codecStartRunnable; } private void maybeThrowException() throws IllegalStateException { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/DedicatedThreadAsyncMediaCodecAdapter.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/DedicatedThreadAsyncMediaCodecAdapter.java index bad21f91f8..b623811453 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/DedicatedThreadAsyncMediaCodecAdapter.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/DedicatedThreadAsyncMediaCodecAdapter.java @@ -26,7 +26,6 @@ import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.annotation.VisibleForTesting; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @@ -54,7 +53,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @MonotonicNonNull private Handler handler; private long pendingFlushCount; private @State int state; - private Runnable onCodecStart; + private Runnable codecStartRunnable; @Nullable private IllegalStateException internalException; /** @@ -77,31 +76,20 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; this.codec = codec; this.handlerThread = handlerThread; state = STATE_CREATED; - onCodecStart = codec::start; + codecStartRunnable = codec::start; } - /** - * Starts the operation of the instance. - * - *

    After a call to this method, make sure to call {@link #shutdown()} to terminate the internal - * Thread. You can only call this method once during the lifetime of this instance; calling this - * method again will throw an {@link IllegalStateException}. - * - * @throws IllegalStateException If this method has been called already. - */ + @Override public synchronized void start() { - Assertions.checkState(state == STATE_CREATED); - handlerThread.start(); handler = new Handler(handlerThread.getLooper()); codec.setCallback(this, handler); + codecStartRunnable.run(); state = STATE_STARTED; } @Override public synchronized int dequeueInputBufferIndex() { - Assertions.checkState(state == STATE_STARTED); - if (isFlushing()) { return MediaCodec.INFO_TRY_AGAIN_LATER; } else { @@ -112,8 +100,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @Override public synchronized int dequeueOutputBufferIndex(MediaCodec.BufferInfo bufferInfo) { - Assertions.checkState(state == STATE_STARTED); - if (isFlushing()) { return MediaCodec.INFO_TRY_AGAIN_LATER; } else { @@ -124,15 +110,11 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @Override public synchronized MediaFormat getOutputFormat() { - Assertions.checkState(state == STATE_STARTED); - return mediaCodecAsyncCallback.getOutputFormat(); } @Override public synchronized void flush() { - Assertions.checkState(state == STATE_STARTED); - codec.flush(); ++pendingFlushCount; Util.castNonNull(handler).post(this::onFlushCompleted); @@ -177,8 +159,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; } @VisibleForTesting - /* package */ void setOnCodecStart(Runnable onCodecStart) { - this.onCodecStart = onCodecStart; + /* package */ void setCodecStartRunnable(Runnable codecStartRunnable) { + this.codecStartRunnable = codecStartRunnable; } private synchronized void onFlushCompleted() { @@ -199,7 +181,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; mediaCodecAsyncCallback.flush(); try { - onCodecStart.run(); + codecStartRunnable.run(); } catch (IllegalStateException e) { internalException = e; } catch (Exception e) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecAdapter.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecAdapter.java index c984443041..2f347de0ae 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecAdapter.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecAdapter.java @@ -31,6 +31,13 @@ import android.media.MediaFormat; */ /* package */ interface MediaCodecAdapter { + /** + * Starts this instance. + * + * @see MediaCodec#start(). + */ + void start(); + /** * Returns the next available input buffer index from the underlying {@link MediaCodec} or {@link * MediaCodec#INFO_TRY_AGAIN_LATER} if no such buffer exists. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java index dbfeed4063..89a0cb5ae1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java @@ -995,11 +995,9 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } else if (mediaCodecOperationMode == OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD && Util.SDK_INT >= 23) { codecAdapter = new DedicatedThreadAsyncMediaCodecAdapter(codec, getTrackType()); - ((DedicatedThreadAsyncMediaCodecAdapter) codecAdapter).start(); } else if (mediaCodecOperationMode == OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD_MULTI_LOCK && Util.SDK_INT >= 23) { codecAdapter = new MultiLockAsyncMediaCodecAdapter(codec, getTrackType()); - ((MultiLockAsyncMediaCodecAdapter) codecAdapter).start(); } else { codecAdapter = new SynchronousMediaCodecAdapter(codec); } @@ -1009,7 +1007,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { configureCodec(codecInfo, codec, inputFormat, crypto, codecOperatingRate); TraceUtil.endSection(); TraceUtil.beginSection("startCodec"); - codec.start(); + codecAdapter.start(); TraceUtil.endSection(); codecInitializedTimestamp = SystemClock.elapsedRealtime(); getCodecBuffers(codec); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MultiLockAsyncMediaCodecAdapter.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MultiLockAsyncMediaCodecAdapter.java index 56f503c71a..48d4ac9a55 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MultiLockAsyncMediaCodecAdapter.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MultiLockAsyncMediaCodecAdapter.java @@ -27,7 +27,6 @@ import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.annotation.VisibleForTesting; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.IntArrayQueue; import com.google.android.exoplayer2.util.Util; import java.util.ArrayDeque; @@ -94,7 +93,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; private final HandlerThread handlerThread; @MonotonicNonNull private Handler handler; - private Runnable onCodecStart; + private Runnable codecStartRunnable; /** Creates a new instance that wraps the specified {@link MediaCodec}. */ /* package */ MultiLockAsyncMediaCodecAdapter(MediaCodec codec, int trackType) { @@ -114,25 +113,16 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; codecException = null; state = STATE_CREATED; this.handlerThread = handlerThread; - onCodecStart = codec::start; + codecStartRunnable = codec::start; } - /** - * Starts the operation of this instance. - * - *

    After a call to this method, make sure to call {@link #shutdown()} to terminate the internal - * Thread. You can only call this method once during the lifetime of an instance; calling this - * method again will throw an {@link IllegalStateException}. - * - * @throws IllegalStateException If this method has been called already. - */ + @Override public void start() { synchronized (objectStateLock) { - Assertions.checkState(state == STATE_CREATED); - handlerThread.start(); handler = new Handler(handlerThread.getLooper()); codec.setCallback(this, handler); + codecStartRunnable.run(); state = STATE_STARTED; } } @@ -140,8 +130,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @Override public int dequeueInputBufferIndex() { synchronized (objectStateLock) { - Assertions.checkState(state == STATE_STARTED); - if (isFlushing()) { return MediaCodec.INFO_TRY_AGAIN_LATER; } else { @@ -154,8 +142,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @Override public int dequeueOutputBufferIndex(MediaCodec.BufferInfo bufferInfo) { synchronized (objectStateLock) { - Assertions.checkState(state == STATE_STARTED); - if (isFlushing()) { return MediaCodec.INFO_TRY_AGAIN_LATER; } else { @@ -168,8 +154,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @Override public MediaFormat getOutputFormat() { synchronized (objectStateLock) { - Assertions.checkState(state == STATE_STARTED); - if (currentFormat == null) { throw new IllegalStateException(); } @@ -181,8 +165,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @Override public void flush() { synchronized (objectStateLock) { - Assertions.checkState(state == STATE_STARTED); - codec.flush(); pendingFlush++; Util.castNonNull(handler).post(this::onFlushComplete); @@ -200,8 +182,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; } @VisibleForTesting - /* package */ void setOnCodecStart(Runnable onCodecStart) { - this.onCodecStart = onCodecStart; + /* package */ void setCodecStartRunnable(Runnable codecStartRunnable) { + this.codecStartRunnable = codecStartRunnable; } private int dequeueAvailableInputBufferIndex() { @@ -307,7 +289,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; clearAvailableOutput(); codecException = null; try { - onCodecStart.run(); + codecStartRunnable.run(); } catch (IllegalStateException e) { codecException = e; } catch (Exception e) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/SynchronousMediaCodecAdapter.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/SynchronousMediaCodecAdapter.java index 7dd7ef8f20..ee9ab857cc 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/SynchronousMediaCodecAdapter.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/SynchronousMediaCodecAdapter.java @@ -23,12 +23,18 @@ import android.media.MediaFormat; * A {@link MediaCodecAdapter} that operates the underlying {@link MediaCodec} in synchronous mode. */ /* package */ final class SynchronousMediaCodecAdapter implements MediaCodecAdapter { + private final MediaCodec codec; public SynchronousMediaCodecAdapter(MediaCodec mediaCodec) { this.codec = mediaCodec; } + @Override + public void start() { + codec.start(); + } + @Override public int dequeueInputBufferIndex() { return codec.dequeueInputBuffer(0); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecAdapterTest.java b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecAdapterTest.java index d2bb0fcc5b..34ed88d2d0 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecAdapterTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecAdapterTest.java @@ -19,7 +19,7 @@ package com.google.android.exoplayer2.mediacodec; import static com.google.android.exoplayer2.mediacodec.MediaCodecTestUtils.areEqual; import static com.google.android.exoplayer2.mediacodec.MediaCodecTestUtils.waitUntilAllEventsAreExecuted; import static com.google.common.truth.Truth.assertThat; -import static org.junit.Assert.fail; +import static org.junit.Assert.assertThrows; import android.media.MediaCodec; import android.media.MediaFormat; @@ -29,7 +29,7 @@ import android.os.Looper; import androidx.test.ext.junit.runners.AndroidJUnit4; import java.io.IOException; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -45,27 +45,32 @@ public class AsynchronousMediaCodecAdapterTest { private MediaCodec.BufferInfo bufferInfo; @Before - public void setup() throws IOException { + public void setUp() throws IOException { handlerThread = new HandlerThread("TestHandlerThread"); handlerThread.start(); looper = handlerThread.getLooper(); codec = MediaCodec.createByCodecName("h264"); adapter = new AsynchronousMediaCodecAdapter(codec, looper); + adapter.setCodecStartRunnable(() -> {}); bufferInfo = new MediaCodec.BufferInfo(); } @After public void tearDown() { + adapter.shutdown(); handlerThread.quit(); } @Test public void dequeueInputBufferIndex_withoutInputBuffer_returnsTryAgainLater() { + adapter.start(); + assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER); } @Test public void dequeueInputBufferIndex_withInputBuffer_returnsInputBuffer() { + adapter.start(); adapter.getMediaCodecCallback().onInputBufferAvailable(codec, /* index=*/ 0); assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(0); @@ -73,6 +78,7 @@ public class AsynchronousMediaCodecAdapterTest { @Test public void dequeueInputBufferIndex_whileFlushing_returnsTryAgainLater() { + adapter.start(); adapter.getMediaCodecCallback().onInputBufferAvailable(codec, /* index=*/ 0); adapter.flush(); adapter.getMediaCodecCallback().onInputBufferAvailable(codec, /* index=*/ 1); @@ -83,9 +89,7 @@ public class AsynchronousMediaCodecAdapterTest { @Test public void dequeueInputBufferIndex_afterFlushCompletes_returnsNextInputBuffer() throws InterruptedException { - // Disable calling codec.start() after flush() completes to avoid receiving buffers from the - // shadow codec impl - adapter.setOnCodecStart(() -> {}); + adapter.start(); Handler handler = new Handler(looper); handler.post( () -> adapter.getMediaCodecCallback().onInputBufferAvailable(codec, /* index=*/ 0)); @@ -100,28 +104,35 @@ public class AsynchronousMediaCodecAdapterTest { @Test public void dequeueInputBufferIndex_afterFlushCompletesWithError_throwsException() throws InterruptedException { - adapter.setOnCodecStart( + AtomicInteger calls = new AtomicInteger(0); + adapter.setCodecStartRunnable( () -> { - throw new IllegalStateException("codec#start() exception"); + if (calls.incrementAndGet() == 2) { + throw new IllegalStateException(); + } }); + adapter.start(); adapter.flush(); assertThat(waitUntilAllEventsAreExecuted(looper, /* time= */ 5, TimeUnit.SECONDS)).isTrue(); - try { - adapter.dequeueInputBufferIndex(); - fail(); - } catch (IllegalStateException expected) { - } + assertThrows( + IllegalStateException.class, + () -> { + adapter.dequeueInputBufferIndex(); + }); } @Test public void dequeueOutputBufferIndex_withoutOutputBuffer_returnsTryAgainLater() { + adapter.start(); + assertThat(adapter.dequeueOutputBufferIndex(bufferInfo)) .isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER); } @Test public void dequeueOutputBufferIndex_withOutputBuffer_returnsOutputBuffer() { + adapter.start(); MediaCodec.BufferInfo outBufferInfo = new MediaCodec.BufferInfo(); outBufferInfo.presentationTimeUs = 10; adapter.getMediaCodecCallback().onOutputBufferAvailable(codec, /* index=*/ 0, outBufferInfo); @@ -132,6 +143,7 @@ public class AsynchronousMediaCodecAdapterTest { @Test public void dequeueOutputBufferIndex_whileFlushing_returnsTryAgainLater() { + adapter.start(); adapter.getMediaCodecCallback().onOutputBufferAvailable(codec, /* index=*/ 0, bufferInfo); adapter.flush(); adapter.getMediaCodecCallback().onOutputBufferAvailable(codec, /* index=*/ 1, bufferInfo); @@ -143,9 +155,7 @@ public class AsynchronousMediaCodecAdapterTest { @Test public void dequeueOutputBufferIndex_afterFlushCompletes_returnsNextOutputBuffer() throws InterruptedException { - // Disable calling codec.start() after flush() completes to avoid receiving buffers from the - // shadow codec impl - adapter.setOnCodecStart(() -> {}); + adapter.start(); Handler handler = new Handler(looper); MediaCodec.BufferInfo info0 = new MediaCodec.BufferInfo(); handler.post( @@ -164,31 +174,23 @@ public class AsynchronousMediaCodecAdapterTest { @Test public void dequeueOutputBufferIndex_afterFlushCompletesWithError_throwsException() throws InterruptedException { - adapter.setOnCodecStart( + AtomicInteger calls = new AtomicInteger(0); + adapter.setCodecStartRunnable( () -> { - throw new RuntimeException("codec#start() exception"); + if (calls.incrementAndGet() == 2) { + throw new RuntimeException("codec#start() exception"); + } }); + adapter.start(); adapter.flush(); assertThat(waitUntilAllEventsAreExecuted(looper, /* time= */ 5, TimeUnit.SECONDS)).isTrue(); - try { - adapter.dequeueOutputBufferIndex(bufferInfo); - fail(); - } catch (IllegalStateException expected) { - } - } - - @Test - public void getOutputFormat_withoutFormat_throwsException() { - try { - adapter.getOutputFormat(); - fail(); - } catch (IllegalStateException expected) { - } + assertThrows(IllegalStateException.class, () -> adapter.dequeueOutputBufferIndex(bufferInfo)); } @Test public void getOutputFormat_withMultipleFormats_returnsFormatsInCorrectOrder() { + adapter.start(); MediaFormat[] formats = new MediaFormat[10]; MediaCodec.Callback mediaCodecCallback = adapter.getMediaCodecCallback(); for (int i = 0; i < formats.length; i++) { @@ -212,6 +214,7 @@ public class AsynchronousMediaCodecAdapterTest { @Test public void getOutputFormat_afterFlush_returnsPreviousFormat() throws InterruptedException { + adapter.start(); MediaFormat format = new MediaFormat(); adapter.getMediaCodecCallback().onOutputFormatChanged(codec, format); adapter.dequeueOutputBufferIndex(bufferInfo); @@ -223,13 +226,13 @@ public class AsynchronousMediaCodecAdapterTest { @Test public void shutdown_withPendingFlush_cancelsFlush() throws InterruptedException { - AtomicBoolean onCodecStartCalled = new AtomicBoolean(false); - Runnable onCodecStart = () -> onCodecStartCalled.set(true); - adapter.setOnCodecStart(onCodecStart); + AtomicInteger onCodecStartCalled = new AtomicInteger(0); + adapter.setCodecStartRunnable(() -> onCodecStartCalled.incrementAndGet()); + adapter.start(); adapter.flush(); adapter.shutdown(); assertThat(waitUntilAllEventsAreExecuted(looper, /* time= */ 5, TimeUnit.SECONDS)).isTrue(); - assertThat(onCodecStartCalled.get()).isFalse(); + assertThat(onCodecStartCalled.get()).isEqualTo(1); } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/DedicatedThreadAsyncMediaCodecAdapterTest.java b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/DedicatedThreadAsyncMediaCodecAdapterTest.java index 2cfb577579..f974144dd6 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/DedicatedThreadAsyncMediaCodecAdapterTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/DedicatedThreadAsyncMediaCodecAdapterTest.java @@ -19,7 +19,7 @@ package com.google.android.exoplayer2.mediacodec; import static com.google.android.exoplayer2.mediacodec.MediaCodecTestUtils.areEqual; import static com.google.android.exoplayer2.mediacodec.MediaCodecTestUtils.waitUntilAllEventsAreExecuted; import static com.google.common.truth.Truth.assertThat; -import static org.junit.Assert.fail; +import static org.junit.Assert.assertThrows; import static org.robolectric.Shadows.shadowOf; import android.media.MediaCodec; @@ -47,16 +47,18 @@ public class DedicatedThreadAsyncMediaCodecAdapterTest { private MediaCodec.BufferInfo bufferInfo = null; @Before - public void setup() throws IOException { + public void setUp() throws IOException { codec = MediaCodec.createByCodecName("h264"); handlerThread = new TestHandlerThread("TestHandlerThread"); adapter = new DedicatedThreadAsyncMediaCodecAdapter(codec, handlerThread); + adapter.setCodecStartRunnable(() -> {}); bufferInfo = new MediaCodec.BufferInfo(); } @After public void tearDown() { adapter.shutdown(); + assertThat(TestHandlerThread.INSTANCES_STARTED.get()).isEqualTo(0); } @@ -66,42 +68,15 @@ public class DedicatedThreadAsyncMediaCodecAdapterTest { adapter.shutdown(); } - @Test - public void start_calledTwice_throwsException() { - adapter.start(); - try { - adapter.start(); - fail(); - } catch (IllegalStateException expected) { - } - } - - @Test - public void dequeueInputBufferIndex_withoutStart_throwsException() { - try { - adapter.dequeueInputBufferIndex(); - fail(); - } catch (IllegalStateException expected) { - } - } - - @Test - public void dequeueInputBufferIndex_afterShutdown_throwsException() { - adapter.start(); - adapter.shutdown(); - try { - adapter.dequeueInputBufferIndex(); - fail(); - } catch (IllegalStateException expected) { - } - } - @Test public void dequeueInputBufferIndex_withAfterFlushFailed_throwsException() throws InterruptedException { - adapter.setOnCodecStart( + AtomicInteger codecStartCalls = new AtomicInteger(0); + adapter.setCodecStartRunnable( () -> { - throw new IllegalStateException("codec#start() exception"); + if (codecStartCalls.incrementAndGet() == 2) { + throw new IllegalStateException("codec#start() exception"); + } }); adapter.start(); adapter.flush(); @@ -110,11 +85,8 @@ public class DedicatedThreadAsyncMediaCodecAdapterTest { waitUntilAllEventsAreExecuted( handlerThread.getLooper(), /* time= */ 5, TimeUnit.SECONDS)) .isTrue(); - try { - adapter.dequeueInputBufferIndex(); - fail(); - } catch (IllegalStateException expected) { - } + + assertThrows(IllegalStateException.class, () -> adapter.dequeueInputBufferIndex()); } @Test @@ -144,9 +116,6 @@ public class DedicatedThreadAsyncMediaCodecAdapterTest { @Test public void dequeueInputBufferIndex_withFlushCompletedAndInputBuffer_returnsInputBuffer() throws InterruptedException { - // Disable calling codec.start() after flush to avoid receiving buffers from the - // shadow codec impl - adapter.setOnCodecStart(() -> {}); adapter.start(); Looper looper = handlerThread.getLooper(); Handler handler = new Handler(looper); @@ -169,39 +138,18 @@ public class DedicatedThreadAsyncMediaCodecAdapterTest { adapter.start(); adapter.onMediaCodecError(new IllegalStateException("error from codec")); - try { - adapter.dequeueInputBufferIndex(); - fail(); - } catch (IllegalStateException expected) { - } - } - - @Test - public void dequeueOutputBufferIndex_withoutStart_throwsException() { - try { - adapter.dequeueOutputBufferIndex(bufferInfo); - fail(); - } catch (IllegalStateException expected) { - } - } - - @Test - public void dequeueOutputBufferIndex_afterShutdown_throwsException() { - adapter.start(); - adapter.shutdown(); - try { - adapter.dequeueOutputBufferIndex(bufferInfo); - fail(); - } catch (IllegalStateException expected) { - } + assertThrows(IllegalStateException.class, () -> adapter.dequeueInputBufferIndex()); } @Test public void dequeueOutputBufferIndex_withInternalException_throwsException() throws InterruptedException { - adapter.setOnCodecStart( + AtomicInteger codecStartCalls = new AtomicInteger(0); + adapter.setCodecStartRunnable( () -> { - throw new RuntimeException("codec#start() exception"); + if (codecStartCalls.incrementAndGet() == 2) { + throw new RuntimeException("codec#start() exception"); + } }); adapter.start(); adapter.flush(); @@ -210,11 +158,7 @@ public class DedicatedThreadAsyncMediaCodecAdapterTest { waitUntilAllEventsAreExecuted( handlerThread.getLooper(), /* time= */ 5, TimeUnit.SECONDS)) .isTrue(); - try { - adapter.dequeueOutputBufferIndex(bufferInfo); - fail(); - } catch (IllegalStateException expected) { - } + assertThrows(IllegalStateException.class, () -> adapter.dequeueOutputBufferIndex(bufferInfo)); } @Test @@ -275,42 +219,14 @@ public class DedicatedThreadAsyncMediaCodecAdapterTest { adapter.start(); adapter.onMediaCodecError(new IllegalStateException("error from codec")); - try { - adapter.dequeueOutputBufferIndex(bufferInfo); - fail(); - } catch (IllegalStateException expected) { - } - } - - @Test - public void getOutputFormat_withoutStart_throwsException() { - try { - adapter.getOutputFormat(); - fail(); - } catch (IllegalStateException expected) { - } - } - - @Test - public void getOutputFormat_afterShutdown_throwsException() { - adapter.start(); - adapter.shutdown(); - try { - adapter.getOutputFormat(); - fail(); - } catch (IllegalStateException expected) { - } + assertThrows(IllegalStateException.class, () -> adapter.dequeueOutputBufferIndex(bufferInfo)); } @Test public void getOutputFormat_withoutFormatReceived_throwsException() { adapter.start(); - try { - adapter.getOutputFormat(); - fail(); - } catch (IllegalStateException expected) { - } + assertThrows(IllegalStateException.class, () -> adapter.getOutputFormat()); } @Test @@ -351,28 +267,10 @@ public class DedicatedThreadAsyncMediaCodecAdapterTest { assertThat(adapter.getOutputFormat()).isEqualTo(format); } - @Test - public void flush_withoutStarted_throwsException() { - try { - adapter.flush(); - } catch (IllegalStateException expected) { - } - } - - @Test - public void flush_afterShutdown_throwsException() { - adapter.start(); - adapter.shutdown(); - try { - adapter.flush(); - } catch (IllegalStateException expected) { - } - } - @Test public void flush_multipleTimes_onlyLastFlushExecutes() throws InterruptedException { - AtomicInteger onCodecStartCount = new AtomicInteger(0); - adapter.setOnCodecStart(() -> onCodecStartCount.incrementAndGet()); + AtomicInteger codecStartCalls = new AtomicInteger(0); + adapter.setCodecStartRunnable(() -> codecStartCalls.incrementAndGet()); adapter.start(); Looper looper = handlerThread.getLooper(); Handler handler = new Handler(looper); @@ -384,23 +282,23 @@ public class DedicatedThreadAsyncMediaCodecAdapterTest { adapter.flush(); // Enqueues a second flush event handler.post(() -> adapter.onInputBufferAvailable(codec, 3)); - // Progress the looper until the milestoneCount is increased - first flush event - // should have been a no-op + // Progress the looper until the milestoneCount is increased. + // adapter.start() will call codec.start(). First flush event should not call codec.start(). ShadowLooper shadowLooper = shadowOf(looper); while (milestoneCount.get() < 1) { shadowLooper.runOneTask(); } - assertThat(onCodecStartCount.get()).isEqualTo(0); + assertThat(codecStartCalls.get()).isEqualTo(1); assertThat(waitUntilAllEventsAreExecuted(looper, /* time= */ 5, TimeUnit.SECONDS)).isTrue(); assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(3); - assertThat(onCodecStartCount.get()).isEqualTo(1); + assertThat(codecStartCalls.get()).isEqualTo(2); } @Test public void flush_andImmediatelyShutdown_flushIsNoOp() throws InterruptedException { AtomicInteger onCodecStartCount = new AtomicInteger(0); - adapter.setOnCodecStart(() -> onCodecStartCount.incrementAndGet()); + adapter.setCodecStartRunnable(() -> onCodecStartCount.incrementAndGet()); adapter.start(); // Obtain looper when adapter is started Looper looper = handlerThread.getLooper(); @@ -408,8 +306,8 @@ public class DedicatedThreadAsyncMediaCodecAdapterTest { adapter.shutdown(); assertThat(waitUntilAllEventsAreExecuted(looper, 5, TimeUnit.SECONDS)).isTrue(); - // only shutdown flushes the MediaCodecAsync handler - assertThat(onCodecStartCount.get()).isEqualTo(0); + // Only adapter.start() calls onCodecStart. + assertThat(onCodecStartCount.get()).isEqualTo(1); } private static class TestHandlerThread extends HandlerThread { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/MultiLockAsyncMediaCodecAdapterTest.java b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/MultiLockAsyncMediaCodecAdapterTest.java index b984d28914..c31b86db39 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/MultiLockAsyncMediaCodecAdapterTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/MultiLockAsyncMediaCodecAdapterTest.java @@ -19,7 +19,7 @@ package com.google.android.exoplayer2.mediacodec; import static com.google.android.exoplayer2.mediacodec.MediaCodecTestUtils.areEqual; import static com.google.android.exoplayer2.mediacodec.MediaCodecTestUtils.waitUntilAllEventsAreExecuted; import static com.google.common.truth.Truth.assertThat; -import static org.junit.Assert.fail; +import static org.junit.Assert.assertThrows; import static org.robolectric.Shadows.shadowOf; import android.media.MediaCodec; @@ -44,20 +44,21 @@ public class MultiLockAsyncMediaCodecAdapterTest { private MultiLockAsyncMediaCodecAdapter adapter; private MediaCodec codec; private MediaCodec.BufferInfo bufferInfo = null; - private MediaCodecAsyncCallback mediaCodecAsyncCallbackSpy; private TestHandlerThread handlerThread; @Before - public void setup() throws IOException { + public void setUp() throws IOException { codec = MediaCodec.createByCodecName("h264"); handlerThread = new TestHandlerThread("TestHandlerThread"); adapter = new MultiLockAsyncMediaCodecAdapter(codec, handlerThread); + adapter.setCodecStartRunnable(() -> {}); bufferInfo = new MediaCodec.BufferInfo(); } @After public void tearDown() { adapter.shutdown(); + assertThat(TestHandlerThread.INSTANCES_STARTED.get()).isEqualTo(0); } @@ -67,42 +68,15 @@ public class MultiLockAsyncMediaCodecAdapterTest { adapter.shutdown(); } - @Test - public void start_calledTwice_throwsException() { - adapter.start(); - try { - adapter.start(); - fail(); - } catch (IllegalStateException expected) { - } - } - - @Test - public void dequeueInputBufferIndex_withoutStart_throwsException() { - try { - adapter.dequeueInputBufferIndex(); - fail(); - } catch (IllegalStateException expected) { - } - } - - @Test - public void dequeueInputBufferIndex_afterShutdown_throwsException() { - adapter.start(); - adapter.shutdown(); - try { - adapter.dequeueInputBufferIndex(); - fail(); - } catch (IllegalStateException expected) { - } - } - @Test public void dequeueInputBufferIndex_withAfterFlushFailed_throwsException() throws InterruptedException { - adapter.setOnCodecStart( + AtomicInteger codecStartCalls = new AtomicInteger(0); + adapter.setCodecStartRunnable( () -> { - throw new IllegalStateException("codec#start() exception"); + if (codecStartCalls.incrementAndGet() == 2) { + throw new IllegalStateException("codec#start() exception"); + } }); adapter.start(); adapter.flush(); @@ -111,11 +85,7 @@ public class MultiLockAsyncMediaCodecAdapterTest { waitUntilAllEventsAreExecuted( handlerThread.getLooper(), /* time= */ 5, TimeUnit.SECONDS)) .isTrue(); - try { - adapter.dequeueInputBufferIndex(); - fail(); - } catch (IllegalStateException expected) { - } + assertThrows(IllegalStateException.class, () -> adapter.dequeueInputBufferIndex()); } @Test @@ -145,9 +115,6 @@ public class MultiLockAsyncMediaCodecAdapterTest { @Test public void dequeueInputBufferIndex_withFlushCompletedAndInputBuffer_returnsInputBuffer() throws InterruptedException { - // Disable calling codec.start() after flush to avoid receiving buffers from the - // shadow codec impl - adapter.setOnCodecStart(() -> {}); adapter.start(); Looper looper = handlerThread.getLooper(); Handler handler = new Handler(looper); @@ -170,39 +137,19 @@ public class MultiLockAsyncMediaCodecAdapterTest { adapter.start(); adapter.onMediaCodecError(new IllegalStateException("error from codec")); - try { - adapter.dequeueInputBufferIndex(); - fail(); - } catch (IllegalStateException expected) { - } + assertThrows(IllegalStateException.class, () -> adapter.dequeueInputBufferIndex()); } - @Test - public void dequeueOutputBufferIndex_withoutStart_throwsException() { - try { - adapter.dequeueOutputBufferIndex(bufferInfo); - fail(); - } catch (IllegalStateException expected) { - } - } - - @Test - public void dequeueOutputBufferIndex_afterShutdown_throwsException() { - adapter.start(); - adapter.shutdown(); - try { - adapter.dequeueOutputBufferIndex(bufferInfo); - fail(); - } catch (IllegalStateException expected) { - } - } @Test public void dequeueOutputBufferIndex_withInternalException_throwsException() throws InterruptedException { - adapter.setOnCodecStart( + AtomicInteger codecStartCalls = new AtomicInteger(0); + adapter.setCodecStartRunnable( () -> { - throw new RuntimeException("codec#start() exception"); + if (codecStartCalls.incrementAndGet() == 2) { + throw new RuntimeException("codec#start() exception"); + } }); adapter.start(); adapter.flush(); @@ -211,11 +158,7 @@ public class MultiLockAsyncMediaCodecAdapterTest { waitUntilAllEventsAreExecuted( handlerThread.getLooper(), /* time= */ 5, TimeUnit.SECONDS)) .isTrue(); - try { - adapter.dequeueOutputBufferIndex(bufferInfo); - fail(); - } catch (IllegalStateException expected) { - } + assertThrows(IllegalStateException.class, () -> adapter.dequeueOutputBufferIndex(bufferInfo)); } @Test @@ -276,42 +219,14 @@ public class MultiLockAsyncMediaCodecAdapterTest { adapter.start(); adapter.onMediaCodecError(new IllegalStateException("error from codec")); - try { - adapter.dequeueOutputBufferIndex(bufferInfo); - fail(); - } catch (IllegalStateException expected) { - } - } - - @Test - public void getOutputFormat_withoutStart_throwsException() { - try { - adapter.getOutputFormat(); - fail(); - } catch (IllegalStateException expected) { - } - } - - @Test - public void getOutputFormat_afterShutdown_throwsException() { - adapter.start(); - adapter.shutdown(); - try { - adapter.getOutputFormat(); - fail(); - } catch (IllegalStateException expected) { - } + assertThrows(IllegalStateException.class, () -> adapter.dequeueOutputBufferIndex(bufferInfo)); } @Test public void getOutputFormat_withoutFormatReceived_throwsException() { adapter.start(); - try { - adapter.getOutputFormat(); - fail(); - } catch (IllegalStateException expected) { - } + assertThrows(IllegalStateException.class, () -> adapter.getOutputFormat()); } @Test @@ -352,28 +267,10 @@ public class MultiLockAsyncMediaCodecAdapterTest { assertThat(adapter.getOutputFormat()).isEqualTo(format); } - @Test - public void flush_withoutStarted_throwsException() { - try { - adapter.flush(); - } catch (IllegalStateException expected) { - } - } - - @Test - public void flush_afterShutdown_throwsException() { - adapter.start(); - adapter.shutdown(); - try { - adapter.flush(); - } catch (IllegalStateException expected) { - } - } - @Test public void flush_multipleTimes_onlyLastFlushExecutes() throws InterruptedException { - AtomicInteger onCodecStartCount = new AtomicInteger(0); - adapter.setOnCodecStart(() -> onCodecStartCount.incrementAndGet()); + AtomicInteger codecStartCalls = new AtomicInteger(0); + adapter.setCodecStartRunnable(() -> codecStartCalls.incrementAndGet()); adapter.start(); Looper looper = handlerThread.getLooper(); Handler handler = new Handler(looper); @@ -385,23 +282,23 @@ public class MultiLockAsyncMediaCodecAdapterTest { adapter.flush(); // Enqueues a second flush event handler.post(() -> adapter.onInputBufferAvailable(codec, 3)); - // Progress the looper until the milestoneCount is increased - first flush event - // should have been a no-op + // Progress the looper until the milestoneCount is increased: + // adapter.start() called codec.start() but first flush event should have been a no-op ShadowLooper shadowLooper = shadowOf(looper); while (milestoneCount.get() < 1) { shadowLooper.runOneTask(); } - assertThat(onCodecStartCount.get()).isEqualTo(0); + assertThat(codecStartCalls.get()).isEqualTo(1); assertThat(waitUntilAllEventsAreExecuted(looper, /* time= */ 5, TimeUnit.SECONDS)).isTrue(); assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(3); - assertThat(onCodecStartCount.get()).isEqualTo(1); + assertThat(codecStartCalls.get()).isEqualTo(2); } @Test public void flush_andImmediatelyShutdown_flushIsNoOp() throws InterruptedException { - AtomicInteger onCodecStartCount = new AtomicInteger(0); - adapter.setOnCodecStart(() -> onCodecStartCount.incrementAndGet()); + AtomicInteger codecStartCalls = new AtomicInteger(0); + adapter.setCodecStartRunnable(() -> codecStartCalls.incrementAndGet()); adapter.start(); // Obtain looper when adapter is started. Looper looper = handlerThread.getLooper(); @@ -409,8 +306,8 @@ public class MultiLockAsyncMediaCodecAdapterTest { adapter.shutdown(); assertThat(waitUntilAllEventsAreExecuted(looper, 5, TimeUnit.SECONDS)).isTrue(); - // Only shutdown flushes the MediaCodecAsync handler. - assertThat(onCodecStartCount.get()).isEqualTo(0); + // Only adapter.start() called codec#start() + assertThat(codecStartCalls.get()).isEqualTo(1); } private static class TestHandlerThread extends HandlerThread { From 63f90adef0fe26156fd5b48babb9a6332b707e83 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 7 Jan 2020 15:39:27 +0000 Subject: [PATCH 34/44] Add package level NonNull to extractor.ts Also remove most classes from the nullness blacklist PiperOrigin-RevId: 288494712 --- .../android/exoplayer2/audio/DtsUtil.java | 5 +- .../exoplayer2/extractor/ts/Ac3Reader.java | 45 ++++++++++++------ .../exoplayer2/extractor/ts/Ac4Reader.java | 21 +++++---- .../exoplayer2/extractor/ts/AdtsReader.java | 41 ++++++++++------ .../ts/DefaultTsPayloadReaderFactory.java | 4 +- .../exoplayer2/extractor/ts/DtsReader.java | 20 ++++---- .../extractor/ts/DvbSubtitleReader.java | 6 +-- .../exoplayer2/extractor/ts/H262Reader.java | 47 ++++++++++--------- .../exoplayer2/extractor/ts/H264Reader.java | 41 ++++++++++++---- .../exoplayer2/extractor/ts/H265Reader.java | 36 +++++++++++--- .../exoplayer2/extractor/ts/Id3Reader.java | 6 ++- .../exoplayer2/extractor/ts/LatmReader.java | 35 ++++++++++---- .../extractor/ts/MpegAudioReader.java | 42 ++++++++++------- .../exoplayer2/extractor/ts/PesReader.java | 14 ++++-- .../exoplayer2/extractor/ts/PsExtractor.java | 12 +++-- .../exoplayer2/extractor/ts/SeiReader.java | 4 +- .../extractor/ts/SpliceInfoSectionReader.java | 14 +++++- .../exoplayer2/extractor/ts/TsExtractor.java | 10 ++-- .../extractor/ts/TsPayloadReader.java | 17 ++++--- .../extractor/ts/UserDataReader.java | 3 +- .../exoplayer2/extractor/ts/package-info.java | 19 ++++++++ .../exoplayer2/offline/DownloadHelper.java | 2 +- .../extractor/ts/TsExtractorTest.java | 2 + 23 files changed, 307 insertions(+), 139 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/package-info.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/DtsUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/DtsUtil.java index 7af9d9f074..f57d3b2895 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/DtsUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/DtsUtil.java @@ -81,7 +81,10 @@ public final class DtsUtil { * @return The DTS format parsed from data in the header. */ public static Format parseDtsFormat( - byte[] frame, String trackId, @Nullable String language, @Nullable DrmInitData drmInitData) { + byte[] frame, + @Nullable String trackId, + @Nullable String language, + @Nullable DrmInitData drmInitData) { ParsableBitArray frameBits = getNormalizedFrameHeader(frame); frameBits.skipBits(32 + 1 + 5 + 1 + 7 + 14); // SYNC, FTYPE, SHORT, CPF, NBLKS, FSIZE int amode = frameBits.readBits(6); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java index cd07a40c6d..af5efc35a7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.extractor.ts; import androidx.annotation.IntDef; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.audio.Ac3Util; @@ -23,11 +24,15 @@ import com.google.android.exoplayer2.audio.Ac3Util.SyncFrameInfo; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.ParsableBitArray; import com.google.android.exoplayer2.util.ParsableByteArray; +import com.google.android.exoplayer2.util.Util; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** * Parses a continuous (E-)AC-3 byte stream and extracts individual samples. @@ -47,10 +52,10 @@ public final class Ac3Reader implements ElementaryStreamReader { private final ParsableBitArray headerScratchBits; private final ParsableByteArray headerScratchBytes; - private final String language; + @Nullable private final String language; - private String trackFormatId; - private TrackOutput output; + @MonotonicNonNull private String formatId; + @MonotonicNonNull private TrackOutput output; @State private int state; private int bytesRead; @@ -60,7 +65,7 @@ public final class Ac3Reader implements ElementaryStreamReader { // Used when parsing the header. private long sampleDurationUs; - private Format format; + @MonotonicNonNull private Format format; private int sampleSize; // Used when reading the samples. @@ -78,7 +83,7 @@ public final class Ac3Reader implements ElementaryStreamReader { * * @param language Track language. */ - public Ac3Reader(String language) { + public Ac3Reader(@Nullable String language) { headerScratchBits = new ParsableBitArray(new byte[HEADER_SIZE]); headerScratchBytes = new ParsableByteArray(headerScratchBits.data); state = STATE_FINDING_SYNC; @@ -95,7 +100,7 @@ public final class Ac3Reader implements ElementaryStreamReader { @Override public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator generator) { generator.generateNewId(); - trackFormatId = generator.getFormatId(); + formatId = generator.getFormatId(); output = extractorOutput.track(generator.getTrackId(), C.TRACK_TYPE_AUDIO); } @@ -106,6 +111,7 @@ public final class Ac3Reader implements ElementaryStreamReader { @Override public void consume(ParsableByteArray data) { + Assertions.checkStateNotNull(output); // Asserts that createTracks has been called. while (data.bytesLeft() > 0) { switch (state) { case STATE_FINDING_SYNC: @@ -185,19 +191,28 @@ public final class Ac3Reader implements ElementaryStreamReader { return false; } - /** - * Parses the sample header. - */ - @SuppressWarnings("ReferenceEquality") + /** Parses the sample header. */ + @RequiresNonNull("output") private void parseHeader() { headerScratchBits.setPosition(0); SyncFrameInfo frameInfo = Ac3Util.parseAc3SyncframeInfo(headerScratchBits); - if (format == null || frameInfo.channelCount != format.channelCount + if (format == null + || frameInfo.channelCount != format.channelCount || frameInfo.sampleRate != format.sampleRate - || frameInfo.mimeType != format.sampleMimeType) { - format = Format.createAudioSampleFormat(trackFormatId, frameInfo.mimeType, null, - Format.NO_VALUE, Format.NO_VALUE, frameInfo.channelCount, frameInfo.sampleRate, null, - null, 0, language); + || Util.areEqual(frameInfo.mimeType, format.sampleMimeType)) { + format = + Format.createAudioSampleFormat( + formatId, + frameInfo.mimeType, + null, + Format.NO_VALUE, + Format.NO_VALUE, + frameInfo.channelCount, + frameInfo.sampleRate, + null, + null, + 0, + language); output.format(format); } sampleSize = frameInfo.frameSize; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac4Reader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac4Reader.java index 48bd07fce4..096eb81119 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac4Reader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac4Reader.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.extractor.ts; import androidx.annotation.IntDef; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.audio.Ac4Util; @@ -23,12 +24,15 @@ import com.google.android.exoplayer2.audio.Ac4Util.SyncFrameInfo; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableBitArray; import com.google.android.exoplayer2.util.ParsableByteArray; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** Parses a continuous AC-4 byte stream and extracts individual samples. */ public final class Ac4Reader implements ElementaryStreamReader { @@ -44,10 +48,10 @@ public final class Ac4Reader implements ElementaryStreamReader { private final ParsableBitArray headerScratchBits; private final ParsableByteArray headerScratchBytes; - private final String language; + @Nullable private final String language; - private String trackFormatId; - private TrackOutput output; + @MonotonicNonNull private String formatId; + @MonotonicNonNull private TrackOutput output; @State private int state; private int bytesRead; @@ -58,7 +62,7 @@ public final class Ac4Reader implements ElementaryStreamReader { // Used when parsing the header. private long sampleDurationUs; - private Format format; + @MonotonicNonNull private Format format; private int sampleSize; // Used when reading the samples. @@ -74,7 +78,7 @@ public final class Ac4Reader implements ElementaryStreamReader { * * @param language Track language. */ - public Ac4Reader(String language) { + public Ac4Reader(@Nullable String language) { headerScratchBits = new ParsableBitArray(new byte[Ac4Util.HEADER_SIZE_FOR_PARSER]); headerScratchBytes = new ParsableByteArray(headerScratchBits.data); state = STATE_FINDING_SYNC; @@ -95,7 +99,7 @@ public final class Ac4Reader implements ElementaryStreamReader { @Override public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator generator) { generator.generateNewId(); - trackFormatId = generator.getFormatId(); + formatId = generator.getFormatId(); output = extractorOutput.track(generator.getTrackId(), C.TRACK_TYPE_AUDIO); } @@ -106,6 +110,7 @@ public final class Ac4Reader implements ElementaryStreamReader { @Override public void consume(ParsableByteArray data) { + Assertions.checkStateNotNull(output); // Asserts that createTracks has been called. while (data.bytesLeft() > 0) { switch (state) { case STATE_FINDING_SYNC: @@ -185,7 +190,7 @@ public final class Ac4Reader implements ElementaryStreamReader { } /** Parses the sample header. */ - @SuppressWarnings("ReferenceEquality") + @RequiresNonNull("output") private void parseHeader() { headerScratchBits.setPosition(0); SyncFrameInfo frameInfo = Ac4Util.parseAc4SyncframeInfo(headerScratchBits); @@ -195,7 +200,7 @@ public final class Ac4Reader implements ElementaryStreamReader { || !MimeTypes.AUDIO_AC4.equals(format.sampleMimeType)) { format = Format.createAudioSampleFormat( - trackFormatId, + formatId, MimeTypes.AUDIO_AC4, /* codecs= */ null, /* bitrate= */ Format.NO_VALUE, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java index 589b543170..56ffc4500e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.extractor.ts; import android.util.Pair; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.ParserException; @@ -23,13 +24,18 @@ import com.google.android.exoplayer2.extractor.DummyTrackOutput; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.CodecSpecificDataUtil; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableBitArray; import com.google.android.exoplayer2.util.ParsableByteArray; +import com.google.android.exoplayer2.util.Util; import java.util.Arrays; import java.util.Collections; +import org.checkerframework.checker.nullness.qual.EnsuresNonNull; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** * Parses a continuous ADTS byte stream and extracts individual frames. @@ -62,11 +68,11 @@ public final class AdtsReader implements ElementaryStreamReader { private final boolean exposeId3; private final ParsableBitArray adtsScratch; private final ParsableByteArray id3HeaderBuffer; - private final String language; + @Nullable private final String language; - private String formatId; - private TrackOutput output; - private TrackOutput id3Output; + @MonotonicNonNull private String formatId; + @MonotonicNonNull private TrackOutput output; + @MonotonicNonNull private TrackOutput id3Output; private int state; private int bytesRead; @@ -90,7 +96,7 @@ public final class AdtsReader implements ElementaryStreamReader { // Used when reading the samples. private long timeUs; - private TrackOutput currentOutput; + @MonotonicNonNull private TrackOutput currentOutput; private long currentSampleDuration; /** @@ -104,7 +110,7 @@ public final class AdtsReader implements ElementaryStreamReader { * @param exposeId3 True if the reader should expose ID3 information. * @param language Track language. */ - public AdtsReader(boolean exposeId3, String language) { + public AdtsReader(boolean exposeId3, @Nullable String language) { adtsScratch = new ParsableBitArray(new byte[HEADER_SIZE + CRC_SIZE]); id3HeaderBuffer = new ParsableByteArray(Arrays.copyOf(ID3_IDENTIFIER, ID3_HEADER_SIZE)); setFindingSampleState(); @@ -130,6 +136,7 @@ public final class AdtsReader implements ElementaryStreamReader { idGenerator.generateNewId(); formatId = idGenerator.getFormatId(); output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_AUDIO); + currentOutput = output; if (exposeId3) { idGenerator.generateNewId(); id3Output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_METADATA); @@ -147,6 +154,7 @@ public final class AdtsReader implements ElementaryStreamReader { @Override public void consume(ParsableByteArray data) throws ParserException { + assertTracksCreated(); while (data.bytesLeft() > 0) { switch (state) { case STATE_FINDING_SAMPLE: @@ -425,9 +433,8 @@ public final class AdtsReader implements ElementaryStreamReader { return true; } - /** - * Parses the Id3 header. - */ + /** Parses the Id3 header. */ + @RequiresNonNull("id3Output") private void parseId3Header() { id3Output.sampleData(id3HeaderBuffer, ID3_HEADER_SIZE); id3HeaderBuffer.setPosition(ID3_SIZE_OFFSET); @@ -435,9 +442,8 @@ public final class AdtsReader implements ElementaryStreamReader { id3HeaderBuffer.readSynchSafeInt() + ID3_HEADER_SIZE); } - /** - * Parses the sample header. - */ + /** Parses the sample header. */ + @RequiresNonNull("output") private void parseAdtsHeader() throws ParserException { adtsScratch.setPosition(0); @@ -487,9 +493,8 @@ public final class AdtsReader implements ElementaryStreamReader { setReadingSampleState(output, sampleDurationUs, 0, sampleSize); } - /** - * Reads the rest of the sample - */ + /** Reads the rest of the sample */ + @RequiresNonNull("currentOutput") private void readSample(ParsableByteArray data) { int bytesToRead = Math.min(data.bytesLeft(), sampleSize - bytesRead); currentOutput.sampleData(data, bytesToRead); @@ -501,4 +506,10 @@ public final class AdtsReader implements ElementaryStreamReader { } } + @EnsuresNonNull({"output", "currentOutput", "id3Output"}) + private void assertTracksCreated() { + Assertions.checkNotNull(output); + Util.castNonNull(currentOutput); + Util.castNonNull(id3Output); + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java index 24d17f4956..480edb0a19 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.extractor.ts; import android.util.SparseArray; import androidx.annotation.IntDef; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.EsInfo; import com.google.android.exoplayer2.text.cea.Cea708InitializationData; @@ -134,6 +135,7 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact return new SparseArray<>(); } + @Nullable @Override public TsPayloadReader createPayloadReader(int streamType, EsInfo esInfo) { switch (streamType) { @@ -247,7 +249,7 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact // Skip reserved (8). scratchDescriptorData.skipBytes(1); - List initializationData = null; + @Nullable List initializationData = null; // The wide_aspect_ratio flag only has meaning for CEA-708. if (isDigital) { boolean isWideAspectRatio = (flags & 0x40) != 0; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DtsReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DtsReader.java index 1f9b0e79d4..127405d661 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DtsReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DtsReader.java @@ -15,13 +15,17 @@ */ package com.google.android.exoplayer2.extractor.ts; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.audio.DtsUtil; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.ParsableByteArray; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** * Parses a continuous DTS byte stream and extracts individual samples. @@ -35,10 +39,10 @@ public final class DtsReader implements ElementaryStreamReader { private static final int HEADER_SIZE = 18; private final ParsableByteArray headerScratchBytes; - private final String language; + @Nullable private final String language; - private String formatId; - private TrackOutput output; + @MonotonicNonNull private String formatId; + @MonotonicNonNull private TrackOutput output; private int state; private int bytesRead; @@ -48,7 +52,7 @@ public final class DtsReader implements ElementaryStreamReader { // Used when parsing the header. private long sampleDurationUs; - private Format format; + @MonotonicNonNull private Format format; private int sampleSize; // Used when reading the samples. @@ -59,7 +63,7 @@ public final class DtsReader implements ElementaryStreamReader { * * @param language Track language. */ - public DtsReader(String language) { + public DtsReader(@Nullable String language) { headerScratchBytes = new ParsableByteArray(new byte[HEADER_SIZE]); state = STATE_FINDING_SYNC; this.language = language; @@ -86,6 +90,7 @@ public final class DtsReader implements ElementaryStreamReader { @Override public void consume(ParsableByteArray data) { + Assertions.checkStateNotNull(output); // Asserts that createTracks has been called. while (data.bytesLeft() > 0) { switch (state) { case STATE_FINDING_SYNC: @@ -162,9 +167,8 @@ public final class DtsReader implements ElementaryStreamReader { return false; } - /** - * Parses the sample header. - */ + /** Parses the sample header. */ + @RequiresNonNull("output") private void parseHeader() { byte[] frameData = headerScratchBytes.data; if (format == null) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DvbSubtitleReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DvbSubtitleReader.java index 3f0a772b1c..146f663bfd 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DvbSubtitleReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DvbSubtitleReader.java @@ -64,12 +64,12 @@ public final class DvbSubtitleReader implements ElementaryStreamReader { Format.createImageSampleFormat( idGenerator.getFormatId(), MimeTypes.APPLICATION_DVBSUBS, - null, + /* codecs= */ null, Format.NO_VALUE, - 0, + /* selectionFlags= */ 0, Collections.singletonList(subtitleInfo.initializationData), subtitleInfo.language, - null)); + /* drmInitData= */ null)); outputs[i] = output; } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java index e7f2c1935b..4d2018ef86 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java @@ -16,16 +16,20 @@ package com.google.android.exoplayer2.extractor.ts; import android.util.Pair; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.NalUnitUtil; import com.google.android.exoplayer2.util.ParsableByteArray; +import com.google.android.exoplayer2.util.Util; import java.util.Arrays; import java.util.Collections; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * Parses a continuous H262 byte stream and extracts individual frames. @@ -38,27 +42,27 @@ public final class H262Reader implements ElementaryStreamReader { private static final int START_GROUP = 0xB8; private static final int START_USER_DATA = 0xB2; - private String formatId; - private TrackOutput output; + @MonotonicNonNull private String formatId; + @MonotonicNonNull private TrackOutput output; // Maps (frame_rate_code - 1) indices to values, as defined in ITU-T H.262 Table 6-4. private static final double[] FRAME_RATE_VALUES = new double[] { 24000d / 1001, 24, 25, 30000d / 1001, 30, 50, 60000d / 1001, 60}; + @Nullable private final UserDataReader userDataReader; + @Nullable private final ParsableByteArray userDataParsable; + + // State that should be reset on seek. + @Nullable private final NalUnitTargetBuffer userData; + private final boolean[] prefixFlags; + private final CsdBuffer csdBuffer; + private long totalBytesWritten; + private boolean startedFirstSample; + // State that should not be reset on seek. private boolean hasOutputFormat; private long frameDurationUs; - private final UserDataReader userDataReader; - private final ParsableByteArray userDataParsable; - - // State that should be reset on seek. - private final boolean[] prefixFlags; - private final CsdBuffer csdBuffer; - private final NalUnitTargetBuffer userData; - private long totalBytesWritten; - private boolean startedFirstSample; - // Per packet state that gets reset at the start of each packet. private long pesTimeUs; @@ -72,7 +76,7 @@ public final class H262Reader implements ElementaryStreamReader { this(null); } - /* package */ H262Reader(UserDataReader userDataReader) { + /* package */ H262Reader(@Nullable UserDataReader userDataReader) { this.userDataReader = userDataReader; prefixFlags = new boolean[4]; csdBuffer = new CsdBuffer(128); @@ -89,7 +93,7 @@ public final class H262Reader implements ElementaryStreamReader { public void seek() { NalUnitUtil.clearPrefixFlags(prefixFlags); csdBuffer.reset(); - if (userDataReader != null) { + if (userData != null) { userData.reset(); } totalBytesWritten = 0; @@ -114,6 +118,7 @@ public final class H262Reader implements ElementaryStreamReader { @Override public void consume(ParsableByteArray data) { + Assertions.checkStateNotNull(output); // Asserts that createTracks has been called. int offset = data.getPosition(); int limit = data.limit(); byte[] dataArray = data.data; @@ -130,7 +135,7 @@ public final class H262Reader implements ElementaryStreamReader { if (!hasOutputFormat) { csdBuffer.onData(dataArray, offset, limit); } - if (userDataReader != null) { + if (userData != null) { userData.appendToNalUnit(dataArray, offset, limit); } return; @@ -157,7 +162,7 @@ public final class H262Reader implements ElementaryStreamReader { hasOutputFormat = true; } } - if (userDataReader != null) { + if (userData != null) { int bytesAlreadyPassed = 0; if (lengthToStartCode > 0) { userData.appendToNalUnit(dataArray, offset, startCodeOffset); @@ -167,8 +172,8 @@ public final class H262Reader implements ElementaryStreamReader { if (userData.endNalUnit(bytesAlreadyPassed)) { int unescapedLength = NalUnitUtil.unescapeStream(userData.nalData, userData.nalLength); - userDataParsable.reset(userData.nalData, unescapedLength); - userDataReader.consume(sampleTimeUs, userDataParsable); + Util.castNonNull(userDataParsable).reset(userData.nalData, unescapedLength); + Util.castNonNull(userDataReader).consume(sampleTimeUs, userDataParsable); } if (startCodeValue == START_USER_DATA && data.data[startCodeOffset + 2] == 0x1) { @@ -211,10 +216,10 @@ public final class H262Reader implements ElementaryStreamReader { * * @param csdBuffer The csd buffer. * @param formatId The id for the generated format. May be null. - * @return A pair consisting of the {@link Format} and the frame duration in microseconds, or - * 0 if the duration could not be determined. + * @return A pair consisting of the {@link Format} and the frame duration in microseconds, or 0 if + * the duration could not be determined. */ - private static Pair parseCsdBuffer(CsdBuffer csdBuffer, String formatId) { + private static Pair parseCsdBuffer(CsdBuffer csdBuffer, @Nullable String formatId) { byte[] csdData = Arrays.copyOf(csdBuffer.data, csdBuffer.length); int firstByte = csdData[4] & 0xFF; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H264Reader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H264Reader.java index d249c1b9da..011b3fd7b2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H264Reader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H264Reader.java @@ -23,15 +23,21 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.CodecSpecificDataUtil; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.NalUnitUtil; import com.google.android.exoplayer2.util.NalUnitUtil.SpsData; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableNalUnitBitArray; +import com.google.android.exoplayer2.util.Util; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import org.checkerframework.checker.nullness.qual.EnsuresNonNull; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** * Parses a continuous H264 byte stream and extracts individual frames. @@ -51,9 +57,9 @@ public final class H264Reader implements ElementaryStreamReader { private long totalBytesWritten; private final boolean[] prefixFlags; - private String formatId; - private TrackOutput output; - private SampleReader sampleReader; + @MonotonicNonNull private String formatId; + @MonotonicNonNull private TrackOutput output; + @MonotonicNonNull private SampleReader sampleReader; // State that should not be reset on seek. private boolean hasOutputFormat; @@ -87,13 +93,15 @@ public final class H264Reader implements ElementaryStreamReader { @Override public void seek() { + totalBytesWritten = 0; + randomAccessIndicator = false; NalUnitUtil.clearPrefixFlags(prefixFlags); sps.reset(); pps.reset(); sei.reset(); - sampleReader.reset(); - totalBytesWritten = 0; - randomAccessIndicator = false; + if (sampleReader != null) { + sampleReader.reset(); + } } @Override @@ -113,6 +121,8 @@ public final class H264Reader implements ElementaryStreamReader { @Override public void consume(ParsableByteArray data) { + assertTracksCreated(); + int offset = data.getPosition(); int limit = data.limit(); byte[] dataArray = data.data; @@ -159,6 +169,7 @@ public final class H264Reader implements ElementaryStreamReader { // Do nothing. } + @RequiresNonNull("sampleReader") private void startNalUnit(long position, int nalUnitType, long pesTimeUs) { if (!hasOutputFormat || sampleReader.needsSpsPps()) { sps.startNalUnit(nalUnitType); @@ -168,6 +179,7 @@ public final class H264Reader implements ElementaryStreamReader { sampleReader.startNalUnit(position, nalUnitType, pesTimeUs); } + @RequiresNonNull("sampleReader") private void nalUnitData(byte[] dataArray, int offset, int limit) { if (!hasOutputFormat || sampleReader.needsSpsPps()) { sps.appendToNalUnit(dataArray, offset, limit); @@ -177,6 +189,7 @@ public final class H264Reader implements ElementaryStreamReader { sampleReader.appendToNalUnit(dataArray, offset, limit); } + @RequiresNonNull({"output", "sampleReader"}) private void endNalUnit(long position, int offset, int discardPadding, long pesTimeUs) { if (!hasOutputFormat || sampleReader.needsSpsPps()) { sps.endNalUnit(discardPadding); @@ -237,6 +250,12 @@ public final class H264Reader implements ElementaryStreamReader { } } + @EnsuresNonNull({"output", "sampleReader"}) + private void assertTracksCreated() { + Assertions.checkStateNotNull(output); + Util.castNonNull(sampleReader); + } + /** Consumes a stream of NAL units and outputs samples. */ private static final class SampleReader { @@ -478,7 +497,7 @@ public final class H264Reader implements ElementaryStreamReader { private boolean isComplete; private boolean hasSliceType; - private SpsData spsData; + @Nullable private SpsData spsData; private int nalRefIdc; private int sliceType; private int frameNum; @@ -542,6 +561,8 @@ public final class H264Reader implements ElementaryStreamReader { private boolean isFirstVclNalUnitOfPicture(SliceHeaderData other) { // See ISO 14496-10 subsection 7.4.1.2.4. + SpsData spsData = Assertions.checkStateNotNull(this.spsData); + SpsData otherSpsData = Assertions.checkStateNotNull(other.spsData); return isComplete && (!other.isComplete || frameNum != other.frameNum @@ -552,15 +573,15 @@ public final class H264Reader implements ElementaryStreamReader { && bottomFieldFlag != other.bottomFieldFlag) || (nalRefIdc != other.nalRefIdc && (nalRefIdc == 0 || other.nalRefIdc == 0)) || (spsData.picOrderCountType == 0 - && other.spsData.picOrderCountType == 0 + && otherSpsData.picOrderCountType == 0 && (picOrderCntLsb != other.picOrderCntLsb || deltaPicOrderCntBottom != other.deltaPicOrderCntBottom)) || (spsData.picOrderCountType == 1 - && other.spsData.picOrderCountType == 1 + && otherSpsData.picOrderCountType == 1 && (deltaPicOrderCnt0 != other.deltaPicOrderCnt0 || deltaPicOrderCnt1 != other.deltaPicOrderCnt1)) || idrPicFlag != other.idrPicFlag - || (idrPicFlag && other.idrPicFlag && idrPicId != other.idrPicId)); + || (idrPicFlag && idrPicId != other.idrPicId)); } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java index 88bde53746..c86cf51866 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java @@ -20,12 +20,18 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; +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.NalUnitUtil; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableNalUnitBitArray; +import com.google.android.exoplayer2.util.Util; import java.util.Collections; +import org.checkerframework.checker.nullness.qual.EnsuresNonNull; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** * Parses a continuous H.265 byte stream and extracts individual frames. @@ -46,9 +52,9 @@ public final class H265Reader implements ElementaryStreamReader { private final SeiReader seiReader; - private String formatId; - private TrackOutput output; - private SampleReader sampleReader; + @MonotonicNonNull private String formatId; + @MonotonicNonNull private TrackOutput output; + @MonotonicNonNull private SampleReader sampleReader; // State that should not be reset on seek. private boolean hasOutputFormat; @@ -84,14 +90,16 @@ public final class H265Reader implements ElementaryStreamReader { @Override public void seek() { + totalBytesWritten = 0; NalUnitUtil.clearPrefixFlags(prefixFlags); vps.reset(); sps.reset(); pps.reset(); prefixSei.reset(); suffixSei.reset(); - sampleReader.reset(); - totalBytesWritten = 0; + if (sampleReader != null) { + sampleReader.reset(); + } } @Override @@ -111,6 +119,8 @@ public final class H265Reader implements ElementaryStreamReader { @Override public void consume(ParsableByteArray data) { + assertTracksCreated(); + while (data.bytesLeft() > 0) { int offset = data.getPosition(); int limit = data.limit(); @@ -160,6 +170,7 @@ public final class H265Reader implements ElementaryStreamReader { // Do nothing. } + @RequiresNonNull("sampleReader") private void startNalUnit(long position, int offset, int nalUnitType, long pesTimeUs) { if (hasOutputFormat) { sampleReader.startNalUnit(position, offset, nalUnitType, pesTimeUs); @@ -172,6 +183,7 @@ public final class H265Reader implements ElementaryStreamReader { suffixSei.startNalUnit(nalUnitType); } + @RequiresNonNull("sampleReader") private void nalUnitData(byte[] dataArray, int offset, int limit) { if (hasOutputFormat) { sampleReader.readNalUnitData(dataArray, offset, limit); @@ -184,6 +196,7 @@ public final class H265Reader implements ElementaryStreamReader { suffixSei.appendToNalUnit(dataArray, offset, limit); } + @RequiresNonNull({"output", "sampleReader"}) private void endNalUnit(long position, int offset, int discardPadding, long pesTimeUs) { if (hasOutputFormat) { sampleReader.endNalUnit(position, offset); @@ -214,8 +227,11 @@ public final class H265Reader implements ElementaryStreamReader { } } - private static Format parseMediaFormat(String formatId, NalUnitTargetBuffer vps, - NalUnitTargetBuffer sps, NalUnitTargetBuffer pps) { + private static Format parseMediaFormat( + @Nullable String formatId, + NalUnitTargetBuffer vps, + NalUnitTargetBuffer sps, + NalUnitTargetBuffer pps) { // Build codec-specific data. byte[] csd = new byte[vps.nalLength + sps.nalLength + pps.nalLength]; System.arraycopy(vps.nalData, 0, csd, 0, vps.nalLength); @@ -389,6 +405,12 @@ public final class H265Reader implements ElementaryStreamReader { } } + @EnsuresNonNull({"output", "sampleReader"}) + private void assertTracksCreated() { + Assertions.checkStateNotNull(output); + Util.castNonNull(sampleReader); + } + private static final class SampleReader { /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Id3Reader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Id3Reader.java index 77ec48d0a7..615d2f8c2e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Id3Reader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Id3Reader.java @@ -23,9 +23,11 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; +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.ParsableByteArray; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * Parses ID3 data and extracts individual text information frames. @@ -36,7 +38,7 @@ public final class Id3Reader implements ElementaryStreamReader { private final ParsableByteArray id3Header; - private TrackOutput output; + @MonotonicNonNull private TrackOutput output; // State that should be reset on seek. private boolean writingSample; @@ -76,6 +78,7 @@ public final class Id3Reader implements ElementaryStreamReader { @Override public void consume(ParsableByteArray data) { + Assertions.checkStateNotNull(output); // Asserts that createTracks has been called. if (!writingSample) { return; } @@ -106,6 +109,7 @@ public final class Id3Reader implements ElementaryStreamReader { @Override public void packetFinished() { + Assertions.checkStateNotNull(output); // Asserts that createTracks has been called. if (!writingSample || sampleSize == 0 || sampleBytesRead != sampleSize) { return; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/LatmReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/LatmReader.java index 4ad9adfa2a..1c8131feaa 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/LatmReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/LatmReader.java @@ -23,11 +23,14 @@ import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.CodecSpecificDataUtil; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableBitArray; import com.google.android.exoplayer2.util.ParsableByteArray; import java.util.Collections; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** * Parses and extracts samples from an AAC/LATM elementary stream. @@ -43,14 +46,14 @@ public final class LatmReader implements ElementaryStreamReader { private static final int SYNC_BYTE_FIRST = 0x56; private static final int SYNC_BYTE_SECOND = 0xE0; - private final String language; + @Nullable private final String language; private final ParsableByteArray sampleDataBuffer; private final ParsableBitArray sampleBitArray; // Track output info. - private TrackOutput output; - private Format format; - private String formatId; + @MonotonicNonNull private TrackOutput output; + @MonotonicNonNull private String formatId; + @MonotonicNonNull private Format format; // Parser state info. private int state; @@ -99,6 +102,7 @@ public final class LatmReader implements ElementaryStreamReader { @Override public void consume(ParsableByteArray data) throws ParserException { + Assertions.checkStateNotNull(output); // Asserts that createTracks has been called. int bytesToRead; while (data.bytesLeft() > 0) { switch (state) { @@ -150,6 +154,7 @@ public final class LatmReader implements ElementaryStreamReader { * * @param data A {@link ParsableBitArray} containing the AudioMuxElement's bytes. */ + @RequiresNonNull("output") private void parseAudioMuxElement(ParsableBitArray data) throws ParserException { boolean useSameStreamMux = data.readBit(); if (!useSameStreamMux) { @@ -173,9 +178,8 @@ public final class LatmReader implements ElementaryStreamReader { } } - /** - * Parses a StreamMuxConfig as defined in ISO/IEC 14496-3:2009 Section 1.7.3.1, Table 1.42. - */ + /** Parses a StreamMuxConfig as defined in ISO/IEC 14496-3:2009 Section 1.7.3.1, Table 1.42. */ + @RequiresNonNull("output") private void parseStreamMuxConfig(ParsableBitArray data) throws ParserException { int audioMuxVersion = data.readBits(1); audioMuxVersionA = audioMuxVersion == 1 ? data.readBits(1) : 0; @@ -198,9 +202,19 @@ public final class LatmReader implements ElementaryStreamReader { data.setPosition(startPosition); byte[] initData = new byte[(readBits + 7) / 8]; data.readBits(initData, 0, readBits); - Format format = Format.createAudioSampleFormat(formatId, MimeTypes.AUDIO_AAC, null, - Format.NO_VALUE, Format.NO_VALUE, channelCount, sampleRateHz, - Collections.singletonList(initData), null, 0, language); + Format format = + Format.createAudioSampleFormat( + formatId, + MimeTypes.AUDIO_AAC, + /* codecs= */ null, + Format.NO_VALUE, + Format.NO_VALUE, + channelCount, + sampleRateHz, + Collections.singletonList(initData), + /* drmInitData= */ null, + /* selectionFlags= */ 0, + language); if (!format.equals(this.format)) { this.format = format; sampleDurationUs = (C.MICROS_PER_SECOND * 1024) / format.sampleRate; @@ -280,6 +294,7 @@ public final class LatmReader implements ElementaryStreamReader { } } + @RequiresNonNull("output") private void parsePayloadMux(ParsableBitArray data, int muxLengthBytes) { // The start of sample data in int bitPosition = data.getPosition(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java index 393e297818..5f41a23246 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java @@ -21,7 +21,11 @@ import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.MpegAudioHeader; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.ParsableByteArray; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** * Parses a continuous MPEG Audio byte stream and extracts individual frames. @@ -36,10 +40,10 @@ public final class MpegAudioReader implements ElementaryStreamReader { private final ParsableByteArray headerScratch; private final MpegAudioHeader header; - private final String language; + @Nullable private final String language; - private String formatId; - private TrackOutput output; + @MonotonicNonNull private TrackOutput output; + @MonotonicNonNull private String formatId; private int state; private int frameBytesRead; @@ -59,7 +63,7 @@ public final class MpegAudioReader implements ElementaryStreamReader { this(null); } - public MpegAudioReader(String language) { + public MpegAudioReader(@Nullable String language) { state = STATE_FINDING_HEADER; // The first byte of an MPEG Audio frame header is always 0xFF. headerScratch = new ParsableByteArray(4); @@ -89,6 +93,7 @@ public final class MpegAudioReader implements ElementaryStreamReader { @Override public void consume(ParsableByteArray data) { + Assertions.checkStateNotNull(output); // Asserts that createTracks has been called. while (data.bytesLeft() > 0) { switch (state) { case STATE_FINDING_HEADER: @@ -146,20 +151,21 @@ public final class MpegAudioReader implements ElementaryStreamReader { /** * Attempts to read the remaining two bytes of the frame header. - *

    - * If a frame header is read in full then the state is changed to {@link #STATE_READING_FRAME}, + * + *

    If a frame header is read in full then the state is changed to {@link #STATE_READING_FRAME}, * the media format is output if this has not previously occurred, the four header bytes are * output as sample data, and the position of the source is advanced to the byte that immediately * follows the header. - *

    - * If a frame header is read in full but cannot be parsed then the state is changed to - * {@link #STATE_READING_HEADER}. - *

    - * If a frame header is not read in full then the position of the source is advanced to the limit, - * and the method should be called again with the next source to continue the read. + * + *

    If a frame header is read in full but cannot be parsed then the state is changed to {@link + * #STATE_READING_HEADER}. + * + *

    If a frame header is not read in full then the position of the source is advanced to the + * limit, and the method should be called again with the next source to continue the read. * * @param source The source from which to read. */ + @RequiresNonNull("output") private void readHeaderRemainder(ParsableByteArray source) { int bytesToRead = Math.min(source.bytesLeft(), HEADER_SIZE - frameBytesRead); source.readBytes(headerScratch.data, frameBytesRead, bytesToRead); @@ -195,16 +201,17 @@ public final class MpegAudioReader implements ElementaryStreamReader { /** * Attempts to read the remainder of the frame. - *

    - * If a frame is read in full then true is returned. The frame will have been output, and the + * + *

    If a frame is read in full then true is returned. The frame will have been output, and the * position of the source will have been advanced to the byte that immediately follows the end of * the frame. - *

    - * If a frame is not read in full then the position of the source will have been advanced to the - * limit, and the method should be called again with the next source to continue the read. + * + *

    If a frame is not read in full then the position of the source will have been advanced to + * the limit, and the method should be called again with the next source to continue the read. * * @param source The source from which to read. */ + @RequiresNonNull("output") private void readFrameRemainder(ParsableByteArray source) { int bytesToRead = Math.min(source.bytesLeft(), frameSize - frameBytesRead); output.sampleData(source, bytesToRead); @@ -219,5 +226,4 @@ public final class MpegAudioReader implements ElementaryStreamReader { frameBytesRead = 0; state = STATE_FINDING_HEADER; } - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PesReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PesReader.java index ff755f4ece..d5d32a6d96 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PesReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PesReader.java @@ -15,13 +15,17 @@ */ package com.google.android.exoplayer2.extractor.ts; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.extractor.ExtractorOutput; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.ParsableBitArray; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.TimestampAdjuster; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** * Parses PES packet data and extracts samples. @@ -45,7 +49,7 @@ public final class PesReader implements TsPayloadReader { private int state; private int bytesRead; - private TimestampAdjuster timestampAdjuster; + @MonotonicNonNull private TimestampAdjuster timestampAdjuster; private boolean ptsFlag; private boolean dtsFlag; private boolean seenFirstDts; @@ -79,6 +83,8 @@ public final class PesReader implements TsPayloadReader { @Override public final void consume(ParsableByteArray data, @Flags int flags) throws ParserException { + Assertions.checkStateNotNull(timestampAdjuster); // Asserts init has been called. + if ((flags & FLAG_PAYLOAD_UNIT_START_INDICATOR) != 0) { switch (state) { case STATE_FINDING_HEADER: @@ -119,7 +125,7 @@ public final class PesReader implements TsPayloadReader { int readLength = Math.min(MAX_HEADER_EXTENSION_SIZE, extendedHeaderLength); // Read as much of the extended header as we're interested in, and skip the rest. if (continueRead(data, pesScratch.data, readLength) - && continueRead(data, null, extendedHeaderLength)) { + && continueRead(data, /* target= */ null, extendedHeaderLength)) { parseHeaderExtension(); flags |= dataAlignmentIndicator ? FLAG_DATA_ALIGNMENT_INDICATOR : 0; reader.packetStarted(timeUs, flags); @@ -162,7 +168,8 @@ public final class PesReader implements TsPayloadReader { * @param targetLength The target length of the read. * @return Whether the target length has been reached. */ - private boolean continueRead(ParsableByteArray source, byte[] target, int targetLength) { + private boolean continueRead( + ParsableByteArray source, @Nullable byte[] target, int targetLength) { int bytesToRead = Math.min(source.bytesLeft(), targetLength - bytesRead); if (bytesToRead <= 0) { return true; @@ -207,6 +214,7 @@ public final class PesReader implements TsPayloadReader { return true; } + @RequiresNonNull("timestampAdjuster") private void parseHeaderExtension() { pesScratch.setPosition(0); timeUs = C.TIME_UNSET; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java index fec108fd5f..3f10a454fc 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.extractor.ts; import android.util.SparseArray; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.extractor.Extractor; @@ -25,10 +26,13 @@ import com.google.android.exoplayer2.extractor.ExtractorsFactory; import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.ParsableBitArray; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.TimestampAdjuster; import java.io.IOException; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** * Extracts data from the MPEG-2 PS container format. @@ -67,8 +71,8 @@ public final class PsExtractor implements Extractor { private long lastTrackPosition; // Accessed only by the loading thread. - private PsBinarySearchSeeker psBinarySearchSeeker; - private ExtractorOutput output; + @Nullable private PsBinarySearchSeeker psBinarySearchSeeker; + @MonotonicNonNull private ExtractorOutput output; private boolean hasOutputSeekMap; public PsExtractor() { @@ -160,6 +164,7 @@ public final class PsExtractor implements Extractor { @Override public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException, InterruptedException { + Assertions.checkStateNotNull(output); // Asserts init has been called. long inputLength = input.getLength(); boolean canReadDuration = inputLength != C.LENGTH_UNSET; @@ -221,7 +226,7 @@ public final class PsExtractor implements Extractor { PesReader payloadReader = psPayloadReaders.get(streamId); if (!foundAllTracks) { if (payloadReader == null) { - ElementaryStreamReader elementaryStreamReader = null; + @Nullable ElementaryStreamReader elementaryStreamReader = null; if (streamId == PRIVATE_STREAM_1) { // Private stream, used for AC3 audio. // NOTE: This may need further parsing to determine if its DTS, but that's likely only @@ -278,6 +283,7 @@ public final class PsExtractor implements Extractor { // Internals. + @RequiresNonNull("output") private void maybeOutputSeekMap(long inputLength) { if (!hasOutputSeekMap) { hasOutputSeekMap = true; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/SeiReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/SeiReader.java index d032ef5883..2541db07a8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/SeiReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/SeiReader.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.extractor.ts; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.ExtractorOutput; @@ -45,7 +46,7 @@ public final class SeiReader { idGenerator.generateNewId(); TrackOutput output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_TEXT); Format channelFormat = closedCaptionFormats.get(i); - String channelMimeType = channelFormat.sampleMimeType; + @Nullable String channelMimeType = channelFormat.sampleMimeType; Assertions.checkArgument(MimeTypes.APPLICATION_CEA608.equals(channelMimeType) || MimeTypes.APPLICATION_CEA708.equals(channelMimeType), "Invalid closed caption mime type provided: " + channelMimeType); @@ -69,5 +70,4 @@ public final class SeiReader { public void consume(long pesTimeUs, ParsableByteArray seiBuffer) { CeaUtil.consume(pesTimeUs, seiBuffer, outputs); } - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/SpliceInfoSectionReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/SpliceInfoSectionReader.java index 27838d4c25..6747a04916 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/SpliceInfoSectionReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/SpliceInfoSectionReader.java @@ -19,17 +19,21 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.TrackOutput; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.TimestampAdjuster; +import com.google.android.exoplayer2.util.Util; +import org.checkerframework.checker.nullness.qual.EnsuresNonNull; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * Parses splice info sections as defined by SCTE35. */ public final class SpliceInfoSectionReader implements SectionPayloadReader { - private TimestampAdjuster timestampAdjuster; - private TrackOutput output; + @MonotonicNonNull private TimestampAdjuster timestampAdjuster; + @MonotonicNonNull private TrackOutput output; private boolean formatDeclared; @Override @@ -44,6 +48,7 @@ public final class SpliceInfoSectionReader implements SectionPayloadReader { @Override public void consume(ParsableByteArray sectionData) { + assertInitialized(); if (!formatDeclared) { if (timestampAdjuster.getTimestampOffsetUs() == C.TIME_UNSET) { // There is not enough information to initialize the timestamp adjuster. @@ -59,4 +64,9 @@ public final class SpliceInfoSectionReader implements SectionPayloadReader { sampleSize, 0, null); } + @EnsuresNonNull({"timestampAdjuster", "output"}) + private void assertInitialized() { + Assertions.checkStateNotNull(timestampAdjuster); + Util.castNonNull(output); + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java index 2cd7398d7c..35e8806a6f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java @@ -21,6 +21,7 @@ import android.util.SparseArray; import android.util.SparseBooleanArray; import android.util.SparseIntArray; import androidx.annotation.IntDef; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.extractor.Extractor; @@ -587,8 +588,11 @@ public final class TsExtractor implements Extractor { continue; } - TsPayloadReader reader = mode == MODE_HLS && streamType == TS_STREAM_TYPE_ID3 ? id3Reader - : payloadReaderFactory.createPayloadReader(streamType, esInfo); + @Nullable + TsPayloadReader reader = + mode == MODE_HLS && streamType == TS_STREAM_TYPE_ID3 + ? id3Reader + : payloadReaderFactory.createPayloadReader(streamType, esInfo); if (mode != MODE_HLS || elementaryPid < trackIdToPidScratch.get(trackId, MAX_PID_PLUS_ONE)) { trackIdToPidScratch.put(trackId, elementaryPid); @@ -602,7 +606,7 @@ public final class TsExtractor implements Extractor { int trackPid = trackIdToPidScratch.valueAt(i); trackIds.put(trackId, true); trackPids.put(trackPid, true); - TsPayloadReader reader = trackIdToReaderScratch.valueAt(i); + @Nullable TsPayloadReader reader = trackIdToReaderScratch.valueAt(i); if (reader != null) { if (reader != id3Reader) { reader.init(timestampAdjuster, output, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java index af27235257..03ed10ff0d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.extractor.ts; import android.util.SparseArray; import androidx.annotation.IntDef; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.TrackOutput; @@ -53,11 +54,11 @@ public interface TsPayloadReader { * * @param streamType Stream type value as defined in the PMT entry or associated descriptors. * @param esInfo Information associated to the elementary stream provided in the PMT. - * @return A {@link TsPayloadReader} for the packet stream carried by the provided pid. + * @return A {@link TsPayloadReader} for the packet stream carried by the provided pid, or * {@code null} if the stream is not supported. */ + @Nullable TsPayloadReader createPayloadReader(int streamType, EsInfo esInfo); - } /** @@ -66,18 +67,21 @@ public interface TsPayloadReader { final class EsInfo { public final int streamType; - public final String language; + @Nullable public final String language; public final List dvbSubtitleInfos; public final byte[] descriptorBytes; /** - * @param streamType The type of the stream as defined by the - * {@link TsExtractor}{@code .TS_STREAM_TYPE_*}. + * @param streamType The type of the stream as defined by the {@link TsExtractor}{@code + * .TS_STREAM_TYPE_*}. * @param language The language of the stream, as defined by ISO/IEC 13818-1, section 2.6.18. * @param dvbSubtitleInfos Information about DVB subtitles associated to the stream. * @param descriptorBytes The descriptor bytes associated to the stream. */ - public EsInfo(int streamType, String language, List dvbSubtitleInfos, + public EsInfo( + int streamType, + @Nullable String language, + @Nullable List dvbSubtitleInfos, byte[] descriptorBytes) { this.streamType = streamType; this.language = language; @@ -134,6 +138,7 @@ public interface TsPayloadReader { this.firstTrackId = firstTrackId; this.trackIdIncrement = trackIdIncrement; trackId = ID_UNSET; + formatId = ""; } /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/UserDataReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/UserDataReader.java index 724eba1d9a..739e5341b8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/UserDataReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/UserDataReader.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.extractor.ts; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.ExtractorOutput; @@ -44,7 +45,7 @@ import java.util.List; idGenerator.generateNewId(); TrackOutput output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_TEXT); Format channelFormat = closedCaptionFormats.get(i); - String channelMimeType = channelFormat.sampleMimeType; + @Nullable String channelMimeType = channelFormat.sampleMimeType; Assertions.checkArgument( MimeTypes.APPLICATION_CEA608.equals(channelMimeType) || MimeTypes.APPLICATION_CEA708.equals(channelMimeType), diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/package-info.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/package-info.java new file mode 100644 index 0000000000..4d93bd5ac5 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/package-info.java @@ -0,0 +1,19 @@ +/* + * 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. + */ +@NonNullApi +package com.google.android.exoplayer2.extractor.ts; + +import com.google.android.exoplayer2.util.NonNullApi; 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 ff95afb1f6..1641b2aef6 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 @@ -773,7 +773,7 @@ public final class DownloadHelper { } // Initialization of array of Lists. - @SuppressWarnings("unchecked") + @SuppressWarnings({"unchecked", "rawtypes"}) private void onMediaPrepared() { Assertions.checkNotNull(mediaPreparer); Assertions.checkNotNull(mediaPreparer.mediaPeriods); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java index f1b962a712..93e3f30d0c 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.extractor.ts; import static com.google.common.truth.Truth.assertThat; import android.util.SparseArray; +import androidx.annotation.Nullable; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.C; @@ -172,6 +173,7 @@ public final class TsExtractorTest { } } + @Nullable @Override public TsPayloadReader createPayloadReader(int streamType, EsInfo esInfo) { if (provideCustomEsReader && streamType == 3) { From 70fe6b45909dabc4daf3a57f1d0306feee68957c Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 8 Jan 2020 09:10:41 +0000 Subject: [PATCH 35/44] Upgrade OkHttp library to fix HTTP2 issue Issue: #4078 PiperOrigin-RevId: 288651166 --- RELEASENOTES.md | 3 +++ extensions/okhttp/build.gradle | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 5f411b7100..d5dd9ddd0e 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -49,6 +49,9 @@ later). * Parse `text-combine-upright` CSS property (i.e. tate-chu-yoko) in WebVTT subtitles (rendering is coming later). +* OkHttp extension: Upgrade OkHttp dependency to 3.12.7, which fixes a class of + `SocketTimeoutException` issues when using HTTP/2 + ([#4078](https://github.com/google/ExoPlayer/issues/4078)). ### 2.11.1 (2019-12-20) ### diff --git a/extensions/okhttp/build.gradle b/extensions/okhttp/build.gradle index bde4e127df..2b4b4854c3 100644 --- a/extensions/okhttp/build.gradle +++ b/extensions/okhttp/build.gradle @@ -41,7 +41,7 @@ dependencies { // https://cashapp.github.io/2019-02-05/okhttp-3-13-requires-android-5 // Since OkHttp is distributed as a jar rather than an aar, Gradle won't // stop us from making this mistake! - api 'com.squareup.okhttp3:okhttp:3.12.5' + api 'com.squareup.okhttp3:okhttp:3.12.7' } ext { From ee091e6a45854c0adbeda926c0bf52201b61f747 Mon Sep 17 00:00:00 2001 From: kimvde Date: Wed, 8 Jan 2020 11:48:58 +0000 Subject: [PATCH 36/44] Use FlacLibrary.isAvailable in FlacExtractor selection PiperOrigin-RevId: 288667790 --- .../flac/DefaultExtractorsFactoryTest.java | 76 ------------------- library/core/proguard-rules.txt | 6 ++ .../extractor/DefaultExtractorsFactory.java | 16 +++- 3 files changed, 18 insertions(+), 80 deletions(-) delete mode 100644 extensions/flac/src/test/java/com/google/android/exoplayer2/ext/flac/DefaultExtractorsFactoryTest.java diff --git a/extensions/flac/src/test/java/com/google/android/exoplayer2/ext/flac/DefaultExtractorsFactoryTest.java b/extensions/flac/src/test/java/com/google/android/exoplayer2/ext/flac/DefaultExtractorsFactoryTest.java deleted file mode 100644 index 611197bbe5..0000000000 --- a/extensions/flac/src/test/java/com/google/android/exoplayer2/ext/flac/DefaultExtractorsFactoryTest.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright (C) 2016 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.ext.flac; - -import static com.google.common.truth.Truth.assertThat; - -import androidx.test.ext.junit.runners.AndroidJUnit4; -import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; -import com.google.android.exoplayer2.extractor.Extractor; -import com.google.android.exoplayer2.extractor.amr.AmrExtractor; -import com.google.android.exoplayer2.extractor.flv.FlvExtractor; -import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor; -import com.google.android.exoplayer2.extractor.mp3.Mp3Extractor; -import com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor; -import com.google.android.exoplayer2.extractor.mp4.Mp4Extractor; -import com.google.android.exoplayer2.extractor.ogg.OggExtractor; -import com.google.android.exoplayer2.extractor.ts.Ac3Extractor; -import com.google.android.exoplayer2.extractor.ts.Ac4Extractor; -import com.google.android.exoplayer2.extractor.ts.AdtsExtractor; -import com.google.android.exoplayer2.extractor.ts.PsExtractor; -import com.google.android.exoplayer2.extractor.ts.TsExtractor; -import com.google.android.exoplayer2.extractor.wav.WavExtractor; -import java.util.ArrayList; -import java.util.List; -import org.junit.Test; -import org.junit.runner.RunWith; - -/** Unit test for {@link DefaultExtractorsFactory}. */ -@RunWith(AndroidJUnit4.class) -public final class DefaultExtractorsFactoryTest { - - @Test - public void testCreateExtractors_returnExpectedClasses() { - DefaultExtractorsFactory defaultExtractorsFactory = new DefaultExtractorsFactory(); - - Extractor[] extractors = defaultExtractorsFactory.createExtractors(); - List> listCreatedExtractorClasses = new ArrayList<>(); - for (Extractor extractor : extractors) { - listCreatedExtractorClasses.add(extractor.getClass()); - } - - Class[] expectedExtractorClassses = - new Class[] { - MatroskaExtractor.class, - FragmentedMp4Extractor.class, - Mp4Extractor.class, - Mp3Extractor.class, - AdtsExtractor.class, - Ac3Extractor.class, - Ac4Extractor.class, - TsExtractor.class, - FlvExtractor.class, - OggExtractor.class, - PsExtractor.class, - WavExtractor.class, - AmrExtractor.class, - FlacExtractor.class - }; - - assertThat(listCreatedExtractorClasses).containsNoDuplicates(); - assertThat(listCreatedExtractorClasses).containsExactlyElementsIn(expectedExtractorClassses); - } -} diff --git a/library/core/proguard-rules.txt b/library/core/proguard-rules.txt index fd4e196945..ff59046049 100644 --- a/library/core/proguard-rules.txt +++ b/library/core/proguard-rules.txt @@ -5,6 +5,12 @@ public static android.net.Uri buildRawResourceUri(int); } +# Methods accessed via reflection in DefaultExtractorsFactory +-dontnote com.google.android.exoplayer2.ext.flac.FlacLibrary +-keepclassmembers class com.google.android.exoplayer2.ext.flac.FlacLibrary { + public static boolean isAvailable(); +} + # Some members of this class are being accessed from native methods. Keep them unobfuscated. -keep class com.google.android.exoplayer2.video.VideoDecoderOutputBuffer { *; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java index 1f7b6f7098..cdbd37493b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java @@ -64,10 +64,18 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory { @Nullable Constructor flacExtensionExtractorConstructor = null; try { // LINT.IfChange - flacExtensionExtractorConstructor = - Class.forName("com.google.android.exoplayer2.ext.flac.FlacExtractor") - .asSubclass(Extractor.class) - .getConstructor(); + @SuppressWarnings("nullness:argument.type.incompatible") + boolean isFlacNativeLibraryAvailable = + Boolean.TRUE.equals( + Class.forName("com.google.android.exoplayer2.ext.flac.FlacLibrary") + .getMethod("isAvailable") + .invoke(/* obj= */ null)); + if (isFlacNativeLibraryAvailable) { + flacExtensionExtractorConstructor = + Class.forName("com.google.android.exoplayer2.ext.flac.FlacExtractor") + .asSubclass(Extractor.class) + .getConstructor(); + } // LINT.ThenChange(../../../../../../../../proguard-rules.txt) } catch (ClassNotFoundException e) { // Expected if the app was built without the FLAC extension. From 448db89446bf91b63c5351b77c7f579a65bd0447 Mon Sep 17 00:00:00 2001 From: ibaker Date: Wed, 8 Jan 2020 15:04:09 +0000 Subject: [PATCH 37/44] Add TypefaceSpan and hasNoFooSpanBetween() support to SpannedSubject Use these to migrate the last WebvttDecoderTest method to SpannedSubject PiperOrigin-RevId: 288688620 --- .../assets/webvtt/with_css_complex_selectors | 10 +- .../text/webvtt/WebvttDecoderTest.java | 67 ++-- .../testutil/truth/SpannedSubject.java | 206 ++++++++++- .../testutil/truth/SpannedSubjectTest.java | 334 ++++++++++++++++++ 4 files changed, 570 insertions(+), 47 deletions(-) diff --git a/library/core/src/test/assets/webvtt/with_css_complex_selectors b/library/core/src/test/assets/webvtt/with_css_complex_selectors index 62e3348ae9..64d2a516d1 100644 --- a/library/core/src/test/assets/webvtt/with_css_complex_selectors +++ b/library/core/src/test/assets/webvtt/with_css_complex_selectors @@ -20,7 +20,7 @@ STYLE id 00:00.000 --> 00:01.001 -This should be underlined and courier and violet. +This should be underlined and courier and violet. íd 00:02.000 --> 00:02.001 @@ -31,10 +31,10 @@ _id This should be courier and bold. 00:04.000 --> 00:04.001 -This shouldn't be bold. -This should be bold. +This shouldn't be bold. +This should be bold. anId 00:05.000 --> 00:05.001 -This is specific - But this is more italic +This is specific +But this is more italic diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java index e07c412fd7..97e3a8f7d5 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java @@ -19,17 +19,14 @@ import static com.google.android.exoplayer2.testutil.truth.SpannedSubject.assert import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; -import android.graphics.Typeface; import android.text.Layout.Alignment; import android.text.Spanned; -import android.text.style.ForegroundColorSpan; -import android.text.style.StyleSpan; -import android.text.style.TypefaceSpan; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.SubtitleDecoderException; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.ColorParser; import com.google.common.collect.Iterables; import com.google.common.truth.Expect; @@ -328,41 +325,39 @@ public class WebvttDecoderTest { @Test public void testWithComplexCssSelectors() throws Exception { WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_CSS_COMPLEX_SELECTORS); - Spanned text = getUniqueSpanTextAt(subtitle, /* timeUs= */ 0); - assertThat(text.getSpans(/* start= */ 30, text.length(), ForegroundColorSpan.class)) - .hasLength(1); - assertThat( - text.getSpans(/* start= */ 30, text.length(), ForegroundColorSpan.class)[0] - .getForegroundColor()) - .isEqualTo(0xFFEE82EE); - assertThat(text.getSpans(/* start= */ 30, text.length(), TypefaceSpan.class)).hasLength(1); - assertThat(text.getSpans(/* start= */ 30, text.length(), TypefaceSpan.class)[0].getFamily()) - .isEqualTo("courier"); + Spanned firstCueText = getUniqueSpanTextAt(subtitle, /* timeUs= */ 0); + assertThat(firstCueText) + .hasForegroundColorSpanBetween( + "This should be underlined and ".length(), firstCueText.length()) + .withColor(ColorParser.parseCssColor("violet")); + assertThat(firstCueText) + .hasTypefaceSpanBetween("This should be underlined and ".length(), firstCueText.length()) + .withFamily("courier"); - text = getUniqueSpanTextAt(subtitle, /* timeUs= */ 2000000); - assertThat(text.getSpans(/* start= */ 5, text.length(), TypefaceSpan.class)).hasLength(1); - assertThat(text.getSpans(/* start= */ 5, text.length(), TypefaceSpan.class)[0].getFamily()) - .isEqualTo("courier"); + Spanned secondCueText = getUniqueSpanTextAt(subtitle, /* timeUs= */ 2_000_000); + assertThat(secondCueText) + .hasTypefaceSpanBetween("This ".length(), secondCueText.length()) + .withFamily("courier"); + assertThat(secondCueText) + .hasNoForegroundColorSpanBetween("This ".length(), secondCueText.length()); - text = getUniqueSpanTextAt(subtitle, /* timeUs= */ 2500000); - assertThat(text.getSpans(/* start= */ 5, text.length(), StyleSpan.class)).hasLength(1); - assertThat(text.getSpans(/* start= */ 5, text.length(), StyleSpan.class)[0].getStyle()) - .isEqualTo(Typeface.BOLD); - assertThat(text.getSpans(/* start= */ 5, text.length(), TypefaceSpan.class)).hasLength(1); - assertThat(text.getSpans(/* start= */ 5, text.length(), TypefaceSpan.class)[0].getFamily()) - .isEqualTo("courier"); + Spanned thirdCueText = getUniqueSpanTextAt(subtitle, /* timeUs= */ 2_500_000); + assertThat(thirdCueText).hasBoldSpanBetween("This ".length(), thirdCueText.length()); + assertThat(thirdCueText) + .hasTypefaceSpanBetween("This ".length(), thirdCueText.length()) + .withFamily("courier"); - text = getUniqueSpanTextAt(subtitle, /* timeUs= */ 4000000); - assertThat(text.getSpans(/* start= */ 6, /* end= */ 22, StyleSpan.class)).hasLength(0); - assertThat(text.getSpans(/* start= */ 30, text.length(), StyleSpan.class)).hasLength(1); - assertThat(text.getSpans(/* start= */ 30, text.length(), StyleSpan.class)[0].getStyle()) - .isEqualTo(Typeface.BOLD); + Spanned fourthCueText = getUniqueSpanTextAt(subtitle, /* timeUs= */ 4_000_000); + assertThat(fourthCueText) + .hasNoStyleSpanBetween("This ".length(), "shouldn't be bold.".length()); + assertThat(fourthCueText) + .hasBoldSpanBetween("This shouldn't be bold.\nThis ".length(), fourthCueText.length()); - text = getUniqueSpanTextAt(subtitle, /* timeUs= */ 5000000); - assertThat(text.getSpans(/* start= */ 9, /* end= */ 17, StyleSpan.class)).hasLength(0); - assertThat(text.getSpans(/* start= */ 19, text.length(), StyleSpan.class)).hasLength(1); - assertThat(text.getSpans(/* start= */ 19, text.length(), StyleSpan.class)[0].getStyle()) - .isEqualTo(Typeface.ITALIC); + Spanned fifthCueText = getUniqueSpanTextAt(subtitle, /* timeUs= */ 5_000_000); + assertThat(fifthCueText) + .hasNoStyleSpanBetween("This is ".length(), "This is specific".length()); + assertThat(fifthCueText) + .hasItalicSpanBetween("This is specific\n".length(), fifthCueText.length()); } @Test @@ -387,6 +382,6 @@ public class WebvttDecoderTest { } private Spanned getUniqueSpanTextAt(WebvttSubtitle sub, long timeUs) { - return (Spanned) sub.getCues(timeUs).get(0).text; + return (Spanned) Assertions.checkNotNull(sub.getCues(timeUs).get(0).text); } } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/truth/SpannedSubject.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/truth/SpannedSubject.java index b6efa1e7b7..78c41a43e8 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/truth/SpannedSubject.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/truth/SpannedSubject.java @@ -26,12 +26,14 @@ import android.text.TextUtils; import android.text.style.BackgroundColorSpan; import android.text.style.ForegroundColorSpan; import android.text.style.StyleSpan; +import android.text.style.TypefaceSpan; import android.text.style.UnderlineSpan; import androidx.annotation.CheckResult; import androidx.annotation.ColorInt; import androidx.annotation.Nullable; import com.google.android.exoplayer2.text.span.HorizontalTextInVerticalContextSpan; import com.google.android.exoplayer2.text.span.RubySpan; +import com.google.android.exoplayer2.util.Util; import com.google.common.truth.FailureMetadata; import com.google.common.truth.Subject; import java.util.ArrayList; @@ -171,6 +173,19 @@ public final class SpannedSubject extends Subject { return ALREADY_FAILED_WITH_FLAGS; } + /** + * Checks that the subject has no {@link StyleSpan}s on any of the text between {@code start} and + * {@code end}. + * + *

    This fails even if the start and end indexes don't exactly match. + * + * @param start The start index to start searching for spans. + * @param end The end index to stop searching for spans. + */ + public void hasNoStyleSpanBetween(int start, int end) { + hasNoSpansOfTypeBetween(StyleSpan.class, start, end); + } + /** * Checks that the subject has an {@link UnderlineSpan} from {@code start} to {@code end}. * @@ -194,6 +209,19 @@ public final class SpannedSubject extends Subject { return ALREADY_FAILED_WITH_FLAGS; } + /** + * Checks that the subject has no {@link UnderlineSpan}s on any of the text between {@code start} + * and {@code end}. + * + *

    This fails even if the start and end indexes don't exactly match. + * + * @param start The start index to start searching for spans. + * @param end The end index to stop searching for spans. + */ + public void hasNoUnderlineSpanBetween(int start, int end) { + hasNoSpansOfTypeBetween(UnderlineSpan.class, start, end); + } + /** * Checks that the subject has a {@link ForegroundColorSpan} from {@code start} to {@code end}. * @@ -222,6 +250,19 @@ public final class SpannedSubject extends Subject { .that(foregroundColorSpans); } + /** + * Checks that the subject has no {@link ForegroundColorSpan}s on any of the text between {@code + * start} and {@code end}. + * + *

    This fails even if the start and end indexes don't exactly match. + * + * @param start The start index to start searching for spans. + * @param end The end index to stop searching for spans. + */ + public void hasNoForegroundColorSpanBetween(int start, int end) { + hasNoSpansOfTypeBetween(ForegroundColorSpan.class, start, end); + } + /** * Checks that the subject has a {@link BackgroundColorSpan} from {@code start} to {@code end}. * @@ -250,6 +291,58 @@ public final class SpannedSubject extends Subject { .that(backgroundColorSpans); } + /** + * Checks that the subject has no {@link BackgroundColorSpan}s on any of the text between {@code + * start} and {@code end}. + * + *

    This fails even if the start and end indexes don't exactly match. + * + * @param start The start index to start searching for spans. + * @param end The end index to stop searching for spans. + */ + public void hasNoBackgroundColorSpanBetween(int start, int end) { + hasNoSpansOfTypeBetween(BackgroundColorSpan.class, start, end); + } + + /** + * Checks that the subject has a {@link TypefaceSpan} from {@code start} to {@code end}. + * + *

    The font is asserted in a follow-up method call on the return {@link Typefaced} object. + * + * @param start The start of the expected span. + * @param end The end of the expected span. + * @return A {@link Typefaced} object to assert on the font of the matching spans. + */ + @CheckResult + public Typefaced hasTypefaceSpanBetween(int start, int end) { + if (actual == null) { + failWithoutActual(simpleFact("Spanned must not be null")); + return ALREADY_FAILED_TYPEFACED; + } + + List backgroundColorSpans = findMatchingSpans(start, end, TypefaceSpan.class); + if (backgroundColorSpans.isEmpty()) { + failWithExpectedSpan(start, end, TypefaceSpan.class, actual.toString().substring(start, end)); + return ALREADY_FAILED_TYPEFACED; + } + return check("TypefaceSpan (start=%s,end=%s)", start, end) + .about(typefaceSpans(actual)) + .that(backgroundColorSpans); + } + + /** + * Checks that the subject has no {@link TypefaceSpan}s on any of the text between {@code start} + * and {@code end}. + * + *

    This fails even if the start and end indexes don't exactly match. + * + * @param start The start index to start searching for spans. + * @param end The end index to stop searching for spans. + */ + public void hasNoTypefaceSpanBetween(int start, int end) { + hasNoSpansOfTypeBetween(TypefaceSpan.class, start, end); + } + /** * Checks that the subject has a {@link RubySpan} from {@code start} to {@code end}. * @@ -274,6 +367,19 @@ public final class SpannedSubject extends Subject { return check("RubySpan (start=%s,end=%s)", start, end).about(rubySpans(actual)).that(rubySpans); } + /** + * Checks that the subject has no {@link RubySpan}s on any of the text between {@code start} and + * {@code end}. + * + *

    This fails even if the start and end indexes don't exactly match. + * + * @param start The start index to start searching for spans. + * @param end The end index to stop searching for spans. + */ + public void hasNoRubySpanBetween(int start, int end) { + hasNoSpansOfTypeBetween(RubySpan.class, start, end); + } + /** * Checks that the subject has an {@link HorizontalTextInVerticalContextSpan} from {@code start} * to {@code end}. @@ -303,6 +409,45 @@ public final class SpannedSubject extends Subject { return ALREADY_FAILED_WITH_FLAGS; } + /** + * Checks that the subject has no {@link HorizontalTextInVerticalContextSpan}s on any of the text + * between {@code start} and {@code end}. + * + *

    This fails even if the start and end indexes don't exactly match. + * + * @param start The start index to start searching for spans. + * @param end The end index to stop searching for spans. + */ + public void hasNoHorizontalTextInVerticalContextSpanBetween(int start, int end) { + hasNoSpansOfTypeBetween(HorizontalTextInVerticalContextSpan.class, start, end); + } + + /** + * Checks that the subject has no spans of type {@code spanClazz} on any of the text between + * {@code start} and {@code end}. + * + *

    This fails even if the start and end indexes don't exactly match. + * + * @param start The start index to start searching for spans. + * @param end The end index to stop searching for spans. + */ + private void hasNoSpansOfTypeBetween(Class spanClazz, int start, int end) { + if (actual == null) { + failWithoutActual(simpleFact("Spanned must not be null")); + return; + } + Object[] matchingSpans = actual.getSpans(start, end, spanClazz); + if (matchingSpans.length != 0) { + failWithoutActual( + simpleFact( + String.format( + "Found unexpected %ss between start=%s,end=%s", + spanClazz.getSimpleName(), start, end)), + simpleFact("expected none"), + fact("but found", getAllSpansAsString(actual))); + } + } + private List findMatchingSpans(int startIndex, int endIndex, Class spanClazz) { List spans = new ArrayList<>(); for (T span : actual.getSpans(startIndex, endIndex, spanClazz)) { @@ -421,8 +566,8 @@ public final class SpannedSubject extends Subject { private static final Colored ALREADY_FAILED_COLORED = color -> ALREADY_FAILED_AND_FLAGS; - private Factory> foregroundColorSpans( - Spanned actualSpanned) { + private static Factory> + foregroundColorSpans(Spanned actualSpanned) { return (FailureMetadata metadata, List spans) -> new ForegroundColorSpansSubject(metadata, spans, actualSpanned); } @@ -458,8 +603,8 @@ public final class SpannedSubject extends Subject { } } - private Factory> backgroundColorSpans( - Spanned actualSpanned) { + private static Factory> + backgroundColorSpans(Spanned actualSpanned) { return (FailureMetadata metadata, List spans) -> new BackgroundColorSpansSubject(metadata, spans, actualSpanned); } @@ -495,6 +640,55 @@ public final class SpannedSubject extends Subject { } } + /** Allows assertions about the typeface of a span. */ + public interface Typefaced { + + /** + * Checks that at least one of the matched spans has the expected {@code fontFamily}. + * + * @param fontFamily The expected font family. + * @return A {@link WithSpanFlags} object for optional additional assertions on the flags. + */ + AndSpanFlags withFamily(String fontFamily); + } + + private static final Typefaced ALREADY_FAILED_TYPEFACED = color -> ALREADY_FAILED_AND_FLAGS; + + private static Factory> typefaceSpans( + Spanned actualSpanned) { + return (FailureMetadata metadata, List spans) -> + new TypefaceSpansSubject(metadata, spans, actualSpanned); + } + + private static final class TypefaceSpansSubject extends Subject implements Typefaced { + + private final List actualSpans; + private final Spanned actualSpanned; + + private TypefaceSpansSubject( + FailureMetadata metadata, List actualSpans, Spanned actualSpanned) { + super(metadata, actualSpans); + this.actualSpans = actualSpans; + this.actualSpanned = actualSpanned; + } + + @Override + public AndSpanFlags withFamily(String fontFamily) { + List matchingSpanFlags = new ArrayList<>(); + List spanFontFamilies = new ArrayList<>(); + + for (TypefaceSpan span : actualSpans) { + spanFontFamilies.add(span.getFamily()); + if (Util.areEqual(span.getFamily(), fontFamily)) { + matchingSpanFlags.add(actualSpanned.getSpanFlags(span)); + } + } + + check("family").that(spanFontFamilies).containsExactly(fontFamily); + return check("flags").about(spanFlags()).that(matchingSpanFlags); + } + } + /** Allows assertions about a span's ruby text and its position. */ public interface RubyText { @@ -511,7 +705,7 @@ public final class SpannedSubject extends Subject { private static final RubyText ALREADY_FAILED_WITH_TEXT = (text, position) -> ALREADY_FAILED_AND_FLAGS; - private Factory> rubySpans(Spanned actualSpanned) { + private static Factory> rubySpans(Spanned actualSpanned) { return (FailureMetadata metadata, List spans) -> new RubySpansSubject(metadata, spans, actualSpanned); } @@ -544,7 +738,7 @@ public final class SpannedSubject extends Subject { return check("flags").about(spanFlags()).that(matchingSpanFlags); } - private static class TextAndPosition { + private static final class TextAndPosition { private final String text; @RubySpan.Position private final int position; diff --git a/testutils/src/test/java/com/google/android/exoplayer2/testutil/truth/SpannedSubjectTest.java b/testutils/src/test/java/com/google/android/exoplayer2/testutil/truth/SpannedSubjectTest.java index c3badd9bb9..d1ee3ee81a 100644 --- a/testutils/src/test/java/com/google/android/exoplayer2/testutil/truth/SpannedSubjectTest.java +++ b/testutils/src/test/java/com/google/android/exoplayer2/testutil/truth/SpannedSubjectTest.java @@ -28,6 +28,7 @@ import android.text.Spanned; import android.text.style.BackgroundColorSpan; import android.text.style.ForegroundColorSpan; import android.text.style.StyleSpan; +import android.text.style.TypefaceSpan; import android.text.style.UnderlineSpan; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.text.span.HorizontalTextInVerticalContextSpan; @@ -170,6 +171,40 @@ public class SpannedSubjectTest { assertThat(expected).factValue("but found").contains("start=" + start); } + @Test + public void noStyleSpan_success() { + SpannableString spannable = SpannableString.valueOf("test with underline then italic spans"); + spannable.setSpan( + new UnderlineSpan(), + "test with ".length(), + "test with underline".length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + spannable.setSpan( + new StyleSpan(Typeface.ITALIC), + "test with underline then ".length(), + "test with underline then italic".length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + assertThat(spannable).hasNoStyleSpanBetween(0, "test with underline then".length()); + } + + @Test + public void noStyleSpan_failure() { + SpannableString spannable = SpannableString.valueOf("test with italic section"); + int start = "test with ".length(); + int end = start + "italic".length(); + spannable.setSpan(new StyleSpan(Typeface.ITALIC), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + AssertionError expected = + expectFailure( + whenTesting -> whenTesting.that(spannable).hasNoStyleSpanBetween(start + 1, end)); + assertThat(expected) + .factKeys() + .contains("Found unexpected StyleSpans between start=" + (start + 1) + ",end=" + end); + assertThat(expected).factKeys().contains("expected none"); + assertThat(expected).factValue("but found").contains("start=" + start); + } + @Test public void underlineSpan_success() { SpannableString spannable = SpannableString.valueOf("test with underlined section"); @@ -182,6 +217,40 @@ public class SpannedSubjectTest { .withFlags(Spanned.SPAN_INCLUSIVE_EXCLUSIVE); } + @Test + public void noUnderlineSpan_success() { + SpannableString spannable = SpannableString.valueOf("test with italic then underline spans"); + spannable.setSpan( + new StyleSpan(Typeface.ITALIC), + "test with ".length(), + "test with italic".length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + spannable.setSpan( + new UnderlineSpan(), + "test with italic then ".length(), + "test with italic then underline".length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + assertThat(spannable).hasNoUnderlineSpanBetween(0, "test with italic then".length()); + } + + @Test + public void noUnderlineSpan_failure() { + SpannableString spannable = SpannableString.valueOf("test with underline section"); + int start = "test with ".length(); + int end = start + "underline".length(); + spannable.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + AssertionError expected = + expectFailure( + whenTesting -> whenTesting.that(spannable).hasNoUnderlineSpanBetween(start + 1, end)); + assertThat(expected) + .factKeys() + .contains("Found unexpected UnderlineSpans between start=" + (start + 1) + ",end=" + end); + assertThat(expected).factKeys().contains("expected none"); + assertThat(expected).factValue("but found").contains("start=" + start); + } + @Test public void foregroundColorSpan_success() { SpannableString spannable = SpannableString.valueOf("test with cyan section"); @@ -261,6 +330,43 @@ public class SpannedSubjectTest { .contains(String.valueOf(Spanned.SPAN_INCLUSIVE_EXCLUSIVE)); } + @Test + public void noForegroundColorSpan_success() { + SpannableString spannable = SpannableString.valueOf("test with underline then cyan spans"); + spannable.setSpan( + new UnderlineSpan(), + "test with ".length(), + "test with underline".length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + spannable.setSpan( + new ForegroundColorSpan(Color.CYAN), + "test with underline then ".length(), + "test with underline then cyan".length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + assertThat(spannable).hasNoForegroundColorSpanBetween(0, "test with underline then".length()); + } + + @Test + public void noForegroundColorSpan_failure() { + SpannableString spannable = SpannableString.valueOf("test with cyan section"); + int start = "test with ".length(); + int end = start + "cyan".length(); + spannable.setSpan( + new ForegroundColorSpan(Color.CYAN), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + AssertionError expected = + expectFailure( + whenTesting -> + whenTesting.that(spannable).hasNoForegroundColorSpanBetween(start + 1, end)); + assertThat(expected) + .factKeys() + .contains( + "Found unexpected ForegroundColorSpans between start=" + (start + 1) + ",end=" + end); + assertThat(expected).factKeys().contains("expected none"); + assertThat(expected).factValue("but found").contains("start=" + start); + } + @Test public void backgroundColorSpan_success() { SpannableString spannable = SpannableString.valueOf("test with cyan section"); @@ -340,6 +446,152 @@ public class SpannedSubjectTest { .contains(String.valueOf(Spanned.SPAN_INCLUSIVE_EXCLUSIVE)); } + @Test + public void noBackgroundColorSpan_success() { + SpannableString spannable = SpannableString.valueOf("test with underline then cyan spans"); + spannable.setSpan( + new UnderlineSpan(), + "test with ".length(), + "test with underline".length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + spannable.setSpan( + new BackgroundColorSpan(Color.CYAN), + "test with underline then ".length(), + "test with underline then cyan".length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + assertThat(spannable).hasNoBackgroundColorSpanBetween(0, "test with underline then".length()); + } + + @Test + public void noBackgroundColorSpan_failure() { + SpannableString spannable = SpannableString.valueOf("test with cyan section"); + int start = "test with ".length(); + int end = start + "cyan".length(); + spannable.setSpan( + new BackgroundColorSpan(Color.CYAN), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + AssertionError expected = + expectFailure( + whenTesting -> + whenTesting.that(spannable).hasNoBackgroundColorSpanBetween(start + 1, end)); + assertThat(expected) + .factKeys() + .contains( + "Found unexpected BackgroundColorSpans between start=" + (start + 1) + ",end=" + end); + assertThat(expected).factKeys().contains("expected none"); + assertThat(expected).factValue("but found").contains("start=" + start); + } + + @Test + public void typefaceSpan_success() { + SpannableString spannable = SpannableString.valueOf("test with courier section"); + int start = "test with ".length(); + int end = start + "courier".length(); + spannable.setSpan(new TypefaceSpan("courier"), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + assertThat(spannable) + .hasTypefaceSpanBetween(start, end) + .withFamily("courier") + .andFlags(Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + } + + @Test + public void typefaceSpan_wrongEndIndex() { + SpannableString spannable = SpannableString.valueOf("test with courier section"); + int start = "test with ".length(); + int end = start + "courier".length(); + spannable.setSpan(new TypefaceSpan("courier"), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + int incorrectEnd = end + 2; + AssertionError expected = + expectFailure( + whenTesting -> + whenTesting + .that(spannable) + .hasTypefaceSpanBetween(start, incorrectEnd) + .withFamily("courier")); + assertThat(expected).factValue("expected").contains("end=" + incorrectEnd); + assertThat(expected).factValue("but found").contains("end=" + end); + } + + @Test + public void typefaceSpan_wrongFamily() { + SpannableString spannable = SpannableString.valueOf("test with courier section"); + int start = "test with ".length(); + int end = start + "courier".length(); + spannable.setSpan(new TypefaceSpan("courier"), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + AssertionError expected = + expectFailure( + whenTesting -> + whenTesting + .that(spannable) + .hasTypefaceSpanBetween(start, end) + .withFamily("roboto")); + assertThat(expected).factValue("value of").contains("family"); + assertThat(expected).factValue("expected").contains("roboto"); + assertThat(expected).factValue("but was").contains("courier"); + } + + @Test + public void typefaceSpan_wrongFlags() { + SpannableString spannable = SpannableString.valueOf("test with courier section"); + int start = "test with ".length(); + int end = start + "courier".length(); + spannable.setSpan(new TypefaceSpan("courier"), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + AssertionError expected = + expectFailure( + whenTesting -> + whenTesting + .that(spannable) + .hasTypefaceSpanBetween(start, end) + .withFamily("courier") + .andFlags(Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)); + assertThat(expected).factValue("value of").contains("flags"); + assertThat(expected) + .factValue("expected to contain") + .contains(String.valueOf(Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)); + assertThat(expected) + .factValue("but was") + .contains(String.valueOf(Spanned.SPAN_INCLUSIVE_EXCLUSIVE)); + } + + @Test + public void noTypefaceSpan_success() { + SpannableString spannable = SpannableString.valueOf("test with underline then courier spans"); + spannable.setSpan( + new UnderlineSpan(), + "test with ".length(), + "test with underline".length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + spannable.setSpan( + new TypefaceSpan("courier"), + "test with underline then ".length(), + "test with underline then courier".length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + assertThat(spannable).hasNoTypefaceSpanBetween(0, "test with underline then".length()); + } + + @Test + public void noTypefaceSpan_failure() { + SpannableString spannable = SpannableString.valueOf("test with courier section"); + int start = "test with ".length(); + int end = start + "courier".length(); + spannable.setSpan(new TypefaceSpan("courier"), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + AssertionError expected = + expectFailure( + whenTesting -> whenTesting.that(spannable).hasNoTypefaceSpanBetween(start + 1, end)); + assertThat(expected) + .factKeys() + .contains("Found unexpected TypefaceSpans between start=" + (start + 1) + ",end=" + end); + assertThat(expected).factKeys().contains("expected none"); + assertThat(expected).factValue("but found").contains("start=" + start); + } + @Test public void rubySpan_success() { SpannableString spannable = SpannableString.valueOf("test with rubied section"); @@ -454,6 +706,44 @@ public class SpannedSubjectTest { .contains(String.valueOf(Spanned.SPAN_INCLUSIVE_EXCLUSIVE)); } + @Test + public void noRubySpan_success() { + SpannableString spannable = SpannableString.valueOf("test with underline then ruby spans"); + spannable.setSpan( + new UnderlineSpan(), + "test with ".length(), + "test with underline".length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + spannable.setSpan( + new RubySpan("ruby text", RubySpan.POSITION_OVER), + "test with underline then ".length(), + "test with underline then ruby".length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + assertThat(spannable).hasNoRubySpanBetween(0, "test with underline then".length()); + } + + @Test + public void noRubySpan_failure() { + SpannableString spannable = SpannableString.valueOf("test with ruby section"); + int start = "test with ".length(); + int end = start + "ruby".length(); + spannable.setSpan( + new RubySpan("ruby text", RubySpan.POSITION_OVER), + start, + end, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + AssertionError expected = + expectFailure( + whenTesting -> whenTesting.that(spannable).hasNoRubySpanBetween(start + 1, end)); + assertThat(expected) + .factKeys() + .contains("Found unexpected RubySpans between start=" + (start + 1) + ",end=" + end); + assertThat(expected).factKeys().contains("expected none"); + assertThat(expected).factValue("but found").contains("start=" + start); + } + @Test public void horizontalTextInVerticalContextSpan_success() { SpannableString spannable = SpannableString.valueOf("vertical text with horizontal section"); @@ -467,6 +757,50 @@ public class SpannedSubjectTest { .withFlags(Spanned.SPAN_INCLUSIVE_EXCLUSIVE); } + @Test + public void noHorizontalTextInVerticalContextSpan_success() { + SpannableString spannable = + SpannableString.valueOf("test with underline then tate-chu-yoko spans"); + spannable.setSpan( + new UnderlineSpan(), + "test with ".length(), + "test with underline".length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + spannable.setSpan( + new HorizontalTextInVerticalContextSpan(), + "test with underline then ".length(), + "test with underline then tate-chu-yoko".length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + assertThat(spannable) + .hasNoHorizontalTextInVerticalContextSpanBetween(0, "test with underline then".length()); + } + + @Test + public void noHorizontalTextInVerticalContextSpan_failure() { + SpannableString spannable = SpannableString.valueOf("test with tate-chu-yoko section"); + int start = "test with ".length(); + int end = start + "tate-chu-yoko".length(); + spannable.setSpan( + new HorizontalTextInVerticalContextSpan(), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + AssertionError expected = + expectFailure( + whenTesting -> + whenTesting + .that(spannable) + .hasNoHorizontalTextInVerticalContextSpanBetween(start + 1, end)); + assertThat(expected) + .factKeys() + .contains( + "Found unexpected HorizontalTextInVerticalContextSpans between start=" + + (start + 1) + + ",end=" + + end); + assertThat(expected).factKeys().contains("expected none"); + assertThat(expected).factValue("but found").contains("start=" + start); + } + private static AssertionError expectFailure( ExpectFailure.SimpleSubjectBuilderCallback callback) { return expectFailureAbout(spanned(), callback); From 762bc18a28f131bcfb6b345f4fcfecf66125e6f0 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 8 Jan 2020 15:04:46 +0000 Subject: [PATCH 38/44] Fix TrueHD chunking in Matroska Issue: #6845 PiperOrigin-RevId: 288688716 --- RELEASENOTES.md | 2 ++ .../android/exoplayer2/extractor/mkv/MatroskaExtractor.java | 4 +--- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index d5dd9ddd0e..ac0fb108b9 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -34,6 +34,8 @@ to proceed. * Fix handling of E-AC-3 streams that contain AC-3 syncframes ([#6602](https://github.com/google/ExoPlayer/issues/6602)). +* Fix playback of TrueHD streams in Matroska + ([#6845](https://github.com/google/ExoPlayer/issues/6845)). * Support "twos" codec (big endian PCM) in MP4 ([#5789](https://github.com/google/ExoPlayer/issues/5789)). * WAV: Support IMA ADPCM encoded data. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java index ee57bbec90..8812d2857e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java @@ -1829,10 +1829,8 @@ public class MatroskaExtractor implements Extractor { chunkSize += size; chunkOffset = offset; // The offset is to the end of the sample. if (chunkSampleCount >= Ac3Util.TRUEHD_RECHUNK_SAMPLE_COUNT) { - // We haven't read enough samples to output a chunk. - return; + outputPendingSampleMetadata(track); } - outputPendingSampleMetadata(track); } public void outputPendingSampleMetadata(Track track) { From 8e26505ee8984988f6f0015e6aadba6516b96a29 Mon Sep 17 00:00:00 2001 From: ibaker Date: Wed, 8 Jan 2020 16:07:50 +0000 Subject: [PATCH 39/44] Fix what I think is a typo in WebVTT test data Without the CSS tweak the additional test assertion fails. PiperOrigin-RevId: 288698323 --- library/core/src/test/assets/webvtt/with_css_complex_selectors | 2 +- .../android/exoplayer2/text/webvtt/WebvttDecoderTest.java | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/library/core/src/test/assets/webvtt/with_css_complex_selectors b/library/core/src/test/assets/webvtt/with_css_complex_selectors index 64d2a516d1..130d4a2529 100644 --- a/library/core/src/test/assets/webvtt/with_css_complex_selectors +++ b/library/core/src/test/assets/webvtt/with_css_complex_selectors @@ -1,7 +1,7 @@ WEBVTT STYLE -::cue(\n#id ){text-decoration:underline;} +::cue(#id ){text-decoration:underline;} STYLE ::cue(#id.class1.class2 ){ color: violet;} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java index 97e3a8f7d5..b778953f01 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java @@ -326,6 +326,7 @@ public class WebvttDecoderTest { public void testWithComplexCssSelectors() throws Exception { WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_CSS_COMPLEX_SELECTORS); Spanned firstCueText = getUniqueSpanTextAt(subtitle, /* timeUs= */ 0); + assertThat(firstCueText).hasUnderlineSpanBetween(0, firstCueText.length()); assertThat(firstCueText) .hasForegroundColorSpanBetween( "This should be underlined and ".length(), firstCueText.length()) From 14e401f53a56a273c2ce784a1aff63b5da7eb2c8 Mon Sep 17 00:00:00 2001 From: ibaker Date: Wed, 8 Jan 2020 16:18:18 +0000 Subject: [PATCH 40/44] Update TtmlDecoder to keep only one Span of each type The current code relies on Android's evaluation order of spans, which doesn't seem to be defined anywhere. PiperOrigin-RevId: 288700011 --- .../android/exoplayer2/text/SpanUtil.java | 55 ++++++++++++ .../exoplayer2/text/ttml/TtmlRenderUtil.java | 48 ++++++++--- .../text/webvtt/WebvttCueParser.java | 76 +++++++++------- .../android/exoplayer2/text/SpanUtilTest.java | 86 +++++++++++++++++++ 4 files changed, 226 insertions(+), 39 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/text/SpanUtil.java create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/text/SpanUtilTest.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/SpanUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/text/SpanUtil.java new file mode 100644 index 0000000000..9e9f350dd7 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/SpanUtil.java @@ -0,0 +1,55 @@ +/* + * 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.text; + +import android.text.Spannable; +import android.text.style.ForegroundColorSpan; + +/** + * Utility methods for Android span + * styling. + */ +public final class SpanUtil { + + /** + * Adds {@code span} to {@code spannable} between {@code start} and {@code end}, removing any + * existing spans of the same type and with the same indices and flags. + * + *

    This is useful for types of spans that don't make sense to duplicate and where the + * evaluation order might have an unexpected impact on the final text, e.g. {@link + * ForegroundColorSpan}. + * + * @param spannable The {@link Spannable} to add {@code span} to. + * @param span The span object to be added. + * @param start The start index to add the new span at. + * @param end The end index to add the new span at. + * @param spanFlags The flags to pass to {@link Spannable#setSpan(Object, int, int, int)}. + */ + public static void addOrReplaceSpan( + Spannable spannable, Object span, int start, int end, int spanFlags) { + Object[] existingSpans = spannable.getSpans(start, end, span.getClass()); + for (Object existingSpan : existingSpans) { + if (spannable.getSpanStart(existingSpan) == start + && spannable.getSpanEnd(existingSpan) == end + && spannable.getSpanFlags(existingSpan) == spanFlags) { + spannable.removeSpan(existingSpan); + } + } + spannable.setSpan(span, start, end, spanFlags); + } + + private SpanUtil() {} +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlRenderUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlRenderUtil.java index 21333081c6..25395431de 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlRenderUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlRenderUtil.java @@ -15,7 +15,6 @@ */ package com.google.android.exoplayer2.text.ttml; -import android.text.Spannable; import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.style.AbsoluteSizeSpan; @@ -27,6 +26,7 @@ import android.text.style.StrikethroughSpan; import android.text.style.StyleSpan; import android.text.style.TypefaceSpan; import android.text.style.UnderlineSpan; +import com.google.android.exoplayer2.text.SpanUtil; import java.util.Map; /** @@ -77,32 +77,60 @@ import java.util.Map; builder.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } if (style.hasFontColor()) { - builder.setSpan(new ForegroundColorSpan(style.getFontColor()), start, end, - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + SpanUtil.addOrReplaceSpan( + builder, + new ForegroundColorSpan(style.getFontColor()), + start, + end, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } if (style.hasBackgroundColor()) { - builder.setSpan(new BackgroundColorSpan(style.getBackgroundColor()), start, end, - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + SpanUtil.addOrReplaceSpan( + builder, + new BackgroundColorSpan(style.getBackgroundColor()), + start, + end, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } if (style.getFontFamily() != null) { - builder.setSpan(new TypefaceSpan(style.getFontFamily()), start, end, + SpanUtil.addOrReplaceSpan( + builder, + new TypefaceSpan(style.getFontFamily()), + start, + end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } if (style.getTextAlign() != null) { - builder.setSpan(new AlignmentSpan.Standard(style.getTextAlign()), start, end, + SpanUtil.addOrReplaceSpan( + builder, + new AlignmentSpan.Standard(style.getTextAlign()), + start, + end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } switch (style.getFontSizeUnit()) { case TtmlStyle.FONT_SIZE_UNIT_PIXEL: - builder.setSpan(new AbsoluteSizeSpan((int) style.getFontSize(), true), start, end, + SpanUtil.addOrReplaceSpan( + builder, + new AbsoluteSizeSpan((int) style.getFontSize(), true), + start, + end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); break; case TtmlStyle.FONT_SIZE_UNIT_EM: - builder.setSpan(new RelativeSizeSpan(style.getFontSize()), start, end, + SpanUtil.addOrReplaceSpan( + builder, + new RelativeSizeSpan(style.getFontSize()), + start, + end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); break; case TtmlStyle.FONT_SIZE_UNIT_PERCENT: - builder.setSpan(new RelativeSizeSpan(style.getFontSize() / 100), start, end, + SpanUtil.addOrReplaceSpan( + builder, + new RelativeSizeSpan(style.getFontSize() / 100), + start, + end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); break; case TtmlStyle.UNSPECIFIED: diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java index fe36043800..f62b073f60 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java @@ -15,11 +15,11 @@ */ package com.google.android.exoplayer2.text.webvtt; +import static com.google.android.exoplayer2.text.SpanUtil.addOrReplaceSpan; import static java.lang.annotation.RetentionPolicy.SOURCE; import android.graphics.Typeface; import android.text.Layout; -import android.text.Spannable; import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.SpannedString; @@ -535,7 +535,12 @@ public final class WebvttCueParser { return; } if (style.getStyle() != WebvttCssStyle.UNSPECIFIED) { - addOrReplaceSpan(spannedText, new StyleSpan(style.getStyle()), start, end); + addOrReplaceSpan( + spannedText, + new StyleSpan(style.getStyle()), + start, + end, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } if (style.isLinethrough()) { spannedText.setSpan(new StrikethroughSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); @@ -544,29 +549,62 @@ public final class WebvttCueParser { spannedText.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } if (style.hasFontColor()) { - addOrReplaceSpan(spannedText, new ForegroundColorSpan(style.getFontColor()), start, end); + addOrReplaceSpan( + spannedText, + new ForegroundColorSpan(style.getFontColor()), + start, + end, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } if (style.hasBackgroundColor()) { addOrReplaceSpan( - spannedText, new BackgroundColorSpan(style.getBackgroundColor()), start, end); + spannedText, + new BackgroundColorSpan(style.getBackgroundColor()), + start, + end, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } if (style.getFontFamily() != null) { - addOrReplaceSpan(spannedText, new TypefaceSpan(style.getFontFamily()), start, end); + addOrReplaceSpan( + spannedText, + new TypefaceSpan(style.getFontFamily()), + start, + end, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } Layout.Alignment textAlign = style.getTextAlign(); if (textAlign != null) { - addOrReplaceSpan(spannedText, new AlignmentSpan.Standard(textAlign), start, end); + addOrReplaceSpan( + spannedText, + new AlignmentSpan.Standard(textAlign), + start, + end, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } switch (style.getFontSizeUnit()) { case WebvttCssStyle.FONT_SIZE_UNIT_PIXEL: addOrReplaceSpan( - spannedText, new AbsoluteSizeSpan((int) style.getFontSize(), true), start, end); + spannedText, + new AbsoluteSizeSpan((int) style.getFontSize(), true), + start, + end, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); break; case WebvttCssStyle.FONT_SIZE_UNIT_EM: - addOrReplaceSpan(spannedText, new RelativeSizeSpan(style.getFontSize()), start, end); + addOrReplaceSpan( + spannedText, + new RelativeSizeSpan(style.getFontSize()), + start, + end, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); break; case WebvttCssStyle.FONT_SIZE_UNIT_PERCENT: - addOrReplaceSpan(spannedText, new RelativeSizeSpan(style.getFontSize() / 100), start, end); + addOrReplaceSpan( + spannedText, + new RelativeSizeSpan(style.getFontSize() / 100), + start, + end, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); break; case WebvttCssStyle.UNSPECIFIED: // Do nothing. @@ -578,26 +616,6 @@ public final class WebvttCueParser { } } - /** - * Adds {@code span} to {@code spannedText} between {@code start} and {@code end}, removing any - * existing spans of the same type and with the same indices. - * - *

    This is useful for types of spans that don't make sense to duplicate and where the - * evaluation order might have an unexpected impact on the final text, e.g. {@link - * ForegroundColorSpan}. - */ - private static void addOrReplaceSpan( - SpannableStringBuilder spannedText, Object span, int start, int end) { - Object[] existingSpans = spannedText.getSpans(start, end, span.getClass()); - for (Object existingSpan : existingSpans) { - if (spannedText.getSpanStart(existingSpan) == start - && spannedText.getSpanEnd(existingSpan) == end) { - spannedText.removeSpan(existingSpan); - } - } - spannedText.setSpan(span, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - } - /** * Returns the tag name for the given tag contents. * diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/SpanUtilTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/SpanUtilTest.java new file mode 100644 index 0000000000..3a71925255 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/SpanUtilTest.java @@ -0,0 +1,86 @@ +/* + * 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.text; + +import static com.google.common.truth.Truth.assertThat; + +import android.graphics.Color; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.Spanned; +import android.text.style.BackgroundColorSpan; +import android.text.style.ForegroundColorSpan; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Tests for {@link SpanUtil}. */ +@RunWith(AndroidJUnit4.class) +public class SpanUtilTest { + + @Test + public void addOrReplaceSpan_replacesSameTypeAndIndexes() { + Spannable spannable = SpannableString.valueOf("test text"); + spannable.setSpan( + new ForegroundColorSpan(Color.CYAN), + /* start= */ 2, + /* end= */ 5, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + ForegroundColorSpan newSpan = new ForegroundColorSpan(Color.BLUE); + SpanUtil.addOrReplaceSpan( + spannable, newSpan, /* start= */ 2, /* end= */ 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + Object[] spans = spannable.getSpans(0, spannable.length(), Object.class); + assertThat(spans).asList().containsExactly(newSpan); + } + + @Test + public void addOrReplaceSpan_ignoresDifferentType() { + Spannable spannable = SpannableString.valueOf("test text"); + ForegroundColorSpan originalSpan = new ForegroundColorSpan(Color.CYAN); + spannable.setSpan(originalSpan, /* start= */ 2, /* end= */ 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + BackgroundColorSpan newSpan = new BackgroundColorSpan(Color.BLUE); + SpanUtil.addOrReplaceSpan(spannable, newSpan, 2, 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + Object[] spans = spannable.getSpans(0, spannable.length(), Object.class); + assertThat(spans).asList().containsExactly(originalSpan, newSpan).inOrder(); + } + + @Test + public void addOrReplaceSpan_ignoresDifferentStartEndAndFlags() { + Spannable spannable = SpannableString.valueOf("test text"); + ForegroundColorSpan originalSpan = new ForegroundColorSpan(Color.CYAN); + spannable.setSpan(originalSpan, /* start= */ 2, /* end= */ 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + ForegroundColorSpan differentStart = new ForegroundColorSpan(Color.GREEN); + SpanUtil.addOrReplaceSpan( + spannable, differentStart, /* start= */ 3, /* end= */ 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + ForegroundColorSpan differentEnd = new ForegroundColorSpan(Color.BLUE); + SpanUtil.addOrReplaceSpan( + spannable, differentEnd, /* start= */ 2, /* end= */ 6, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + ForegroundColorSpan differentFlags = new ForegroundColorSpan(Color.GREEN); + SpanUtil.addOrReplaceSpan( + spannable, differentFlags, /* start= */ 2, /* end= */ 5, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + Object[] spans = spannable.getSpans(0, spannable.length(), Object.class); + assertThat(spans) + .asList() + .containsExactly(originalSpan, differentStart, differentEnd, differentFlags) + .inOrder(); + } +} From fa9bf9c8280bf4853238388c9e94603e11798365 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 8 Jan 2020 16:51:25 +0000 Subject: [PATCH 41/44] Ensure seeks to new windows are reported as seeking. Currently, seeks are only tracked if both the start and the end of the seek is within the same window. This means no seeking state is reported if the playback switches to a new window during the seek. This problem is fixed by tracking seek start and end events in all cases, but only report seeking state once the window becomes the foreground window. PiperOrigin-RevId: 288706674 --- .../analytics/PlaybackStatsListener.java | 34 ++++++++++++------- 1 file changed, 21 insertions(+), 13 deletions(-) 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 5927b9dd6e..3f3803f5c0 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 @@ -84,6 +84,7 @@ public final class PlaybackStatsListener @Player.State private int playbackState; private boolean isSuppressed; private float playbackSpeed; + private boolean isSeeking; /** * Creates listener for playback stats. @@ -169,6 +170,9 @@ public final class PlaybackStatsListener @Override public void onSessionCreated(EventTime eventTime, String session) { PlaybackStatsTracker tracker = new PlaybackStatsTracker(keepHistory, eventTime); + if (isSeeking) { + tracker.onSeekStarted(eventTime, /* belongsToPlayback= */ true); + } tracker.onPlayerStateChanged( eventTime, playWhenReady, playbackState, /* belongsToPlayback= */ true); tracker.onIsSuppressedChanged(eventTime, isSuppressed, /* belongsToPlayback= */ true); @@ -288,20 +292,20 @@ public final class PlaybackStatsListener public void onSeekStarted(EventTime eventTime) { sessionManager.updateSessions(eventTime); for (String session : playbackStatsTrackers.keySet()) { - if (sessionManager.belongsToSession(eventTime, session)) { - playbackStatsTrackers.get(session).onSeekStarted(eventTime); - } + boolean belongsToPlayback = sessionManager.belongsToSession(eventTime, session); + playbackStatsTrackers.get(session).onSeekStarted(eventTime, belongsToPlayback); } + isSeeking = true; } @Override public void onSeekProcessed(EventTime eventTime) { sessionManager.updateSessions(eventTime); for (String session : playbackStatsTrackers.keySet()) { - if (sessionManager.belongsToSession(eventTime, session)) { - playbackStatsTrackers.get(session).onSeekProcessed(eventTime); - } + boolean belongsToPlayback = sessionManager.belongsToSession(eventTime, session); + playbackStatsTrackers.get(session).onSeekProcessed(eventTime, belongsToPlayback); } + isSeeking = false; } @Override @@ -563,23 +567,27 @@ public final class PlaybackStatsListener } /** - * Notifies the tracker of the start of a seek in the current playback. + * Notifies the tracker of the start of a seek, including all seeks while the playback is not in + * the foreground. * * @param eventTime The {@link EventTime}. + * @param belongsToPlayback Whether the {@code eventTime} belongs to the current playback. */ - public void onSeekStarted(EventTime eventTime) { + public void onSeekStarted(EventTime eventTime, boolean belongsToPlayback) { isSeeking = true; - maybeUpdatePlaybackState(eventTime, /* belongsToPlayback= */ true); + maybeUpdatePlaybackState(eventTime, belongsToPlayback); } /** - * Notifies the tracker of a seek has been processed in the current playback. + * Notifies the tracker that a seek has been processed, including all seeks while the playback + * is not in the foreground. * * @param eventTime The {@link EventTime}. + * @param belongsToPlayback Whether the {@code eventTime} belongs to the current playback. */ - public void onSeekProcessed(EventTime eventTime) { + public void onSeekProcessed(EventTime eventTime, boolean belongsToPlayback) { isSeeking = false; - maybeUpdatePlaybackState(eventTime, /* belongsToPlayback= */ true); + maybeUpdatePlaybackState(eventTime, belongsToPlayback); } /** @@ -875,7 +883,7 @@ public final class PlaybackStatsListener return currentPlaybackState == PlaybackStats.PLAYBACK_STATE_ENDED ? PlaybackStats.PLAYBACK_STATE_ENDED : PlaybackStats.PLAYBACK_STATE_ABANDONED; - } else if (isSeeking) { + } else if (isSeeking && isForeground) { // Seeking takes precedence over errors such that we report a seek while in error state. return PlaybackStats.PLAYBACK_STATE_SEEKING; } else if (hasFatalError) { From e5eaacec20615b8c85eea520955d33d6c0fe7094 Mon Sep 17 00:00:00 2001 From: ibaker Date: Wed, 8 Jan 2020 17:15:03 +0000 Subject: [PATCH 42/44] Fix typo in SpannedSubject.hasBoldItalicSpanBetween PiperOrigin-RevId: 288710939 --- .../testutil/truth/SpannedSubject.java | 4 ++-- .../testutil/truth/SpannedSubjectTest.java | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/truth/SpannedSubject.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/truth/SpannedSubject.java index 78c41a43e8..1751502ac4 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/truth/SpannedSubject.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/truth/SpannedSubject.java @@ -167,8 +167,8 @@ public final class SpannedSubject extends Subject { simpleFact( String.format("No matching StyleSpans found between start=%s,end=%s", start, end)), fact("in text", actual.toString()), - fact("expected either styles", Arrays.asList(Typeface.BOLD_ITALIC)), - fact("or styles", Arrays.asList(Typeface.BOLD, Typeface.BOLD_ITALIC)), + fact("expected either styles", Collections.singletonList(Typeface.BOLD_ITALIC)), + fact("or styles", Arrays.asList(Typeface.BOLD, Typeface.ITALIC)), fact("but found styles", styles)); return ALREADY_FAILED_WITH_FLAGS; } diff --git a/testutils/src/test/java/com/google/android/exoplayer2/testutil/truth/SpannedSubjectTest.java b/testutils/src/test/java/com/google/android/exoplayer2/testutil/truth/SpannedSubjectTest.java index d1ee3ee81a..32ce419c19 100644 --- a/testutils/src/test/java/com/google/android/exoplayer2/testutil/truth/SpannedSubjectTest.java +++ b/testutils/src/test/java/com/google/android/exoplayer2/testutil/truth/SpannedSubjectTest.java @@ -151,6 +151,23 @@ public class SpannedSubjectTest { .withFlags(Spanned.SPAN_INCLUSIVE_EXCLUSIVE); } + @Test + public void boldItalicSpan_onlyItalic() { + SpannableString spannable = SpannableString.valueOf("test with italic section"); + int start = "test with ".length(); + int end = start + "italic".length(); + spannable.setSpan(new StyleSpan(Typeface.ITALIC), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + AssertionError expected = + expectFailure( + whenTesting -> whenTesting.that(spannable).hasBoldItalicSpanBetween(start, end)); + assertThat(expected) + .factKeys() + .contains( + String.format("No matching StyleSpans found between start=%s,end=%s", start, end)); + assertThat(expected).factValue("but found styles").contains("[" + Typeface.ITALIC + "]"); + } + @Test public void boldItalicSpan_mismatchedStartIndex() { SpannableString spannable = SpannableString.valueOf("test with bold & italic section"); From 216518eb0ea1beaf567ae14eae00c0d19055c9cf Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 8 Jan 2020 17:20:19 +0000 Subject: [PATCH 43/44] Disable chronometer for playback speeds != 1.0 This doesn't work because the Chronometer text layout can only count in realtime. Issue:#6816 PiperOrigin-RevId: 288711702 --- RELEASENOTES.md | 2 ++ .../exoplayer2/ui/PlayerNotificationManager.java | 16 ++++++++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index ac0fb108b9..bdef903be1 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -54,6 +54,8 @@ * OkHttp extension: Upgrade OkHttp dependency to 3.12.7, which fixes a class of `SocketTimeoutException` issues when using HTTP/2 ([#4078](https://github.com/google/ExoPlayer/issues/4078)). +* Don't use notification chronometer if playback speed is != 1.0 + ([#6816](https://github.com/google/ExoPlayer/issues/6816)). ### 2.11.1 (2019-12-20) ### 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 e572bc5a11..9f0c8280c4 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 @@ -927,7 +927,18 @@ public class PlayerNotificationManager { } /** - * Sets whether the elapsed time of the media playback should be displayed + * Sets whether the elapsed time of the media playback should be displayed. + * + *

    Note that this setting only works if all of the following are true: + * + *

      + *
    • The media is {@link Player#isPlaying() actively playing}. + *
    • The media is not {@link Player#isCurrentWindowDynamic() dynamically changing its + * duration} (like for example a live stream). + *
    • The media is not {@link Player#isPlayingAd() interrupted by an ad}. + *
    • The media is played at {@link Player#getPlaybackParameters() regular speed}. + *
    • The device is running at least API 21 (Lollipop). + *
    * *

    See {@link NotificationCompat.Builder#setUsesChronometer(boolean)}. * @@ -1082,7 +1093,8 @@ public class PlayerNotificationManager { && useChronometer && player.isPlaying() && !player.isPlayingAd() - && !player.isCurrentWindowDynamic()) { + && !player.isCurrentWindowDynamic() + && player.getPlaybackParameters().speed == 1f) { builder .setWhen(System.currentTimeMillis() - player.getContentPosition()) .setShowWhen(true) From 79edf7cce23098a94901e1b351dd2ab49d0e8049 Mon Sep 17 00:00:00 2001 From: kimvde Date: Wed, 8 Jan 2020 17:21:09 +0000 Subject: [PATCH 44/44] FlacExtractor: add condition for zero-length reads This improves readability by making clearer that reading no bytes from the input in readFrames() is an expected case if the buffer is full. PiperOrigin-RevId: 288711841 --- .../extractor/flac/FlacExtractor.java | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flac/FlacExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flac/FlacExtractor.java index 8c31bde2a2..79dd20065b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flac/FlacExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flac/FlacExtractor.java @@ -256,15 +256,18 @@ public final class FlacExtractor implements Extractor { // Copy more bytes into the buffer. int currentLimit = buffer.limit(); - int bytesRead = - input.read( - buffer.data, /* offset= */ currentLimit, /* length= */ BUFFER_LENGTH - currentLimit); - boolean foundEndOfInput = bytesRead == C.RESULT_END_OF_INPUT; - if (!foundEndOfInput) { - buffer.setLimit(currentLimit + bytesRead); - } else if (buffer.bytesLeft() == 0) { - outputSampleMetadata(); - return Extractor.RESULT_END_OF_INPUT; + boolean foundEndOfInput = false; + if (currentLimit < BUFFER_LENGTH) { + int bytesRead = + input.read( + buffer.data, /* offset= */ currentLimit, /* length= */ BUFFER_LENGTH - currentLimit); + foundEndOfInput = bytesRead == C.RESULT_END_OF_INPUT; + if (!foundEndOfInput) { + buffer.setLimit(currentLimit + bytesRead); + } else if (buffer.bytesLeft() == 0) { + outputSampleMetadata(); + return Extractor.RESULT_END_OF_INPUT; + } } // Search for a frame.